@n-seiji/nuthatch 0.1.3 → 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 +47 -6
- package/README.md +47 -9
- package/dist/cli.js +1601 -530
- package/package.json +2 -2
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 === "--") {
|
|
@@ -2054,6 +2057,31 @@ var dispatchCliArgs = (rawArgs, reservedNames) => {
|
|
|
2054
2057
|
return { kind: "jump", args: rawArgs };
|
|
2055
2058
|
};
|
|
2056
2059
|
|
|
2060
|
+
// src/domain/git-executable.ts
|
|
2061
|
+
var GIT_EXECUTABLE_OVERRIDE_ENV = "HOP_GIT";
|
|
2062
|
+
var GIT_NOT_FOUND_ERROR_CODE = "HOP_GIT_NOT_FOUND";
|
|
2063
|
+
var GIT_NOT_FOUND_HINT = `Install git, or set ${GIT_EXECUTABLE_OVERRIDE_ENV} to its absolute path.`;
|
|
2064
|
+
var GIT_FALLBACK_BIN_DIRS = [
|
|
2065
|
+
"/opt/homebrew/bin",
|
|
2066
|
+
"/usr/local/bin",
|
|
2067
|
+
"/usr/bin",
|
|
2068
|
+
"/bin"
|
|
2069
|
+
];
|
|
2070
|
+
var PATH_SEPARATOR = ":";
|
|
2071
|
+
var joinBin = (dir) => {
|
|
2072
|
+
const base = dir.endsWith("/") ? dir.slice(0, -1) : dir;
|
|
2073
|
+
return `${base}/git`;
|
|
2074
|
+
};
|
|
2075
|
+
var gitExecutableCandidates = (input) => {
|
|
2076
|
+
const override = input.override?.trim() ?? "";
|
|
2077
|
+
if (override.length > 0) {
|
|
2078
|
+
return [override];
|
|
2079
|
+
}
|
|
2080
|
+
const dirs = (input.path ?? "").split(PATH_SEPARATOR).map((entry) => entry.trim()).filter((entry) => entry.startsWith("/"));
|
|
2081
|
+
return [...new Set([...dirs, ...GIT_FALLBACK_BIN_DIRS].map((dir) => joinBin(dir)))];
|
|
2082
|
+
};
|
|
2083
|
+
var gitNotFoundMessage = (candidates) => `git executable not found. Looked in: ${candidates.join(", ")}. ${GIT_NOT_FOUND_HINT}`;
|
|
2084
|
+
|
|
2057
2085
|
// src/domain/result.ts
|
|
2058
2086
|
var EXIT_SUCCESS = 0;
|
|
2059
2087
|
var EXIT_GENERAL_ERROR = 1;
|
|
@@ -2072,6 +2100,78 @@ var fail = (exitCode, errorMessage, warnings) => ({
|
|
|
2072
2100
|
...warnings === undefined || warnings.length === 0 ? {} : { warnings }
|
|
2073
2101
|
});
|
|
2074
2102
|
|
|
2103
|
+
// src/domain/fatal-error.ts
|
|
2104
|
+
var fieldsOf = (error) => typeof error === "object" && error !== null ? error : {};
|
|
2105
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
2106
|
+
var trimmedStringOf = (value) => typeof value === "string" ? value.trim() : "";
|
|
2107
|
+
var isSpawnFailure = (fields) => fields.code === "ENOENT" && typeof fields.syscall === "string" && fields.syscall.startsWith("spawn");
|
|
2108
|
+
var isGitFailure = (fields) => typeof fields.code === "number" || typeof fields.signal === "string" && fields.signal !== "";
|
|
2109
|
+
var describeFatalError = (error) => {
|
|
2110
|
+
const fields = fieldsOf(error);
|
|
2111
|
+
if (isSpawnFailure(fields)) {
|
|
2112
|
+
const binary = trimmedStringOf(fields.path) || "git";
|
|
2113
|
+
return {
|
|
2114
|
+
message: `failed to run ${binary}: no such executable. ${GIT_NOT_FOUND_HINT}`,
|
|
2115
|
+
exitCode: EXIT_GENERAL_ERROR
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (isGitFailure(fields)) {
|
|
2119
|
+
const stderr = trimmedStringOf(fields.stderr);
|
|
2120
|
+
return {
|
|
2121
|
+
message: stderr.length > 0 ? stderr : messageOf(error),
|
|
2122
|
+
exitCode: EXIT_SAFE_REJECTION
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
return { message: messageOf(error), exitCode: EXIT_GENERAL_ERROR };
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
// src/render.ts
|
|
2129
|
+
var PLAIN_JSON_INDENT = 2;
|
|
2130
|
+
var render = (command, result, json) => {
|
|
2131
|
+
if (json) {
|
|
2132
|
+
const envelope = {
|
|
2133
|
+
schemaVersion: 1,
|
|
2134
|
+
command,
|
|
2135
|
+
data: result.ok ? result.data : undefined,
|
|
2136
|
+
warnings: result.warnings ?? []
|
|
2137
|
+
};
|
|
2138
|
+
process.stdout.write(`${JSON.stringify(envelope)}
|
|
2139
|
+
`);
|
|
2140
|
+
if (!result.ok && result.errorMessage !== undefined) {
|
|
2141
|
+
process.stderr.write(`${result.errorMessage}
|
|
2142
|
+
`);
|
|
2143
|
+
}
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (result.path !== undefined) {
|
|
2147
|
+
process.stdout.write(`${result.path}
|
|
2148
|
+
`);
|
|
2149
|
+
} else if (result.ok && result.data !== undefined) {
|
|
2150
|
+
process.stdout.write(`${JSON.stringify(result.data, null, PLAIN_JSON_INDENT)}
|
|
2151
|
+
`);
|
|
2152
|
+
}
|
|
2153
|
+
for (const warning of result.warnings ?? []) {
|
|
2154
|
+
process.stderr.write(`warning: ${warning}
|
|
2155
|
+
`);
|
|
2156
|
+
}
|
|
2157
|
+
if (!result.ok && result.errorMessage !== undefined) {
|
|
2158
|
+
process.stderr.write(`${result.errorMessage}
|
|
2159
|
+
`);
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
var reportResult = (command, result, json) => {
|
|
2163
|
+
render(command, result, json);
|
|
2164
|
+
process.exitCode = result.exitCode;
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
// src/cli-fatal.ts
|
|
2168
|
+
var wantsJson = (args) => args.some((arg) => arg === "--json" || arg.startsWith("--json="));
|
|
2169
|
+
var reportFatalError = (command, json, error) => {
|
|
2170
|
+
const fatal = describeFatalError(error);
|
|
2171
|
+
const result = fail(fatal.exitCode, `hop: ${fatal.message}`);
|
|
2172
|
+
reportResult(command, result, json);
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2075
2175
|
// src/infra/lock.ts
|
|
2076
2176
|
import { randomUUID } from "node:crypto";
|
|
2077
2177
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -2168,17 +2268,27 @@ var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
|
|
|
2168
2268
|
}
|
|
2169
2269
|
};
|
|
2170
2270
|
};
|
|
2171
|
-
var
|
|
2271
|
+
var acquireUnlessHeld = async (commonDir) => {
|
|
2172
2272
|
try {
|
|
2173
|
-
|
|
2174
|
-
return { ok: true, lock };
|
|
2273
|
+
return await acquireRepoLock(commonDir);
|
|
2175
2274
|
} catch (error) {
|
|
2176
2275
|
if (error instanceof LockHeldError) {
|
|
2177
|
-
return
|
|
2276
|
+
return error;
|
|
2178
2277
|
}
|
|
2179
2278
|
throw error;
|
|
2180
2279
|
}
|
|
2181
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
|
+
};
|
|
2182
2292
|
|
|
2183
2293
|
// src/infra/repo.ts
|
|
2184
2294
|
import { basename, dirname, join as join2 } from "node:path";
|
|
@@ -2286,6 +2396,21 @@ var parsePorcelain = (output) => {
|
|
|
2286
2396
|
return result;
|
|
2287
2397
|
};
|
|
2288
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
|
+
|
|
2289
2414
|
// src/infra/repo.ts
|
|
2290
2415
|
var loadRepoContext = async (git, fs, cwd) => {
|
|
2291
2416
|
const [commonDir, porcelainOut] = await Promise.all([
|
|
@@ -2309,8 +2434,34 @@ var loadRepoContext = async (git, fs, cwd) => {
|
|
|
2309
2434
|
}));
|
|
2310
2435
|
return { rootPath, managedRoot, commonDir, worktrees };
|
|
2311
2436
|
};
|
|
2312
|
-
var
|
|
2437
|
+
var nestedWorktreePaths = (worktrees, path) => nestedWorktrees(worktrees, path).map((wt) => wt.path);
|
|
2313
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
|
+
};
|
|
2314
2465
|
var realpathOrRaw = async (fs, path) => {
|
|
2315
2466
|
try {
|
|
2316
2467
|
return await fs.realpath(path);
|
|
@@ -2321,6 +2472,7 @@ var realpathOrRaw = async (fs, path) => {
|
|
|
2321
2472
|
|
|
2322
2473
|
// src/commands/root-holder-swap.ts
|
|
2323
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").`);
|
|
2324
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.`);
|
|
2325
2477
|
var resolveHolderSwap = async ({
|
|
2326
2478
|
git,
|
|
@@ -2360,7 +2512,10 @@ var resolveHolderSwap = async ({
|
|
|
2360
2512
|
rejection: holderRejection(target, freshHolder.path, `is locked by git${freshHolder.lockReason === null ? "" : ` (${freshHolder.lockReason})`}`)
|
|
2361
2513
|
};
|
|
2362
2514
|
}
|
|
2363
|
-
|
|
2515
|
+
if (freshHolder.prunable) {
|
|
2516
|
+
return { rejection: prunableHolderRejection(target, freshHolder.path) };
|
|
2517
|
+
}
|
|
2518
|
+
const holderDirty = await isWorktreeDirty(git, fresh.worktrees, freshHolder.path);
|
|
2364
2519
|
if (holderDirty) {
|
|
2365
2520
|
return {
|
|
2366
2521
|
rejection: holderRejection(target, freshHolder.path, "has uncommitted or untracked changes")
|
|
@@ -2373,20 +2528,19 @@ var resolveHolderSwap = async ({
|
|
|
2373
2528
|
// src/commands/root.ts
|
|
2374
2529
|
var root = async (git, fs, options) => {
|
|
2375
2530
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
2376
|
-
const rootWorktree = context.worktrees.find((wt) => wt.kind === "root");
|
|
2377
2531
|
if (options.target === undefined) {
|
|
2378
2532
|
return ok({
|
|
2379
2533
|
path: context.rootPath,
|
|
2380
2534
|
data: {
|
|
2381
|
-
branch:
|
|
2535
|
+
branch: rootBranchOf(context),
|
|
2382
2536
|
switched: false,
|
|
2383
2537
|
detachedHolder: null
|
|
2384
2538
|
}
|
|
2385
2539
|
});
|
|
2386
2540
|
}
|
|
2387
|
-
const
|
|
2388
|
-
if (
|
|
2389
|
-
return
|
|
2541
|
+
const dirtyRejection = await dirtyRootRejection(git, context);
|
|
2542
|
+
if (dirtyRejection !== null) {
|
|
2543
|
+
return dirtyRejection;
|
|
2390
2544
|
}
|
|
2391
2545
|
if (options.target === "-") {
|
|
2392
2546
|
return switchAndReport({
|
|
@@ -2398,57 +2552,64 @@ var root = async (git, fs, options) => {
|
|
|
2398
2552
|
holderPolicy: { allowExternal: true }
|
|
2399
2553
|
});
|
|
2400
2554
|
}
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
let track = initialTrack;
|
|
2405
|
-
if (!branchExistsLocally && track === undefined) {
|
|
2406
|
-
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
2407
|
-
const [firstRemote] = remotes;
|
|
2408
|
-
if (remotes.includes("origin")) {
|
|
2409
|
-
track = `origin/${options.target}`;
|
|
2410
|
-
} else if (remotes.length === 1 && firstRemote !== undefined) {
|
|
2411
|
-
track = `${firstRemote}/${options.target}`;
|
|
2412
|
-
} else if (remotes.length > 1) {
|
|
2413
|
-
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
2414
|
-
}
|
|
2555
|
+
const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
|
|
2556
|
+
if (!checkout.ok) {
|
|
2557
|
+
return checkout.rejection;
|
|
2415
2558
|
}
|
|
2416
2559
|
return switchAndReport({
|
|
2417
2560
|
git,
|
|
2418
2561
|
fs,
|
|
2419
2562
|
context,
|
|
2420
2563
|
target: options.target,
|
|
2421
|
-
switchOptions:
|
|
2422
|
-
createBranch: !branchExistsLocally,
|
|
2423
|
-
...track === undefined ? {} : { track }
|
|
2424
|
-
},
|
|
2564
|
+
switchOptions: checkout.options,
|
|
2425
2565
|
holderPolicy: {
|
|
2426
2566
|
allowExternal: options.allowExternalHolderSwap ?? true,
|
|
2427
2567
|
...options.expectedHolderPath === undefined ? {} : { expectedPath: options.expectedHolderPath }
|
|
2428
2568
|
}
|
|
2429
2569
|
});
|
|
2430
2570
|
};
|
|
2431
|
-
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 = ({
|
|
2432
2598
|
git,
|
|
2433
2599
|
fs,
|
|
2434
2600
|
context,
|
|
2435
2601
|
target,
|
|
2436
2602
|
switchOptions,
|
|
2437
2603
|
holderPolicy
|
|
2438
|
-
}) => {
|
|
2439
|
-
const acquisition = await acquireRepoLockOrRejection(context.commonDir);
|
|
2440
|
-
if (!acquisition.ok) {
|
|
2441
|
-
return acquisition.rejection;
|
|
2442
|
-
}
|
|
2443
|
-
const { lock } = acquisition;
|
|
2604
|
+
}) => withRepoLock(context.commonDir, async () => {
|
|
2444
2605
|
let detachedHolder = null;
|
|
2445
2606
|
let previousBranch = null;
|
|
2446
2607
|
try {
|
|
2447
2608
|
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
2448
|
-
previousBranch = fresh
|
|
2449
|
-
const stillDirty = await git
|
|
2450
|
-
if (stillDirty) {
|
|
2451
|
-
return
|
|
2609
|
+
previousBranch = rootBranchOf(fresh);
|
|
2610
|
+
const stillDirty = await dirtyRootRejection(git, fresh);
|
|
2611
|
+
if (stillDirty !== null) {
|
|
2612
|
+
return stillDirty;
|
|
2452
2613
|
}
|
|
2453
2614
|
const holderSwap = await resolveHolderSwap({
|
|
2454
2615
|
git,
|
|
@@ -2465,7 +2626,7 @@ var switchAndReport = async ({
|
|
|
2465
2626
|
let resolvedBranch = target;
|
|
2466
2627
|
if (target === "-") {
|
|
2467
2628
|
const afterSwitch = await loadRepoContext(git, fs, fresh.rootPath);
|
|
2468
|
-
resolvedBranch = afterSwitch
|
|
2629
|
+
resolvedBranch = rootBranchOf(afterSwitch);
|
|
2469
2630
|
}
|
|
2470
2631
|
return ok({
|
|
2471
2632
|
path: fresh.rootPath,
|
|
@@ -2477,61 +2638,14 @@ var switchAndReport = async ({
|
|
|
2477
2638
|
warnings: detachedHolder === null ? [] : [`Put ${detachedHolder.path} into detached HEAD`]
|
|
2478
2639
|
});
|
|
2479
2640
|
} catch (error) {
|
|
2480
|
-
const
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
if (detachedHolder !== null) {
|
|
2489
|
-
try {
|
|
2490
|
-
await git.switchBranch(detachedHolder.path, detachedHolder.branch, {});
|
|
2491
|
-
} catch {
|
|
2492
|
-
rollbackWarnings.push(`Could not restore ${detachedHolder.path} to its original branch (${detachedHolder.branch}); it remains in detached HEAD`);
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${error.message}`, rollbackWarnings);
|
|
2496
|
-
} finally {
|
|
2497
|
-
await lock.release();
|
|
2498
|
-
}
|
|
2499
|
-
};
|
|
2500
|
-
|
|
2501
|
-
// src/render.ts
|
|
2502
|
-
var PLAIN_JSON_INDENT = 2;
|
|
2503
|
-
var render = (command, result, json) => {
|
|
2504
|
-
if (json) {
|
|
2505
|
-
const envelope = {
|
|
2506
|
-
schemaVersion: 1,
|
|
2507
|
-
command,
|
|
2508
|
-
data: result.ok ? result.data : undefined,
|
|
2509
|
-
warnings: result.warnings ?? []
|
|
2510
|
-
};
|
|
2511
|
-
process.stdout.write(`${JSON.stringify(envelope)}
|
|
2512
|
-
`);
|
|
2513
|
-
if (!result.ok && result.errorMessage !== undefined) {
|
|
2514
|
-
process.stderr.write(`${result.errorMessage}
|
|
2515
|
-
`);
|
|
2516
|
-
}
|
|
2517
|
-
return;
|
|
2518
|
-
}
|
|
2519
|
-
if (result.path !== undefined) {
|
|
2520
|
-
process.stdout.write(`${result.path}
|
|
2521
|
-
`);
|
|
2522
|
-
} else if (result.ok && result.data !== undefined) {
|
|
2523
|
-
process.stdout.write(`${JSON.stringify(result.data, null, PLAIN_JSON_INDENT)}
|
|
2524
|
-
`);
|
|
2525
|
-
}
|
|
2526
|
-
for (const warning of result.warnings ?? []) {
|
|
2527
|
-
process.stderr.write(`warning: ${warning}
|
|
2528
|
-
`);
|
|
2529
|
-
}
|
|
2530
|
-
if (!result.ok && result.errorMessage !== undefined) {
|
|
2531
|
-
process.stderr.write(`${result.errorMessage}
|
|
2532
|
-
`);
|
|
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);
|
|
2533
2647
|
}
|
|
2534
|
-
};
|
|
2648
|
+
});
|
|
2535
2649
|
|
|
2536
2650
|
// src/cli-root-command.ts
|
|
2537
2651
|
var ROOT_PREVIOUS_TOKEN = " hop-root-previous ";
|
|
@@ -2553,7 +2667,7 @@ var rewriteRootPreviousToken = (args) => {
|
|
|
2553
2667
|
return rewritten;
|
|
2554
2668
|
};
|
|
2555
2669
|
var resolveRootTarget = (branch) => branch === ROOT_PREVIOUS_TOKEN ? "-" : branch;
|
|
2556
|
-
var createRootCommand = (git, fs
|
|
2670
|
+
var createRootCommand = (git, fs) => defineCommand({
|
|
2557
2671
|
meta: {
|
|
2558
2672
|
name: "root",
|
|
2559
2673
|
description: "cd to the root clone, or temporarily switch its branch"
|
|
@@ -2577,8 +2691,7 @@ var createRootCommand = (git, fs, applyExitCode) => defineCommand({
|
|
|
2577
2691
|
...target === undefined ? {} : { target: String(target) },
|
|
2578
2692
|
...args.track === undefined ? {} : { track: String(args.track) }
|
|
2579
2693
|
});
|
|
2580
|
-
|
|
2581
|
-
applyExitCode(result);
|
|
2694
|
+
reportResult("root", result, Boolean(args.json));
|
|
2582
2695
|
}
|
|
2583
2696
|
});
|
|
2584
2697
|
|
|
@@ -2618,7 +2731,7 @@ var pick = async (git, fs, options) => {
|
|
|
2618
2731
|
git.listRemoteBranches(context.rootPath),
|
|
2619
2732
|
Promise.all(context.worktrees.map(async (worktree) => [
|
|
2620
2733
|
worktree.path,
|
|
2621
|
-
|
|
2734
|
+
await worktreeDirtyState(git, fs, context.worktrees, worktree)
|
|
2622
2735
|
]))
|
|
2623
2736
|
]);
|
|
2624
2737
|
const dirtyByPath = new Map(dirtyEntries);
|
|
@@ -2629,6 +2742,8 @@ var pick = async (git, fs, options) => {
|
|
|
2629
2742
|
// src/commands/rm.ts
|
|
2630
2743
|
var EXT_DEPRECATION_WARNING = "--ext is deprecated and no longer required: hop rm can now remove external worktrees without it.";
|
|
2631
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.`;
|
|
2632
2747
|
var resolveTarget = (worktrees, branch, expectedPath) => {
|
|
2633
2748
|
const matches = worktrees.filter((wt) => wt.branch === branch);
|
|
2634
2749
|
if (expectedPath !== undefined) {
|
|
@@ -2655,7 +2770,7 @@ var nestedWorktreeRejection = (branch, targetPath, worktrees) => {
|
|
|
2655
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:
|
|
2656
2771
|
${listing}`);
|
|
2657
2772
|
};
|
|
2658
|
-
var targetSafetyRejection = async (git, context,
|
|
2773
|
+
var targetSafetyRejection = async ({ git, fs, context, options }, target) => {
|
|
2659
2774
|
const nestedRejection = nestedWorktreeRejection(options.branch, target.path, context.worktrees);
|
|
2660
2775
|
if (nestedRejection !== null) {
|
|
2661
2776
|
return nestedRejection;
|
|
@@ -2663,61 +2778,64 @@ var targetSafetyRejection = async (git, context, target, options) => {
|
|
|
2663
2778
|
if (target.locked) {
|
|
2664
2779
|
return lockedRejection(options.branch, target.lockReason);
|
|
2665
2780
|
}
|
|
2666
|
-
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) {
|
|
2667
2785
|
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
2668
2786
|
}
|
|
2669
2787
|
return null;
|
|
2670
2788
|
};
|
|
2671
|
-
var
|
|
2672
|
-
const
|
|
2673
|
-
...result,
|
|
2674
|
-
warnings: [...result.warnings ?? [], EXT_DEPRECATION_WARNING]
|
|
2675
|
-
} : result;
|
|
2676
|
-
const context = await loadRepoContext(git, fs, options.cwd);
|
|
2789
|
+
var validateRemoval = async (check) => {
|
|
2790
|
+
const { context, options } = check;
|
|
2677
2791
|
const resolved = resolveTarget(context.worktrees, options.branch, options.expectedPath);
|
|
2678
2792
|
if (resolved.rejection !== undefined) {
|
|
2679
|
-
return
|
|
2793
|
+
return { ok: false, rejection: resolved.rejection };
|
|
2680
2794
|
}
|
|
2681
2795
|
const { target } = resolved;
|
|
2682
2796
|
if (target === undefined) {
|
|
2683
|
-
return
|
|
2797
|
+
return {
|
|
2798
|
+
ok: false,
|
|
2799
|
+
rejection: fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`)
|
|
2800
|
+
};
|
|
2684
2801
|
}
|
|
2685
2802
|
if (target.kind === "root") {
|
|
2686
|
-
return
|
|
2687
|
-
}
|
|
2688
|
-
const safetyRejection = await targetSafetyRejection(git, context, target, options);
|
|
2689
|
-
if (safetyRejection !== null) {
|
|
2690
|
-
return finish(safetyRejection);
|
|
2803
|
+
return { ok: false, rejection: fail(EXIT_USAGE_ERROR, "Cannot remove the root clone.") };
|
|
2691
2804
|
}
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
|
|
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;
|
|
2695
2817
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
return
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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)}`);
|
|
2710
2833
|
}
|
|
2711
|
-
|
|
2712
|
-
return finish(ok({ data: { branch: options.branch, path: freshTarget.path } }));
|
|
2713
|
-
} catch (error) {
|
|
2714
|
-
return finish(fail(EXIT_SAFE_REJECTION, `Failed to remove worktree: ${error.message}`));
|
|
2715
|
-
} finally {
|
|
2716
|
-
await lock.release();
|
|
2717
|
-
}
|
|
2834
|
+
});
|
|
2718
2835
|
};
|
|
2836
|
+
var rm2 = async (git, fs, options) => withExtWarning(options.ext, await removeWorktree(git, fs, options));
|
|
2719
2837
|
|
|
2720
|
-
// src/ui/picker.ts
|
|
2838
|
+
// src/ui/picker-render.ts
|
|
2721
2839
|
import { homedir } from "node:os";
|
|
2722
2840
|
|
|
2723
2841
|
// src/domain/display-width.ts
|
|
@@ -2796,6 +2914,19 @@ var padToWidth = (value, width) => {
|
|
|
2796
2914
|
return current >= width ? value : value + " ".repeat(width - current);
|
|
2797
2915
|
};
|
|
2798
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
|
+
};
|
|
2799
2930
|
var truncateToWidth = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
2800
2931
|
if (displayWidth(value) <= width) {
|
|
2801
2932
|
return value;
|
|
@@ -2804,17 +2935,7 @@ var truncateToWidth = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
|
2804
2935
|
if (budget < WIDTH_ZERO) {
|
|
2805
2936
|
return "";
|
|
2806
2937
|
}
|
|
2807
|
-
|
|
2808
|
-
let usedWidth = WIDTH_ZERO;
|
|
2809
|
-
for (const grapheme of graphemes(value)) {
|
|
2810
|
-
const nextWidth = usedWidth + graphemeWidth(grapheme);
|
|
2811
|
-
if (nextWidth > budget) {
|
|
2812
|
-
break;
|
|
2813
|
-
}
|
|
2814
|
-
kept += grapheme;
|
|
2815
|
-
usedWidth = nextWidth;
|
|
2816
|
-
}
|
|
2817
|
-
return kept + ellipsis;
|
|
2938
|
+
return takeWithinWidth(graphemes(value), budget).join("") + ellipsis;
|
|
2818
2939
|
};
|
|
2819
2940
|
var truncateToWidthKeepingTail = (value, width, ellipsis = DEFAULT_ELLIPSIS) => {
|
|
2820
2941
|
if (displayWidth(value) <= width) {
|
|
@@ -2824,21 +2945,14 @@ var truncateToWidthKeepingTail = (value, width, ellipsis = DEFAULT_ELLIPSIS) =>
|
|
|
2824
2945
|
if (budget < WIDTH_ZERO) {
|
|
2825
2946
|
return "";
|
|
2826
2947
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
for (const grapheme of graphemes(value).toReversed()) {
|
|
2830
|
-
const nextWidth = usedWidth + graphemeWidth(grapheme);
|
|
2831
|
-
if (nextWidth > budget) {
|
|
2832
|
-
break;
|
|
2833
|
-
}
|
|
2834
|
-
kept = grapheme + kept;
|
|
2835
|
-
usedWidth = nextWidth;
|
|
2836
|
-
}
|
|
2837
|
-
return ellipsis + kept;
|
|
2948
|
+
const keptFromEnd = takeWithinWidth(graphemes(value).toReversed(), budget);
|
|
2949
|
+
return ellipsis + keptFromEnd.toReversed().join("");
|
|
2838
2950
|
};
|
|
2839
2951
|
|
|
2840
|
-
// src/ui/
|
|
2952
|
+
// src/ui/ansi.ts
|
|
2841
2953
|
var ESC = "\x1B";
|
|
2954
|
+
|
|
2955
|
+
// src/ui/picker-frame.ts
|
|
2842
2956
|
var CURSOR_HOME = `${ESC}[H`;
|
|
2843
2957
|
var CLEAR_TO_END = `${ESC}[J`;
|
|
2844
2958
|
var SGR_RESET = `${ESC}[0m`;
|
|
@@ -2975,28 +3089,6 @@ var buildFrameLines = (left, right, stacked, colorEnabled) => {
|
|
|
2975
3089
|
return buildSideBySide(left, right, colorEnabled);
|
|
2976
3090
|
};
|
|
2977
3091
|
|
|
2978
|
-
// src/ui/picker-viewport.ts
|
|
2979
|
-
var DEFAULT_TERMINAL_HEIGHT = 24;
|
|
2980
|
-
var MIN_VISIBLE_CANDIDATE_ROWS = 3;
|
|
2981
|
-
var BASE_CHROME_ROWS = 3;
|
|
2982
|
-
var SECTION_HEADER_ROWS = 2;
|
|
2983
|
-
var HIDDEN_COUNT_INDICATOR_ROWS = 2;
|
|
2984
|
-
var rowBudget = ({ terminalHeight, stackedPanelRows }) => {
|
|
2985
|
-
const reserved = BASE_CHROME_ROWS + SECTION_HEADER_ROWS + HIDDEN_COUNT_INDICATOR_ROWS + stackedPanelRows;
|
|
2986
|
-
return Math.max(MIN_VISIBLE_CANDIDATE_ROWS, terminalHeight - reserved);
|
|
2987
|
-
};
|
|
2988
|
-
var computeViewport = (total, selectedIndex, budget) => {
|
|
2989
|
-
if (total <= budget) {
|
|
2990
|
-
return { start: 0, end: total, hiddenAbove: 0, hiddenBelow: 0 };
|
|
2991
|
-
}
|
|
2992
|
-
const HALF_DIVISOR = 2;
|
|
2993
|
-
const half = Math.floor(budget / HALF_DIVISOR);
|
|
2994
|
-
const maxStart = total - budget;
|
|
2995
|
-
const start = Math.min(maxStart, Math.max(0, selectedIndex - half));
|
|
2996
|
-
const end = start + budget;
|
|
2997
|
-
return { start, end, hiddenAbove: start, hiddenBelow: total - end };
|
|
2998
|
-
};
|
|
2999
|
-
|
|
3000
3092
|
// src/ui/picker-layout.ts
|
|
3001
3093
|
var MAX_PATH_LENGTH = 40;
|
|
3002
3094
|
var MAX_BRANCH_COLUMN_WIDTH = 24;
|
|
@@ -3067,6 +3159,15 @@ var toCandidateRow = (candidate, options) => ({
|
|
|
3067
3159
|
kindLabel: candidateKindLabel(candidate).padEnd(KIND_COLUMN_WIDTH, " "),
|
|
3068
3160
|
pathLabel: candidatePathLabel(candidate, options.homeDir, options.pathMaxLength)
|
|
3069
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
|
+
};
|
|
3070
3171
|
var buildDisplayRows = (candidates, homeDir, indexOffset = 0, columnWidths) => {
|
|
3071
3172
|
const { branchWidth, pathMaxLength } = columnWidths ?? {
|
|
3072
3173
|
branchWidth: branchColumnWidth(candidates),
|
|
@@ -3078,32 +3179,11 @@ var buildDisplayRows = (candidates, homeDir, indexOffset = 0, columnWidths) => {
|
|
|
3078
3179
|
}));
|
|
3079
3180
|
const worktreeEntries = indexed.filter((entry) => isWorktreeCandidate(entry.candidate));
|
|
3080
3181
|
const branchEntries = indexed.filter((entry) => isCreatableCandidate(entry.candidate));
|
|
3081
|
-
const
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
index: entry.index,
|
|
3087
|
-
section: "worktree",
|
|
3088
|
-
branchWidth,
|
|
3089
|
-
pathMaxLength,
|
|
3090
|
-
homeDir
|
|
3091
|
-
}));
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
if (branchEntries.length > 0) {
|
|
3095
|
-
rows.push({ kind: "header", label: "BRANCHES — Enter creates a worktree" });
|
|
3096
|
-
for (const entry of branchEntries) {
|
|
3097
|
-
rows.push(toCandidateRow(entry.candidate, {
|
|
3098
|
-
index: entry.index,
|
|
3099
|
-
section: "branch",
|
|
3100
|
-
branchWidth,
|
|
3101
|
-
pathMaxLength,
|
|
3102
|
-
homeDir
|
|
3103
|
-
}));
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
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
|
+
];
|
|
3107
3187
|
};
|
|
3108
3188
|
|
|
3109
3189
|
// src/domain/actions.ts
|
|
@@ -3124,7 +3204,7 @@ var availableActions = (candidate) => {
|
|
|
3124
3204
|
}
|
|
3125
3205
|
return actions;
|
|
3126
3206
|
};
|
|
3127
|
-
var requiresDeleteConfirmation = (candidate) => candidate.kind === "worktree" && candidate.worktree.kind === "external";
|
|
3207
|
+
var requiresDeleteConfirmation = (candidate) => candidate.kind === "worktree" && (candidate.worktree.kind === "external" || candidate.worktree.prunable);
|
|
3128
3208
|
var requiresSwitchRootConfirmation = (candidate) => candidate.kind === "worktree" && candidate.worktree.kind === "external";
|
|
3129
3209
|
|
|
3130
3210
|
// src/ui/side-panel.ts
|
|
@@ -3180,6 +3260,9 @@ var buildConfirmPanelRows = ({
|
|
|
3180
3260
|
if (action === "switchRoot") {
|
|
3181
3261
|
rows.push([{ text: "This will put it into detached HEAD.", style: "dim" }]);
|
|
3182
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
|
+
}
|
|
3183
3266
|
rows.push([{ text: "(y/N)", style: "dim" }]);
|
|
3184
3267
|
return rows;
|
|
3185
3268
|
};
|
|
@@ -3218,17 +3301,142 @@ var footerHintForWidth = (kind, width) => {
|
|
|
3218
3301
|
return truncateToWidth(short, width);
|
|
3219
3302
|
};
|
|
3220
3303
|
|
|
3221
|
-
// src/ui/picker-
|
|
3222
|
-
var
|
|
3223
|
-
|
|
3224
|
-
var
|
|
3225
|
-
|
|
3226
|
-
|
|
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 };
|
|
3227
3317
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
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;
|
|
3230
3340
|
}
|
|
3231
|
-
|
|
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
|
+
|
|
3429
|
+
// src/ui/picker-keys.ts
|
|
3430
|
+
var isCtrlJByte = (input) => input === `
|
|
3431
|
+
`;
|
|
3432
|
+
var resolvePickerKeyAction = (input, key) => {
|
|
3433
|
+
if (key.escape) {
|
|
3434
|
+
return { type: "cancel", reason: "esc" };
|
|
3435
|
+
}
|
|
3436
|
+
if (key.ctrl && input === "c") {
|
|
3437
|
+
return { type: "cancel", reason: "ctrlC" };
|
|
3438
|
+
}
|
|
3439
|
+
if (key.return) {
|
|
3232
3440
|
return { type: "select" };
|
|
3233
3441
|
}
|
|
3234
3442
|
if (key.tab || key.rightArrow || key.ctrl && (input === "l" || input === "f")) {
|
|
@@ -3304,7 +3512,7 @@ var handleConfirmInput = (input, key, confirmMode, ctx) => {
|
|
|
3304
3512
|
};
|
|
3305
3513
|
var runOrConfirm = (candidate, action, ctx) => {
|
|
3306
3514
|
if ((action === "delete" || action === "switchRoot") && requiresConfirmation(candidate, action)) {
|
|
3307
|
-
ctx.setMode({ kind: "confirm", action, candidate
|
|
3515
|
+
ctx.setMode({ kind: "confirm", action, candidate });
|
|
3308
3516
|
return;
|
|
3309
3517
|
}
|
|
3310
3518
|
ctx.runAction(candidate, action);
|
|
@@ -3382,7 +3590,7 @@ var handleListInput = (input, key, ctx) => {
|
|
|
3382
3590
|
}
|
|
3383
3591
|
case "openPanel": {
|
|
3384
3592
|
if (ctx.selectedCandidate !== undefined) {
|
|
3385
|
-
ctx.setPanelIndex(0);
|
|
3593
|
+
ctx.setPanelIndex(() => 0);
|
|
3386
3594
|
ctx.setMode({
|
|
3387
3595
|
kind: "panel",
|
|
3388
3596
|
candidate: ctx.selectedCandidate,
|
|
@@ -3396,24 +3604,14 @@ var handleListInput = (input, key, ctx) => {
|
|
|
3396
3604
|
ctx.setMode({
|
|
3397
3605
|
kind: "confirm",
|
|
3398
3606
|
action: "delete",
|
|
3399
|
-
candidate: ctx.selectedCandidate
|
|
3400
|
-
error: null
|
|
3607
|
+
candidate: ctx.selectedCandidate
|
|
3401
3608
|
});
|
|
3402
3609
|
}
|
|
3403
3610
|
break;
|
|
3404
3611
|
}
|
|
3405
3612
|
case "rootSwitchShortcut": {
|
|
3406
3613
|
if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("switchRoot")) {
|
|
3407
|
-
|
|
3408
|
-
ctx.setMode({
|
|
3409
|
-
kind: "confirm",
|
|
3410
|
-
action: "switchRoot",
|
|
3411
|
-
candidate: ctx.selectedCandidate,
|
|
3412
|
-
error: null
|
|
3413
|
-
});
|
|
3414
|
-
} else {
|
|
3415
|
-
ctx.runAction(ctx.selectedCandidate, "switchRoot");
|
|
3416
|
-
}
|
|
3614
|
+
runOrConfirm(ctx.selectedCandidate, "switchRoot", ctx);
|
|
3417
3615
|
}
|
|
3418
3616
|
break;
|
|
3419
3617
|
}
|
|
@@ -3444,20 +3642,18 @@ var runMutation = async (callbacks, candidate, action) => {
|
|
|
3444
3642
|
if (action === "switchRoot") {
|
|
3445
3643
|
const result2 = await callbacks.switchRootHere(candidate);
|
|
3446
3644
|
if (!result2.ok || result2.path === undefined) {
|
|
3447
|
-
const transition = panelErrorTransition(candidate, result2.message ?? "Failed to switch root.");
|
|
3448
3645
|
return {
|
|
3449
3646
|
type: "patch",
|
|
3450
|
-
patch:
|
|
3647
|
+
patch: panelErrorTransition(candidate, result2.message ?? "Failed to switch root.")
|
|
3451
3648
|
};
|
|
3452
3649
|
}
|
|
3453
3650
|
return { type: "exit", outcome: { type: "path", path: result2.path } };
|
|
3454
3651
|
}
|
|
3455
3652
|
const result = await callbacks.deleteWorktree(candidate);
|
|
3456
3653
|
if (!result.ok) {
|
|
3457
|
-
const transition = panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.");
|
|
3458
3654
|
return {
|
|
3459
3655
|
type: "patch",
|
|
3460
|
-
patch:
|
|
3656
|
+
patch: panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.")
|
|
3461
3657
|
};
|
|
3462
3658
|
}
|
|
3463
3659
|
const fresh = await callbacks.reloadCandidates();
|
|
@@ -3543,10 +3739,7 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3543
3739
|
const setQuery = (updater) => {
|
|
3544
3740
|
setState({ query: updater(state.query) });
|
|
3545
3741
|
};
|
|
3546
|
-
const
|
|
3547
|
-
setState({ panelIndex: value });
|
|
3548
|
-
};
|
|
3549
|
-
const setPanelIndexByUpdater = (updater) => {
|
|
3742
|
+
const setPanelIndex = (updater) => {
|
|
3550
3743
|
setState({ panelIndex: updater(state.panelIndex) });
|
|
3551
3744
|
};
|
|
3552
3745
|
const setMode = (mode) => {
|
|
@@ -3565,13 +3758,12 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3565
3758
|
handlePanelInput(input, key, mode, {
|
|
3566
3759
|
panelIndex,
|
|
3567
3760
|
runAction,
|
|
3568
|
-
setPanelIndex
|
|
3761
|
+
setPanelIndex,
|
|
3569
3762
|
setMode
|
|
3570
3763
|
});
|
|
3571
3764
|
return;
|
|
3572
3765
|
}
|
|
3573
|
-
const filtered =
|
|
3574
|
-
const clampedIndex = Math.min(state.index, Math.max(filtered.length - 1, 0));
|
|
3766
|
+
const { filtered, clampedIndex } = getSnapshot();
|
|
3575
3767
|
handleListInput(input, key, {
|
|
3576
3768
|
selectedCandidate: filtered[clampedIndex],
|
|
3577
3769
|
filteredLength: filtered.length,
|
|
@@ -3579,7 +3771,7 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3579
3771
|
onCancel: guardedOnCancel,
|
|
3580
3772
|
setIndex,
|
|
3581
3773
|
setQuery,
|
|
3582
|
-
setPanelIndex
|
|
3774
|
+
setPanelIndex,
|
|
3583
3775
|
setMode
|
|
3584
3776
|
});
|
|
3585
3777
|
};
|
|
@@ -3593,12 +3785,10 @@ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
|
3593
3785
|
};
|
|
3594
3786
|
|
|
3595
3787
|
// src/ui/alt-screen.ts
|
|
3596
|
-
var
|
|
3597
|
-
var
|
|
3598
|
-
var
|
|
3599
|
-
var
|
|
3600
|
-
var HIDE_CURSOR = `${ESCAPE}[?25l`;
|
|
3601
|
-
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`;
|
|
3602
3792
|
var enterAltScreen = (target) => {
|
|
3603
3793
|
if (!target.isTTY) {
|
|
3604
3794
|
return;
|
|
@@ -3789,6 +3979,10 @@ var pastedTextToEvents = (text) => [...text].filter((char) => isPrintableChar(ch
|
|
|
3789
3979
|
var CSI_INTRODUCER_CODE = "[".codePointAt(0);
|
|
3790
3980
|
var SS3_INTRODUCER_CODE = "O".codePointAt(0);
|
|
3791
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
|
+
]);
|
|
3792
3986
|
|
|
3793
3987
|
class PickerKeyParser {
|
|
3794
3988
|
buffer = Buffer.alloc(0);
|
|
@@ -3817,19 +4011,18 @@ class PickerKeyParser {
|
|
|
3817
4011
|
if (this.buffer.length === 0) {
|
|
3818
4012
|
return null;
|
|
3819
4013
|
}
|
|
3820
|
-
const
|
|
3821
|
-
const markerIndex = this.buffer.indexOf(endMarker);
|
|
4014
|
+
const markerIndex = this.buffer.indexOf(PASTE_END_MARKER);
|
|
3822
4015
|
if (markerIndex !== -1) {
|
|
3823
4016
|
const text2 = decodeUtf8(this.buffer.subarray(0, markerIndex));
|
|
3824
|
-
this.buffer = this.buffer.subarray(markerIndex +
|
|
4017
|
+
this.buffer = this.buffer.subarray(markerIndex + PASTE_END_MARKER.length);
|
|
3825
4018
|
this.inPaste = false;
|
|
3826
4019
|
return pastedTextToEvents(text2);
|
|
3827
4020
|
}
|
|
3828
|
-
const maxOverlap = Math.min(
|
|
4021
|
+
const maxOverlap = Math.min(PASTE_END_MARKER.length - 1, this.buffer.length);
|
|
3829
4022
|
let overlap = 0;
|
|
3830
4023
|
for (let candidate = maxOverlap;candidate > 0; candidate -= 1) {
|
|
3831
4024
|
const tail = this.buffer.subarray(this.buffer.length - candidate);
|
|
3832
|
-
if (tail.equals(
|
|
4025
|
+
if (tail.equals(PASTE_END_MARKER.subarray(0, candidate))) {
|
|
3833
4026
|
overlap = candidate;
|
|
3834
4027
|
break;
|
|
3835
4028
|
}
|
|
@@ -3926,9 +4119,8 @@ class PickerKeyParser {
|
|
|
3926
4119
|
}
|
|
3927
4120
|
|
|
3928
4121
|
// src/ui/terminal-session.ts
|
|
3929
|
-
var
|
|
3930
|
-
var
|
|
3931
|
-
var DISABLE_BRACKETED_PASTE = `${ESC3}[?2004l`;
|
|
4122
|
+
var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
|
|
4123
|
+
var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
|
|
3932
4124
|
var PENDING_ESCAPE_TIMEOUT_MS = 25;
|
|
3933
4125
|
var EXIT_SIGTERM = 143;
|
|
3934
4126
|
var EXIT_SIGHUP = 129;
|
|
@@ -4093,107 +4285,6 @@ var runTerminalSession = (createHandlers, streams = defaultStreams()) => new Pro
|
|
|
4093
4285
|
var DEFAULT_TERMINAL_WIDTH = 80;
|
|
4094
4286
|
var terminalDimension = (value, fallback) => value !== undefined && value > 0 ? value : fallback;
|
|
4095
4287
|
var colorEnabled = (stderr) => stderr.isTTY === true && (process.env["NO_COLOR"] ?? "") === "";
|
|
4096
|
-
var QUERY_PREFIX = "hop: ";
|
|
4097
|
-
var queryLine = (query, maxWidth) => {
|
|
4098
|
-
const hint = query.length === 0 ? " (type to filter)" : "";
|
|
4099
|
-
const budget = Math.max(0, maxWidth - QUERY_PREFIX.length - hint.length);
|
|
4100
|
-
return [
|
|
4101
|
-
{ text: QUERY_PREFIX },
|
|
4102
|
-
{ text: truncateToWidthKeepingTail(query, budget), style: "cyan" },
|
|
4103
|
-
{ text: hint, style: "dim" }
|
|
4104
|
-
];
|
|
4105
|
-
};
|
|
4106
|
-
var hiddenCountLine = (count, direction) => {
|
|
4107
|
-
if (count === 0) {
|
|
4108
|
-
return null;
|
|
4109
|
-
}
|
|
4110
|
-
const arrow = direction === "above" ? "↑ " : "↓ ";
|
|
4111
|
-
const word = direction === "above" ? "above" : "below";
|
|
4112
|
-
return [
|
|
4113
|
-
{
|
|
4114
|
-
text: `${arrow}${count} more ${word} (keep typing to narrow down)`,
|
|
4115
|
-
style: "dim"
|
|
4116
|
-
}
|
|
4117
|
-
];
|
|
4118
|
-
};
|
|
4119
|
-
var candidateRowLine = (row, selected) => {
|
|
4120
|
-
const marker = selected ? "❯ " : " ";
|
|
4121
|
-
const text = `${marker}${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`;
|
|
4122
|
-
if (selected) {
|
|
4123
|
-
return [{ text, style: "inverse" }];
|
|
4124
|
-
}
|
|
4125
|
-
return [{ text, style: row.section === "branch" ? "dim" : "plain" }];
|
|
4126
|
-
};
|
|
4127
|
-
var displayRowLine = (row, clampedIndex) => row.kind === "header" ? [{ text: row.label, style: "bold-dim" }] : candidateRowLine(row, row.index === clampedIndex);
|
|
4128
|
-
var buildListLines = ({
|
|
4129
|
-
query,
|
|
4130
|
-
maxWidth,
|
|
4131
|
-
rows,
|
|
4132
|
-
clampedIndex,
|
|
4133
|
-
hiddenAbove,
|
|
4134
|
-
hiddenBelow,
|
|
4135
|
-
footerHint
|
|
4136
|
-
}) => {
|
|
4137
|
-
const lines = [queryLine(query, maxWidth)];
|
|
4138
|
-
if (rows.length === 0) {
|
|
4139
|
-
lines.push([{ text: "No matches.", style: "dim" }]);
|
|
4140
|
-
}
|
|
4141
|
-
const above = hiddenCountLine(hiddenAbove, "above");
|
|
4142
|
-
if (above !== null) {
|
|
4143
|
-
lines.push(above);
|
|
4144
|
-
}
|
|
4145
|
-
for (const row of rows) {
|
|
4146
|
-
lines.push(displayRowLine(row, clampedIndex));
|
|
4147
|
-
}
|
|
4148
|
-
const below = hiddenCountLine(hiddenBelow, "below");
|
|
4149
|
-
if (below !== null) {
|
|
4150
|
-
lines.push(below);
|
|
4151
|
-
}
|
|
4152
|
-
lines.push([{ text: `(${LEGEND_TEXT})`, style: "dim" }], [{ text: footerHint, style: "dim" }]);
|
|
4153
|
-
return lines;
|
|
4154
|
-
};
|
|
4155
|
-
var buildSidePanelLines = (mode, panelIndex, busy) => {
|
|
4156
|
-
if (mode.kind === "panel") {
|
|
4157
|
-
return wrapInBox(buildActionPanelRows({
|
|
4158
|
-
candidate: mode.candidate,
|
|
4159
|
-
panelIndex,
|
|
4160
|
-
error: mode.error,
|
|
4161
|
-
busy
|
|
4162
|
-
}), SIDE_PANEL_WIDTH);
|
|
4163
|
-
}
|
|
4164
|
-
if (mode.kind === "confirm") {
|
|
4165
|
-
return wrapInBox(buildConfirmPanelRows({ candidate: mode.candidate, action: mode.action }), SIDE_PANEL_WIDTH);
|
|
4166
|
-
}
|
|
4167
|
-
return null;
|
|
4168
|
-
};
|
|
4169
|
-
var renderPickerFrame = ({
|
|
4170
|
-
snapshot,
|
|
4171
|
-
width,
|
|
4172
|
-
height,
|
|
4173
|
-
colorEnabled: color
|
|
4174
|
-
}) => {
|
|
4175
|
-
const { query, filtered, clampedIndex, mode, panelIndex, busy } = snapshot;
|
|
4176
|
-
const narrow = isNarrowTerminal(width);
|
|
4177
|
-
const right = buildSidePanelLines(mode, panelIndex, busy);
|
|
4178
|
-
const stacked = right !== null && narrow;
|
|
4179
|
-
const listColumnWidth = right !== null && !stacked ? width - GUTTER.length - SIDE_PANEL_WIDTH : width;
|
|
4180
|
-
const stackedPanelRows = stacked && right !== null ? right.length : 0;
|
|
4181
|
-
const budget = rowBudget({ terminalHeight: height, stackedPanelRows });
|
|
4182
|
-
const viewport = computeViewport(filtered.length, clampedIndex, budget);
|
|
4183
|
-
const visible = filtered.slice(viewport.start, viewport.end);
|
|
4184
|
-
const columnWidths = constrainRowColumnWidths(rawBranchColumnWidth(visible), listColumnWidth);
|
|
4185
|
-
const rows = buildDisplayRows(visible, homedir(), viewport.start, columnWidths);
|
|
4186
|
-
const left = buildListLines({
|
|
4187
|
-
query,
|
|
4188
|
-
maxWidth: listColumnWidth,
|
|
4189
|
-
rows,
|
|
4190
|
-
clampedIndex,
|
|
4191
|
-
hiddenAbove: viewport.hiddenAbove,
|
|
4192
|
-
hiddenBelow: viewport.hiddenBelow,
|
|
4193
|
-
footerHint: footerHintForWidth(right === null ? "list" : "panel", listColumnWidth)
|
|
4194
|
-
}).map((line) => truncateLineToWidth(line, listColumnWidth));
|
|
4195
|
-
return buildFrame({ left, right, stacked, colorEnabled: color });
|
|
4196
|
-
};
|
|
4197
4288
|
var isCtrlC = (event2) => event2.key.ctrl && event2.input === "c";
|
|
4198
4289
|
var runPicker = (candidates, callbacks) => runTerminalSession(({ requestRender, finish }) => {
|
|
4199
4290
|
const store = createPickerStore(candidates, callbacks, (outcome) => finish(outcome), (reason) => finish({ type: "cancelled", reason }));
|
|
@@ -4219,8 +4310,7 @@ var runPicker = (candidates, callbacks) => runTerminalSession(({ requestRender,
|
|
|
4219
4310
|
var loadPickCandidates = async (git, fs, json) => {
|
|
4220
4311
|
const pickResult = await pick(git, fs, { cwd: process.cwd() });
|
|
4221
4312
|
if (!pickResult.ok) {
|
|
4222
|
-
|
|
4223
|
-
process.exitCode = pickResult.exitCode;
|
|
4313
|
+
reportResult("pick", pickResult, json);
|
|
4224
4314
|
return null;
|
|
4225
4315
|
}
|
|
4226
4316
|
return pickResult.data?.candidates ?? [];
|
|
@@ -4234,6 +4324,7 @@ var deleteWorktree = async (git, fs, candidate) => {
|
|
|
4234
4324
|
cwd: process.cwd(),
|
|
4235
4325
|
branch,
|
|
4236
4326
|
...candidate.kind === "worktree" ? { expectedPath: candidate.worktree.path } : {},
|
|
4327
|
+
allowPrunable: candidate.kind === "worktree" && candidate.worktree.prunable,
|
|
4237
4328
|
force: false,
|
|
4238
4329
|
ext: false
|
|
4239
4330
|
});
|
|
@@ -4284,52 +4375,1006 @@ var renderSwitchRootOutcome = (path, outcome, json) => {
|
|
|
4284
4375
|
const branch = outcome?.branch ?? null;
|
|
4285
4376
|
const detachedHolder = outcome?.detachedHolder ?? null;
|
|
4286
4377
|
const warnings = outcome?.warnings ?? [];
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
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)`);
|
|
4290
4441
|
}
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
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}` };
|
|
4296
4452
|
}
|
|
4453
|
+
return { ok: true, digest: digest.toLowerCase() };
|
|
4297
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");
|
|
4298
4463
|
|
|
4299
|
-
// src/domain/
|
|
4300
|
-
var
|
|
4301
|
-
|
|
4302
|
-
|
|
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`;
|
|
4303
4480
|
}
|
|
4304
|
-
if (
|
|
4305
|
-
return
|
|
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`;
|
|
4306
4483
|
}
|
|
4307
|
-
|
|
4308
|
-
|
|
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");
|
|
4309
4489
|
}
|
|
4310
|
-
if (
|
|
4311
|
-
return "
|
|
4490
|
+
if (scriptPath.includes(BUNX_MARKER)) {
|
|
4491
|
+
return unsupported("running through bunx, which already fetches the published version on every run");
|
|
4312
4492
|
}
|
|
4313
|
-
|
|
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)");
|
|
4314
4504
|
};
|
|
4315
4505
|
|
|
4316
|
-
// src/
|
|
4317
|
-
var
|
|
4318
|
-
|
|
4319
|
-
const
|
|
4320
|
-
const
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
return { branch: wt.branch ?? "", path: wt.path, reason };
|
|
4326
|
-
}));
|
|
4327
|
-
return results.filter((candidate) => candidate !== null);
|
|
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);
|
|
4328
4515
|
};
|
|
4329
|
-
var
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
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);
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
|
|
5344
|
+
// src/domain/garbage.ts
|
|
5345
|
+
var classifyGarbage = (input) => {
|
|
5346
|
+
if (input.prunable) {
|
|
5347
|
+
return "prunable";
|
|
5348
|
+
}
|
|
5349
|
+
if (!input.clean) {
|
|
5350
|
+
return null;
|
|
5351
|
+
}
|
|
5352
|
+
if (input.mergedIntoDefault === true) {
|
|
5353
|
+
return "merged";
|
|
5354
|
+
}
|
|
5355
|
+
if (input.upstreamGone && input.allCommitsReachableFromDefault === true) {
|
|
5356
|
+
return "gone";
|
|
5357
|
+
}
|
|
5358
|
+
return null;
|
|
5359
|
+
};
|
|
5360
|
+
|
|
5361
|
+
// src/commands/clean-candidates.ts
|
|
5362
|
+
var buildCleanCandidates = async (git, fs, context, ext) => {
|
|
5363
|
+
const defaultRef = await git.resolveDefaultBranchRef(context.rootPath);
|
|
5364
|
+
const targets = context.worktrees.filter((wt) => wt.kind === "managed" || ext && wt.kind === "external");
|
|
5365
|
+
const results = await Promise.all(targets.map(async (wt) => {
|
|
5366
|
+
const reason = await classifyWorktree(wt, { git, fs, defaultRef, context });
|
|
5367
|
+
if (reason === null) {
|
|
5368
|
+
return null;
|
|
5369
|
+
}
|
|
5370
|
+
return { branch: wt.branch ?? "", path: wt.path, reason };
|
|
5371
|
+
}));
|
|
5372
|
+
return results.filter((candidate) => candidate !== null);
|
|
5373
|
+
};
|
|
5374
|
+
var classifyWorktree = async (wt, { git, fs, defaultRef, context }) => {
|
|
5375
|
+
if (wt.prunable) {
|
|
5376
|
+
return classifyGarbage({
|
|
5377
|
+
prunable: true,
|
|
4333
5378
|
clean: false,
|
|
4334
5379
|
mergedIntoDefault: "unknown",
|
|
4335
5380
|
upstreamGone: false,
|
|
@@ -4340,15 +5385,15 @@ var classifyWorktree = async (wt, { git, defaultRef, context }) => {
|
|
|
4340
5385
|
return null;
|
|
4341
5386
|
}
|
|
4342
5387
|
const { rootPath, worktrees } = context;
|
|
4343
|
-
const [
|
|
4344
|
-
|
|
5388
|
+
const [dirtyState, upstreamGone] = await Promise.all([
|
|
5389
|
+
worktreeDirtyState(git, fs, worktrees, wt),
|
|
4345
5390
|
git.isUpstreamGone(rootPath, wt.branch)
|
|
4346
5391
|
]);
|
|
4347
5392
|
const mergedIntoDefault = defaultRef === null ? "unknown" : await git.isAncestor(rootPath, wt.branch, defaultRef);
|
|
4348
5393
|
const allCommitsReachableFromDefault = defaultRef === null ? "unknown" : await git.hasEquivalentCommits(rootPath, wt.branch, defaultRef);
|
|
4349
5394
|
const input = {
|
|
4350
5395
|
prunable: false,
|
|
4351
|
-
clean:
|
|
5396
|
+
clean: dirtyState === false,
|
|
4352
5397
|
mergedIntoDefault,
|
|
4353
5398
|
upstreamGone,
|
|
4354
5399
|
allCommitsReachableFromDefault
|
|
@@ -4359,7 +5404,7 @@ var classifyWorktree = async (wt, { git, defaultRef, context }) => {
|
|
|
4359
5404
|
// src/commands/clean.ts
|
|
4360
5405
|
var clean = async (git, fs, term, options) => {
|
|
4361
5406
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4362
|
-
const candidates = await buildCleanCandidates(git, context, options.ext);
|
|
5407
|
+
const candidates = await buildCleanCandidates(git, fs, context, options.ext);
|
|
4363
5408
|
if (options.dryRun) {
|
|
4364
5409
|
return ok({ data: { candidates } });
|
|
4365
5410
|
}
|
|
@@ -4378,20 +5423,13 @@ var clean = async (git, fs, term, options) => {
|
|
|
4378
5423
|
return ok({ data: { candidates, removed: [] } });
|
|
4379
5424
|
}
|
|
4380
5425
|
}
|
|
4381
|
-
|
|
5426
|
+
return executeClean({
|
|
4382
5427
|
git,
|
|
4383
5428
|
fs,
|
|
4384
5429
|
context,
|
|
4385
5430
|
candidates,
|
|
4386
5431
|
cleanOptions: options
|
|
4387
5432
|
});
|
|
4388
|
-
if (!execution.ok) {
|
|
4389
|
-
return execution.rejection;
|
|
4390
|
-
}
|
|
4391
|
-
return ok({
|
|
4392
|
-
data: { candidates, removed: execution.removed },
|
|
4393
|
-
...execution.warnings.length === 0 ? {} : { warnings: execution.warnings }
|
|
4394
|
-
});
|
|
4395
5433
|
};
|
|
4396
5434
|
var removeCandidate = async ({
|
|
4397
5435
|
git,
|
|
@@ -4401,16 +5439,16 @@ var removeCandidate = async ({
|
|
|
4401
5439
|
worktrees
|
|
4402
5440
|
}) => {
|
|
4403
5441
|
if (nestedWorktrees(worktrees, candidate.path).length > 0) {
|
|
4404
|
-
return
|
|
5442
|
+
return "nested";
|
|
4405
5443
|
}
|
|
4406
5444
|
try {
|
|
4407
5445
|
await git.removeWorktree(rootPath, candidate.path, false);
|
|
4408
5446
|
if (deleteBranch && candidate.branch.length > 0) {
|
|
4409
5447
|
await git.deleteBranch(rootPath, candidate.branch);
|
|
4410
5448
|
}
|
|
4411
|
-
return
|
|
5449
|
+
return "removed";
|
|
4412
5450
|
} catch {
|
|
4413
|
-
return
|
|
5451
|
+
return "failed";
|
|
4414
5452
|
}
|
|
4415
5453
|
};
|
|
4416
5454
|
var canDeletePrunableBranch = async (git, rootPath, candidate, defaultRef) => {
|
|
@@ -4431,21 +5469,16 @@ var canDeletePrunableBranch = async (git, rootPath, candidate, defaultRef) => {
|
|
|
4431
5469
|
} catch {}
|
|
4432
5470
|
return false;
|
|
4433
5471
|
};
|
|
4434
|
-
var executeClean =
|
|
5472
|
+
var executeClean = ({
|
|
4435
5473
|
git,
|
|
4436
5474
|
fs,
|
|
4437
5475
|
context,
|
|
4438
5476
|
candidates,
|
|
4439
5477
|
cleanOptions
|
|
4440
|
-
}) => {
|
|
4441
|
-
const acquisition = await acquireRepoLockOrRejection(context.commonDir);
|
|
4442
|
-
if (!acquisition.ok) {
|
|
4443
|
-
return { ok: false, rejection: acquisition.rejection };
|
|
4444
|
-
}
|
|
4445
|
-
const { lock } = acquisition;
|
|
5478
|
+
}) => withRepoLock(context.commonDir, async () => {
|
|
4446
5479
|
try {
|
|
4447
5480
|
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
4448
|
-
const freshCandidates = await buildCleanCandidates(git, fresh, cleanOptions.ext);
|
|
5481
|
+
const freshCandidates = await buildCleanCandidates(git, fs, fresh, cleanOptions.ext);
|
|
4449
5482
|
const freshByPath = new Map(freshCandidates.map((candidate) => [candidate.path, candidate]));
|
|
4450
5483
|
const stillValid = candidates.flatMap((candidate) => {
|
|
4451
5484
|
const freshCandidate = freshByPath.get(candidate.path);
|
|
@@ -4460,30 +5493,29 @@ var executeClean = async ({
|
|
|
4460
5493
|
if (cleanOptions.withBranch && candidate.reason === "prunable" && candidate.branch.length > 0 && !branchSafe) {
|
|
4461
5494
|
warnings.push(`Keeping branch "${candidate.branch}": prunable worktree is not confirmed merged or gone.`);
|
|
4462
5495
|
}
|
|
4463
|
-
|
|
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") {
|
|
4464
5506
|
warnings.push(`Skipped ${candidate.path}: still contains a registered worktree.`);
|
|
4465
|
-
} else {
|
|
4466
|
-
const success = await removeCandidate({
|
|
4467
|
-
git,
|
|
4468
|
-
rootPath: fresh.rootPath,
|
|
4469
|
-
candidate,
|
|
4470
|
-
deleteBranch,
|
|
4471
|
-
worktrees: fresh.worktrees
|
|
4472
|
-
});
|
|
4473
|
-
if (success) {
|
|
4474
|
-
removed.push(candidate.branch);
|
|
4475
|
-
}
|
|
4476
5507
|
}
|
|
4477
5508
|
}
|
|
4478
|
-
return {
|
|
5509
|
+
return ok({
|
|
5510
|
+
data: { candidates: [...candidates], removed },
|
|
5511
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
5512
|
+
});
|
|
4479
5513
|
} catch (error) {
|
|
4480
|
-
throw new Error(`Failed to clean worktrees: ${error
|
|
5514
|
+
throw new Error(`Failed to clean worktrees: ${messageOf(error)}`, {
|
|
4481
5515
|
cause: error
|
|
4482
5516
|
});
|
|
4483
|
-
} finally {
|
|
4484
|
-
await lock.release();
|
|
4485
5517
|
}
|
|
4486
|
-
};
|
|
5518
|
+
});
|
|
4487
5519
|
|
|
4488
5520
|
// src/commands/init.ts
|
|
4489
5521
|
var ZSH_TEMPLATE = `# nuthatch shell integration for zsh.
|
|
@@ -4542,7 +5574,7 @@ var renderInit = (options) => {
|
|
|
4542
5574
|
};
|
|
4543
5575
|
|
|
4544
5576
|
// src/commands/jump.ts
|
|
4545
|
-
import { join as
|
|
5577
|
+
import { join as join5 } from "node:path";
|
|
4546
5578
|
|
|
4547
5579
|
// src/domain/sanitize.ts
|
|
4548
5580
|
var MAX_DIR_NAME_LENGTH = 200;
|
|
@@ -4559,10 +5591,10 @@ var shortHash = (input) => {
|
|
|
4559
5591
|
}
|
|
4560
5592
|
return (hash >>> 0).toString(HEX_RADIX).padStart(FULL_HASH_HEX_LENGTH, "0").slice(0, HASH_LENGTH);
|
|
4561
5593
|
};
|
|
4562
|
-
var
|
|
5594
|
+
var baseName2 = (branch) => branch.replaceAll("/", "__");
|
|
4563
5595
|
var truncate = (name, maxLength) => name.length > maxLength ? name.slice(0, maxLength) : name;
|
|
4564
5596
|
var sanitizeBranchName = (branch, isTaken) => {
|
|
4565
|
-
const base = truncate(
|
|
5597
|
+
const base = truncate(baseName2(branch), MAX_DIR_NAME_LENGTH);
|
|
4566
5598
|
if (!isTaken(base)) {
|
|
4567
5599
|
return base;
|
|
4568
5600
|
}
|
|
@@ -4578,7 +5610,8 @@ var sanitizeBranchName = (branch, isTaken) => {
|
|
|
4578
5610
|
};
|
|
4579
5611
|
|
|
4580
5612
|
// src/commands/jump.ts
|
|
4581
|
-
var
|
|
5613
|
+
var existingWorktreeResult = (branch, path) => ok({ path, data: { branch, created: false } });
|
|
5614
|
+
var jump = async (git, fs, options) => {
|
|
4582
5615
|
if (options.target === "-") {
|
|
4583
5616
|
const previous = process.env.OLDPWD;
|
|
4584
5617
|
if (previous === undefined || previous.length === 0) {
|
|
@@ -4592,57 +5625,35 @@ var jump = async (git, fs, term, options) => {
|
|
|
4592
5625
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4593
5626
|
const existing = context.worktrees.find((wt) => wt.branch === options.target);
|
|
4594
5627
|
if (existing !== undefined) {
|
|
4595
|
-
return
|
|
4596
|
-
path: existing.path,
|
|
4597
|
-
data: { branch: options.target, created: false }
|
|
4598
|
-
});
|
|
5628
|
+
return existingWorktreeResult(options.target, existing.path);
|
|
4599
5629
|
}
|
|
4600
5630
|
if (!options.create) {
|
|
4601
|
-
if (!term.isTTY()) {
|
|
4602
|
-
return fail(EXIT_SAFE_REJECTION, `No worktree for branch "${options.target}". Re-run with --create to create it.`);
|
|
4603
|
-
}
|
|
4604
5631
|
return fail(EXIT_SAFE_REJECTION, `No worktree for branch "${options.target}". Re-run with --create to create it.`);
|
|
4605
5632
|
}
|
|
4606
|
-
const
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
if (!branchExistsLocally && track === undefined) {
|
|
4610
|
-
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
4611
|
-
if (remotes.includes("origin")) {
|
|
4612
|
-
track = `origin/${options.target}`;
|
|
4613
|
-
} else if (remotes.length === 1) {
|
|
4614
|
-
track = `${remotes[0]}/${options.target}`;
|
|
4615
|
-
} else if (remotes.length > 1) {
|
|
4616
|
-
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
4617
|
-
}
|
|
5633
|
+
const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
|
|
5634
|
+
if (!checkout.ok) {
|
|
5635
|
+
return checkout.rejection;
|
|
4618
5636
|
}
|
|
4619
5637
|
const managedDirNames = await fs.listDirNames(context.managedRoot);
|
|
4620
5638
|
const existingDirNames = new Set(managedDirNames.map((name) => name.toLowerCase()));
|
|
4621
5639
|
const dirName = sanitizeBranchName(options.target, (candidate) => existingDirNames.has(candidate.toLowerCase()));
|
|
4622
|
-
const targetPath =
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
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);
|
|
4628
5650
|
return ok({
|
|
4629
|
-
path:
|
|
4630
|
-
data: { branch: options.target, created:
|
|
5651
|
+
path: targetPath,
|
|
5652
|
+
data: { branch: options.target, created: true }
|
|
4631
5653
|
});
|
|
5654
|
+
} catch (error) {
|
|
5655
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to create worktree: ${messageOf(error)}`);
|
|
4632
5656
|
}
|
|
4633
|
-
await fs.mkdir(context.managedRoot);
|
|
4634
|
-
await git.addWorktree(context.rootPath, targetPath, options.target, {
|
|
4635
|
-
createBranch: !branchExistsLocally,
|
|
4636
|
-
...track === undefined ? {} : { track }
|
|
4637
|
-
});
|
|
4638
|
-
} catch (error) {
|
|
4639
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to create worktree: ${error.message}`);
|
|
4640
|
-
} finally {
|
|
4641
|
-
await lock.release();
|
|
4642
|
-
}
|
|
4643
|
-
return ok({
|
|
4644
|
-
path: targetPath,
|
|
4645
|
-
data: { branch: options.target, created: true }
|
|
4646
5657
|
});
|
|
4647
5658
|
};
|
|
4648
5659
|
|
|
@@ -4650,13 +5661,13 @@ var jump = async (git, fs, term, options) => {
|
|
|
4650
5661
|
var ls = async (git, fs, options) => {
|
|
4651
5662
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
4652
5663
|
const entries = await Promise.all(context.worktrees.map(async (wt) => {
|
|
4653
|
-
const [
|
|
4654
|
-
|
|
5664
|
+
const [dirtyState, aheadBehind] = await Promise.all([
|
|
5665
|
+
worktreeDirtyState(git, fs, context.worktrees, wt),
|
|
4655
5666
|
wt.branch === null ? Promise.resolve(null) : git.aheadBehind(context.rootPath, wt.branch)
|
|
4656
5667
|
]);
|
|
4657
5668
|
return {
|
|
4658
5669
|
...wt,
|
|
4659
|
-
dirty,
|
|
5670
|
+
dirty: dirtyState ?? false,
|
|
4660
5671
|
ahead: aheadBehind?.ahead ?? null,
|
|
4661
5672
|
behind: aheadBehind?.behind ?? null
|
|
4662
5673
|
};
|
|
@@ -4696,26 +5707,26 @@ import { execFile as execFileCb } from "node:child_process";
|
|
|
4696
5707
|
import { promisify } from "node:util";
|
|
4697
5708
|
|
|
4698
5709
|
// src/domain/dirty.ts
|
|
4699
|
-
var filterOutNestedWorktreePaths = (relativePaths, targetPath,
|
|
5710
|
+
var filterOutNestedWorktreePaths = (relativePaths, targetPath, otherWorktreePaths) => {
|
|
4700
5711
|
const target = stripTrailingSlash(targetPath);
|
|
4701
|
-
const others =
|
|
5712
|
+
const others = otherWorktreePaths.map((path) => stripTrailingSlash(path)).filter((path) => path !== target);
|
|
4702
5713
|
if (others.length === 0) {
|
|
4703
5714
|
return [...relativePaths];
|
|
4704
5715
|
}
|
|
4705
5716
|
return relativePaths.filter((relativePath) => {
|
|
4706
5717
|
const absolutePath = stripTrailingSlash(`${target}/${relativePath}`);
|
|
4707
|
-
return !others.some((other) =>
|
|
5718
|
+
return !others.some((other) => absolutePath === other || isWithin(other, absolutePath));
|
|
4708
5719
|
});
|
|
4709
5720
|
};
|
|
4710
|
-
var isDirtyFromStatus = (statusOutput, targetPath,
|
|
5721
|
+
var isDirtyFromStatus = (statusOutput, targetPath, otherWorktreePaths) => {
|
|
4711
5722
|
const paths = parseStatusPaths(statusOutput);
|
|
4712
|
-
return filterOutNestedWorktreePaths(paths, targetPath,
|
|
5723
|
+
return filterOutNestedWorktreePaths(paths, targetPath, otherWorktreePaths).length > 0;
|
|
4713
5724
|
};
|
|
4714
5725
|
var stripTrailingSlash = (path) => path.endsWith("/") ? path.slice(0, -1) : path;
|
|
4715
|
-
var
|
|
4716
|
-
var isRenameOrCopyCode = (code) => code.includes("R") || code.includes("C");
|
|
5726
|
+
var STATUS_CODE_WIDTH = 3;
|
|
4717
5727
|
var RECORDS_PER_RENAME_OR_COPY = 2;
|
|
4718
5728
|
var RECORDS_PER_ORDINARY_ENTRY = 1;
|
|
5729
|
+
var isRenameOrCopyCode = (code) => code.includes("R") || code.includes("C");
|
|
4719
5730
|
var parseStatusPaths = (statusOutput) => {
|
|
4720
5731
|
const records = statusOutput.split("\x00").filter((record) => record.length > 0);
|
|
4721
5732
|
const paths = [];
|
|
@@ -4732,14 +5743,53 @@ var parseStatusPaths = (statusOutput) => {
|
|
|
4732
5743
|
}
|
|
4733
5744
|
return paths;
|
|
4734
5745
|
};
|
|
4735
|
-
|
|
5746
|
+
|
|
5747
|
+
// src/infra/git-executable.ts
|
|
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) => {
|
|
5751
|
+
try {
|
|
5752
|
+
const stats = await stat2(path);
|
|
5753
|
+
if (!stats.isFile()) {
|
|
5754
|
+
return false;
|
|
5755
|
+
}
|
|
5756
|
+
await access3(path, constants3.X_OK);
|
|
5757
|
+
return true;
|
|
5758
|
+
} catch {
|
|
5759
|
+
return false;
|
|
5760
|
+
}
|
|
5761
|
+
};
|
|
5762
|
+
var resolveGitExecutable = async (env2) => {
|
|
5763
|
+
const candidates = gitExecutableCandidates({
|
|
5764
|
+
override: env2[GIT_EXECUTABLE_OVERRIDE_ENV],
|
|
5765
|
+
path: env2.PATH
|
|
5766
|
+
});
|
|
5767
|
+
const probes = candidates.map(async (candidate) => await isExecutableFile2(candidate) ? candidate : undefined);
|
|
5768
|
+
for (const probe of probes) {
|
|
5769
|
+
const found = await probe;
|
|
5770
|
+
if (found !== undefined) {
|
|
5771
|
+
return found;
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
throw Object.assign(new Error(gitNotFoundMessage(candidates)), {
|
|
5775
|
+
code: GIT_NOT_FOUND_ERROR_CODE
|
|
5776
|
+
});
|
|
5777
|
+
};
|
|
5778
|
+
var createGitExecutableResolver = (env2) => {
|
|
5779
|
+
let cached = null;
|
|
5780
|
+
return () => {
|
|
5781
|
+
cached ??= resolveGitExecutable(env2);
|
|
5782
|
+
return cached;
|
|
5783
|
+
};
|
|
5784
|
+
};
|
|
4736
5785
|
|
|
4737
5786
|
// src/infra/git.ts
|
|
4738
5787
|
var execFile = promisify(execFileCb);
|
|
5788
|
+
var gitExecutable = createGitExecutableResolver(process.env);
|
|
4739
5789
|
var MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
4740
5790
|
var GIT_ANCESTOR_EXIT_CODE = 1;
|
|
4741
5791
|
var run = async (cwd, args) => {
|
|
4742
|
-
const { stdout: stdout2 } = await execFile(
|
|
5792
|
+
const { stdout: stdout2 } = await execFile(await gitExecutable(), [...args], {
|
|
4743
5793
|
cwd,
|
|
4744
5794
|
maxBuffer: MAX_BUFFER_BYTES
|
|
4745
5795
|
});
|
|
@@ -4768,9 +5818,9 @@ var createWorktreeMethods = () => ({
|
|
|
4768
5818
|
const out = await run(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
4769
5819
|
return out.trim();
|
|
4770
5820
|
},
|
|
4771
|
-
async isDirty(path,
|
|
5821
|
+
async isDirty(path, otherWorktreePaths) {
|
|
4772
5822
|
const status = await run(path, ["status", "--porcelain", "-z", "--untracked-files=all"]);
|
|
4773
|
-
return isDirtyFromStatus(status, path,
|
|
5823
|
+
return isDirtyFromStatus(status, path, otherWorktreePaths);
|
|
4774
5824
|
},
|
|
4775
5825
|
async addWorktree(cwd, path, branch, options) {
|
|
4776
5826
|
const args = ["worktree", "add"];
|
|
@@ -4953,7 +6003,7 @@ var createTermPort = () => ({
|
|
|
4953
6003
|
var USAGE = `Usage: hop [command] [options]
|
|
4954
6004
|
|
|
4955
6005
|
hop Pick a worktree/branch interactively and cd into it (TTY); lists worktrees otherwise
|
|
4956
|
-
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)
|
|
4957
6007
|
hop root cd into the root clone
|
|
4958
6008
|
hop - cd back to the previous worktree
|
|
4959
6009
|
hop -- <branch> Escape a branch name that collides with a reserved command (ls/rm/clean/root/init/help)
|
|
@@ -4965,12 +6015,16 @@ var USAGE = `Usage: hop [command] [options]
|
|
|
4965
6015
|
hop root - Switch the root clone back
|
|
4966
6016
|
hop init zsh Print the zsh shell integration (eval "$(hop init zsh)")
|
|
4967
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
|
+
|
|
4968
6022
|
Interactive picker keys:
|
|
4969
6023
|
Enter cd into the selected candidate
|
|
4970
6024
|
Tab, →, Ctrl+L, Ctrl+F Open the action panel, as a column beside the list
|
|
4971
6025
|
(stacks below it instead on narrow terminals)
|
|
4972
6026
|
Ctrl+X Delete the selected worktree (y/N confirmation)
|
|
4973
|
-
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)
|
|
4974
6028
|
↑/↓, Ctrl+P/N, Ctrl+K/J Move the selection (arrow, emacs, and vim keys all work)
|
|
4975
6029
|
Esc Cancel (exit 0, no output)
|
|
4976
6030
|
Ctrl+C Cancel like an interrupt (exit 130, same as SIGINT)
|
|
@@ -4978,7 +6032,7 @@ Interactive picker keys:
|
|
|
4978
6032
|
action, c/d/r to run cd/delete/switchRoot directly, Esc/Tab/←/Ctrl+H to close
|
|
4979
6033
|
|
|
4980
6034
|
Options:
|
|
4981
|
-
--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)
|
|
4982
6036
|
--json Output JSON instead of plain text
|
|
4983
6037
|
--force Force removal even if the worktree is dirty (hop rm)
|
|
4984
6038
|
--ext [deprecated, no-op for hop rm] hop rm no longer requires it to
|
|
@@ -4993,8 +6047,9 @@ Options:
|
|
|
4993
6047
|
var git = createGitPort();
|
|
4994
6048
|
var fs = createFsPort();
|
|
4995
6049
|
var term = createTermPort();
|
|
4996
|
-
var
|
|
4997
|
-
|
|
6050
|
+
var runLs = async (json) => {
|
|
6051
|
+
const result = await ls(git, fs, { cwd: process.cwd() });
|
|
6052
|
+
reportResult("ls", result, json);
|
|
4998
6053
|
};
|
|
4999
6054
|
var lsCommand = defineCommand({
|
|
5000
6055
|
meta: { name: "ls", description: "List worktrees" },
|
|
@@ -5002,9 +6057,7 @@ var lsCommand = defineCommand({
|
|
|
5002
6057
|
json: { type: "boolean", description: "Output JSON" }
|
|
5003
6058
|
},
|
|
5004
6059
|
async run({ args }) {
|
|
5005
|
-
|
|
5006
|
-
render("ls", result, Boolean(args.json));
|
|
5007
|
-
applyExitCode(result);
|
|
6060
|
+
await runLs(Boolean(args.json));
|
|
5008
6061
|
}
|
|
5009
6062
|
});
|
|
5010
6063
|
var rmCommand = defineCommand({
|
|
@@ -5029,11 +6082,10 @@ var rmCommand = defineCommand({
|
|
|
5029
6082
|
force: Boolean(args.force),
|
|
5030
6083
|
ext: Boolean(args.ext)
|
|
5031
6084
|
});
|
|
5032
|
-
|
|
5033
|
-
applyExitCode(result);
|
|
6085
|
+
reportResult("rm", result, Boolean(args.json));
|
|
5034
6086
|
}
|
|
5035
6087
|
});
|
|
5036
|
-
var rootCommand = createRootCommand(git, fs
|
|
6088
|
+
var rootCommand = createRootCommand(git, fs);
|
|
5037
6089
|
var cleanCommand = defineCommand({
|
|
5038
6090
|
meta: {
|
|
5039
6091
|
name: "clean",
|
|
@@ -5054,8 +6106,7 @@ var cleanCommand = defineCommand({
|
|
|
5054
6106
|
dryRun: Boolean(args.dryRun),
|
|
5055
6107
|
yes: Boolean(args.yes)
|
|
5056
6108
|
});
|
|
5057
|
-
|
|
5058
|
-
applyExitCode(result);
|
|
6109
|
+
reportResult("clean", result, Boolean(args.json));
|
|
5059
6110
|
}
|
|
5060
6111
|
});
|
|
5061
6112
|
var initCommand = defineCommand({
|
|
@@ -5079,14 +6130,13 @@ var initCommand = defineCommand({
|
|
|
5079
6130
|
}
|
|
5080
6131
|
});
|
|
5081
6132
|
var runJump = async (target, options) => {
|
|
5082
|
-
const result = await jump(git, fs,
|
|
6133
|
+
const result = await jump(git, fs, {
|
|
5083
6134
|
cwd: process.cwd(),
|
|
5084
6135
|
target,
|
|
5085
6136
|
create: options.create,
|
|
5086
6137
|
...options.track === undefined ? {} : { track: options.track }
|
|
5087
6138
|
});
|
|
5088
|
-
|
|
5089
|
-
applyExitCode(result);
|
|
6139
|
+
reportResult("jump", result, options.json);
|
|
5090
6140
|
};
|
|
5091
6141
|
var RESERVED_COMMANDS = {
|
|
5092
6142
|
ls: lsCommand,
|
|
@@ -5135,12 +6185,7 @@ var runInteractivePick = async (json) => {
|
|
|
5135
6185
|
path: selected.worktree.path,
|
|
5136
6186
|
data: { branch: selected.worktree.branch, created: false }
|
|
5137
6187
|
});
|
|
5138
|
-
|
|
5139
|
-
render("pick", result, true);
|
|
5140
|
-
} else {
|
|
5141
|
-
process.stdout.write(`${selected.worktree.path}
|
|
5142
|
-
`);
|
|
5143
|
-
}
|
|
6188
|
+
render("pick", result, json);
|
|
5144
6189
|
return;
|
|
5145
6190
|
}
|
|
5146
6191
|
await runJump(selected.branch, { create: true, json });
|
|
@@ -5152,9 +6197,7 @@ var runJumpFromArgs = async (rawArgs) => {
|
|
|
5152
6197
|
await runInteractivePick(Boolean(args.json));
|
|
5153
6198
|
return;
|
|
5154
6199
|
}
|
|
5155
|
-
|
|
5156
|
-
render("ls", result, Boolean(args.json));
|
|
5157
|
-
applyExitCode(result);
|
|
6200
|
+
await runLs(Boolean(args.json));
|
|
5158
6201
|
return;
|
|
5159
6202
|
}
|
|
5160
6203
|
await runJump(String(args.target), {
|
|
@@ -5164,19 +6207,47 @@ var runJumpFromArgs = async (rawArgs) => {
|
|
|
5164
6207
|
});
|
|
5165
6208
|
};
|
|
5166
6209
|
var ARGV_USER_ARGS_START = 2;
|
|
5167
|
-
var
|
|
5168
|
-
if (isHelpRequest(rawArgs)) {
|
|
5169
|
-
process.stderr.write(USAGE);
|
|
5170
|
-
process.exitCode = 0;
|
|
5171
|
-
} else {
|
|
5172
|
-
const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
|
|
6210
|
+
var dispatchAndRun = async (dispatch) => {
|
|
5173
6211
|
if (dispatch.kind === "reserved") {
|
|
5174
6212
|
const command = RESERVED_COMMANDS[dispatch.name];
|
|
5175
6213
|
const commandArgs = dispatch.name === "root" ? rewriteRootPreviousToken(dispatch.args) : [...dispatch.args];
|
|
5176
6214
|
await runCommand(command, {
|
|
5177
6215
|
rawArgs: commandArgs
|
|
5178
6216
|
});
|
|
5179
|
-
|
|
5180
|
-
await runJumpFromArgs(dispatch.args);
|
|
6217
|
+
return;
|
|
5181
6218
|
}
|
|
6219
|
+
await runJumpFromArgs(dispatch.args);
|
|
6220
|
+
};
|
|
6221
|
+
var main = async () => {
|
|
6222
|
+
const rawArgs = normalizeCliArgs(process.argv.slice(ARGV_USER_ARGS_START), process.argv0, isRunningAsCompiledBinary());
|
|
6223
|
+
if (isHelpRequest(rawArgs)) {
|
|
6224
|
+
process.stderr.write(USAGE);
|
|
6225
|
+
process.exitCode = 0;
|
|
6226
|
+
return;
|
|
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
|
+
}
|
|
6238
|
+
const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
|
|
6239
|
+
try {
|
|
6240
|
+
await dispatchAndRun(dispatch);
|
|
6241
|
+
} catch (error) {
|
|
6242
|
+
const command = dispatch.kind === "reserved" ? dispatch.name : "jump";
|
|
6243
|
+
reportFatalError(command, wantsJson(rawArgs), error);
|
|
6244
|
+
}
|
|
6245
|
+
};
|
|
6246
|
+
try {
|
|
6247
|
+
await main();
|
|
6248
|
+
} catch (error) {
|
|
6249
|
+
const fatal = describeFatalError(error);
|
|
6250
|
+
process.stderr.write(`hop: ${fatal.message}
|
|
6251
|
+
`);
|
|
6252
|
+
process.exitCode = fatal.exitCode;
|
|
5182
6253
|
}
|