@n-seiji/nuthatch 0.1.4 → 0.1.5
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 +36 -6
- package/README.md +36 -9
- package/dist/cli.js +1415 -461
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2041,8 +2041,11 @@ async function runCommand(cmd, opts) {
|
|
|
2041
2041
|
// src/cli-dispatch.ts
|
|
2042
2042
|
var normalizeCliArgs = (rawArgs, argv0, isCompiledBinary) => isCompiledBinary && rawArgs.length === 1 && rawArgs[0] === argv0 ? [] : rawArgs;
|
|
2043
2043
|
var isRunningAsCompiledBinary = () => typeof Bun !== "undefined" && Bun.main.startsWith("/$bunfs/");
|
|
2044
|
+
var isUpdateRequest = (rawArgs) => rawArgs[0] === "--update";
|
|
2044
2045
|
var HELP_FLAGS = new Set(["--help", "-h", "help"]);
|
|
2045
|
-
var
|
|
2046
|
+
var UPDATE_HELP_FLAGS = new Set(["--help", "-h"]);
|
|
2047
|
+
var isHelpRequest = (rawArgs) => rawArgs.length > 0 && (HELP_FLAGS.has(rawArgs[0] ?? "") || isUpdateRequest(rawArgs) && rawArgs.slice(1).some((token) => UPDATE_HELP_FLAGS.has(token)));
|
|
2048
|
+
var isVersionRequest = (rawArgs) => rawArgs[0] === "--version";
|
|
2046
2049
|
var dispatchCliArgs = (rawArgs, reservedNames) => {
|
|
2047
2050
|
const [first, ...rest] = rawArgs;
|
|
2048
2051
|
if (first === "--") {
|
|
@@ -2156,14 +2159,17 @@ var render = (command, result, json) => {
|
|
|
2156
2159
|
`);
|
|
2157
2160
|
}
|
|
2158
2161
|
};
|
|
2162
|
+
var reportResult = (command, result, json) => {
|
|
2163
|
+
render(command, result, json);
|
|
2164
|
+
process.exitCode = result.exitCode;
|
|
2165
|
+
};
|
|
2159
2166
|
|
|
2160
2167
|
// src/cli-fatal.ts
|
|
2161
2168
|
var wantsJson = (args) => args.some((arg) => arg === "--json" || arg.startsWith("--json="));
|
|
2162
2169
|
var reportFatalError = (command, json, error) => {
|
|
2163
2170
|
const fatal = describeFatalError(error);
|
|
2164
2171
|
const result = fail(fatal.exitCode, `hop: ${fatal.message}`);
|
|
2165
|
-
|
|
2166
|
-
process.exitCode = result.exitCode;
|
|
2172
|
+
reportResult(command, result, json);
|
|
2167
2173
|
};
|
|
2168
2174
|
|
|
2169
2175
|
// src/infra/lock.ts
|
|
@@ -2262,17 +2268,27 @@ var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
|
|
|
2262
2268
|
}
|
|
2263
2269
|
};
|
|
2264
2270
|
};
|
|
2265
|
-
var
|
|
2271
|
+
var acquireUnlessHeld = async (commonDir) => {
|
|
2266
2272
|
try {
|
|
2267
|
-
|
|
2268
|
-
return { ok: true, lock };
|
|
2273
|
+
return await acquireRepoLock(commonDir);
|
|
2269
2274
|
} catch (error) {
|
|
2270
2275
|
if (error instanceof LockHeldError) {
|
|
2271
|
-
return
|
|
2276
|
+
return error;
|
|
2272
2277
|
}
|
|
2273
2278
|
throw error;
|
|
2274
2279
|
}
|
|
2275
2280
|
};
|
|
2281
|
+
var withRepoLock = async (commonDir, mutate) => {
|
|
2282
|
+
const lock = await acquireUnlessHeld(commonDir);
|
|
2283
|
+
if (lock instanceof LockHeldError) {
|
|
2284
|
+
return fail(EXIT_SAFE_REJECTION, lock.message);
|
|
2285
|
+
}
|
|
2286
|
+
try {
|
|
2287
|
+
return await mutate();
|
|
2288
|
+
} finally {
|
|
2289
|
+
await lock.release();
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2276
2292
|
|
|
2277
2293
|
// src/infra/repo.ts
|
|
2278
2294
|
import { basename, dirname, join as join2 } from "node:path";
|
|
@@ -2380,6 +2396,21 @@ var parsePorcelain = (output) => {
|
|
|
2380
2396
|
return result;
|
|
2381
2397
|
};
|
|
2382
2398
|
|
|
2399
|
+
// src/domain/tracking.ts
|
|
2400
|
+
var resolveTrackingRef = (branch, remotes) => {
|
|
2401
|
+
if (remotes.includes("origin")) {
|
|
2402
|
+
return { kind: "track", ref: `origin/${branch}` };
|
|
2403
|
+
}
|
|
2404
|
+
const [first, second] = remotes;
|
|
2405
|
+
if (first === undefined) {
|
|
2406
|
+
return { kind: "none" };
|
|
2407
|
+
}
|
|
2408
|
+
if (second !== undefined) {
|
|
2409
|
+
return { kind: "ambiguous" };
|
|
2410
|
+
}
|
|
2411
|
+
return { kind: "track", ref: `${first}/${branch}` };
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2383
2414
|
// src/infra/repo.ts
|
|
2384
2415
|
var loadRepoContext = async (git, fs, cwd) => {
|
|
2385
2416
|
const [commonDir, porcelainOut] = await Promise.all([
|
|
@@ -2403,8 +2434,34 @@ var loadRepoContext = async (git, fs, cwd) => {
|
|
|
2403
2434
|
}));
|
|
2404
2435
|
return { rootPath, managedRoot, commonDir, worktrees };
|
|
2405
2436
|
};
|
|
2406
|
-
var
|
|
2437
|
+
var nestedWorktreePaths = (worktrees, path) => nestedWorktrees(worktrees, path).map((wt) => wt.path);
|
|
2407
2438
|
var nestedWorktrees = (worktrees, path) => worktrees.filter((wt) => wt.path !== path && !wt.prunable && isWithin(path, wt.path));
|
|
2439
|
+
var isWorktreeDirty = (git, worktrees, path) => git.isDirty(path, nestedWorktreePaths(worktrees, path));
|
|
2440
|
+
var worktreeDirtyState = async (git, fs, worktrees, wt) => {
|
|
2441
|
+
if (wt.bare || wt.prunable || !await fs.exists(wt.path)) {
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
return isWorktreeDirty(git, worktrees, wt.path);
|
|
2445
|
+
};
|
|
2446
|
+
var resolveBranchCheckout = async (git, rootPath, branch, explicitTrack) => {
|
|
2447
|
+
const localBranches = await git.listBranches(rootPath);
|
|
2448
|
+
const createBranch = !localBranches.includes(branch);
|
|
2449
|
+
let track = explicitTrack;
|
|
2450
|
+
if (createBranch && track === undefined) {
|
|
2451
|
+
const remotes = await git.remotesWithBranch(rootPath, branch);
|
|
2452
|
+
const resolution = resolveTrackingRef(branch, remotes);
|
|
2453
|
+
if (resolution.kind === "ambiguous") {
|
|
2454
|
+
return {
|
|
2455
|
+
ok: false,
|
|
2456
|
+
rejection: fail(EXIT_USAGE_ERROR, `Branch "${branch}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
if (resolution.kind === "track") {
|
|
2460
|
+
track = resolution.ref;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return { ok: true, options: { createBranch, ...track === undefined ? {} : { track } } };
|
|
2464
|
+
};
|
|
2408
2465
|
var realpathOrRaw = async (fs, path) => {
|
|
2409
2466
|
try {
|
|
2410
2467
|
return await fs.realpath(path);
|
|
@@ -2415,6 +2472,7 @@ var realpathOrRaw = async (fs, path) => {
|
|
|
2415
2472
|
|
|
2416
2473
|
// src/commands/root-holder-swap.ts
|
|
2417
2474
|
var holderRejection = (branch, holderPath, reason) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is already checked out at ${holderPath}, and it ${reason}. Not swapping — cd there instead of using hop root.`);
|
|
2475
|
+
var prunableHolderRejection = (branch, holderPath) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is held by a stale worktree registration at ${holderPath} (git reports it prunable). Not swapping — if that worktree was moved, re-link it with "git worktree repair <new path>"; otherwise drop the registration first ("hop rm ${branch}" or "git worktree prune").`);
|
|
2418
2476
|
var multipleHoldersRejection = (branch, holders) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is checked out at ${holders.length} worktrees at once (${holders.map((holder) => holder.path).join(", ")}). Not swapping — hop won't guess which one to detach.`);
|
|
2419
2477
|
var resolveHolderSwap = async ({
|
|
2420
2478
|
git,
|
|
@@ -2454,7 +2512,10 @@ var resolveHolderSwap = async ({
|
|
|
2454
2512
|
rejection: holderRejection(target, freshHolder.path, `is locked by git${freshHolder.lockReason === null ? "" : ` (${freshHolder.lockReason})`}`)
|
|
2455
2513
|
};
|
|
2456
2514
|
}
|
|
2457
|
-
|
|
2515
|
+
if (freshHolder.prunable) {
|
|
2516
|
+
return { rejection: prunableHolderRejection(target, freshHolder.path) };
|
|
2517
|
+
}
|
|
2518
|
+
const holderDirty = await isWorktreeDirty(git, fresh.worktrees, freshHolder.path);
|
|
2458
2519
|
if (holderDirty) {
|
|
2459
2520
|
return {
|
|
2460
2521
|
rejection: holderRejection(target, freshHolder.path, "has uncommitted or untracked changes")
|
|
@@ -2467,20 +2528,19 @@ var resolveHolderSwap = async ({
|
|
|
2467
2528
|
// src/commands/root.ts
|
|
2468
2529
|
var root = async (git, fs, options) => {
|
|
2469
2530
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
2470
|
-
const rootWorktree = context.worktrees.find((wt) => wt.kind === "root");
|
|
2471
2531
|
if (options.target === undefined) {
|
|
2472
2532
|
return ok({
|
|
2473
2533
|
path: context.rootPath,
|
|
2474
2534
|
data: {
|
|
2475
|
-
branch:
|
|
2535
|
+
branch: rootBranchOf(context),
|
|
2476
2536
|
switched: false,
|
|
2477
2537
|
detachedHolder: null
|
|
2478
2538
|
}
|
|
2479
2539
|
});
|
|
2480
2540
|
}
|
|
2481
|
-
const
|
|
2482
|
-
if (
|
|
2483
|
-
return
|
|
2541
|
+
const dirtyRejection = await dirtyRootRejection(git, context);
|
|
2542
|
+
if (dirtyRejection !== null) {
|
|
2543
|
+
return dirtyRejection;
|
|
2484
2544
|
}
|
|
2485
2545
|
if (options.target === "-") {
|
|
2486
2546
|
return switchAndReport({
|
|
@@ -2492,57 +2552,64 @@ var root = async (git, fs, options) => {
|
|
|
2492
2552
|
holderPolicy: { allowExternal: true }
|
|
2493
2553
|
});
|
|
2494
2554
|
}
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
let track = initialTrack;
|
|
2499
|
-
if (!branchExistsLocally && track === undefined) {
|
|
2500
|
-
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
2501
|
-
const [firstRemote] = remotes;
|
|
2502
|
-
if (remotes.includes("origin")) {
|
|
2503
|
-
track = `origin/${options.target}`;
|
|
2504
|
-
} else if (remotes.length === 1 && firstRemote !== undefined) {
|
|
2505
|
-
track = `${firstRemote}/${options.target}`;
|
|
2506
|
-
} else if (remotes.length > 1) {
|
|
2507
|
-
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
2508
|
-
}
|
|
2555
|
+
const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
|
|
2556
|
+
if (!checkout.ok) {
|
|
2557
|
+
return checkout.rejection;
|
|
2509
2558
|
}
|
|
2510
2559
|
return switchAndReport({
|
|
2511
2560
|
git,
|
|
2512
2561
|
fs,
|
|
2513
2562
|
context,
|
|
2514
2563
|
target: options.target,
|
|
2515
|
-
switchOptions:
|
|
2516
|
-
createBranch: !branchExistsLocally,
|
|
2517
|
-
...track === undefined ? {} : { track }
|
|
2518
|
-
},
|
|
2564
|
+
switchOptions: checkout.options,
|
|
2519
2565
|
holderPolicy: {
|
|
2520
2566
|
allowExternal: options.allowExternalHolderSwap ?? true,
|
|
2521
2567
|
...options.expectedHolderPath === undefined ? {} : { expectedPath: options.expectedHolderPath }
|
|
2522
2568
|
}
|
|
2523
2569
|
});
|
|
2524
2570
|
};
|
|
2525
|
-
var
|
|
2571
|
+
var rootBranchOf = ({ worktrees }) => worktrees.find((wt) => wt.kind === "root")?.branch ?? null;
|
|
2572
|
+
var dirtyRootRejection = async (git, context) => {
|
|
2573
|
+
const dirty = await isWorktreeDirty(git, context.worktrees, context.rootPath);
|
|
2574
|
+
if (!dirty) {
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
|
|
2578
|
+
};
|
|
2579
|
+
var rollBackSwitch = async (git, { rootPath, previousBranch, detachedHolder }) => {
|
|
2580
|
+
const warnings = [];
|
|
2581
|
+
if (previousBranch !== null) {
|
|
2582
|
+
try {
|
|
2583
|
+
await git.switchBranch(rootPath, previousBranch, {});
|
|
2584
|
+
} catch {
|
|
2585
|
+
warnings.push(`Could not restore ${rootPath} to its original branch (${previousBranch})`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (detachedHolder !== null) {
|
|
2589
|
+
try {
|
|
2590
|
+
await git.switchBranch(detachedHolder.path, detachedHolder.branch, {});
|
|
2591
|
+
} catch {
|
|
2592
|
+
warnings.push(`Could not restore ${detachedHolder.path} to its original branch (${detachedHolder.branch}); it remains in detached HEAD`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return warnings;
|
|
2596
|
+
};
|
|
2597
|
+
var switchAndReport = ({
|
|
2526
2598
|
git,
|
|
2527
2599
|
fs,
|
|
2528
2600
|
context,
|
|
2529
2601
|
target,
|
|
2530
2602
|
switchOptions,
|
|
2531
2603
|
holderPolicy
|
|
2532
|
-
}) => {
|
|
2533
|
-
const acquisition = await acquireRepoLockOrRejection(context.commonDir);
|
|
2534
|
-
if (!acquisition.ok) {
|
|
2535
|
-
return acquisition.rejection;
|
|
2536
|
-
}
|
|
2537
|
-
const { lock } = acquisition;
|
|
2604
|
+
}) => withRepoLock(context.commonDir, async () => {
|
|
2538
2605
|
let detachedHolder = null;
|
|
2539
2606
|
let previousBranch = null;
|
|
2540
2607
|
try {
|
|
2541
2608
|
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
2542
|
-
previousBranch = fresh
|
|
2543
|
-
const stillDirty = await git
|
|
2544
|
-
if (stillDirty) {
|
|
2545
|
-
return
|
|
2609
|
+
previousBranch = rootBranchOf(fresh);
|
|
2610
|
+
const stillDirty = await dirtyRootRejection(git, fresh);
|
|
2611
|
+
if (stillDirty !== null) {
|
|
2612
|
+
return stillDirty;
|
|
2546
2613
|
}
|
|
2547
2614
|
const holderSwap = await resolveHolderSwap({
|
|
2548
2615
|
git,
|
|
@@ -2559,7 +2626,7 @@ var switchAndReport = async ({
|
|
|
2559
2626
|
let resolvedBranch = target;
|
|
2560
2627
|
if (target === "-") {
|
|
2561
2628
|
const afterSwitch = await loadRepoContext(git, fs, fresh.rootPath);
|
|
2562
|
-
resolvedBranch = afterSwitch
|
|
2629
|
+
resolvedBranch = rootBranchOf(afterSwitch);
|
|
2563
2630
|
}
|
|
2564
2631
|
return ok({
|
|
2565
2632
|
path: fresh.rootPath,
|
|
@@ -2571,26 +2638,14 @@ var switchAndReport = async ({
|
|
|
2571
2638
|
warnings: detachedHolder === null ? [] : [`Put ${detachedHolder.path} into detached HEAD`]
|
|
2572
2639
|
});
|
|
2573
2640
|
} catch (error) {
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
if (detachedHolder !== null) {
|
|
2583
|
-
try {
|
|
2584
|
-
await git.switchBranch(detachedHolder.path, detachedHolder.branch, {});
|
|
2585
|
-
} catch {
|
|
2586
|
-
rollbackWarnings.push(`Could not restore ${detachedHolder.path} to its original branch (${detachedHolder.branch}); it remains in detached HEAD`);
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${error.message}`, rollbackWarnings);
|
|
2590
|
-
} finally {
|
|
2591
|
-
await lock.release();
|
|
2641
|
+
const warnings = await rollBackSwitch(git, {
|
|
2642
|
+
rootPath: context.rootPath,
|
|
2643
|
+
previousBranch,
|
|
2644
|
+
detachedHolder
|
|
2645
|
+
});
|
|
2646
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${messageOf(error)}`, warnings);
|
|
2592
2647
|
}
|
|
2593
|
-
};
|
|
2648
|
+
});
|
|
2594
2649
|
|
|
2595
2650
|
// src/cli-root-command.ts
|
|
2596
2651
|
var ROOT_PREVIOUS_TOKEN = " hop-root-previous ";
|
|
@@ -2612,7 +2667,7 @@ var rewriteRootPreviousToken = (args) => {
|
|
|
2612
2667
|
return rewritten;
|
|
2613
2668
|
};
|
|
2614
2669
|
var resolveRootTarget = (branch) => branch === ROOT_PREVIOUS_TOKEN ? "-" : branch;
|
|
2615
|
-
var createRootCommand = (git, fs
|
|
2670
|
+
var createRootCommand = (git, fs) => defineCommand({
|
|
2616
2671
|
meta: {
|
|
2617
2672
|
name: "root",
|
|
2618
2673
|
description: "cd to the root clone, or temporarily switch its branch"
|
|
@@ -2636,8 +2691,7 @@ var createRootCommand = (git, fs, applyExitCode) => defineCommand({
|
|
|
2636
2691
|
...target === undefined ? {} : { target: String(target) },
|
|
2637
2692
|
...args.track === undefined ? {} : { track: String(args.track) }
|
|
2638
2693
|
});
|
|
2639
|
-
|
|
2640
|
-
applyExitCode(result);
|
|
2694
|
+
reportResult("root", result, Boolean(args.json));
|
|
2641
2695
|
}
|
|
2642
2696
|
});
|
|
2643
2697
|
|
|
@@ -2677,7 +2731,7 @@ var pick = async (git, fs, options) => {
|
|
|
2677
2731
|
git.listRemoteBranches(context.rootPath),
|
|
2678
2732
|
Promise.all(context.worktrees.map(async (worktree) => [
|
|
2679
2733
|
worktree.path,
|
|
2680
|
-
|
|
2734
|
+
await worktreeDirtyState(git, fs, context.worktrees, worktree)
|
|
2681
2735
|
]))
|
|
2682
2736
|
]);
|
|
2683
2737
|
const dirtyByPath = new Map(dirtyEntries);
|
|
@@ -2688,6 +2742,8 @@ var pick = async (git, fs, options) => {
|
|
|
2688
2742
|
// src/commands/rm.ts
|
|
2689
2743
|
var EXT_DEPRECATION_WARNING = "--ext is deprecated and no longer required: hop rm can now remove external worktrees without it.";
|
|
2690
2744
|
var lockedRejection = (branch, lockReason) => fail(EXIT_SAFE_REJECTION, `Worktree for "${branch}" is locked by git${lockReason === null ? "" : ` (${lockReason})`}. hop never unlocks worktrees automatically — run "git worktree unlock" yourself first if you're sure.`);
|
|
2745
|
+
var turnedPrunableRejection = (branch) => fail(EXIT_SAFE_REJECTION, `The selected worktree for "${branch}" changed before removal (git now reports it prunable). Refresh the picker and retry.`);
|
|
2746
|
+
var stalePrunableWarning = ({ path, prunableReason }) => `Worktree at ${path} no longer exists${prunableReason === null ? "" : ` (${prunableReason})`}; removed its stale registration — if it had been moved by hand, the moved copy is no longer linked to this repository.`;
|
|
2691
2747
|
var resolveTarget = (worktrees, branch, expectedPath) => {
|
|
2692
2748
|
const matches = worktrees.filter((wt) => wt.branch === branch);
|
|
2693
2749
|
if (expectedPath !== undefined) {
|
|
@@ -2714,7 +2770,7 @@ var nestedWorktreeRejection = (branch, targetPath, worktrees) => {
|
|
|
2714
2770
|
return fail(EXIT_SAFE_REJECTION, `Worktree for "${branch}" still contains ${nested.length} registered worktree(s) — refusing to remove it, since that would destroy their files too. Remove these first, then retry:
|
|
2715
2771
|
${listing}`);
|
|
2716
2772
|
};
|
|
2717
|
-
var targetSafetyRejection = async (git, context,
|
|
2773
|
+
var targetSafetyRejection = async ({ git, fs, context, options }, target) => {
|
|
2718
2774
|
const nestedRejection = nestedWorktreeRejection(options.branch, target.path, context.worktrees);
|
|
2719
2775
|
if (nestedRejection !== null) {
|
|
2720
2776
|
return nestedRejection;
|
|
@@ -2722,61 +2778,64 @@ var targetSafetyRejection = async (git, context, target, options) => {
|
|
|
2722
2778
|
if (target.locked) {
|
|
2723
2779
|
return lockedRejection(options.branch, target.lockReason);
|
|
2724
2780
|
}
|
|
2725
|
-
if (
|
|
2781
|
+
if (target.prunable && options.allowPrunable === false) {
|
|
2782
|
+
return turnedPrunableRejection(options.branch);
|
|
2783
|
+
}
|
|
2784
|
+
if (!options.force && await worktreeDirtyState(git, fs, context.worktrees, target) === true) {
|
|
2726
2785
|
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
2727
2786
|
}
|
|
2728
2787
|
return null;
|
|
2729
2788
|
};
|
|
2730
|
-
var
|
|
2731
|
-
const
|
|
2732
|
-
...result,
|
|
2733
|
-
warnings: [...result.warnings ?? [], EXT_DEPRECATION_WARNING]
|
|
2734
|
-
} : result;
|
|
2735
|
-
const context = await loadRepoContext(git, fs, options.cwd);
|
|
2789
|
+
var validateRemoval = async (check) => {
|
|
2790
|
+
const { context, options } = check;
|
|
2736
2791
|
const resolved = resolveTarget(context.worktrees, options.branch, options.expectedPath);
|
|
2737
2792
|
if (resolved.rejection !== undefined) {
|
|
2738
|
-
return
|
|
2793
|
+
return { ok: false, rejection: resolved.rejection };
|
|
2739
2794
|
}
|
|
2740
2795
|
const { target } = resolved;
|
|
2741
2796
|
if (target === undefined) {
|
|
2742
|
-
return
|
|
2797
|
+
return {
|
|
2798
|
+
ok: false,
|
|
2799
|
+
rejection: fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`)
|
|
2800
|
+
};
|
|
2743
2801
|
}
|
|
2744
2802
|
if (target.kind === "root") {
|
|
2745
|
-
return
|
|
2746
|
-
}
|
|
2747
|
-
const safetyRejection = await targetSafetyRejection(git, context, target, options);
|
|
2748
|
-
if (safetyRejection !== null) {
|
|
2749
|
-
return finish(safetyRejection);
|
|
2803
|
+
return { ok: false, rejection: fail(EXIT_USAGE_ERROR, "Cannot remove the root clone.") };
|
|
2750
2804
|
}
|
|
2751
|
-
const
|
|
2752
|
-
|
|
2753
|
-
|
|
2805
|
+
const rejection = await targetSafetyRejection(check, target);
|
|
2806
|
+
return rejection === null ? { ok: true, target } : { ok: false, rejection };
|
|
2807
|
+
};
|
|
2808
|
+
var withExtWarning = (ext, result) => ext ? {
|
|
2809
|
+
...result,
|
|
2810
|
+
warnings: [...result.warnings ?? [], EXT_DEPRECATION_WARNING]
|
|
2811
|
+
} : result;
|
|
2812
|
+
var removeWorktree = async (git, fs, options) => {
|
|
2813
|
+
const context = await loadRepoContext(git, fs, options.cwd);
|
|
2814
|
+
const validation = await validateRemoval({ git, fs, context, options });
|
|
2815
|
+
if (!validation.ok) {
|
|
2816
|
+
return validation.rejection;
|
|
2754
2817
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
return
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2818
|
+
return withRepoLock(context.commonDir, async () => {
|
|
2819
|
+
try {
|
|
2820
|
+
const fresh = await loadRepoContext(git, fs, options.cwd);
|
|
2821
|
+
const freshValidation = await validateRemoval({ git, fs, context: fresh, options });
|
|
2822
|
+
if (!freshValidation.ok) {
|
|
2823
|
+
return freshValidation.rejection;
|
|
2824
|
+
}
|
|
2825
|
+
const { target } = freshValidation;
|
|
2826
|
+
await git.removeWorktree(context.rootPath, target.path, options.force);
|
|
2827
|
+
return ok({
|
|
2828
|
+
data: { branch: options.branch, path: target.path },
|
|
2829
|
+
...target.prunable ? { warnings: [stalePrunableWarning(target)] } : {}
|
|
2830
|
+
});
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to remove worktree: ${messageOf(error)}`);
|
|
2769
2833
|
}
|
|
2770
|
-
|
|
2771
|
-
return finish(ok({ data: { branch: options.branch, path: freshTarget.path } }));
|
|
2772
|
-
} catch (error) {
|
|
2773
|
-
return finish(fail(EXIT_SAFE_REJECTION, `Failed to remove worktree: ${error.message}`));
|
|
2774
|
-
} finally {
|
|
2775
|
-
await lock.release();
|
|
2776
|
-
}
|
|
2834
|
+
});
|
|
2777
2835
|
};
|
|
2836
|
+
var rm2 = async (git, fs, options) => withExtWarning(options.ext, await removeWorktree(git, fs, options));
|
|
2778
2837
|
|
|
2779
|
-
// src/ui/picker.ts
|
|
2838
|
+
// src/ui/picker-render.ts
|
|
2780
2839
|
import { homedir } from "node:os";
|
|
2781
2840
|
|
|
2782
2841
|
// src/domain/display-width.ts
|
|
@@ -2855,6 +2914,19 @@ var padToWidth = (value, width) => {
|
|
|
2855
2914
|
return current >= width ? value : value + " ".repeat(width - current);
|
|
2856
2915
|
};
|
|
2857
2916
|
var DEFAULT_ELLIPSIS = "…";
|
|
2917
|
+
var takeWithinWidth = (clusters, budget) => {
|
|
2918
|
+
const taken = [];
|
|
2919
|
+
let usedWidth = WIDTH_ZERO;
|
|
2920
|
+
for (const grapheme of clusters) {
|
|
2921
|
+
const nextWidth = usedWidth + graphemeWidth(grapheme);
|
|
2922
|
+
if (nextWidth > budget) {
|
|
2923
|
+
break;
|
|
2924
|
+
}
|
|
2925
|
+
taken.push(grapheme);
|
|
2926
|
+
usedWidth = nextWidth;
|
|
2927
|
+
}
|
|
2928
|
+
return taken;
|
|
2929
|
+
};
|
|
2858
2930
|
var truncateToWidth = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
2859
2931
|
if (displayWidth(value) <= width) {
|
|
2860
2932
|
return value;
|
|
@@ -2863,17 +2935,7 @@ var truncateToWidth = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
|
2863
2935
|
if (budget < WIDTH_ZERO) {
|
|
2864
2936
|
return "";
|
|
2865
2937
|
}
|
|
2866
|
-
|
|
2867
|
-
let usedWidth = WIDTH_ZERO;
|
|
2868
|
-
for (const grapheme of graphemes(value)) {
|
|
2869
|
-
const nextWidth = usedWidth + graphemeWidth(grapheme);
|
|
2870
|
-
if (nextWidth > budget) {
|
|
2871
|
-
break;
|
|
2872
|
-
}
|
|
2873
|
-
kept += grapheme;
|
|
2874
|
-
usedWidth = nextWidth;
|
|
2875
|
-
}
|
|
2876
|
-
return kept + ellipsis;
|
|
2938
|
+
return takeWithinWidth(graphemes(value), budget).join("") + ellipsis;
|
|
2877
2939
|
};
|
|
2878
2940
|
var truncateToWidthKeepingTail = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
2879
2941
|
if (displayWidth(value) <= width) {
|
|
@@ -2883,21 +2945,14 @@ var truncateToWidthKeepingTail = (value, width, ellipsis = DEFAULT_ELLIPSIS) =>
|
|
|
2883
2945
|
if (budget < WIDTH_ZERO) {
|
|
2884
2946
|
return "";
|
|
2885
2947
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
for (const grapheme of graphemes(value).toReversed()) {
|
|
2889
|
-
const nextWidth = usedWidth + graphemeWidth(grapheme);
|
|
2890
|
-
if (nextWidth > budget) {
|
|
2891
|
-
break;
|
|
2892
|
-
}
|
|
2893
|
-
kept = grapheme + kept;
|
|
2894
|
-
usedWidth = nextWidth;
|
|
2895
|
-
}
|
|
2896
|
-
return ellipsis + kept;
|
|
2948
|
+
const keptFromEnd = takeWithinWidth(graphemes(value).toReversed(), budget);
|
|
2949
|
+
return ellipsis + keptFromEnd.toReversed().join("");
|
|
2897
2950
|
};
|
|
2898
2951
|
|
|
2899
|
-
// src/ui/
|
|
2952
|
+
// src/ui/ansi.ts
|
|
2900
2953
|
var ESC = "\x1B";
|
|
2954
|
+
|
|
2955
|
+
// src/ui/picker-frame.ts
|
|
2901
2956
|
var CURSOR_HOME = `${ESC}[H`;
|
|
2902
2957
|
var CLEAR_TO_END = `${ESC}[J`;
|
|
2903
2958
|
var SGR_RESET = `${ESC}[0m`;
|
|
@@ -3034,28 +3089,6 @@ var buildFrameLines = (left, right, stacked, colorEnabled) => {
|
|
|
3034
3089
|
return buildSideBySide(left, right, colorEnabled);
|
|
3035
3090
|
};
|
|
3036
3091
|
|
|
3037
|
-
// src/ui/picker-viewport.ts
|
|
3038
|
-
var DEFAULT_TERMINAL_HEIGHT = 24;
|
|
3039
|
-
var MIN_VISIBLE_CANDIDATE_ROWS = 3;
|
|
3040
|
-
var BASE_CHROME_ROWS = 3;
|
|
3041
|
-
var SECTION_HEADER_ROWS = 2;
|
|
3042
|
-
var HIDDEN_COUNT_INDICATOR_ROWS = 2;
|
|
3043
|
-
var rowBudget = ({ terminalHeight, stackedPanelRows }) => {
|
|
3044
|
-
const reserved = BASE_CHROME_ROWS + SECTION_HEADER_ROWS + HIDDEN_COUNT_INDICATOR_ROWS + stackedPanelRows;
|
|
3045
|
-
return Math.max(MIN_VISIBLE_CANDIDATE_ROWS, terminalHeight - reserved);
|
|
3046
|
-
};
|
|
3047
|
-
var computeViewport = (total, selectedIndex, budget) => {
|
|
3048
|
-
if (total <= budget) {
|
|
3049
|
-
return { start: 0, end: total, hiddenAbove: 0, hiddenBelow: 0 };
|
|
3050
|
-
}
|
|
3051
|
-
const HALF_DIVISOR = 2;
|
|
3052
|
-
const half = Math.floor(budget / HALF_DIVISOR);
|
|
3053
|
-
const maxStart = total - budget;
|
|
3054
|
-
const start = Math.min(maxStart, Math.max(0, selectedIndex - half));
|
|
3055
|
-
const end = start + budget;
|
|
3056
|
-
return { start, end, hiddenAbove: start, hiddenBelow: total - end };
|
|
3057
|
-
};
|
|
3058
|
-
|
|
3059
3092
|
// src/ui/picker-layout.ts
|
|
3060
3093
|
var MAX_PATH_LENGTH = 40;
|
|
3061
3094
|
var MAX_BRANCH_COLUMN_WIDTH = 24;
|
|
@@ -3126,6 +3159,15 @@ var toCandidateRow = (candidate, options) => ({
|
|
|
3126
3159
|
kindLabel: candidateKindLabel(candidate).padEnd(KIND_COLUMN_WIDTH, " "),
|
|
3127
3160
|
pathLabel: candidatePathLabel(candidate, options.homeDir, options.pathMaxLength)
|
|
3128
3161
|
});
|
|
3162
|
+
var sectionRows = (label, section, entries, rowOptions) => {
|
|
3163
|
+
if (entries.length === 0) {
|
|
3164
|
+
return [];
|
|
3165
|
+
}
|
|
3166
|
+
return [
|
|
3167
|
+
{ kind: "header", label },
|
|
3168
|
+
...entries.map((entry) => toCandidateRow(entry.candidate, { ...rowOptions, index: entry.index, section }))
|
|
3169
|
+
];
|
|
3170
|
+
};
|
|
3129
3171
|
var buildDisplayRows = (candidates, homeDir, indexOffset = 0, columnWidths) => {
|
|
3130
3172
|
const { branchWidth, pathMaxLength } = columnWidths ?? {
|
|
3131
3173
|
branchWidth: branchColumnWidth(candidates),
|
|
@@ -3137,32 +3179,11 @@ var buildDisplayRows = (candidates, homeDir, indexOffset = 0, columnWidths) => {
|
|
|
3137
3179
|
}));
|
|
3138
3180
|
const worktreeEntries = indexed.filter((entry) => isWorktreeCandidate(entry.candidate));
|
|
3139
3181
|
const branchEntries = indexed.filter((entry) => isCreatableCandidate(entry.candidate));
|
|
3140
|
-
const
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
index: entry.index,
|
|
3146
|
-
section: "worktree",
|
|
3147
|
-
branchWidth,
|
|
3148
|
-
pathMaxLength,
|
|
3149
|
-
homeDir
|
|
3150
|
-
}));
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
if (branchEntries.length > 0) {
|
|
3154
|
-
rows.push({ kind: "header", label: "BRANCHES — Enter creates a worktree" });
|
|
3155
|
-
for (const entry of branchEntries) {
|
|
3156
|
-
rows.push(toCandidateRow(entry.candidate, {
|
|
3157
|
-
index: entry.index,
|
|
3158
|
-
section: "branch",
|
|
3159
|
-
branchWidth,
|
|
3160
|
-
pathMaxLength,
|
|
3161
|
-
homeDir
|
|
3162
|
-
}));
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
return rows;
|
|
3182
|
+
const rowOptions = { branchWidth, pathMaxLength, homeDir };
|
|
3183
|
+
return [
|
|
3184
|
+
...sectionRows("WORKTREES", "worktree", worktreeEntries, rowOptions),
|
|
3185
|
+
...sectionRows("BRANCHES — Enter creates a worktree", "branch", branchEntries, rowOptions)
|
|
3186
|
+
];
|
|
3166
3187
|
};
|
|
3167
3188
|
|
|
3168
3189
|
// src/domain/actions.ts
|
|
@@ -3183,7 +3204,7 @@ var availableActions = (candidate) => {
|
|
|
3183
3204
|
}
|
|
3184
3205
|
return actions;
|
|
3185
3206
|
};
|
|
3186
|
-
var requiresDeleteConfirmation = (candidate) => candidate.kind === "worktree" && candidate.worktree.kind === "external";
|
|
3207
|
+
var requiresDeleteConfirmation = (candidate) => candidate.kind === "worktree" && (candidate.worktree.kind === "external" || candidate.worktree.prunable);
|
|
3187
3208
|
var requiresSwitchRootConfirmation = (candidate) => candidate.kind === "worktree" && candidate.worktree.kind === "external";
|
|
3188
3209
|
|
|
3189
3210
|
// src/ui/side-panel.ts
|
|
@@ -3239,6 +3260,9 @@ var buildConfirmPanelRows = ({
|
|
|
3239
3260
|
if (action === "switchRoot") {
|
|
3240
3261
|
rows.push([{ text: "This will put it into detached HEAD.", style: "dim" }]);
|
|
3241
3262
|
}
|
|
3263
|
+
if (action === "delete" && candidate.kind === "worktree" && candidate.worktree.prunable) {
|
|
3264
|
+
rows.push([{ text: "Prunable, not dirty-checked:", style: "dim" }], [{ text: "removes just its registration.", style: "dim" }]);
|
|
3265
|
+
}
|
|
3242
3266
|
rows.push([{ text: "(y/N)", style: "dim" }]);
|
|
3243
3267
|
return rows;
|
|
3244
3268
|
};
|
|
@@ -3277,6 +3301,131 @@ var footerHintForWidth = (kind, width) => {
|
|
|
3277
3301
|
return truncateToWidth(short, width);
|
|
3278
3302
|
};
|
|
3279
3303
|
|
|
3304
|
+
// src/ui/picker-viewport.ts
|
|
3305
|
+
var DEFAULT_TERMINAL_HEIGHT = 24;
|
|
3306
|
+
var MIN_VISIBLE_CANDIDATE_ROWS = 3;
|
|
3307
|
+
var BASE_CHROME_ROWS = 3;
|
|
3308
|
+
var SECTION_HEADER_ROWS = 2;
|
|
3309
|
+
var HIDDEN_COUNT_INDICATOR_ROWS = 2;
|
|
3310
|
+
var rowBudget = ({ terminalHeight, stackedPanelRows }) => {
|
|
3311
|
+
const reserved = BASE_CHROME_ROWS + SECTION_HEADER_ROWS + HIDDEN_COUNT_INDICATOR_ROWS + stackedPanelRows;
|
|
3312
|
+
return Math.max(MIN_VISIBLE_CANDIDATE_ROWS, terminalHeight - reserved);
|
|
3313
|
+
};
|
|
3314
|
+
var computeViewport = (total, selectedIndex, budget) => {
|
|
3315
|
+
if (total <= budget) {
|
|
3316
|
+
return { start: 0, end: total, hiddenAbove: 0, hiddenBelow: 0 };
|
|
3317
|
+
}
|
|
3318
|
+
const HALF_DIVISOR2 = 2;
|
|
3319
|
+
const half = Math.floor(budget / HALF_DIVISOR2);
|
|
3320
|
+
const maxStart = total - budget;
|
|
3321
|
+
const start = Math.min(maxStart, Math.max(0, selectedIndex - half));
|
|
3322
|
+
const end = start + budget;
|
|
3323
|
+
return { start, end, hiddenAbove: start, hiddenBelow: total - end };
|
|
3324
|
+
};
|
|
3325
|
+
|
|
3326
|
+
// src/ui/picker-render.ts
|
|
3327
|
+
var QUERY_PREFIX = "hop: ";
|
|
3328
|
+
var queryLine = (query, maxWidth) => {
|
|
3329
|
+
const hint = query.length === 0 ? " (type to filter)" : "";
|
|
3330
|
+
const budget = Math.max(0, maxWidth - QUERY_PREFIX.length - hint.length);
|
|
3331
|
+
return [
|
|
3332
|
+
{ text: QUERY_PREFIX },
|
|
3333
|
+
{ text: truncateToWidthKeepingTail(query, budget), style: "cyan" },
|
|
3334
|
+
{ text: hint, style: "dim" }
|
|
3335
|
+
];
|
|
3336
|
+
};
|
|
3337
|
+
var hiddenCountLine = (count, direction) => {
|
|
3338
|
+
if (count === 0) {
|
|
3339
|
+
return null;
|
|
3340
|
+
}
|
|
3341
|
+
const arrow = direction === "above" ? "↑ " : "↓ ";
|
|
3342
|
+
const word = direction === "above" ? "above" : "below";
|
|
3343
|
+
return [
|
|
3344
|
+
{
|
|
3345
|
+
text: `${arrow}${count} more ${word} (keep typing to narrow down)`,
|
|
3346
|
+
style: "dim"
|
|
3347
|
+
}
|
|
3348
|
+
];
|
|
3349
|
+
};
|
|
3350
|
+
var candidateRowLine = (row, selected) => {
|
|
3351
|
+
const marker = selected ? "❯ " : " ";
|
|
3352
|
+
const text = `${marker}${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`;
|
|
3353
|
+
if (selected) {
|
|
3354
|
+
return [{ text, style: "inverse" }];
|
|
3355
|
+
}
|
|
3356
|
+
return [{ text, style: row.section === "branch" ? "dim" : "plain" }];
|
|
3357
|
+
};
|
|
3358
|
+
var displayRowLine = (row, clampedIndex) => row.kind === "header" ? [{ text: row.label, style: "bold-dim" }] : candidateRowLine(row, row.index === clampedIndex);
|
|
3359
|
+
var buildListLines = ({
|
|
3360
|
+
query,
|
|
3361
|
+
maxWidth,
|
|
3362
|
+
rows,
|
|
3363
|
+
clampedIndex,
|
|
3364
|
+
hiddenAbove,
|
|
3365
|
+
hiddenBelow,
|
|
3366
|
+
footerHint
|
|
3367
|
+
}) => {
|
|
3368
|
+
const lines = [queryLine(query, maxWidth)];
|
|
3369
|
+
if (rows.length === 0) {
|
|
3370
|
+
lines.push([{ text: "No matches.", style: "dim" }]);
|
|
3371
|
+
}
|
|
3372
|
+
const above = hiddenCountLine(hiddenAbove, "above");
|
|
3373
|
+
if (above !== null) {
|
|
3374
|
+
lines.push(above);
|
|
3375
|
+
}
|
|
3376
|
+
for (const row of rows) {
|
|
3377
|
+
lines.push(displayRowLine(row, clampedIndex));
|
|
3378
|
+
}
|
|
3379
|
+
const below = hiddenCountLine(hiddenBelow, "below");
|
|
3380
|
+
if (below !== null) {
|
|
3381
|
+
lines.push(below);
|
|
3382
|
+
}
|
|
3383
|
+
lines.push([{ text: `(${LEGEND_TEXT})`, style: "dim" }], [{ text: footerHint, style: "dim" }]);
|
|
3384
|
+
return lines;
|
|
3385
|
+
};
|
|
3386
|
+
var buildSidePanelLines = (mode, panelIndex, busy) => {
|
|
3387
|
+
if (mode.kind === "panel") {
|
|
3388
|
+
return wrapInBox(buildActionPanelRows({
|
|
3389
|
+
candidate: mode.candidate,
|
|
3390
|
+
panelIndex,
|
|
3391
|
+
error: mode.error,
|
|
3392
|
+
busy
|
|
3393
|
+
}), SIDE_PANEL_WIDTH);
|
|
3394
|
+
}
|
|
3395
|
+
if (mode.kind === "confirm") {
|
|
3396
|
+
return wrapInBox(buildConfirmPanelRows({ candidate: mode.candidate, action: mode.action }), SIDE_PANEL_WIDTH);
|
|
3397
|
+
}
|
|
3398
|
+
return null;
|
|
3399
|
+
};
|
|
3400
|
+
var renderPickerFrame = ({
|
|
3401
|
+
snapshot,
|
|
3402
|
+
width,
|
|
3403
|
+
height,
|
|
3404
|
+
colorEnabled: color
|
|
3405
|
+
}) => {
|
|
3406
|
+
const { query, filtered, clampedIndex, mode, panelIndex, busy } = snapshot;
|
|
3407
|
+
const narrow = isNarrowTerminal(width);
|
|
3408
|
+
const right = buildSidePanelLines(mode, panelIndex, busy);
|
|
3409
|
+
const stacked = right !== null && narrow;
|
|
3410
|
+
const listColumnWidth = right !== null && !stacked ? width - GUTTER.length - SIDE_PANEL_WIDTH : width;
|
|
3411
|
+
const stackedPanelRows = stacked && right !== null ? right.length : 0;
|
|
3412
|
+
const budget = rowBudget({ terminalHeight: height, stackedPanelRows });
|
|
3413
|
+
const viewport = computeViewport(filtered.length, clampedIndex, budget);
|
|
3414
|
+
const visible = filtered.slice(viewport.start, viewport.end);
|
|
3415
|
+
const columnWidths = constrainRowColumnWidths(rawBranchColumnWidth(visible), listColumnWidth);
|
|
3416
|
+
const rows = buildDisplayRows(visible, homedir(), viewport.start, columnWidths);
|
|
3417
|
+
const left = buildListLines({
|
|
3418
|
+
query,
|
|
3419
|
+
maxWidth: listColumnWidth,
|
|
3420
|
+
rows,
|
|
3421
|
+
clampedIndex,
|
|
3422
|
+
hiddenAbove: viewport.hiddenAbove,
|
|
3423
|
+
hiddenBelow: viewport.hiddenBelow,
|
|
3424
|
+
footerHint: footerHintForWidth(right === null ? "list" : "panel", listColumnWidth)
|
|
3425
|
+
}).map((line) => truncateLineToWidth(line, listColumnWidth));
|
|
3426
|
+
return buildFrame({ left, right, stacked, colorEnabled: color });
|
|
3427
|
+
};
|
|
3428
|
+
|
|
3280
3429
|
// src/ui/picker-keys.ts
|
|
3281
3430
|
var isCtrlJByte = (input) => input === `
|
|
3282
3431
|
`;
|
|
@@ -3363,7 +3512,7 @@ var handleConfirmInput = (input, key, confirmMode, ctx) => {
|
|
|
3363
3512
|
};
|
|
3364
3513
|
var runOrConfirm = (candidate, action, ctx) => {
|
|
3365
3514
|
if ((action === "delete" || action === "switchRoot") && requiresConfirmation(candidate, action)) {
|
|
3366
|
-
ctx.setMode({ kind: "confirm", action, candidate
|
|
3515
|
+
ctx.setMode({ kind: "confirm", action, candidate });
|
|
3367
3516
|
return;
|
|
3368
3517
|
}
|
|
3369
3518
|
ctx.runAction(candidate, action);
|
|
@@ -3441,7 +3590,7 @@ var handleListInput = (input, key, ctx) => {
|
|
|
3441
3590
|
}
|
|
3442
3591
|
case "openPanel": {
|
|
3443
3592
|
if (ctx.selectedCandidate !== undefined) {
|
|
3444
|
-
ctx.setPanelIndex(0);
|
|
3593
|
+
ctx.setPanelIndex(() => 0);
|
|
3445
3594
|
ctx.setMode({
|
|
3446
3595
|
kind: "panel",
|
|
3447
3596
|
candidate: ctx.selectedCandidate,
|
|
@@ -3455,24 +3604,14 @@ var handleListInput = (input, key, ctx) => {
|
|
|
3455
3604
|
ctx.setMode({
|
|
3456
3605
|
kind: "confirm",
|
|
3457
3606
|
action: "delete",
|
|
3458
|
-
candidate: ctx.selectedCandidate
|
|
3459
|
-
error: null
|
|
3607
|
+
candidate: ctx.selectedCandidate
|
|
3460
3608
|
});
|
|
3461
3609
|
}
|
|
3462
3610
|
break;
|
|
3463
3611
|
}
|
|
3464
3612
|
case "rootSwitchShortcut": {
|
|
3465
3613
|
if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("switchRoot")) {
|
|
3466
|
-
|
|
3467
|
-
ctx.setMode({
|
|
3468
|
-
kind: "confirm",
|
|
3469
|
-
action: "switchRoot",
|
|
3470
|
-
candidate: ctx.selectedCandidate,
|
|
3471
|
-
error: null
|
|
3472
|
-
});
|
|
3473
|
-
} else {
|
|
3474
|
-
ctx.runAction(ctx.selectedCandidate, "switchRoot");
|
|
3475
|
-
}
|
|
3614
|
+
runOrConfirm(ctx.selectedCandidate, "switchRoot", ctx);
|
|
3476
3615
|
}
|
|
3477
3616
|
break;
|
|
3478
3617
|
}
|
|
@@ -3503,20 +3642,18 @@ var runMutation = async (callbacks, candidate, action) => {
|
|
|
3503
3642
|
if (action === "switchRoot") {
|
|
3504
3643
|
const result2 = await callbacks.switchRootHere(candidate);
|
|
3505
3644
|
if (!result2.ok || result2.path === undefined) {
|
|
3506
|
-
const transition = panelErrorTransition(candidate, result2.message ?? "Failed to switch root.");
|
|
3507
3645
|
return {
|
|
3508
3646
|
type: "patch",
|
|
3509
|
-
patch:
|
|
3647
|
+
patch: panelErrorTransition(candidate, result2.message ?? "Failed to switch root.")
|
|
3510
3648
|
};
|
|
3511
3649
|
}
|
|
3512
3650
|
return { type: "exit", outcome: { type: "path", path: result2.path } };
|
|
3513
3651
|
}
|
|
3514
3652
|
const result = await callbacks.deleteWorktree(candidate);
|
|
3515
3653
|
if (!result.ok) {
|
|
3516
|
-
const transition = panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.");
|
|
3517
3654
|
return {
|
|
3518
3655
|
type: "patch",
|
|
3519
|
-
patch:
|
|
3656
|
+
patch: panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.")
|
|
3520
3657
|
};
|
|
3521
3658
|
}
|
|
3522
3659
|
const fresh = await callbacks.reloadCandidates();
|
|
@@ -3602,10 +3739,7 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3602
3739
|
const setQuery = (updater) => {
|
|
3603
3740
|
setState({ query: updater(state.query) });
|
|
3604
3741
|
};
|
|
3605
|
-
const
|
|
3606
|
-
setState({ panelIndex: value });
|
|
3607
|
-
};
|
|
3608
|
-
const setPanelIndexByUpdater = (updater) => {
|
|
3742
|
+
const setPanelIndex = (updater) => {
|
|
3609
3743
|
setState({ panelIndex: updater(state.panelIndex) });
|
|
3610
3744
|
};
|
|
3611
3745
|
const setMode = (mode) => {
|
|
@@ -3624,13 +3758,12 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3624
3758
|
handlePanelInput(input, key, mode, {
|
|
3625
3759
|
panelIndex,
|
|
3626
3760
|
runAction,
|
|
3627
|
-
setPanelIndex
|
|
3761
|
+
setPanelIndex,
|
|
3628
3762
|
setMode
|
|
3629
3763
|
});
|
|
3630
3764
|
return;
|
|
3631
3765
|
}
|
|
3632
|
-
const filtered =
|
|
3633
|
-
const clampedIndex = Math.min(state.index, Math.max(filtered.length - 1, 0));
|
|
3766
|
+
const { filtered, clampedIndex } = getSnapshot();
|
|
3634
3767
|
handleListInput(input, key, {
|
|
3635
3768
|
selectedCandidate: filtered[clampedIndex],
|
|
3636
3769
|
filteredLength: filtered.length,
|
|
@@ -3638,7 +3771,7 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3638
3771
|
onCancel: guardedOnCancel,
|
|
3639
3772
|
setIndex,
|
|
3640
3773
|
setQuery,
|
|
3641
|
-
setPanelIndex
|
|
3774
|
+
setPanelIndex,
|
|
3642
3775
|
setMode
|
|
3643
3776
|
});
|
|
3644
3777
|
};
|
|
@@ -3652,12 +3785,10 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3652
3785
|
};
|
|
3653
3786
|
|
|
3654
3787
|
// src/ui/alt-screen.ts
|
|
3655
|
-
var
|
|
3656
|
-
var
|
|
3657
|
-
var
|
|
3658
|
-
var
|
|
3659
|
-
var HIDE_CURSOR = `${ESCAPE}[?25l`;
|
|
3660
|
-
var SHOW_CURSOR = `${ESCAPE}[?25h`;
|
|
3788
|
+
var ENTER_ALT_SCREEN = `${ESC}[?1049h`;
|
|
3789
|
+
var LEAVE_ALT_SCREEN = `${ESC}[?1049l`;
|
|
3790
|
+
var HIDE_CURSOR = `${ESC}[?25l`;
|
|
3791
|
+
var SHOW_CURSOR = `${ESC}[?25h`;
|
|
3661
3792
|
var enterAltScreen = (target) => {
|
|
3662
3793
|
if (!target.isTTY) {
|
|
3663
3794
|
return;
|
|
@@ -3848,6 +3979,10 @@ var pastedTextToEvents = (text) => [...text].filter((char) => isPrintableChar(ch
|
|
|
3848
3979
|
var CSI_INTRODUCER_CODE = "[".codePointAt(0);
|
|
3849
3980
|
var SS3_INTRODUCER_CODE = "O".codePointAt(0);
|
|
3850
3981
|
var MAX_PENDING_SEQUENCE_BYTES = 32;
|
|
3982
|
+
var PASTE_END_MARKER = Buffer.concat([
|
|
3983
|
+
Buffer.from([ESC2]),
|
|
3984
|
+
Buffer.from(`[${BRACKETED_PASTE_END}`)
|
|
3985
|
+
]);
|
|
3851
3986
|
|
|
3852
3987
|
class PickerKeyParser {
|
|
3853
3988
|
buffer = Buffer.alloc(0);
|
|
@@ -3876,19 +4011,18 @@ class PickerKeyParser {
|
|
|
3876
4011
|
if (this.buffer.length === 0) {
|
|
3877
4012
|
return null;
|
|
3878
4013
|
}
|
|
3879
|
-
const
|
|
3880
|
-
const markerIndex = this.buffer.indexOf(endMarker);
|
|
4014
|
+
const markerIndex = this.buffer.indexOf(PASTE_END_MARKER);
|
|
3881
4015
|
if (markerIndex !== -1) {
|
|
3882
4016
|
const text2 = decodeUtf8(this.buffer.subarray(0, markerIndex));
|
|
3883
|
-
this.buffer = this.buffer.subarray(markerIndex +
|
|
4017
|
+
this.buffer = this.buffer.subarray(markerIndex + PASTE_END_MARKER.length);
|
|
3884
4018
|
this.inPaste = false;
|
|
3885
4019
|
return pastedTextToEvents(text2);
|
|
3886
4020
|
}
|
|
3887
|
-
const maxOverlap = Math.min(
|
|
4021
|
+
const maxOverlap = Math.min(PASTE_END_MARKER.length - 1, this.buffer.length);
|
|
3888
4022
|
let overlap = 0;
|
|
3889
4023
|
for (let candidate = maxOverlap;candidate > 0; candidate -= 1) {
|
|
3890
4024
|
const tail = this.buffer.subarray(this.buffer.length - candidate);
|
|
3891
|
-
if (tail.equals(
|
|
4025
|
+
if (tail.equals(PASTE_END_MARKER.subarray(0, candidate))) {
|
|
3892
4026
|
overlap = candidate;
|
|
3893
4027
|
break;
|
|
3894
4028
|
}
|
|
@@ -3985,9 +4119,8 @@ class PickerKeyParser {
|
|
|
3985
4119
|
}
|
|
3986
4120
|
|
|
3987
4121
|
// src/ui/terminal-session.ts
|
|
3988
|
-
var
|
|
3989
|
-
var
|
|
3990
|
-
var DISABLE_BRACKETED_PASTE = `${ESC3}[?2004l`;
|
|
4122
|
+
var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
|
|
4123
|
+
var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
|
|
3991
4124
|
var PENDING_ESCAPE_TIMEOUT_MS = 25;
|
|
3992
4125
|
var EXIT_SIGTERM = 143;
|
|
3993
4126
|
var EXIT_SIGHUP = 129;
|
|
@@ -4152,107 +4285,6 @@ var runTerminalSession = (createHandlers, streams = defaultStreams()) => new Pro
|
|
|
4152
4285
|
var DEFAULT_TERMINAL_WIDTH = 80;
|
|
4153
4286
|
var terminalDimension = (value, fallback) => value !== undefined && value > 0 ? value : fallback;
|
|
4154
4287
|
var colorEnabled = (stderr) => stderr.isTTY === true && (process.env["NO_COLOR"] ?? "") === "";
|
|
4155
|
-
var QUERY_PREFIX = "hop: ";
|
|
4156
|
-
var queryLine = (query, maxWidth) => {
|
|
4157
|
-
const hint = query.length === 0 ? " (type to filter)" : "";
|
|
4158
|
-
const budget = Math.max(0, maxWidth - QUERY_PREFIX.length - hint.length);
|
|
4159
|
-
return [
|
|
4160
|
-
{ text: QUERY_PREFIX },
|
|
4161
|
-
{ text: truncateToWidthKeepingTail(query, budget), style: "cyan" },
|
|
4162
|
-
{ text: hint, style: "dim" }
|
|
4163
|
-
];
|
|
4164
|
-
};
|
|
4165
|
-
var hiddenCountLine = (count, direction) => {
|
|
4166
|
-
if (count === 0) {
|
|
4167
|
-
return null;
|
|
4168
|
-
}
|
|
4169
|
-
const arrow = direction === "above" ? "↑ " : "↓ ";
|
|
4170
|
-
const word = direction === "above" ? "above" : "below";
|
|
4171
|
-
return [
|
|
4172
|
-
{
|
|
4173
|
-
text: `${arrow}${count} more ${word} (keep typing to narrow down)`,
|
|
4174
|
-
style: "dim"
|
|
4175
|
-
}
|
|
4176
|
-
];
|
|
4177
|
-
};
|
|
4178
|
-
var candidateRowLine = (row, selected) => {
|
|
4179
|
-
const marker = selected ? "❯ " : " ";
|
|
4180
|
-
const text = `${marker}${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`;
|
|
4181
|
-
if (selected) {
|
|
4182
|
-
return [{ text, style: "inverse" }];
|
|
4183
|
-
}
|
|
4184
|
-
return [{ text, style: row.section === "branch" ? "dim" : "plain" }];
|
|
4185
|
-
};
|
|
4186
|
-
var displayRowLine = (row, clampedIndex) => row.kind === "header" ? [{ text: row.label, style: "bold-dim" }] : candidateRowLine(row, row.index === clampedIndex);
|
|
4187
|
-
var buildListLines = ({
|
|
4188
|
-
query,
|
|
4189
|
-
maxWidth,
|
|
4190
|
-
rows,
|
|
4191
|
-
clampedIndex,
|
|
4192
|
-
hiddenAbove,
|
|
4193
|
-
hiddenBelow,
|
|
4194
|
-
footerHint
|
|
4195
|
-
}) => {
|
|
4196
|
-
const lines = [queryLine(query, maxWidth)];
|
|
4197
|
-
if (rows.length === 0) {
|
|
4198
|
-
lines.push([{ text: "No matches.", style: "dim" }]);
|
|
4199
|
-
}
|
|
4200
|
-
const above = hiddenCountLine(hiddenAbove, "above");
|
|
4201
|
-
if (above !== null) {
|
|
4202
|
-
lines.push(above);
|
|
4203
|
-
}
|
|
4204
|
-
for (const row of rows) {
|
|
4205
|
-
lines.push(displayRowLine(row, clampedIndex));
|
|
4206
|
-
}
|
|
4207
|
-
const below = hiddenCountLine(hiddenBelow, "below");
|
|
4208
|
-
if (below !== null) {
|
|
4209
|
-
lines.push(below);
|
|
4210
|
-
}
|
|
4211
|
-
lines.push([{ text: `(${LEGEND_TEXT})`, style: "dim" }], [{ text: footerHint, style: "dim" }]);
|
|
4212
|
-
return lines;
|
|
4213
|
-
};
|
|
4214
|
-
var buildSidePanelLines = (mode, panelIndex, busy) => {
|
|
4215
|
-
if (mode.kind === "panel") {
|
|
4216
|
-
return wrapInBox(buildActionPanelRows({
|
|
4217
|
-
candidate: mode.candidate,
|
|
4218
|
-
panelIndex,
|
|
4219
|
-
error: mode.error,
|
|
4220
|
-
busy
|
|
4221
|
-
}), SIDE_PANEL_WIDTH);
|
|
4222
|
-
}
|
|
4223
|
-
if (mode.kind === "confirm") {
|
|
4224
|
-
return wrapInBox(buildConfirmPanelRows({ candidate: mode.candidate, action: mode.action }), SIDE_PANEL_WIDTH);
|
|
4225
|
-
}
|
|
4226
|
-
return null;
|
|
4227
|
-
};
|
|
4228
|
-
var renderPickerFrame = ({
|
|
4229
|
-
snapshot,
|
|
4230
|
-
width,
|
|
4231
|
-
height,
|
|
4232
|
-
colorEnabled: color
|
|
4233
|
-
}) => {
|
|
4234
|
-
const { query, filtered, clampedIndex, mode, panelIndex, busy } = snapshot;
|
|
4235
|
-
const narrow = isNarrowTerminal(width);
|
|
4236
|
-
const right = buildSidePanelLines(mode, panelIndex, busy);
|
|
4237
|
-
const stacked = right !== null && narrow;
|
|
4238
|
-
const listColumnWidth = right !== null && !stacked ? width - GUTTER.length - SIDE_PANEL_WIDTH : width;
|
|
4239
|
-
const stackedPanelRows = stacked && right !== null ? right.length : 0;
|
|
4240
|
-
const budget = rowBudget({ terminalHeight: height, stackedPanelRows });
|
|
4241
|
-
const viewport = computeViewport(filtered.length, clampedIndex, budget);
|
|
4242
|
-
const visible = filtered.slice(viewport.start, viewport.end);
|
|
4243
|
-
const columnWidths = constrainRowColumnWidths(rawBranchColumnWidth(visible), listColumnWidth);
|
|
4244
|
-
const rows = buildDisplayRows(visible, homedir(), viewport.start, columnWidths);
|
|
4245
|
-
const left = buildListLines({
|
|
4246
|
-
query,
|
|
4247
|
-
maxWidth: listColumnWidth,
|
|
4248
|
-
rows,
|
|
4249
|
-
clampedIndex,
|
|
4250
|
-
hiddenAbove: viewport.hiddenAbove,
|
|
4251
|
-
hiddenBelow: viewport.hiddenBelow,
|
|
4252
|
-
footerHint: footerHintForWidth(right === null ? "list" : "panel", listColumnWidth)
|
|
4253
|
-
}).map((line) => truncateLineToWidth(line, listColumnWidth));
|
|
4254
|
-
return buildFrame({ left, right, stacked, colorEnabled: color });
|
|
4255
|
-
};
|
|
4256
4288
|
var isCtrlC = (event2) => event2.key.ctrl && event2.input === "c";
|
|
4257
4289
|
var runPicker = (candidates, callbacks) => runTerminalSession(({ requestRender, finish }) => {
|
|
4258
4290
|
const store = createPickerStore(candidates, callbacks, (outcome) => finish(outcome), (reason) => finish({ type: "cancelled", reason }));
|
|
@@ -4278,8 +4310,7 @@ var runPicker = (candidates, callbacks) => runTerminalSession(({ requestRender,
|
|
|
4278
4310
|
var loadPickCandidates = async (git, fs, json) => {
|
|
4279
4311
|
const pickResult = await pick(git, fs, { cwd: process.cwd() });
|
|
4280
4312
|
if (!pickResult.ok) {
|
|
4281
|
-
|
|
4282
|
-
process.exitCode = pickResult.exitCode;
|
|
4313
|
+
reportResult("pick", pickResult, json);
|
|
4283
4314
|
return null;
|
|
4284
4315
|
}
|
|
4285
4316
|
return pickResult.data?.candidates ?? [];
|
|
@@ -4293,6 +4324,7 @@ var deleteWorktree = async (git, fs, candidate) => {
|
|
|
4293
4324
|
cwd: process.cwd(),
|
|
4294
4325
|
branch,
|
|
4295
4326
|
...candidate.kind === "worktree" ? { expectedPath: candidate.worktree.path } : {},
|
|
4327
|
+
allowPrunable: candidate.kind === "worktree" && candidate.worktree.prunable,
|
|
4296
4328
|
force: false,
|
|
4297
4329
|
ext: false
|
|
4298
4330
|
});
|
|
@@ -4343,15 +4375,969 @@ var renderSwitchRootOutcome = (path, outcome, json) => {
|
|
|
4343
4375
|
const branch = outcome?.branch ?? null;
|
|
4344
4376
|
const detachedHolder = outcome?.detachedHolder ?? null;
|
|
4345
4377
|
const warnings = outcome?.warnings ?? [];
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4378
|
+
render("root", ok({ path, data: { branch, switched: true, detachedHolder }, warnings }), json);
|
|
4379
|
+
};
|
|
4380
|
+
|
|
4381
|
+
// src/domain/install-roots.ts
|
|
4382
|
+
var MANAGER_ROOTS = [
|
|
4383
|
+
{
|
|
4384
|
+
markers: ["/Cellar/", "/Caskroom/"],
|
|
4385
|
+
manager: "Homebrew",
|
|
4386
|
+
update: '"brew upgrade"',
|
|
4387
|
+
ownsNpmPrefix: true
|
|
4388
|
+
},
|
|
4389
|
+
{
|
|
4390
|
+
markers: ["/nix/store/"],
|
|
4391
|
+
manager: "Nix",
|
|
4392
|
+
update: "Nix (the store is read-only)",
|
|
4393
|
+
ownsNpmPrefix: true
|
|
4394
|
+
},
|
|
4395
|
+
{
|
|
4396
|
+
markers: ["/aquaproj-aqua/", "/aqua/pkgs/"],
|
|
4397
|
+
manager: "aqua",
|
|
4398
|
+
update: "aqua",
|
|
4399
|
+
ownsNpmPrefix: false
|
|
4400
|
+
},
|
|
4401
|
+
{ markers: ["/.proto/tools/"], manager: "proto", update: "proto", ownsNpmPrefix: false }
|
|
4402
|
+
];
|
|
4403
|
+
var reasonWithin = (path, roots) => {
|
|
4404
|
+
const root2 = roots.find(({ markers }) => markers.some((marker) => path.includes(marker)));
|
|
4405
|
+
return root2 === undefined ? null : `installed by ${root2.manager} (${path}); update it with ${root2.update}`;
|
|
4406
|
+
};
|
|
4407
|
+
var managerRootReason = (binaryPath) => reasonWithin(binaryPath, MANAGER_ROOTS);
|
|
4408
|
+
var managerNpmPrefixReason = (prefix) => reasonWithin(prefix, MANAGER_ROOTS.filter(({ ownsNpmPrefix }) => ownsNpmPrefix));
|
|
4409
|
+
|
|
4410
|
+
// src/domain/self-update.ts
|
|
4411
|
+
var NPM_PACKAGE = "@n-seiji/nuthatch";
|
|
4412
|
+
var NPM_PACKAGE_LATEST = `${NPM_PACKAGE}@latest`;
|
|
4413
|
+
var GITHUB_REPO = "n-seiji/nuthatch";
|
|
4414
|
+
var GITHUB_LATEST_RELEASE_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
4415
|
+
var NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE.replace("/", "%2F")}/latest`;
|
|
4416
|
+
var releaseAssetUrl = (version, assetName) => `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${assetName}`;
|
|
4417
|
+
var VERSION_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$/u;
|
|
4418
|
+
var TAG_PATTERN = /^v(?<version>\d+\.\d+\.\d+)$/u;
|
|
4419
|
+
var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/iu;
|
|
4420
|
+
var PATH_SEPARATOR2 = ":";
|
|
4421
|
+
var parseVersion = (text) => {
|
|
4422
|
+
const groups = VERSION_PATTERN.exec(text)?.groups;
|
|
4423
|
+
if (groups?.major === undefined || groups.minor === undefined || groups.patch === undefined) {
|
|
4424
|
+
throw new Error(`unrecognized version "${text}" (expected x.y.z)`);
|
|
4425
|
+
}
|
|
4426
|
+
return [Number(groups.major), Number(groups.minor), Number(groups.patch)];
|
|
4427
|
+
};
|
|
4428
|
+
var compareVersions = (a2, b2) => {
|
|
4429
|
+
const [aMajor, aMinor, aPatch] = parseVersion(a2);
|
|
4430
|
+
const [bMajor, bMinor, bPatch] = parseVersion(b2);
|
|
4431
|
+
return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
|
|
4432
|
+
};
|
|
4433
|
+
var requireVersion = (text) => {
|
|
4434
|
+
parseVersion(text);
|
|
4435
|
+
return text;
|
|
4436
|
+
};
|
|
4437
|
+
var versionFromTag = (tag) => {
|
|
4438
|
+
const version = TAG_PATTERN.exec(tag)?.groups?.version;
|
|
4439
|
+
if (version === undefined) {
|
|
4440
|
+
throw new Error(`unrecognized release tag "${tag}" (expected vX.Y.Z)`);
|
|
4441
|
+
}
|
|
4442
|
+
return version;
|
|
4443
|
+
};
|
|
4444
|
+
var parseChecksumFile = (text, assetName) => {
|
|
4445
|
+
const [digest, file] = text.trim().split(/\s+/u);
|
|
4446
|
+
if (digest === undefined || !SHA256_HEX_PATTERN.test(digest)) {
|
|
4447
|
+
return { ok: false, reason: "is malformed" };
|
|
4448
|
+
}
|
|
4449
|
+
const named = file?.replace(/^\*/u, "");
|
|
4450
|
+
if (named !== undefined && named !== assetName && !named.endsWith(`/${assetName}`)) {
|
|
4451
|
+
return { ok: false, reason: `is for "${named}", not ${assetName}` };
|
|
4452
|
+
}
|
|
4453
|
+
return { ok: true, digest: digest.toLowerCase() };
|
|
4454
|
+
};
|
|
4455
|
+
var pathExecutableCandidates = (name, pathEnv, preferredDir = null) => {
|
|
4456
|
+
const dirs = [
|
|
4457
|
+
...preferredDir === null ? [] : [preferredDir],
|
|
4458
|
+
...(pathEnv ?? "").split(PATH_SEPARATOR2)
|
|
4459
|
+
].map((entry) => entry.trim()).filter((entry) => entry.startsWith("/"));
|
|
4460
|
+
return [...new Set(dirs.map((dir) => `${dir.endsWith("/") ? dir.slice(0, -1) : dir}/${name}`))];
|
|
4461
|
+
};
|
|
4462
|
+
var isWriteDenied = (error) => error instanceof Error && ("code" in error) && (error.code === "EACCES" || error.code === "EPERM" || error.code === "EROFS");
|
|
4463
|
+
|
|
4464
|
+
// src/domain/install-script.ts
|
|
4465
|
+
var NPX_MARKER = "/_npx/";
|
|
4466
|
+
var BUNX_MARKER = "/bunx-";
|
|
4467
|
+
var BUN_GLOBAL_MARKER = "/.bun/install/global/node_modules/";
|
|
4468
|
+
var NPM_GLOBAL_MARKER = `/lib/node_modules/${NPM_PACKAGE}/`;
|
|
4469
|
+
var NODE_MODULES_MARKER = `/node_modules/${NPM_PACKAGE}/`;
|
|
4470
|
+
var PNPM_GLOBAL_MARKER = "/pnpm/global/";
|
|
4471
|
+
var YARN_GLOBAL_MARKER = "/yarn/global/";
|
|
4472
|
+
var unsupported = (reason) => ({ kind: "unsupported", reason });
|
|
4473
|
+
var npmGlobalPrefixOf = (scriptPath) => {
|
|
4474
|
+
const index = scriptPath.lastIndexOf(NPM_GLOBAL_MARKER);
|
|
4475
|
+
return index > 0 ? scriptPath.slice(0, index) : null;
|
|
4476
|
+
};
|
|
4477
|
+
var otherNodeModulesReason = (scriptPath) => {
|
|
4478
|
+
if (scriptPath.includes(PNPM_GLOBAL_MARKER)) {
|
|
4479
|
+
return `installed globally with pnpm, which hop does not drive; run "pnpm add -g ${NPM_PACKAGE_LATEST}" yourself`;
|
|
4349
4480
|
}
|
|
4350
|
-
|
|
4351
|
-
`
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4481
|
+
if (scriptPath.includes(YARN_GLOBAL_MARKER)) {
|
|
4482
|
+
return `installed globally with yarn, which hop does not drive; run "yarn global add ${NPM_PACKAGE_LATEST}" yourself`;
|
|
4483
|
+
}
|
|
4484
|
+
return "installed as a project dependency (a local node_modules); update it in that project";
|
|
4485
|
+
};
|
|
4486
|
+
var detectScriptInstall = (scriptPath, npmGlobalPrefix) => {
|
|
4487
|
+
if (scriptPath.includes(NPX_MARKER)) {
|
|
4488
|
+
return unsupported("running through npx, which already fetches the published version on every run");
|
|
4489
|
+
}
|
|
4490
|
+
if (scriptPath.includes(BUNX_MARKER)) {
|
|
4491
|
+
return unsupported("running through bunx, which already fetches the published version on every run");
|
|
4492
|
+
}
|
|
4493
|
+
if (scriptPath.includes(BUN_GLOBAL_MARKER)) {
|
|
4494
|
+
return { kind: "bun" };
|
|
4495
|
+
}
|
|
4496
|
+
if (npmGlobalPrefix !== null) {
|
|
4497
|
+
const managedBy = managerNpmPrefixReason(npmGlobalPrefix);
|
|
4498
|
+
return managedBy === null ? { kind: "npm", prefix: npmGlobalPrefix } : unsupported(managedBy);
|
|
4499
|
+
}
|
|
4500
|
+
if (scriptPath.includes(NODE_MODULES_MARKER)) {
|
|
4501
|
+
return unsupported(otherNodeModulesReason(scriptPath));
|
|
4502
|
+
}
|
|
4503
|
+
return unsupported("running from a source checkout; update it with git (git pull)");
|
|
4504
|
+
};
|
|
4505
|
+
|
|
4506
|
+
// src/domain/posix-path.ts
|
|
4507
|
+
var withoutTrailingSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
4508
|
+
var parentDir = (path) => {
|
|
4509
|
+
const trimmed = withoutTrailingSlash(path);
|
|
4510
|
+
const index = trimmed.lastIndexOf("/");
|
|
4511
|
+
if (index === -1) {
|
|
4512
|
+
return ".";
|
|
4513
|
+
}
|
|
4514
|
+
return index === 0 ? "/" : trimmed.slice(0, index);
|
|
4515
|
+
};
|
|
4516
|
+
var baseName = (path) => {
|
|
4517
|
+
const trimmed = withoutTrailingSlash(path);
|
|
4518
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
4519
|
+
};
|
|
4520
|
+
|
|
4521
|
+
// src/domain/mise-tool.ts
|
|
4522
|
+
var MISE_WALK_LIMIT = 8;
|
|
4523
|
+
var HOP_BACKEND_PATTERN = /^[a-z0-9_-]+:@?n-seiji\/nuthatch$/iu;
|
|
4524
|
+
var HOP_TOOL_DIR_PATTERN = /(?:^|-)n-seiji-nuthatch$/iu;
|
|
4525
|
+
var parseMiseBackendToml = (text) => {
|
|
4526
|
+
const match = /^\s*full\s*=\s*"(?<tool>[^"]+)"\s*$/mu.exec(text);
|
|
4527
|
+
return match?.groups?.tool ?? null;
|
|
4528
|
+
};
|
|
4529
|
+
var parseLegacyMiseBackend = (text) => {
|
|
4530
|
+
const lines = text.split(`
|
|
4531
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4532
|
+
return lines.at(-1) ?? null;
|
|
4533
|
+
};
|
|
4534
|
+
var withoutToolOptions = (backend) => backend.replace(/\[[^\]]*\]$/u, "");
|
|
4535
|
+
var isHopBackend = (backend) => HOP_BACKEND_PATTERN.test(backend);
|
|
4536
|
+
var isHopToolDirName = (name) => HOP_TOOL_DIR_PATTERN.test(name);
|
|
4537
|
+
var ancestorDirs = (path, limit) => {
|
|
4538
|
+
const dirs = [];
|
|
4539
|
+
let dir = parentDir(path);
|
|
4540
|
+
while (dirs.length < limit && dir !== "/" && dir !== ".") {
|
|
4541
|
+
dirs.push(dir);
|
|
4542
|
+
dir = parentDir(dir);
|
|
4543
|
+
}
|
|
4544
|
+
return dirs;
|
|
4545
|
+
};
|
|
4546
|
+
var miseToolDir = (path) => ancestorDirs(path, MISE_WALK_LIMIT).find((dir) => baseName(parentDir(dir)) === "installs") ?? null;
|
|
4547
|
+
|
|
4548
|
+
// src/domain/install-method.ts
|
|
4549
|
+
var RELEASE_ASSETS = new Map([
|
|
4550
|
+
["darwin-arm64", "hop-darwin-arm64"],
|
|
4551
|
+
["darwin-x64", "hop-darwin-x64"],
|
|
4552
|
+
["linux-x64", "hop-linux-x64"]
|
|
4553
|
+
]);
|
|
4554
|
+
var releaseAssetName = (platform2, arch) => RELEASE_ASSETS.get(`${platform2}-${arch}`) ?? null;
|
|
4555
|
+
var GITHUB_BACKEND_PREFIXES = ["github:", "ubi:", "aqua:"];
|
|
4556
|
+
var NPM_BACKEND_PREFIX = "npm:";
|
|
4557
|
+
var unsupported2 = (reason) => ({
|
|
4558
|
+
kind: "unsupported",
|
|
4559
|
+
reason
|
|
4560
|
+
});
|
|
4561
|
+
var miseChannel = (tool) => {
|
|
4562
|
+
if (tool.startsWith(NPM_BACKEND_PREFIX)) {
|
|
4563
|
+
return "npm";
|
|
4564
|
+
}
|
|
4565
|
+
return GITHUB_BACKEND_PREFIXES.some((prefix) => tool.startsWith(prefix)) ? "github" : null;
|
|
4566
|
+
};
|
|
4567
|
+
var insideVersionManager = (dir, marker) => unsupported2(`installed under a version manager's install dir (${dir}) ${marker}; update it with the tool that installed it`);
|
|
4568
|
+
var detectUnmarkedToolDir = (dir, compiled) => {
|
|
4569
|
+
if (isHopToolDirName(baseName(dir))) {
|
|
4570
|
+
return unsupported2(`installed by mise, but its backend is unknown (no readable marker in ${dir}); run "mise upgrade" yourself`);
|
|
4571
|
+
}
|
|
4572
|
+
return compiled ? insideVersionManager(dir, "without a readable mise marker") : null;
|
|
4573
|
+
};
|
|
4574
|
+
var detectMise = ({ mise, compiled }) => {
|
|
4575
|
+
if (mise === null) {
|
|
4576
|
+
return null;
|
|
4577
|
+
}
|
|
4578
|
+
if (mise.backend === null) {
|
|
4579
|
+
return detectUnmarkedToolDir(mise.dir, compiled);
|
|
4580
|
+
}
|
|
4581
|
+
const tool = withoutToolOptions(mise.backend);
|
|
4582
|
+
if (!isHopBackend(tool)) {
|
|
4583
|
+
return compiled ? insideVersionManager(mise.dir, `whose marker names "${tool}", not hop`) : null;
|
|
4584
|
+
}
|
|
4585
|
+
const channel = miseChannel(tool);
|
|
4586
|
+
return channel === null ? unsupported2(`mise installed hop through "${tool}", which is neither a GitHub nor an npm backend; run "mise upgrade ${tool}" yourself`) : { kind: "mise", tool, channel };
|
|
4587
|
+
};
|
|
4588
|
+
var detectStandalone = (facts) => {
|
|
4589
|
+
const { executablePath, platform: platform2, arch } = facts;
|
|
4590
|
+
if (executablePath === null) {
|
|
4591
|
+
return unsupported2("cannot tell where the hop binary is installed");
|
|
4592
|
+
}
|
|
4593
|
+
const managedBy = managerRootReason(executablePath);
|
|
4594
|
+
if (managedBy !== null) {
|
|
4595
|
+
return unsupported2(managedBy);
|
|
4596
|
+
}
|
|
4597
|
+
const assetName = releaseAssetName(platform2, arch);
|
|
4598
|
+
if (assetName === null) {
|
|
4599
|
+
return unsupported2(`there is no prebuilt binary for ${platform2}-${arch}; install with npm instead (npm i -g ${NPM_PACKAGE})`);
|
|
4600
|
+
}
|
|
4601
|
+
return {
|
|
4602
|
+
kind: "standalone",
|
|
4603
|
+
binaryPath: executablePath,
|
|
4604
|
+
installDir: parentDir(executablePath),
|
|
4605
|
+
assetName
|
|
4606
|
+
};
|
|
4607
|
+
};
|
|
4608
|
+
var detectInstallMethod = (facts) => {
|
|
4609
|
+
const mise = detectMise(facts);
|
|
4610
|
+
if (mise !== null) {
|
|
4611
|
+
return mise;
|
|
4612
|
+
}
|
|
4613
|
+
return facts.compiled ? detectStandalone(facts) : detectScriptInstall(facts.scriptPath ?? "", facts.npmGlobalPrefix);
|
|
4614
|
+
};
|
|
4615
|
+
var latestChannel = (method) => {
|
|
4616
|
+
if (method.kind === "mise") {
|
|
4617
|
+
return method.channel;
|
|
4618
|
+
}
|
|
4619
|
+
return method.kind === "standalone" ? "github" : "npm";
|
|
4620
|
+
};
|
|
4621
|
+
var upgradeCommand = (method) => {
|
|
4622
|
+
if (method.kind === "mise") {
|
|
4623
|
+
return ["mise", "upgrade", method.tool];
|
|
4624
|
+
}
|
|
4625
|
+
return method.kind === "npm" ? ["npm", "install", "-g", "--prefix", method.prefix, NPM_PACKAGE_LATEST] : ["bun", "add", "-g", NPM_PACKAGE_LATEST];
|
|
4626
|
+
};
|
|
4627
|
+
var miseDryRunCommand = (method) => [
|
|
4628
|
+
"mise",
|
|
4629
|
+
"upgrade",
|
|
4630
|
+
"--dry-run-code",
|
|
4631
|
+
method.tool
|
|
4632
|
+
];
|
|
4633
|
+
var preferredProgramDir = (method) => method.kind === "npm" ? `${method.prefix}/bin` : null;
|
|
4634
|
+
|
|
4635
|
+
// src/commands/self-update-attempt.ts
|
|
4636
|
+
var attempt = async (run) => {
|
|
4637
|
+
try {
|
|
4638
|
+
return { ok: true, value: await run() };
|
|
4639
|
+
} catch (error) {
|
|
4640
|
+
return { ok: false, error };
|
|
4641
|
+
}
|
|
4642
|
+
};
|
|
4643
|
+
|
|
4644
|
+
// src/commands/self-update-delegated.ts
|
|
4645
|
+
var MISE_WOULD_NOT_UPGRADE = 0;
|
|
4646
|
+
var runLogged = (port, term, executable, [, ...args]) => {
|
|
4647
|
+
const command = [executable, ...args];
|
|
4648
|
+
term.logStderr(`Running: ${command.join(" ")}`);
|
|
4649
|
+
return attempt(() => port.runCommand(command));
|
|
4650
|
+
};
|
|
4651
|
+
var miseNothingToUpgradeWarning = ({ tool }, { current, latest }) => [
|
|
4652
|
+
`mise has nothing to upgrade for ${tool}, so hop stays at ${current} although ${latest} is the latest release.`,
|
|
4653
|
+
"Likely causes: the version is pinned in your global mise config, a minimum_release_age setting holds the release back,",
|
|
4654
|
+
`or ${tool} is not in your global mise config (hop runs mise from your home directory, so a project's mise.toml does not count).`,
|
|
4655
|
+
`Run "mise upgrade --dry-run ${tool}" from your home directory to see what mise decides.`
|
|
4656
|
+
].join(" ");
|
|
4657
|
+
var runDelegated = async (port, term, method, base) => {
|
|
4658
|
+
const argv2 = upgradeCommand(method);
|
|
4659
|
+
const [program] = argv2;
|
|
4660
|
+
const command = argv2.join(" ");
|
|
4661
|
+
const executable = await port.resolveExecutable(program, preferredProgramDir(method));
|
|
4662
|
+
if (executable === null) {
|
|
4663
|
+
return fail(EXIT_GENERAL_ERROR, `Cannot find "${program}" on PATH. Run it yourself: ${command}`);
|
|
4664
|
+
}
|
|
4665
|
+
if (method.kind === "mise") {
|
|
4666
|
+
const question = miseDryRunCommand(method);
|
|
4667
|
+
const answer = await runLogged(port, term, executable, question);
|
|
4668
|
+
if (!answer.ok) {
|
|
4669
|
+
return fail(EXIT_GENERAL_ERROR, `Failed to run ${question.join(" ")}: ${messageOf(answer.error)}`);
|
|
4670
|
+
}
|
|
4671
|
+
if (answer.value === MISE_WOULD_NOT_UPGRADE) {
|
|
4672
|
+
return ok({ data: base, warnings: [miseNothingToUpgradeWarning(method, base)] });
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
const run = await runLogged(port, term, executable, argv2);
|
|
4676
|
+
if (!run.ok) {
|
|
4677
|
+
return fail(EXIT_GENERAL_ERROR, `Failed to run ${command}: ${messageOf(run.error)}`);
|
|
4678
|
+
}
|
|
4679
|
+
if (run.value !== 0) {
|
|
4680
|
+
return fail(EXIT_GENERAL_ERROR, `${command} exited with code ${run.value}.`);
|
|
4681
|
+
}
|
|
4682
|
+
term.logStderr(`${command} finished; run "hop --version" to see which version you have now.`);
|
|
4683
|
+
return ok({ data: { ...base, action: "delegated", command: [...argv2] } });
|
|
4684
|
+
};
|
|
4685
|
+
|
|
4686
|
+
// src/commands/self-update-standalone.ts
|
|
4687
|
+
var checksumRejection = (port, assetName, checksumText, bytes) => {
|
|
4688
|
+
const expected = parseChecksumFile(checksumText, assetName);
|
|
4689
|
+
if (!expected.ok) {
|
|
4690
|
+
return fail(EXIT_SAFE_REJECTION, `The checksum file ${assetName}.sha256 ${expected.reason}; refusing to install an unverified binary.`);
|
|
4691
|
+
}
|
|
4692
|
+
const actual = port.sha256Hex(bytes);
|
|
4693
|
+
if (actual !== expected.digest) {
|
|
4694
|
+
return fail(EXIT_SAFE_REJECTION, `Checksum mismatch for ${assetName} (expected ${expected.digest}, got ${actual}); refusing to install it. Nothing was changed.`);
|
|
4695
|
+
}
|
|
4696
|
+
return null;
|
|
4697
|
+
};
|
|
4698
|
+
var replaceFailureMessage = (method, error) => isWriteDenied(error) ? `Cannot write ${method.installDir}; reinstall hop somewhere writable (install.sh uses ~/.local/bin).` : `Failed to replace ${method.binaryPath}: ${messageOf(error)}`;
|
|
4699
|
+
var replaceStandalone = async (port, term, method, base) => {
|
|
4700
|
+
const { assetName, binaryPath } = method;
|
|
4701
|
+
const checksumAsset = `${assetName}.sha256`;
|
|
4702
|
+
const writable = await attempt(() => port.assertWritableDir(method.installDir));
|
|
4703
|
+
if (!writable.ok) {
|
|
4704
|
+
return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, writable.error));
|
|
4705
|
+
}
|
|
4706
|
+
term.logStderr(`Downloading ${assetName} v${base.latest}...`);
|
|
4707
|
+
const checksum = await attempt(() => port.downloadReleaseText(base.latest, checksumAsset));
|
|
4708
|
+
if (!checksum.ok) {
|
|
4709
|
+
return fail(EXIT_GENERAL_ERROR, `Failed to download ${checksumAsset}: ${messageOf(checksum.error)}`);
|
|
4710
|
+
}
|
|
4711
|
+
const binary = await attempt(() => port.downloadReleaseAsset(base.latest, assetName));
|
|
4712
|
+
if (!binary.ok) {
|
|
4713
|
+
return fail(EXIT_GENERAL_ERROR, `Failed to download ${assetName}: ${messageOf(binary.error)}`);
|
|
4714
|
+
}
|
|
4715
|
+
const rejection = checksumRejection(port, assetName, checksum.value, binary.value);
|
|
4716
|
+
if (rejection !== null) {
|
|
4717
|
+
return rejection;
|
|
4718
|
+
}
|
|
4719
|
+
term.logStderr(`Checksum verified. Replacing ${binaryPath}...`);
|
|
4720
|
+
const replaced = await attempt(() => port.replaceExecutable(binaryPath, binary.value));
|
|
4721
|
+
if (!replaced.ok) {
|
|
4722
|
+
return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, replaced.error));
|
|
4723
|
+
}
|
|
4724
|
+
term.logStderr(`Updated hop ${base.current} -> ${base.latest}.`);
|
|
4725
|
+
return ok({ data: { ...base, action: "replaced" } });
|
|
4726
|
+
};
|
|
4727
|
+
|
|
4728
|
+
// src/commands/self-update.ts
|
|
4729
|
+
var fetchLatest = (port, method) => latestChannel(method) === "npm" ? port.latestNpmVersion() : port.latestGithubVersion();
|
|
4730
|
+
var checkCommand = (method) => method.kind === "standalone" ? null : [...upgradeCommand(method)];
|
|
4731
|
+
var describeCheck = (method, base) => {
|
|
4732
|
+
const how = method.kind === "standalone" ? `replaces ${method.binaryPath}` : `runs: ${upgradeCommand(method).join(" ")}`;
|
|
4733
|
+
return `Update available: ${base.current} -> ${base.latest}. "hop --update" ${how}.`;
|
|
4734
|
+
};
|
|
4735
|
+
var selfUpdate = async (port, term, options) => {
|
|
4736
|
+
const facts = await attempt(() => port.installFacts());
|
|
4737
|
+
if (!facts.ok) {
|
|
4738
|
+
return fail(EXIT_GENERAL_ERROR, `Cannot update hop: could not tell how it was installed (${messageOf(facts.error)}).`);
|
|
4739
|
+
}
|
|
4740
|
+
const method = detectInstallMethod(facts.value);
|
|
4741
|
+
if (method.kind === "unsupported") {
|
|
4742
|
+
return fail(EXIT_GENERAL_ERROR, `Cannot update hop: ${method.reason}.`);
|
|
4743
|
+
}
|
|
4744
|
+
term.logStderr("Checking for the latest release...");
|
|
4745
|
+
const release = await attempt(async () => {
|
|
4746
|
+
const latest = await fetchLatest(port, method);
|
|
4747
|
+
return {
|
|
4748
|
+
latest,
|
|
4749
|
+
updateAvailable: compareVersions(latest, options.currentVersion) > 0
|
|
4750
|
+
};
|
|
4751
|
+
});
|
|
4752
|
+
if (!release.ok) {
|
|
4753
|
+
return fail(EXIT_GENERAL_ERROR, `Failed to check for the latest release: ${messageOf(release.error)}`);
|
|
4754
|
+
}
|
|
4755
|
+
const base = {
|
|
4756
|
+
current: options.currentVersion,
|
|
4757
|
+
latest: release.value.latest,
|
|
4758
|
+
updateAvailable: release.value.updateAvailable,
|
|
4759
|
+
method: method.kind,
|
|
4760
|
+
action: "none",
|
|
4761
|
+
command: null
|
|
4762
|
+
};
|
|
4763
|
+
if (!base.updateAvailable) {
|
|
4764
|
+
term.logStderr(`hop ${base.current} is up to date (latest release: ${base.latest}).`);
|
|
4765
|
+
return ok({ data: base });
|
|
4766
|
+
}
|
|
4767
|
+
if (options.check) {
|
|
4768
|
+
term.logStderr(describeCheck(method, base));
|
|
4769
|
+
return ok({ data: { ...base, command: checkCommand(method) } });
|
|
4770
|
+
}
|
|
4771
|
+
return method.kind === "standalone" ? replaceStandalone(port, term, method, base) : runDelegated(port, term, method, base);
|
|
4772
|
+
};
|
|
4773
|
+
|
|
4774
|
+
// src/infra/install-facts.ts
|
|
4775
|
+
import { readFile as readFile2, realpath } from "node:fs/promises";
|
|
4776
|
+
import { join as join3 } from "node:path";
|
|
4777
|
+
var MISE_BACKEND_TOML = ".mise.backend.toml";
|
|
4778
|
+
var MISE_BACKEND_LEGACY = ".mise.backend";
|
|
4779
|
+
var realpathOrNull = async (path) => {
|
|
4780
|
+
try {
|
|
4781
|
+
return await realpath(path);
|
|
4782
|
+
} catch {
|
|
4783
|
+
return null;
|
|
4784
|
+
}
|
|
4785
|
+
};
|
|
4786
|
+
var realpathOrSelf = async (path) => await realpathOrNull(path) ?? path;
|
|
4787
|
+
var readTextOrNull = async (path) => {
|
|
4788
|
+
try {
|
|
4789
|
+
return await readFile2(path, "utf8");
|
|
4790
|
+
} catch {
|
|
4791
|
+
return null;
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
var readMiseBackend = async (dir) => {
|
|
4795
|
+
const toml = await readTextOrNull(join3(dir, MISE_BACKEND_TOML));
|
|
4796
|
+
const fromToml = toml === null ? null : parseMiseBackendToml(toml);
|
|
4797
|
+
if (fromToml !== null) {
|
|
4798
|
+
return fromToml;
|
|
4799
|
+
}
|
|
4800
|
+
const legacy = await readTextOrNull(join3(dir, MISE_BACKEND_LEGACY));
|
|
4801
|
+
return legacy === null ? null : parseLegacyMiseBackend(legacy);
|
|
4802
|
+
};
|
|
4803
|
+
var findMiseTool = async (path) => {
|
|
4804
|
+
const dir = path === null ? null : miseToolDir(path);
|
|
4805
|
+
return dir === null ? null : { dir, backend: await readMiseBackend(dir) };
|
|
4806
|
+
};
|
|
4807
|
+
var verifiedNpmPrefix = async (scriptPath) => {
|
|
4808
|
+
const prefix = scriptPath === null ? null : npmGlobalPrefixOf(scriptPath);
|
|
4809
|
+
if (prefix === null) {
|
|
4810
|
+
return null;
|
|
4811
|
+
}
|
|
4812
|
+
return await realpathOrNull(join3(prefix, "bin", "hop")) === scriptPath ? prefix : null;
|
|
4813
|
+
};
|
|
4814
|
+
var readInstallFacts = async (source) => {
|
|
4815
|
+
const executablePath = source.compiled ? await realpathOrSelf(source.execPath) : null;
|
|
4816
|
+
const scriptPath = source.compiled || source.argv1 === undefined ? null : await realpathOrSelf(source.argv1);
|
|
4817
|
+
return {
|
|
4818
|
+
compiled: source.compiled,
|
|
4819
|
+
executablePath,
|
|
4820
|
+
scriptPath,
|
|
4821
|
+
mise: await findMiseTool(executablePath ?? scriptPath),
|
|
4822
|
+
npmGlobalPrefix: await verifiedNpmPrefix(scriptPath),
|
|
4823
|
+
platform: source.platform,
|
|
4824
|
+
arch: source.arch
|
|
4825
|
+
};
|
|
4826
|
+
};
|
|
4827
|
+
|
|
4828
|
+
// src/infra/package-manager.ts
|
|
4829
|
+
import { spawn } from "node:child_process";
|
|
4830
|
+
import { constants } from "node:fs";
|
|
4831
|
+
import { access, stat } from "node:fs/promises";
|
|
4832
|
+
import { homedir as homedir2 } from "node:os";
|
|
4833
|
+
import { basename as basename2 } from "node:path";
|
|
4834
|
+
var STDERR_FD = 2;
|
|
4835
|
+
var isExecutableFile = async (path) => {
|
|
4836
|
+
try {
|
|
4837
|
+
const stats = await stat(path);
|
|
4838
|
+
if (!stats.isFile()) {
|
|
4839
|
+
return false;
|
|
4840
|
+
}
|
|
4841
|
+
await access(path, constants.X_OK);
|
|
4842
|
+
return true;
|
|
4843
|
+
} catch {
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4846
|
+
};
|
|
4847
|
+
var resolveExecutable = async (name, pathEnv, preferredDir = null) => {
|
|
4848
|
+
const probes = pathExecutableCandidates(name, pathEnv, preferredDir).map(async (candidate) => await isExecutableFile(candidate) ? candidate : null);
|
|
4849
|
+
for (const probe of probes) {
|
|
4850
|
+
const found = await probe;
|
|
4851
|
+
if (found !== null) {
|
|
4852
|
+
return found;
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
return null;
|
|
4856
|
+
};
|
|
4857
|
+
var runCommand2 = (argv2) => new Promise((resolve, reject) => {
|
|
4858
|
+
const [program, ...args] = argv2;
|
|
4859
|
+
const child = spawn(program, args, {
|
|
4860
|
+
cwd: homedir2(),
|
|
4861
|
+
stdio: ["inherit", STDERR_FD, STDERR_FD]
|
|
4862
|
+
});
|
|
4863
|
+
child.once("error", reject);
|
|
4864
|
+
child.once("close", (code, signal) => {
|
|
4865
|
+
if (code === null) {
|
|
4866
|
+
reject(new Error(`${basename2(program)} was killed by signal ${signal ?? "unknown"}`));
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
resolve(code);
|
|
4870
|
+
});
|
|
4871
|
+
});
|
|
4872
|
+
|
|
4873
|
+
// src/infra/release-binary.ts
|
|
4874
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4875
|
+
import { constants as constants2 } from "node:fs";
|
|
4876
|
+
import { access as access2, open, realpath as realpath2, rename, rm as rm3 } from "node:fs/promises";
|
|
4877
|
+
import { basename as basename3, dirname as dirname2, join as join4 } from "node:path";
|
|
4878
|
+
var EXECUTABLE_MODE = 493;
|
|
4879
|
+
var TEMP_SUFFIX_BYTES = 6;
|
|
4880
|
+
var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
4881
|
+
var removeQuietly = async (path) => {
|
|
4882
|
+
try {
|
|
4883
|
+
await rm3(path, { force: true });
|
|
4884
|
+
} catch {}
|
|
4885
|
+
};
|
|
4886
|
+
var writeDurably = async (path, bytes) => {
|
|
4887
|
+
const handle = await open(path, "wx", EXECUTABLE_MODE);
|
|
4888
|
+
try {
|
|
4889
|
+
await handle.writeFile(bytes);
|
|
4890
|
+
await handle.chmod(EXECUTABLE_MODE);
|
|
4891
|
+
await handle.sync();
|
|
4892
|
+
} finally {
|
|
4893
|
+
await handle.close();
|
|
4894
|
+
}
|
|
4895
|
+
};
|
|
4896
|
+
var replaceExecutable = async (path, bytes) => {
|
|
4897
|
+
const target = await realpath2(path);
|
|
4898
|
+
const suffix = randomBytes(TEMP_SUFFIX_BYTES).toString("hex");
|
|
4899
|
+
const temp = join4(dirname2(target), `.${basename3(target)}.update-${suffix}`);
|
|
4900
|
+
try {
|
|
4901
|
+
await writeDurably(temp, bytes);
|
|
4902
|
+
await rename(temp, target);
|
|
4903
|
+
} catch (error) {
|
|
4904
|
+
await removeQuietly(temp);
|
|
4905
|
+
throw error;
|
|
4906
|
+
}
|
|
4907
|
+
};
|
|
4908
|
+
var assertWritableDir = (dir) => access2(dir, constants2.W_OK | constants2.X_OK);
|
|
4909
|
+
|
|
4910
|
+
// node_modules/valibot/dist/index.mjs
|
|
4911
|
+
var store$4;
|
|
4912
|
+
var DEFAULT_CONFIG = {
|
|
4913
|
+
lang: undefined,
|
|
4914
|
+
message: undefined,
|
|
4915
|
+
abortEarly: undefined,
|
|
4916
|
+
abortPipeEarly: undefined
|
|
4917
|
+
};
|
|
4918
|
+
function getGlobalConfig(config$1) {
|
|
4919
|
+
if (!config$1 && !store$4)
|
|
4920
|
+
return DEFAULT_CONFIG;
|
|
4921
|
+
return {
|
|
4922
|
+
lang: config$1?.lang ?? store$4?.lang,
|
|
4923
|
+
message: config$1?.message,
|
|
4924
|
+
abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
|
|
4925
|
+
abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4928
|
+
var store$3;
|
|
4929
|
+
function getGlobalMessage(lang) {
|
|
4930
|
+
return store$3?.get(lang);
|
|
4931
|
+
}
|
|
4932
|
+
var store$2;
|
|
4933
|
+
function getSchemaMessage(lang) {
|
|
4934
|
+
return store$2?.get(lang);
|
|
4935
|
+
}
|
|
4936
|
+
var store$1;
|
|
4937
|
+
function getSpecificMessage(reference, lang) {
|
|
4938
|
+
return store$1?.get(reference)?.get(lang);
|
|
4939
|
+
}
|
|
4940
|
+
function _stringify(input) {
|
|
4941
|
+
const type = typeof input;
|
|
4942
|
+
if (type === "string")
|
|
4943
|
+
return `"${input}"`;
|
|
4944
|
+
if (type === "number" || type === "bigint" || type === "boolean")
|
|
4945
|
+
return `${input}`;
|
|
4946
|
+
if (type === "object" || type === "function")
|
|
4947
|
+
return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
|
|
4948
|
+
return type;
|
|
4949
|
+
}
|
|
4950
|
+
function _addIssue(context, label, dataset, config$1, other) {
|
|
4951
|
+
const input = other && "input" in other ? other.input : dataset.value;
|
|
4952
|
+
const expected = other?.expected ?? context.expects ?? null;
|
|
4953
|
+
const received = other?.received ?? /* @__PURE__ */ _stringify(input);
|
|
4954
|
+
const issue = {
|
|
4955
|
+
kind: context.kind,
|
|
4956
|
+
type: context.type,
|
|
4957
|
+
input,
|
|
4958
|
+
expected,
|
|
4959
|
+
received,
|
|
4960
|
+
message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
|
|
4961
|
+
requirement: context.requirement,
|
|
4962
|
+
path: other?.path,
|
|
4963
|
+
issues: other?.issues,
|
|
4964
|
+
lang: config$1.lang,
|
|
4965
|
+
abortEarly: config$1.abortEarly,
|
|
4966
|
+
abortPipeEarly: config$1.abortPipeEarly
|
|
4967
|
+
};
|
|
4968
|
+
const isSchema = context.kind === "schema";
|
|
4969
|
+
const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
|
|
4970
|
+
if (message$1 !== undefined)
|
|
4971
|
+
issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
|
|
4972
|
+
if (isSchema)
|
|
4973
|
+
dataset.typed = false;
|
|
4974
|
+
if (dataset.issues)
|
|
4975
|
+
dataset.issues.push(issue);
|
|
4976
|
+
else
|
|
4977
|
+
dataset.issues = [issue];
|
|
4978
|
+
}
|
|
4979
|
+
var _standardCache = /* @__PURE__ */ new WeakMap;
|
|
4980
|
+
function _getStandardProps(context) {
|
|
4981
|
+
let cached = _standardCache.get(context);
|
|
4982
|
+
if (!cached) {
|
|
4983
|
+
cached = {
|
|
4984
|
+
version: 1,
|
|
4985
|
+
vendor: "valibot",
|
|
4986
|
+
validate(value$1) {
|
|
4987
|
+
return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
|
|
4988
|
+
}
|
|
4989
|
+
};
|
|
4990
|
+
_standardCache.set(context, cached);
|
|
4991
|
+
}
|
|
4992
|
+
return cached;
|
|
4993
|
+
}
|
|
4994
|
+
function getFallback(schema, dataset, config$1) {
|
|
4995
|
+
return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
|
|
4996
|
+
}
|
|
4997
|
+
function getDefault(schema, dataset, config$1) {
|
|
4998
|
+
return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
|
|
4999
|
+
}
|
|
5000
|
+
function object(entries$1, message$1) {
|
|
5001
|
+
return {
|
|
5002
|
+
kind: "schema",
|
|
5003
|
+
type: "object",
|
|
5004
|
+
reference: object,
|
|
5005
|
+
expects: "Object",
|
|
5006
|
+
async: false,
|
|
5007
|
+
entries: entries$1,
|
|
5008
|
+
message: message$1,
|
|
5009
|
+
get "~standard"() {
|
|
5010
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
5011
|
+
},
|
|
5012
|
+
"~run"(dataset, config$1) {
|
|
5013
|
+
const input = dataset.value;
|
|
5014
|
+
if (input && typeof input === "object") {
|
|
5015
|
+
dataset.typed = true;
|
|
5016
|
+
dataset.value = {};
|
|
5017
|
+
for (const key in this.entries) {
|
|
5018
|
+
const valueSchema = this.entries[key];
|
|
5019
|
+
if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
|
|
5020
|
+
const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
|
|
5021
|
+
const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
|
|
5022
|
+
if (valueDataset.issues) {
|
|
5023
|
+
const pathItem = {
|
|
5024
|
+
type: "object",
|
|
5025
|
+
origin: "value",
|
|
5026
|
+
input,
|
|
5027
|
+
key,
|
|
5028
|
+
value: value$1
|
|
5029
|
+
};
|
|
5030
|
+
for (const issue of valueDataset.issues) {
|
|
5031
|
+
if (issue.path)
|
|
5032
|
+
issue.path.unshift(pathItem);
|
|
5033
|
+
else
|
|
5034
|
+
issue.path = [pathItem];
|
|
5035
|
+
dataset.issues?.push(issue);
|
|
5036
|
+
}
|
|
5037
|
+
if (!dataset.issues)
|
|
5038
|
+
dataset.issues = valueDataset.issues;
|
|
5039
|
+
if (config$1.abortEarly) {
|
|
5040
|
+
dataset.typed = false;
|
|
5041
|
+
break;
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
if (!valueDataset.typed)
|
|
5045
|
+
dataset.typed = false;
|
|
5046
|
+
dataset.value[key] = valueDataset.value;
|
|
5047
|
+
} else if (valueSchema.fallback !== undefined)
|
|
5048
|
+
dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
|
|
5049
|
+
else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
|
|
5050
|
+
_addIssue(this, "key", dataset, config$1, {
|
|
5051
|
+
input: undefined,
|
|
5052
|
+
expected: `"${key}"`,
|
|
5053
|
+
path: [{
|
|
5054
|
+
type: "object",
|
|
5055
|
+
origin: "key",
|
|
5056
|
+
input,
|
|
5057
|
+
key,
|
|
5058
|
+
value: input[key]
|
|
5059
|
+
}]
|
|
5060
|
+
});
|
|
5061
|
+
if (config$1.abortEarly)
|
|
5062
|
+
break;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
} else
|
|
5066
|
+
_addIssue(this, "type", dataset, config$1);
|
|
5067
|
+
return dataset;
|
|
5068
|
+
}
|
|
5069
|
+
};
|
|
5070
|
+
}
|
|
5071
|
+
function string(message$1) {
|
|
5072
|
+
return {
|
|
5073
|
+
kind: "schema",
|
|
5074
|
+
type: "string",
|
|
5075
|
+
reference: string,
|
|
5076
|
+
expects: "string",
|
|
5077
|
+
async: false,
|
|
5078
|
+
message: message$1,
|
|
5079
|
+
get "~standard"() {
|
|
5080
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
5081
|
+
},
|
|
5082
|
+
"~run"(dataset, config$1) {
|
|
5083
|
+
if (typeof dataset.value === "string")
|
|
5084
|
+
dataset.typed = true;
|
|
5085
|
+
else
|
|
5086
|
+
_addIssue(this, "type", dataset, config$1);
|
|
5087
|
+
return dataset;
|
|
5088
|
+
}
|
|
5089
|
+
};
|
|
5090
|
+
}
|
|
5091
|
+
function safeParse(schema, input, config$1) {
|
|
5092
|
+
const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
|
|
5093
|
+
return {
|
|
5094
|
+
typed: dataset.typed,
|
|
5095
|
+
success: !dataset.issues,
|
|
5096
|
+
output: dataset.value,
|
|
5097
|
+
issues: dataset.issues
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
// src/infra/capped-body.ts
|
|
5102
|
+
var tooLarge = (maxBytes) => new Error(`the response is larger than ${maxBytes} bytes`);
|
|
5103
|
+
var concatenate = (chunks, total) => {
|
|
5104
|
+
const bytes = new Uint8Array(total);
|
|
5105
|
+
let offset = 0;
|
|
5106
|
+
for (const chunk of chunks) {
|
|
5107
|
+
bytes.set(chunk, offset);
|
|
5108
|
+
offset += chunk.length;
|
|
5109
|
+
}
|
|
5110
|
+
return bytes;
|
|
5111
|
+
};
|
|
5112
|
+
var readCapped = async (response, maxBytes) => {
|
|
5113
|
+
const declared = Math.trunc(Number(response.headers.get("content-length") ?? ""));
|
|
5114
|
+
if (declared > maxBytes) {
|
|
5115
|
+
await response.body?.cancel();
|
|
5116
|
+
throw tooLarge(maxBytes);
|
|
5117
|
+
}
|
|
5118
|
+
const chunks = [];
|
|
5119
|
+
let received = 0;
|
|
5120
|
+
for await (const chunk of response.body ?? []) {
|
|
5121
|
+
received += chunk.length;
|
|
5122
|
+
if (received > maxBytes) {
|
|
5123
|
+
throw tooLarge(maxBytes);
|
|
5124
|
+
}
|
|
5125
|
+
chunks.push(chunk);
|
|
5126
|
+
}
|
|
5127
|
+
return concatenate(chunks, received);
|
|
5128
|
+
};
|
|
5129
|
+
|
|
5130
|
+
// src/infra/release-http.ts
|
|
5131
|
+
var METADATA_TIMEOUT_MS = 15000;
|
|
5132
|
+
var ASSET_TIMEOUT_MS = 300000;
|
|
5133
|
+
var METADATA_MAX_BYTES = 1048576;
|
|
5134
|
+
var CHECKSUM_MAX_BYTES = 4096;
|
|
5135
|
+
var BINARY_MAX_BYTES = 268435456;
|
|
5136
|
+
var GITHUB_API_ORIGIN = "https://api.github.com/";
|
|
5137
|
+
var HTTP_FORBIDDEN = 403;
|
|
5138
|
+
var HTTP_TOO_MANY_REQUESTS = 429;
|
|
5139
|
+
var GithubLatestSchema = object({ tag_name: string() });
|
|
5140
|
+
var NpmLatestSchema = object({ version: string() });
|
|
5141
|
+
var DECODER = new TextDecoder;
|
|
5142
|
+
var send = async (fetcher, url, { headers, timeoutMs }) => {
|
|
5143
|
+
try {
|
|
5144
|
+
return await fetcher(url, {
|
|
5145
|
+
headers,
|
|
5146
|
+
redirect: "follow",
|
|
5147
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5148
|
+
});
|
|
5149
|
+
} catch (error) {
|
|
5150
|
+
throw new Error(`GET ${url} failed: ${messageOf(error)}`, { cause: error });
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
var rateLimitHint = (url, response) => {
|
|
5154
|
+
const refused = response.status === HTTP_FORBIDDEN || response.status === HTTP_TOO_MANY_REQUESTS;
|
|
5155
|
+
const exhausted = refused && response.headers.get("x-ratelimit-remaining") === "0";
|
|
5156
|
+
return exhausted && url.startsWith(GITHUB_API_ORIGIN) ? " (the unauthenticated GitHub API rate limit was hit; try again later)" : "";
|
|
5157
|
+
};
|
|
5158
|
+
var discardBody = async (response) => {
|
|
5159
|
+
try {
|
|
5160
|
+
await response.body?.cancel();
|
|
5161
|
+
} catch {}
|
|
5162
|
+
};
|
|
5163
|
+
var get = async (fetcher, url, options) => {
|
|
5164
|
+
const response = await send(fetcher, url, options);
|
|
5165
|
+
if (!response.ok) {
|
|
5166
|
+
await discardBody(response);
|
|
5167
|
+
throw new Error(`GET ${url} failed: HTTP ${response.status}${rateLimitHint(url, response)}`);
|
|
5168
|
+
}
|
|
5169
|
+
if (response.url.startsWith("http://")) {
|
|
5170
|
+
await discardBody(response);
|
|
5171
|
+
throw new Error(`GET ${url} failed: redirected to ${response.url}, which is not HTTPS`);
|
|
5172
|
+
}
|
|
5173
|
+
return response;
|
|
5174
|
+
};
|
|
5175
|
+
var readBody = async (url, read) => {
|
|
5176
|
+
try {
|
|
5177
|
+
return await read();
|
|
5178
|
+
} catch (error) {
|
|
5179
|
+
throw new Error(`GET ${url} failed while reading the response: ${messageOf(error)}`, {
|
|
5180
|
+
cause: error
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
};
|
|
5184
|
+
var getBytes = async (fetcher, url, options) => {
|
|
5185
|
+
const response = await get(fetcher, url, options);
|
|
5186
|
+
return await readBody(url, () => readCapped(response, options.maxBytes));
|
|
5187
|
+
};
|
|
5188
|
+
var getJson = async (fetcher, url, schema, options) => {
|
|
5189
|
+
const bytes = await getBytes(fetcher, url, options);
|
|
5190
|
+
const body = await readBody(url, () => JSON.parse(DECODER.decode(bytes)));
|
|
5191
|
+
const parsed = safeParse(schema, body);
|
|
5192
|
+
if (!parsed.success) {
|
|
5193
|
+
throw new Error(`GET ${url} returned an unexpected response`);
|
|
5194
|
+
}
|
|
5195
|
+
return parsed.output;
|
|
5196
|
+
};
|
|
5197
|
+
var createReleaseHttp = (userAgent, fetcher = fetch) => ({
|
|
5198
|
+
async latestGithubVersion() {
|
|
5199
|
+
const release = await getJson(fetcher, GITHUB_LATEST_RELEASE_URL, GithubLatestSchema, {
|
|
5200
|
+
headers: {
|
|
5201
|
+
Accept: "application/vnd.github+json",
|
|
5202
|
+
"User-Agent": userAgent
|
|
5203
|
+
},
|
|
5204
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
5205
|
+
maxBytes: METADATA_MAX_BYTES
|
|
5206
|
+
});
|
|
5207
|
+
return versionFromTag(release.tag_name);
|
|
5208
|
+
},
|
|
5209
|
+
async latestNpmVersion() {
|
|
5210
|
+
const latest = await getJson(fetcher, NPM_LATEST_URL, NpmLatestSchema, {
|
|
5211
|
+
headers: { Accept: "application/json", "User-Agent": userAgent },
|
|
5212
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
5213
|
+
maxBytes: METADATA_MAX_BYTES
|
|
5214
|
+
});
|
|
5215
|
+
return requireVersion(latest.version);
|
|
5216
|
+
},
|
|
5217
|
+
async downloadReleaseAsset(version, assetName) {
|
|
5218
|
+
return await getBytes(fetcher, releaseAssetUrl(version, assetName), {
|
|
5219
|
+
headers: { Accept: "application/octet-stream", "User-Agent": userAgent },
|
|
5220
|
+
timeoutMs: ASSET_TIMEOUT_MS,
|
|
5221
|
+
maxBytes: BINARY_MAX_BYTES
|
|
5222
|
+
});
|
|
5223
|
+
},
|
|
5224
|
+
async downloadReleaseText(version, assetName) {
|
|
5225
|
+
const bytes = await getBytes(fetcher, releaseAssetUrl(version, assetName), {
|
|
5226
|
+
headers: { Accept: "text/plain", "User-Agent": userAgent },
|
|
5227
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
5228
|
+
maxBytes: CHECKSUM_MAX_BYTES
|
|
5229
|
+
});
|
|
5230
|
+
return DECODER.decode(bytes);
|
|
5231
|
+
}
|
|
5232
|
+
});
|
|
5233
|
+
|
|
5234
|
+
// src/infra/self-update.ts
|
|
5235
|
+
var createSelfUpdatePort = ({
|
|
5236
|
+
compiled,
|
|
5237
|
+
version
|
|
5238
|
+
}) => ({
|
|
5239
|
+
installFacts: () => readInstallFacts({
|
|
5240
|
+
compiled,
|
|
5241
|
+
execPath: process.execPath,
|
|
5242
|
+
argv1: process.argv[1],
|
|
5243
|
+
platform: process.platform,
|
|
5244
|
+
arch: process.arch
|
|
5245
|
+
}),
|
|
5246
|
+
...createReleaseHttp(`hop/${version}`),
|
|
5247
|
+
sha256Hex,
|
|
5248
|
+
assertWritableDir,
|
|
5249
|
+
replaceExecutable,
|
|
5250
|
+
resolveExecutable: (name, preferredDir) => resolveExecutable(name, process.env.PATH, preferredDir),
|
|
5251
|
+
runCommand: runCommand2
|
|
5252
|
+
});
|
|
5253
|
+
// package.json
|
|
5254
|
+
var package_default = {
|
|
5255
|
+
name: "@n-seiji/nuthatch",
|
|
5256
|
+
version: "0.1.5",
|
|
5257
|
+
description: "git worktree manager (hop)",
|
|
5258
|
+
homepage: "https://github.com/n-seiji/nuthatch#readme",
|
|
5259
|
+
bugs: {
|
|
5260
|
+
url: "https://github.com/n-seiji/nuthatch/issues"
|
|
5261
|
+
},
|
|
5262
|
+
license: "GPL-3.0-only",
|
|
5263
|
+
repository: {
|
|
5264
|
+
type: "git",
|
|
5265
|
+
url: "git+https://github.com/n-seiji/nuthatch.git"
|
|
5266
|
+
},
|
|
5267
|
+
bin: {
|
|
5268
|
+
hop: "dist/cli.js"
|
|
5269
|
+
},
|
|
5270
|
+
files: [
|
|
5271
|
+
"dist",
|
|
5272
|
+
"shell"
|
|
5273
|
+
],
|
|
5274
|
+
type: "module",
|
|
5275
|
+
publishConfig: {
|
|
5276
|
+
access: "public"
|
|
5277
|
+
},
|
|
5278
|
+
scripts: {
|
|
5279
|
+
build: "bun build ./src/cli.ts --outfile dist/cli.js --target node --format esm",
|
|
5280
|
+
typecheck: "tsc --noEmit",
|
|
5281
|
+
lint: "oxlint .",
|
|
5282
|
+
"lint:fix": "oxlint . --fix",
|
|
5283
|
+
format: "oxfmt --write .",
|
|
5284
|
+
"format:check": "oxfmt --check .",
|
|
5285
|
+
test: "bun test"
|
|
5286
|
+
},
|
|
5287
|
+
dependencies: {
|
|
5288
|
+
citty: "^0.1.6",
|
|
5289
|
+
valibot: "^1.4.2"
|
|
5290
|
+
},
|
|
5291
|
+
devDependencies: {
|
|
5292
|
+
"@types/bun": "latest",
|
|
5293
|
+
oxfmt: "^0.66.0",
|
|
5294
|
+
oxlint: "^1.81.0",
|
|
5295
|
+
typescript: "^5.6.0"
|
|
5296
|
+
},
|
|
5297
|
+
engines: {
|
|
5298
|
+
node: ">=22"
|
|
5299
|
+
}
|
|
5300
|
+
};
|
|
5301
|
+
|
|
5302
|
+
// src/version.ts
|
|
5303
|
+
var VERSION = package_default.version;
|
|
5304
|
+
|
|
5305
|
+
// src/cli-update.ts
|
|
5306
|
+
var UPDATE_ARGS = {
|
|
5307
|
+
check: { type: "boolean", description: "Only report; change nothing" },
|
|
5308
|
+
json: { type: "boolean", description: "Output JSON" }
|
|
5309
|
+
};
|
|
5310
|
+
var UPDATE_FLAGS = new Set(["--check", "--json"]);
|
|
5311
|
+
var parseUpdateArgs = (tokens) => {
|
|
5312
|
+
const stray = tokens.filter((token) => !UPDATE_FLAGS.has(token));
|
|
5313
|
+
if (stray.length > 0) {
|
|
5314
|
+
return {
|
|
5315
|
+
ok: false,
|
|
5316
|
+
json: wantsJson(tokens),
|
|
5317
|
+
message: `Unknown argument for --update: ${stray.join(" ")}. Usage: hop --update [--check] [--json]`
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
const args = parseArgs([...tokens], UPDATE_ARGS);
|
|
5321
|
+
return { ok: true, check: Boolean(args.check), json: Boolean(args.json) };
|
|
5322
|
+
};
|
|
5323
|
+
var runUpdate = async (rawArgs, term) => {
|
|
5324
|
+
try {
|
|
5325
|
+
const parsed = parseUpdateArgs(rawArgs.slice(1));
|
|
5326
|
+
if (!parsed.ok) {
|
|
5327
|
+
reportResult("update", fail(EXIT_USAGE_ERROR, parsed.message), parsed.json);
|
|
5328
|
+
return;
|
|
5329
|
+
}
|
|
5330
|
+
const port = createSelfUpdatePort({
|
|
5331
|
+
compiled: isRunningAsCompiledBinary(),
|
|
5332
|
+
version: VERSION
|
|
5333
|
+
});
|
|
5334
|
+
const result = await selfUpdate(port, term, {
|
|
5335
|
+
currentVersion: VERSION,
|
|
5336
|
+
check: parsed.check
|
|
5337
|
+
});
|
|
5338
|
+
reportResult("update", result, parsed.json);
|
|
5339
|
+
} catch (error) {
|
|
5340
|
+
reportFatalError("update", wantsJson(rawArgs), error);
|
|
4355
5341
|
}
|
|
4356
5342
|
};
|
|
4357
5343
|
|
|
@@ -4373,11 +5359,11 @@ var classifyGarbage = (input) => {
|
|
|
4373
5359
|
};
|
|
4374
5360
|
|
|
4375
5361
|
// src/commands/clean-candidates.ts
|
|
4376
|
-
var buildCleanCandidates = async (git, context, ext) => {
|
|
5362
|
+
var buildCleanCandidates = async (git, fs, context, ext) => {
|
|
4377
5363
|
const defaultRef = await git.resolveDefaultBranchRef(context.rootPath);
|
|
4378
5364
|
const targets = context.worktrees.filter((wt) => wt.kind === "managed" || ext && wt.kind === "external");
|
|
4379
5365
|
const results = await Promise.all(targets.map(async (wt) => {
|
|
4380
|
-
const reason = await classifyWorktree(wt, { git, defaultRef, context });
|
|
5366
|
+
const reason = await classifyWorktree(wt, { git, fs, defaultRef, context });
|
|
4381
5367
|
if (reason === null) {
|
|
4382
5368
|
return null;
|
|
4383
5369
|
}
|
|
@@ -4385,7 +5371,7 @@ var buildCleanCandidates = async (git, context, ext) => {
|
|
|
4385
5371
|
}));
|
|
4386
5372
|
return results.filter((candidate) => candidate !== null);
|
|
4387
5373
|
};
|
|
4388
|
-
var classifyWorktree = async (wt, { git, defaultRef, context }) => {
|
|
5374
|
+
var classifyWorktree = async (wt, { git, fs, defaultRef, context }) => {
|
|
4389
5375
|
if (wt.prunable) {
|
|
4390
5376
|
return classifyGarbage({
|
|
4391
5377
|
prunable: true,
|
|
@@ -4399,15 +5385,15 @@ var classifyWorktree = async (wt, { git, defaultRef, context }) => {
|
|
|
4399
5385
|
return null;
|
|
4400
5386
|
}
|
|
4401
5387
|
const { rootPath, worktrees } = context;
|
|
4402
|
-
const [
|
|
4403
|
-
|
|
5388
|
+
const [dirtyState, upstreamGone] = await Promise.all([
|
|
5389
|
+
worktreeDirtyState(git, fs, worktrees, wt),
|
|
4404
5390
|
git.isUpstreamGone(rootPath, wt.branch)
|
|
4405
5391
|
]);
|
|
4406
5392
|
const mergedIntoDefault = defaultRef === null ? "unknown" : await git.isAncestor(rootPath, wt.branch, defaultRef);
|
|
4407
5393
|
const allCommitsReachableFromDefault = defaultRef === null ? "unknown" : await git.hasEquivalentCommits(rootPath, wt.branch, defaultRef);
|
|
4408
5394
|
const input = {
|
|
4409
5395
|
prunable: false,
|
|
4410
|
-
clean:
|
|
5396
|
+
clean: dirtyState === false,
|
|
4411
5397
|
mergedIntoDefault,
|
|
4412
5398
|
upstreamGone,
|
|
4413
5399
|
allCommitsReachableFromDefault
|
|
@@ -4418,7 +5404,7 @@ var classifyWorktree = async (wt, { git, defaultRef, context }) => {
|
|
|
4418
5404
|
// src/commands/clean.ts
|
|
4419
5405
|
var clean = async (git, fs, term, options) => {
|
|
4420
5406
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4421
|
-
const candidates = await buildCleanCandidates(git, context, options.ext);
|
|
5407
|
+
const candidates = await buildCleanCandidates(git, fs, context, options.ext);
|
|
4422
5408
|
if (options.dryRun) {
|
|
4423
5409
|
return ok({ data: { candidates } });
|
|
4424
5410
|
}
|
|
@@ -4437,20 +5423,13 @@ var clean = async (git, fs, term, options) => {
|
|
|
4437
5423
|
return ok({ data: { candidates, removed: [] } });
|
|
4438
5424
|
}
|
|
4439
5425
|
}
|
|
4440
|
-
|
|
5426
|
+
return executeClean({
|
|
4441
5427
|
git,
|
|
4442
5428
|
fs,
|
|
4443
5429
|
context,
|
|
4444
5430
|
candidates,
|
|
4445
5431
|
cleanOptions: options
|
|
4446
5432
|
});
|
|
4447
|
-
if (!execution.ok) {
|
|
4448
|
-
return execution.rejection;
|
|
4449
|
-
}
|
|
4450
|
-
return ok({
|
|
4451
|
-
data: { candidates, removed: execution.removed },
|
|
4452
|
-
...execution.warnings.length === 0 ? {} : { warnings: execution.warnings }
|
|
4453
|
-
});
|
|
4454
5433
|
};
|
|
4455
5434
|
var removeCandidate = async ({
|
|
4456
5435
|
git,
|
|
@@ -4460,16 +5439,16 @@ var removeCandidate = async ({
|
|
|
4460
5439
|
worktrees
|
|
4461
5440
|
}) => {
|
|
4462
5441
|
if (nestedWorktrees(worktrees, candidate.path).length > 0) {
|
|
4463
|
-
return
|
|
5442
|
+
return "nested";
|
|
4464
5443
|
}
|
|
4465
5444
|
try {
|
|
4466
5445
|
await git.removeWorktree(rootPath, candidate.path, false);
|
|
4467
5446
|
if (deleteBranch && candidate.branch.length > 0) {
|
|
4468
5447
|
await git.deleteBranch(rootPath, candidate.branch);
|
|
4469
5448
|
}
|
|
4470
|
-
return
|
|
5449
|
+
return "removed";
|
|
4471
5450
|
} catch {
|
|
4472
|
-
return
|
|
5451
|
+
return "failed";
|
|
4473
5452
|
}
|
|
4474
5453
|
};
|
|
4475
5454
|
var canDeletePrunableBranch = async (git, rootPath, candidate, defaultRef) => {
|
|
@@ -4490,21 +5469,16 @@ var canDeletePrunableBranch = async (git, rootPath, candidate, defaultRef) => {
|
|
|
4490
5469
|
} catch {}
|
|
4491
5470
|
return false;
|
|
4492
5471
|
};
|
|
4493
|
-
var executeClean =
|
|
5472
|
+
var executeClean = ({
|
|
4494
5473
|
git,
|
|
4495
5474
|
fs,
|
|
4496
5475
|
context,
|
|
4497
5476
|
candidates,
|
|
4498
5477
|
cleanOptions
|
|
4499
|
-
}) => {
|
|
4500
|
-
const acquisition = await acquireRepoLockOrRejection(context.commonDir);
|
|
4501
|
-
if (!acquisition.ok) {
|
|
4502
|
-
return { ok: false, rejection: acquisition.rejection };
|
|
4503
|
-
}
|
|
4504
|
-
const { lock } = acquisition;
|
|
5478
|
+
}) => withRepoLock(context.commonDir, async () => {
|
|
4505
5479
|
try {
|
|
4506
5480
|
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
4507
|
-
const freshCandidates = await buildCleanCandidates(git, fresh, cleanOptions.ext);
|
|
5481
|
+
const freshCandidates = await buildCleanCandidates(git, fs, fresh, cleanOptions.ext);
|
|
4508
5482
|
const freshByPath = new Map(freshCandidates.map((candidate) => [candidate.path, candidate]));
|
|
4509
5483
|
const stillValid = candidates.flatMap((candidate) => {
|
|
4510
5484
|
const freshCandidate = freshByPath.get(candidate.path);
|
|
@@ -4519,30 +5493,29 @@ var executeClean = async ({
|
|
|
4519
5493
|
if (cleanOptions.withBranch && candidate.reason === "prunable" && candidate.branch.length > 0 && !branchSafe) {
|
|
4520
5494
|
warnings.push(`Keeping branch "${candidate.branch}": prunable worktree is not confirmed merged or gone.`);
|
|
4521
5495
|
}
|
|
4522
|
-
|
|
5496
|
+
const outcome = await removeCandidate({
|
|
5497
|
+
git,
|
|
5498
|
+
rootPath: fresh.rootPath,
|
|
5499
|
+
candidate,
|
|
5500
|
+
deleteBranch,
|
|
5501
|
+
worktrees: fresh.worktrees
|
|
5502
|
+
});
|
|
5503
|
+
if (outcome === "removed") {
|
|
5504
|
+
removed.push(candidate.branch);
|
|
5505
|
+
} else if (outcome === "nested") {
|
|
4523
5506
|
warnings.push(`Skipped ${candidate.path}: still contains a registered worktree.`);
|
|
4524
|
-
} else {
|
|
4525
|
-
const success = await removeCandidate({
|
|
4526
|
-
git,
|
|
4527
|
-
rootPath: fresh.rootPath,
|
|
4528
|
-
candidate,
|
|
4529
|
-
deleteBranch,
|
|
4530
|
-
worktrees: fresh.worktrees
|
|
4531
|
-
});
|
|
4532
|
-
if (success) {
|
|
4533
|
-
removed.push(candidate.branch);
|
|
4534
|
-
}
|
|
4535
5507
|
}
|
|
4536
5508
|
}
|
|
4537
|
-
return {
|
|
5509
|
+
return ok({
|
|
5510
|
+
data: { candidates: [...candidates], removed },
|
|
5511
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
5512
|
+
});
|
|
4538
5513
|
} catch (error) {
|
|
4539
|
-
throw new Error(`Failed to clean worktrees: ${error
|
|
5514
|
+
throw new Error(`Failed to clean worktrees: ${messageOf(error)}`, {
|
|
4540
5515
|
cause: error
|
|
4541
5516
|
});
|
|
4542
|
-
} finally {
|
|
4543
|
-
await lock.release();
|
|
4544
5517
|
}
|
|
4545
|
-
};
|
|
5518
|
+
});
|
|
4546
5519
|
|
|
4547
5520
|
// src/commands/init.ts
|
|
4548
5521
|
var ZSH_TEMPLATE = `# nuthatch shell integration for zsh.
|
|
@@ -4601,7 +5574,7 @@ var renderInit = (options) => {
|
|
|
4601
5574
|
};
|
|
4602
5575
|
|
|
4603
5576
|
// src/commands/jump.ts
|
|
4604
|
-
import { join as
|
|
5577
|
+
import { join as join5 } from "node:path";
|
|
4605
5578
|
|
|
4606
5579
|
// src/domain/sanitize.ts
|
|
4607
5580
|
var MAX_DIR_NAME_LENGTH = 200;
|
|
@@ -4618,10 +5591,10 @@ var shortHash = (input) => {
|
|
|
4618
5591
|
}
|
|
4619
5592
|
return (hash >>> 0).toString(HEX_RADIX).padStart(FULL_HASH_HEX_LENGTH, "0").slice(0, HASH_LENGTH);
|
|
4620
5593
|
};
|
|
4621
|
-
var
|
|
5594
|
+
var baseName2 = (branch) => branch.replaceAll("/", "__");
|
|
4622
5595
|
var truncate = (name, maxLength) => name.length > maxLength ? name.slice(0, maxLength) : name;
|
|
4623
5596
|
var sanitizeBranchName = (branch, isTaken) => {
|
|
4624
|
-
const base = truncate(
|
|
5597
|
+
const base = truncate(baseName2(branch), MAX_DIR_NAME_LENGTH);
|
|
4625
5598
|
if (!isTaken(base)) {
|
|
4626
5599
|
return base;
|
|
4627
5600
|
}
|
|
@@ -4637,7 +5610,8 @@ var sanitizeBranchName = (branch, isTaken) => {
|
|
|
4637
5610
|
};
|
|
4638
5611
|
|
|
4639
5612
|
// src/commands/jump.ts
|
|
4640
|
-
var
|
|
5613
|
+
var existingWorktreeResult = (branch, path) => ok({ path, data: { branch, created: false } });
|
|
5614
|
+
var jump = async (git, fs, options) => {
|
|
4641
5615
|
if (options.target === "-") {
|
|
4642
5616
|
const previous = process.env.OLDPWD;
|
|
4643
5617
|
if (previous === undefined || previous.length === 0) {
|
|
@@ -4651,57 +5625,35 @@ var jump = async (git, fs, term, options) => {
|
|
|
4651
5625
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4652
5626
|
const existing = context.worktrees.find((wt) => wt.branch === options.target);
|
|
4653
5627
|
if (existing !== undefined) {
|
|
4654
|
-
return
|
|
4655
|
-
path: existing.path,
|
|
4656
|
-
data: { branch: options.target, created: false }
|
|
4657
|
-
});
|
|
5628
|
+
return existingWorktreeResult(options.target, existing.path);
|
|
4658
5629
|
}
|
|
4659
5630
|
if (!options.create) {
|
|
4660
|
-
if (!term.isTTY()) {
|
|
4661
|
-
return fail(EXIT_SAFE_REJECTION, `No worktree for branch "${options.target}". Re-run with --create to create it.`);
|
|
4662
|
-
}
|
|
4663
5631
|
return fail(EXIT_SAFE_REJECTION, `No worktree for branch "${options.target}". Re-run with --create to create it.`);
|
|
4664
5632
|
}
|
|
4665
|
-
const
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
if (!branchExistsLocally && track === undefined) {
|
|
4669
|
-
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
4670
|
-
if (remotes.includes("origin")) {
|
|
4671
|
-
track = `origin/${options.target}`;
|
|
4672
|
-
} else if (remotes.length === 1) {
|
|
4673
|
-
track = `${remotes[0]}/${options.target}`;
|
|
4674
|
-
} else if (remotes.length > 1) {
|
|
4675
|
-
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
4676
|
-
}
|
|
5633
|
+
const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
|
|
5634
|
+
if (!checkout.ok) {
|
|
5635
|
+
return checkout.rejection;
|
|
4677
5636
|
}
|
|
4678
5637
|
const managedDirNames = await fs.listDirNames(context.managedRoot);
|
|
4679
5638
|
const existingDirNames = new Set(managedDirNames.map((name) => name.toLowerCase()));
|
|
4680
5639
|
const dirName = sanitizeBranchName(options.target, (candidate) => existingDirNames.has(candidate.toLowerCase()));
|
|
4681
|
-
const targetPath =
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
5640
|
+
const targetPath = join5(context.managedRoot, dirName);
|
|
5641
|
+
return withRepoLock(context.commonDir, async () => {
|
|
5642
|
+
try {
|
|
5643
|
+
const fresh = await loadRepoContext(git, fs, options.cwd);
|
|
5644
|
+
const racedExisting = fresh.worktrees.find((wt) => wt.branch === options.target);
|
|
5645
|
+
if (racedExisting !== undefined) {
|
|
5646
|
+
return existingWorktreeResult(options.target, racedExisting.path);
|
|
5647
|
+
}
|
|
5648
|
+
await fs.mkdir(context.managedRoot);
|
|
5649
|
+
await git.addWorktree(context.rootPath, targetPath, options.target, checkout.options);
|
|
4687
5650
|
return ok({
|
|
4688
|
-
path:
|
|
4689
|
-
data: { branch: options.target, created:
|
|
5651
|
+
path: targetPath,
|
|
5652
|
+
data: { branch: options.target, created: true }
|
|
4690
5653
|
});
|
|
5654
|
+
} catch (error) {
|
|
5655
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to create worktree: ${messageOf(error)}`);
|
|
4691
5656
|
}
|
|
4692
|
-
await fs.mkdir(context.managedRoot);
|
|
4693
|
-
await git.addWorktree(context.rootPath, targetPath, options.target, {
|
|
4694
|
-
createBranch: !branchExistsLocally,
|
|
4695
|
-
...track === undefined ? {} : { track }
|
|
4696
|
-
});
|
|
4697
|
-
} catch (error) {
|
|
4698
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to create worktree: ${error.message}`);
|
|
4699
|
-
} finally {
|
|
4700
|
-
await lock.release();
|
|
4701
|
-
}
|
|
4702
|
-
return ok({
|
|
4703
|
-
path: targetPath,
|
|
4704
|
-
data: { branch: options.target, created: true }
|
|
4705
5657
|
});
|
|
4706
5658
|
};
|
|
4707
5659
|
|
|
@@ -4709,13 +5661,13 @@ var jump = async (git, fs, term, options) => {
|
|
|
4709
5661
|
var ls = async (git, fs, options) => {
|
|
4710
5662
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4711
5663
|
const entries = await Promise.all(context.worktrees.map(async (wt) => {
|
|
4712
|
-
const [
|
|
4713
|
-
|
|
5664
|
+
const [dirtyState, aheadBehind] = await Promise.all([
|
|
5665
|
+
worktreeDirtyState(git, fs, context.worktrees, wt),
|
|
4714
5666
|
wt.branch === null ? Promise.resolve(null) : git.aheadBehind(context.rootPath, wt.branch)
|
|
4715
5667
|
]);
|
|
4716
5668
|
return {
|
|
4717
5669
|
...wt,
|
|
4718
|
-
dirty,
|
|
5670
|
+
dirty: dirtyState ?? false,
|
|
4719
5671
|
ahead: aheadBehind?.ahead ?? null,
|
|
4720
5672
|
behind: aheadBehind?.behind ?? null
|
|
4721
5673
|
};
|
|
@@ -4755,26 +5707,26 @@ import { execFile as execFileCb } from "node:child_process";
|
|
|
4755
5707
|
import { promisify } from "node:util";
|
|
4756
5708
|
|
|
4757
5709
|
// src/domain/dirty.ts
|
|
4758
|
-
var filterOutNestedWorktreePaths = (relativePaths, targetPath,
|
|
5710
|
+
var filterOutNestedWorktreePaths = (relativePaths, targetPath, otherWorktreePaths) => {
|
|
4759
5711
|
const target = stripTrailingSlash(targetPath);
|
|
4760
|
-
const others =
|
|
5712
|
+
const others = otherWorktreePaths.map((path) => stripTrailingSlash(path)).filter((path) => path !== target);
|
|
4761
5713
|
if (others.length === 0) {
|
|
4762
5714
|
return [...relativePaths];
|
|
4763
5715
|
}
|
|
4764
5716
|
return relativePaths.filter((relativePath) => {
|
|
4765
5717
|
const absolutePath = stripTrailingSlash(`${target}/${relativePath}`);
|
|
4766
|
-
return !others.some((other) =>
|
|
5718
|
+
return !others.some((other) => absolutePath === other || isWithin(other, absolutePath));
|
|
4767
5719
|
});
|
|
4768
5720
|
};
|
|
4769
|
-
var isDirtyFromStatus = (statusOutput, targetPath,
|
|
5721
|
+
var isDirtyFromStatus = (statusOutput, targetPath, otherWorktreePaths) => {
|
|
4770
5722
|
const paths = parseStatusPaths(statusOutput);
|
|
4771
|
-
return filterOutNestedWorktreePaths(paths, targetPath,
|
|
5723
|
+
return filterOutNestedWorktreePaths(paths, targetPath, otherWorktreePaths).length > 0;
|
|
4772
5724
|
};
|
|
4773
5725
|
var stripTrailingSlash = (path) => path.endsWith("/") ? path.slice(0, -1) : path;
|
|
4774
|
-
var
|
|
4775
|
-
var isRenameOrCopyCode = (code) => code.includes("R") || code.includes("C");
|
|
5726
|
+
var STATUS_CODE_WIDTH = 3;
|
|
4776
5727
|
var RECORDS_PER_RENAME_OR_COPY = 2;
|
|
4777
5728
|
var RECORDS_PER_ORDINARY_ENTRY = 1;
|
|
5729
|
+
var isRenameOrCopyCode = (code) => code.includes("R") || code.includes("C");
|
|
4778
5730
|
var parseStatusPaths = (statusOutput) => {
|
|
4779
5731
|
const records = statusOutput.split("\x00").filter((record) => record.length > 0);
|
|
4780
5732
|
const paths = [];
|
|
@@ -4791,18 +5743,17 @@ var parseStatusPaths = (statusOutput) => {
|
|
|
4791
5743
|
}
|
|
4792
5744
|
return paths;
|
|
4793
5745
|
};
|
|
4794
|
-
var STATUS_CODE_WIDTH = 3;
|
|
4795
5746
|
|
|
4796
5747
|
// src/infra/git-executable.ts
|
|
4797
|
-
import { constants } from "node:fs";
|
|
4798
|
-
import { access, stat } from "node:fs/promises";
|
|
4799
|
-
var
|
|
5748
|
+
import { constants as constants3 } from "node:fs";
|
|
5749
|
+
import { access as access3, stat as stat2 } from "node:fs/promises";
|
|
5750
|
+
var isExecutableFile2 = async (path) => {
|
|
4800
5751
|
try {
|
|
4801
|
-
const stats = await
|
|
5752
|
+
const stats = await stat2(path);
|
|
4802
5753
|
if (!stats.isFile()) {
|
|
4803
5754
|
return false;
|
|
4804
5755
|
}
|
|
4805
|
-
await
|
|
5756
|
+
await access3(path, constants3.X_OK);
|
|
4806
5757
|
return true;
|
|
4807
5758
|
} catch {
|
|
4808
5759
|
return false;
|
|
@@ -4813,7 +5764,7 @@ var resolveGitExecutable = async (env2) => {
|
|
|
4813
5764
|
override: env2[GIT_EXECUTABLE_OVERRIDE_ENV],
|
|
4814
5765
|
path: env2.PATH
|
|
4815
5766
|
});
|
|
4816
|
-
const probes = candidates.map(async (candidate) => await
|
|
5767
|
+
const probes = candidates.map(async (candidate) => await isExecutableFile2(candidate) ? candidate : undefined);
|
|
4817
5768
|
for (const probe of probes) {
|
|
4818
5769
|
const found = await probe;
|
|
4819
5770
|
if (found !== undefined) {
|
|
@@ -4867,9 +5818,9 @@ var createWorktreeMethods = () => ({
|
|
|
4867
5818
|
const out = await run(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
4868
5819
|
return out.trim();
|
|
4869
5820
|
},
|
|
4870
|
-
async isDirty(path,
|
|
5821
|
+
async isDirty(path, otherWorktreePaths) {
|
|
4871
5822
|
const status = await run(path, ["status", "--porcelain", "-z", "--untracked-files=all"]);
|
|
4872
|
-
return isDirtyFromStatus(status, path,
|
|
5823
|
+
return isDirtyFromStatus(status, path, otherWorktreePaths);
|
|
4873
5824
|
},
|
|
4874
5825
|
async addWorktree(cwd, path, branch, options) {
|
|
4875
5826
|
const args = ["worktree", "add"];
|
|
@@ -5052,7 +6003,7 @@ var createTermPort = () => ({
|
|
|
5052
6003
|
var USAGE = `Usage: hop [command] [options]
|
|
5053
6004
|
|
|
5054
6005
|
hop Pick a worktree/branch interactively and cd into it (TTY); lists worktrees otherwise
|
|
5055
|
-
hop <branch> Create-or-jump: cd into <branch>'s worktree
|
|
6006
|
+
hop <branch> Create-or-jump: cd into <branch>'s worktree (pass --create to create a missing one)
|
|
5056
6007
|
hop root cd into the root clone
|
|
5057
6008
|
hop - cd back to the previous worktree
|
|
5058
6009
|
hop -- <branch> Escape a branch name that collides with a reserved command (ls/rm/clean/root/init/help)
|
|
@@ -5064,12 +6015,16 @@ var USAGE = `Usage: hop [command] [options]
|
|
|
5064
6015
|
hop root - Switch the root clone back
|
|
5065
6016
|
hop init zsh Print the zsh shell integration (eval "$(hop init zsh)")
|
|
5066
6017
|
|
|
6018
|
+
hop --update Update hop to the latest release, the way it was installed
|
|
6019
|
+
hop --update --check Only report whether an update is available; change nothing
|
|
6020
|
+
hop --version Print hop's version
|
|
6021
|
+
|
|
5067
6022
|
Interactive picker keys:
|
|
5068
6023
|
Enter cd into the selected candidate
|
|
5069
6024
|
Tab, →, Ctrl+L, Ctrl+F Open the action panel, as a column beside the list
|
|
5070
6025
|
(stacks below it instead on narrow terminals)
|
|
5071
6026
|
Ctrl+X Delete the selected worktree (y/N confirmation)
|
|
5072
|
-
Ctrl+R Switch the root clone to the selected branch
|
|
6027
|
+
Ctrl+R Switch the root clone to the selected branch (y/N first for an external worktree)
|
|
5073
6028
|
↑/↓, Ctrl+P/N, Ctrl+K/J Move the selection (arrow, emacs, and vim keys all work)
|
|
5074
6029
|
Esc Cancel (exit 0, no output)
|
|
5075
6030
|
Ctrl+C Cancel like an interrupt (exit 130, same as SIGINT)
|
|
@@ -5077,7 +6032,7 @@ Interactive picker keys:
|
|
|
5077
6032
|
action, c/d/r to run cd/delete/switchRoot directly, Esc/Tab/←/Ctrl+H to close
|
|
5078
6033
|
|
|
5079
6034
|
Options:
|
|
5080
|
-
--create Create the worktree when jumping to a branch without one (required
|
|
6035
|
+
--create Create the worktree when jumping to a branch without one (always required, TTY or not)
|
|
5081
6036
|
--json Output JSON instead of plain text
|
|
5082
6037
|
--force Force removal even if the worktree is dirty (hop rm)
|
|
5083
6038
|
--ext [deprecated, no-op for hop rm] hop rm no longer requires it to
|
|
@@ -5092,8 +6047,9 @@ Options:
|
|
|
5092
6047
|
var git = createGitPort();
|
|
5093
6048
|
var fs = createFsPort();
|
|
5094
6049
|
var term = createTermPort();
|
|
5095
|
-
var
|
|
5096
|
-
|
|
6050
|
+
var runLs = async (json) => {
|
|
6051
|
+
const result = await ls(git, fs, { cwd: process.cwd() });
|
|
6052
|
+
reportResult("ls", result, json);
|
|
5097
6053
|
};
|
|
5098
6054
|
var lsCommand = defineCommand({
|
|
5099
6055
|
meta: { name: "ls", description: "List worktrees" },
|
|
@@ -5101,9 +6057,7 @@ var lsCommand = defineCommand({
|
|
|
5101
6057
|
json: { type: "boolean", description: "Output JSON" }
|
|
5102
6058
|
},
|
|
5103
6059
|
async run({ args }) {
|
|
5104
|
-
|
|
5105
|
-
render("ls", result, Boolean(args.json));
|
|
5106
|
-
applyExitCode(result);
|
|
6060
|
+
await runLs(Boolean(args.json));
|
|
5107
6061
|
}
|
|
5108
6062
|
});
|
|
5109
6063
|
var rmCommand = defineCommand({
|
|
@@ -5128,11 +6082,10 @@ var rmCommand = defineCommand({
|
|
|
5128
6082
|
force: Boolean(args.force),
|
|
5129
6083
|
ext: Boolean(args.ext)
|
|
5130
6084
|
});
|
|
5131
|
-
|
|
5132
|
-
applyExitCode(result);
|
|
6085
|
+
reportResult("rm", result, Boolean(args.json));
|
|
5133
6086
|
}
|
|
5134
6087
|
});
|
|
5135
|
-
var rootCommand = createRootCommand(git, fs
|
|
6088
|
+
var rootCommand = createRootCommand(git, fs);
|
|
5136
6089
|
var cleanCommand = defineCommand({
|
|
5137
6090
|
meta: {
|
|
5138
6091
|
name: "clean",
|
|
@@ -5153,8 +6106,7 @@ var cleanCommand = defineCommand({
|
|
|
5153
6106
|
dryRun: Boolean(args.dryRun),
|
|
5154
6107
|
yes: Boolean(args.yes)
|
|
5155
6108
|
});
|
|
5156
|
-
|
|
5157
|
-
applyExitCode(result);
|
|
6109
|
+
reportResult("clean", result, Boolean(args.json));
|
|
5158
6110
|
}
|
|
5159
6111
|
});
|
|
5160
6112
|
var initCommand = defineCommand({
|
|
@@ -5178,14 +6130,13 @@ var initCommand = defineCommand({
|
|
|
5178
6130
|
}
|
|
5179
6131
|
});
|
|
5180
6132
|
var runJump = async (target, options) => {
|
|
5181
|
-
const result = await jump(git, fs,
|
|
6133
|
+
const result = await jump(git, fs, {
|
|
5182
6134
|
cwd: process.cwd(),
|
|
5183
6135
|
target,
|
|
5184
6136
|
create: options.create,
|
|
5185
6137
|
...options.track === undefined ? {} : { track: options.track }
|
|
5186
6138
|
});
|
|
5187
|
-
|
|
5188
|
-
applyExitCode(result);
|
|
6139
|
+
reportResult("jump", result, options.json);
|
|
5189
6140
|
};
|
|
5190
6141
|
var RESERVED_COMMANDS = {
|
|
5191
6142
|
ls: lsCommand,
|
|
@@ -5234,12 +6185,7 @@ var runInteractivePick = async (json) => {
|
|
|
5234
6185
|
path: selected.worktree.path,
|
|
5235
6186
|
data: { branch: selected.worktree.branch, created: false }
|
|
5236
6187
|
});
|
|
5237
|
-
|
|
5238
|
-
render("pick", result, true);
|
|
5239
|
-
} else {
|
|
5240
|
-
process.stdout.write(`${selected.worktree.path}
|
|
5241
|
-
`);
|
|
5242
|
-
}
|
|
6188
|
+
render("pick", result, json);
|
|
5243
6189
|
return;
|
|
5244
6190
|
}
|
|
5245
6191
|
await runJump(selected.branch, { create: true, json });
|
|
@@ -5251,9 +6197,7 @@ var runJumpFromArgs = async (rawArgs) => {
|
|
|
5251
6197
|
await runInteractivePick(Boolean(args.json));
|
|
5252
6198
|
return;
|
|
5253
6199
|
}
|
|
5254
|
-
|
|
5255
|
-
render("ls", result, Boolean(args.json));
|
|
5256
|
-
applyExitCode(result);
|
|
6200
|
+
await runLs(Boolean(args.json));
|
|
5257
6201
|
return;
|
|
5258
6202
|
}
|
|
5259
6203
|
await runJump(String(args.target), {
|
|
@@ -5281,6 +6225,16 @@ var main = async () => {
|
|
|
5281
6225
|
process.exitCode = 0;
|
|
5282
6226
|
return;
|
|
5283
6227
|
}
|
|
6228
|
+
if (isVersionRequest(rawArgs)) {
|
|
6229
|
+
process.stdout.write(`${VERSION}
|
|
6230
|
+
`);
|
|
6231
|
+
process.exitCode = EXIT_SUCCESS;
|
|
6232
|
+
return;
|
|
6233
|
+
}
|
|
6234
|
+
if (isUpdateRequest(rawArgs)) {
|
|
6235
|
+
await runUpdate(rawArgs, term);
|
|
6236
|
+
return;
|
|
6237
|
+
}
|
|
5284
6238
|
const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
|
|
5285
6239
|
try {
|
|
5286
6240
|
await dispatchAndRun(dispatch);
|