@n-seiji/nuthatch 0.1.0 → 0.1.2
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.md +67 -0
- package/dist/cli.js +1155 -488
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -809,7 +809,18 @@ var buildPickCandidates = (worktrees, dirtyByPath, localBranches, remoteBranches
|
|
|
809
809
|
source: "remote"
|
|
810
810
|
}))
|
|
811
811
|
];
|
|
812
|
-
}, candidateBranchLabel = (candidate) => candidate.kind === "worktree" ? candidate.worktree.branch ?? "(detached)" : candidate.branch;
|
|
812
|
+
}, candidateBranchLabel = (candidate) => candidate.kind === "worktree" ? candidate.worktree.branch ?? "(detached)" : candidate.branch, candidateBranchName = (candidate) => candidate.kind === "worktree" ? candidate.worktree.branch : candidate.branch;
|
|
813
|
+
|
|
814
|
+
// src/domain/result.ts
|
|
815
|
+
var EXIT_SUCCESS = 0, EXIT_GENERAL_ERROR = 1, EXIT_USAGE_ERROR = 2, EXIT_SAFE_REJECTION = 3, EXIT_CANCELLED = 130, ok = (fields = {}) => ({
|
|
816
|
+
ok: true,
|
|
817
|
+
exitCode: EXIT_SUCCESS,
|
|
818
|
+
...fields
|
|
819
|
+
}), fail = (exitCode, errorMessage) => ({
|
|
820
|
+
ok: false,
|
|
821
|
+
exitCode,
|
|
822
|
+
errorMessage
|
|
823
|
+
});
|
|
813
824
|
|
|
814
825
|
// node_modules/react/cjs/react.development.js
|
|
815
826
|
var require_react_development = __commonJS((exports, module) => {
|
|
@@ -41982,6 +41993,182 @@ var init_build2 = __esm(async () => {
|
|
|
41982
41993
|
init_kitty_keyboard();
|
|
41983
41994
|
});
|
|
41984
41995
|
|
|
41996
|
+
// src/ui/alt-screen.ts
|
|
41997
|
+
var ASCII_ESCAPE_CODE = 27, ESCAPE2, ENTER_ALT_SCREEN, LEAVE_ALT_SCREEN, HIDE_CURSOR, SHOW_CURSOR, enterAltScreen = (target) => {
|
|
41998
|
+
if (!target.isTTY) {
|
|
41999
|
+
return;
|
|
42000
|
+
}
|
|
42001
|
+
target.write(ENTER_ALT_SCREEN + HIDE_CURSOR);
|
|
42002
|
+
}, leaveAltScreen = (target) => {
|
|
42003
|
+
if (!target.isTTY) {
|
|
42004
|
+
return;
|
|
42005
|
+
}
|
|
42006
|
+
target.write(SHOW_CURSOR + LEAVE_ALT_SCREEN);
|
|
42007
|
+
};
|
|
42008
|
+
var init_alt_screen = __esm(() => {
|
|
42009
|
+
ESCAPE2 = String.fromCodePoint(ASCII_ESCAPE_CODE);
|
|
42010
|
+
ENTER_ALT_SCREEN = `${ESCAPE2}[?1049h`;
|
|
42011
|
+
LEAVE_ALT_SCREEN = `${ESCAPE2}[?1049l`;
|
|
42012
|
+
HIDE_CURSOR = `${ESCAPE2}[?25l`;
|
|
42013
|
+
SHOW_CURSOR = `${ESCAPE2}[?25h`;
|
|
42014
|
+
});
|
|
42015
|
+
|
|
42016
|
+
// src/ui/alt-screen-session.ts
|
|
42017
|
+
var createAltScreenTarget = () => ({
|
|
42018
|
+
isTTY: process.stderr.isTTY === true,
|
|
42019
|
+
write: (data) => {
|
|
42020
|
+
process.stderr.write(data);
|
|
42021
|
+
}
|
|
42022
|
+
}), runInAltScreenSession = (renderElement) => new Promise((resolve) => {
|
|
42023
|
+
const altScreen = createAltScreenTarget();
|
|
42024
|
+
let hasLeftAltScreen = false;
|
|
42025
|
+
const leaveOnce = () => {
|
|
42026
|
+
if (hasLeftAltScreen) {
|
|
42027
|
+
return;
|
|
42028
|
+
}
|
|
42029
|
+
hasLeftAltScreen = true;
|
|
42030
|
+
leaveAltScreen(altScreen);
|
|
42031
|
+
};
|
|
42032
|
+
process.once("exit", leaveOnce);
|
|
42033
|
+
const instanceRef = {
|
|
42034
|
+
current: null
|
|
42035
|
+
};
|
|
42036
|
+
const handleSigint = () => {
|
|
42037
|
+
leaveOnce();
|
|
42038
|
+
instanceRef.current?.unmount();
|
|
42039
|
+
process.exitCode = EXIT_CANCELLED;
|
|
42040
|
+
process.exit(EXIT_CANCELLED);
|
|
42041
|
+
};
|
|
42042
|
+
process.once("SIGINT", handleSigint);
|
|
42043
|
+
const finish = (result) => {
|
|
42044
|
+
leaveOnce();
|
|
42045
|
+
process.removeListener("SIGINT", handleSigint);
|
|
42046
|
+
instanceRef.current?.unmount();
|
|
42047
|
+
resolve(result);
|
|
42048
|
+
};
|
|
42049
|
+
enterAltScreen(altScreen);
|
|
42050
|
+
try {
|
|
42051
|
+
instanceRef.current = renderElement(finish);
|
|
42052
|
+
} catch (error) {
|
|
42053
|
+
leaveOnce();
|
|
42054
|
+
process.removeListener("SIGINT", handleSigint);
|
|
42055
|
+
throw error;
|
|
42056
|
+
}
|
|
42057
|
+
});
|
|
42058
|
+
var init_alt_screen_session = __esm(() => {
|
|
42059
|
+
init_alt_screen();
|
|
42060
|
+
});
|
|
42061
|
+
|
|
42062
|
+
// src/ui/picker-layout.ts
|
|
42063
|
+
var MAX_PATH_LENGTH = 40, MAX_BRANCH_COLUMN_WIDTH = 24, LEGEND_TEXT = "●=dirty ○=clean +=未作成", NARROW_TERMINAL_WIDTH_THRESHOLD = 60, isNarrowTerminal = (columns) => columns < NARROW_TERMINAL_WIDTH_THRESHOLD, WORKTREE_KIND_LABELS, CREATABLE_SOURCE_LABELS, statusMarker = (candidate) => {
|
|
42064
|
+
if (candidate.kind === "creatable") {
|
|
42065
|
+
return "+";
|
|
42066
|
+
}
|
|
42067
|
+
if (candidate.dirty === null) {
|
|
42068
|
+
return " ";
|
|
42069
|
+
}
|
|
42070
|
+
return candidate.dirty ? "●" : "○";
|
|
42071
|
+
}, candidateKindLabel = (candidate) => candidate.kind === "worktree" ? WORKTREE_KIND_LABELS[candidate.worktree.kind] : CREATABLE_SOURCE_LABELS[candidate.source], isWorktreeCandidate = (candidate) => candidate.kind === "worktree", isCreatableCandidate = (candidate) => candidate.kind === "creatable", WORKTREE_KIND_ORDER, CREATABLE_SOURCE_ORDER, compareByBranchLabel = (a2, b2) => candidateBranchLabel(a2).localeCompare(candidateBranchLabel(b2)), detachedRank = (candidate) => candidate.worktree.branch === null ? 1 : 0, compareWorktreeCandidates = (a2, b2) => {
|
|
42072
|
+
const kindDiff = WORKTREE_KIND_ORDER[a2.worktree.kind] - WORKTREE_KIND_ORDER[b2.worktree.kind];
|
|
42073
|
+
if (kindDiff !== 0) {
|
|
42074
|
+
return kindDiff;
|
|
42075
|
+
}
|
|
42076
|
+
const detachedDiff = detachedRank(a2) - detachedRank(b2);
|
|
42077
|
+
return detachedDiff === 0 ? compareByBranchLabel(a2, b2) : detachedDiff;
|
|
42078
|
+
}, compareCreatableCandidates = (a2, b2) => {
|
|
42079
|
+
const sourceDiff = CREATABLE_SOURCE_ORDER[a2.source] - CREATABLE_SOURCE_ORDER[b2.source];
|
|
42080
|
+
return sourceDiff === 0 ? compareByBranchLabel(a2, b2) : sourceDiff;
|
|
42081
|
+
}, sortCandidatesForDisplay = (candidates) => [
|
|
42082
|
+
...candidates.filter((candidate) => isWorktreeCandidate(candidate)).toSorted((a2, b2) => compareWorktreeCandidates(a2, b2)),
|
|
42083
|
+
...candidates.filter((candidate) => isCreatableCandidate(candidate)).toSorted((a2, b2) => compareCreatableCandidates(a2, b2))
|
|
42084
|
+
], KIND_COLUMN_WIDTH, shortenPath = (path, homeDir, maxLength = MAX_PATH_LENGTH) => {
|
|
42085
|
+
const withTilde = homeDir.length > 0 && (path === homeDir || path.startsWith(`${homeDir}/`)) ? `~${path.slice(homeDir.length)}` : path;
|
|
42086
|
+
if (withTilde.length <= maxLength) {
|
|
42087
|
+
return withTilde;
|
|
42088
|
+
}
|
|
42089
|
+
const ellipsis = "…";
|
|
42090
|
+
const keepLength = maxLength - ellipsis.length;
|
|
42091
|
+
return `${ellipsis}${withTilde.slice(withTilde.length - keepLength)}`;
|
|
42092
|
+
}, candidatePathLabel = (candidate, homeDir) => candidate.kind === "worktree" ? shortenPath(candidate.worktree.path, homeDir) : "", branchColumnWidth = (candidates) => candidates.reduce((max, candidate) => Math.min(MAX_BRANCH_COLUMN_WIDTH, Math.max(max, candidateBranchLabel(candidate).length)), 0), padBranchLabel = (label, width) => label.length >= width ? label : label.padEnd(width, " "), displayRowKey = (row) => row.kind === "header" ? `header:${row.label}` : `candidate:${row.index}`, toCandidateRow = (candidate, options) => ({
|
|
42093
|
+
kind: "candidate",
|
|
42094
|
+
index: options.index,
|
|
42095
|
+
section: options.section,
|
|
42096
|
+
statusMarker: statusMarker(candidate),
|
|
42097
|
+
branchLabel: padBranchLabel(candidateBranchLabel(candidate), options.branchWidth),
|
|
42098
|
+
kindLabel: candidateKindLabel(candidate).padEnd(KIND_COLUMN_WIDTH, " "),
|
|
42099
|
+
pathLabel: candidatePathLabel(candidate, options.homeDir)
|
|
42100
|
+
}), buildDisplayRows = (candidates, homeDir) => {
|
|
42101
|
+
const branchWidth = branchColumnWidth(candidates);
|
|
42102
|
+
const indexed = candidates.map((candidate, index) => ({ candidate, index }));
|
|
42103
|
+
const worktreeEntries = indexed.filter((entry) => isWorktreeCandidate(entry.candidate));
|
|
42104
|
+
const branchEntries = indexed.filter((entry) => isCreatableCandidate(entry.candidate));
|
|
42105
|
+
const rows = [];
|
|
42106
|
+
if (worktreeEntries.length > 0) {
|
|
42107
|
+
rows.push({ kind: "header", label: "WORKTREES" });
|
|
42108
|
+
for (const entry of worktreeEntries) {
|
|
42109
|
+
rows.push(toCandidateRow(entry.candidate, {
|
|
42110
|
+
index: entry.index,
|
|
42111
|
+
section: "worktree",
|
|
42112
|
+
branchWidth,
|
|
42113
|
+
homeDir
|
|
42114
|
+
}));
|
|
42115
|
+
}
|
|
42116
|
+
}
|
|
42117
|
+
if (branchEntries.length > 0) {
|
|
42118
|
+
rows.push({ kind: "header", label: "BRANCHES — Enter で worktree 作成" });
|
|
42119
|
+
for (const entry of branchEntries) {
|
|
42120
|
+
rows.push(toCandidateRow(entry.candidate, {
|
|
42121
|
+
index: entry.index,
|
|
42122
|
+
section: "branch",
|
|
42123
|
+
branchWidth,
|
|
42124
|
+
homeDir
|
|
42125
|
+
}));
|
|
42126
|
+
}
|
|
42127
|
+
}
|
|
42128
|
+
return rows;
|
|
42129
|
+
};
|
|
42130
|
+
var init_picker_layout = __esm(() => {
|
|
42131
|
+
WORKTREE_KIND_LABELS = {
|
|
42132
|
+
root: "root",
|
|
42133
|
+
managed: "managed",
|
|
42134
|
+
external: "ext"
|
|
42135
|
+
};
|
|
42136
|
+
CREATABLE_SOURCE_LABELS = {
|
|
42137
|
+
local: "local",
|
|
42138
|
+
remote: "remote"
|
|
42139
|
+
};
|
|
42140
|
+
WORKTREE_KIND_ORDER = {
|
|
42141
|
+
root: 0,
|
|
42142
|
+
managed: 1,
|
|
42143
|
+
external: 2
|
|
42144
|
+
};
|
|
42145
|
+
CREATABLE_SOURCE_ORDER = {
|
|
42146
|
+
local: 0,
|
|
42147
|
+
remote: 1
|
|
42148
|
+
};
|
|
42149
|
+
KIND_COLUMN_WIDTH = Math.max(...Object.values(WORKTREE_KIND_LABELS).map((label) => label.length), ...Object.values(CREATABLE_SOURCE_LABELS).map((label) => label.length));
|
|
42150
|
+
});
|
|
42151
|
+
|
|
42152
|
+
// src/domain/actions.ts
|
|
42153
|
+
var availableActions = (candidate) => {
|
|
42154
|
+
const actions = ["cd"];
|
|
42155
|
+
if (candidate.kind !== "worktree") {
|
|
42156
|
+
actions.push("switchRoot");
|
|
42157
|
+
return actions;
|
|
42158
|
+
}
|
|
42159
|
+
if (candidate.worktree.branch === null) {
|
|
42160
|
+
return actions;
|
|
42161
|
+
}
|
|
42162
|
+
if (candidate.worktree.kind === "managed") {
|
|
42163
|
+
actions.push("delete");
|
|
42164
|
+
}
|
|
42165
|
+
if (candidate.worktree.kind !== "root") {
|
|
42166
|
+
actions.push("switchRoot");
|
|
42167
|
+
}
|
|
42168
|
+
return actions;
|
|
42169
|
+
};
|
|
42170
|
+
var init_actions = () => {};
|
|
42171
|
+
|
|
41985
42172
|
// node_modules/react/cjs/react-jsx-dev-runtime.development.js
|
|
41986
42173
|
var require_react_jsx_dev_runtime_development = __commonJS((exports) => {
|
|
41987
42174
|
var React11 = __toESM(require_react(), 1);
|
|
@@ -42205,111 +42392,497 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
|
|
|
42205
42392
|
}
|
|
42206
42393
|
});
|
|
42207
42394
|
|
|
42208
|
-
// src/ui/
|
|
42209
|
-
var
|
|
42210
|
-
|
|
42211
|
-
|
|
42212
|
-
|
|
42395
|
+
// src/ui/side-panel.tsx
|
|
42396
|
+
var jsx_dev_runtime, ACTION_LABELS, ACTION_LETTERS, SIDE_PANEL_WIDTH = 34, ActionPanel = ({ candidate, panelIndex, error, busy }) => {
|
|
42397
|
+
const actions = availableActions(candidate);
|
|
42398
|
+
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Box_default, {
|
|
42399
|
+
flexDirection: "column",
|
|
42400
|
+
borderStyle: "round",
|
|
42401
|
+
paddingX: 1,
|
|
42402
|
+
width: SIDE_PANEL_WIDTH,
|
|
42403
|
+
children: [
|
|
42404
|
+
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42405
|
+
children: [
|
|
42406
|
+
"Actions for ",
|
|
42407
|
+
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42408
|
+
color: "cyan",
|
|
42409
|
+
children: candidateBranchLabel(candidate)
|
|
42410
|
+
}, undefined, false, undefined, this)
|
|
42411
|
+
]
|
|
42412
|
+
}, undefined, true, undefined, this),
|
|
42413
|
+
actions.map((action, actionIndex) => /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42414
|
+
inverse: actionIndex === panelIndex,
|
|
42415
|
+
children: `${actionIndex === panelIndex ? "> " : " "}[${ACTION_LETTERS[action]}] ${ACTION_LABELS[action]}`
|
|
42416
|
+
}, action, false, undefined, this)),
|
|
42417
|
+
busy && /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42418
|
+
dimColor: true,
|
|
42419
|
+
children: "Working…"
|
|
42420
|
+
}, undefined, false, undefined, this),
|
|
42421
|
+
error !== null && /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42422
|
+
color: "red",
|
|
42423
|
+
children: error
|
|
42424
|
+
}, undefined, false, undefined, this)
|
|
42425
|
+
]
|
|
42426
|
+
}, undefined, true, undefined, this);
|
|
42427
|
+
}, ConfirmDeletePanel = ({ candidate }) => /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Box_default, {
|
|
42428
|
+
flexDirection: "column",
|
|
42429
|
+
borderStyle: "round",
|
|
42430
|
+
paddingX: 1,
|
|
42431
|
+
width: SIDE_PANEL_WIDTH,
|
|
42432
|
+
children: [
|
|
42433
|
+
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42434
|
+
children: [
|
|
42435
|
+
"Delete worktree for ",
|
|
42436
|
+
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42437
|
+
color: "cyan",
|
|
42438
|
+
children: candidateBranchName(candidate)
|
|
42439
|
+
}, undefined, false, undefined, this),
|
|
42440
|
+
"?"
|
|
42441
|
+
]
|
|
42442
|
+
}, undefined, true, undefined, this),
|
|
42443
|
+
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42444
|
+
dimColor: true,
|
|
42445
|
+
children: "(y/N)"
|
|
42446
|
+
}, undefined, false, undefined, this)
|
|
42447
|
+
]
|
|
42448
|
+
}, undefined, true, undefined, this);
|
|
42449
|
+
var init_side_panel = __esm(async () => {
|
|
42450
|
+
await init_build2();
|
|
42451
|
+
init_actions();
|
|
42452
|
+
jsx_dev_runtime = __toESM(require_jsx_dev_runtime(), 1);
|
|
42453
|
+
ACTION_LABELS = {
|
|
42454
|
+
cd: "cd into this worktree",
|
|
42455
|
+
delete: "delete worktree",
|
|
42456
|
+
switchRoot: "switch root here"
|
|
42457
|
+
};
|
|
42458
|
+
ACTION_LETTERS = {
|
|
42459
|
+
cd: "c",
|
|
42460
|
+
delete: "d",
|
|
42461
|
+
switchRoot: "r"
|
|
42462
|
+
};
|
|
42213
42463
|
});
|
|
42214
|
-
|
|
42215
|
-
|
|
42216
|
-
|
|
42464
|
+
|
|
42465
|
+
// src/ui/picker-keys.ts
|
|
42466
|
+
var isCtrlJByte = (input) => input === `
|
|
42467
|
+
`, resolvePickerKeyAction = (input, key) => {
|
|
42468
|
+
if (key.escape) {
|
|
42469
|
+
return { type: "cancel", reason: "esc" };
|
|
42217
42470
|
}
|
|
42218
|
-
if (
|
|
42219
|
-
return "";
|
|
42471
|
+
if (key.ctrl && input === "c") {
|
|
42472
|
+
return { type: "cancel", reason: "ctrlC" };
|
|
42473
|
+
}
|
|
42474
|
+
if (key.return) {
|
|
42475
|
+
return { type: "select" };
|
|
42476
|
+
}
|
|
42477
|
+
if (key.tab || key.rightArrow || key.ctrl && (input === "l" || input === "f")) {
|
|
42478
|
+
return { type: "openPanel" };
|
|
42479
|
+
}
|
|
42480
|
+
if (key.ctrl && input === "x") {
|
|
42481
|
+
return { type: "deleteShortcut" };
|
|
42482
|
+
}
|
|
42483
|
+
if (key.ctrl && input === "r") {
|
|
42484
|
+
return { type: "rootSwitchShortcut" };
|
|
42220
42485
|
}
|
|
42221
|
-
|
|
42222
|
-
|
|
42486
|
+
if (key.upArrow || key.ctrl && (input === "p" || input === "k")) {
|
|
42487
|
+
return { type: "up" };
|
|
42488
|
+
}
|
|
42489
|
+
if (key.downArrow || key.ctrl && (input === "n" || input === "j") || isCtrlJByte(input)) {
|
|
42490
|
+
return { type: "down" };
|
|
42491
|
+
}
|
|
42492
|
+
if (key.ctrl && input === "u") {
|
|
42493
|
+
return { type: "clear" };
|
|
42494
|
+
}
|
|
42495
|
+
if (key.backspace || key.delete) {
|
|
42496
|
+
return { type: "backspace" };
|
|
42497
|
+
}
|
|
42498
|
+
if (input.length > 0 && !key.ctrl && !key.meta) {
|
|
42499
|
+
return { type: "char", char: input };
|
|
42500
|
+
}
|
|
42501
|
+
return { type: "ignore" };
|
|
42502
|
+
}, PANEL_LETTER_SHORTCUTS, resolvePanelKeyAction = (input, key) => {
|
|
42503
|
+
if (key.escape || key.tab || key.leftArrow || key.backspace || key.ctrl && input === "h") {
|
|
42504
|
+
return { type: "close" };
|
|
42505
|
+
}
|
|
42506
|
+
if (key.return) {
|
|
42507
|
+
return { type: "confirm" };
|
|
42508
|
+
}
|
|
42509
|
+
if (key.upArrow || key.ctrl && (input === "p" || input === "k")) {
|
|
42510
|
+
return { type: "up" };
|
|
42511
|
+
}
|
|
42512
|
+
if (key.downArrow || key.ctrl && (input === "n" || input === "j") || isCtrlJByte(input)) {
|
|
42513
|
+
return { type: "down" };
|
|
42514
|
+
}
|
|
42515
|
+
if (!key.ctrl && !key.meta && isPanelLetterShortcut(input)) {
|
|
42516
|
+
return { type: "letter", char: input };
|
|
42517
|
+
}
|
|
42518
|
+
return { type: "ignore" };
|
|
42519
|
+
}, isPanelLetterShortcut = (input) => PANEL_LETTER_SHORTCUTS.includes(input), resolveConfirmKeyAction = (input, key) => {
|
|
42520
|
+
if (!key.ctrl && !key.meta && (input === "y" || input === "Y")) {
|
|
42521
|
+
return { type: "yes" };
|
|
42522
|
+
}
|
|
42523
|
+
return { type: "no" };
|
|
42524
|
+
};
|
|
42525
|
+
var init_picker_keys = __esm(() => {
|
|
42526
|
+
PANEL_LETTER_SHORTCUTS = ["c", "d", "r"];
|
|
42527
|
+
});
|
|
42528
|
+
|
|
42529
|
+
// src/ui/picker-input.ts
|
|
42530
|
+
var handleConfirmDeleteInput = (input, key, confirmMode, ctx) => {
|
|
42531
|
+
const confirmAction = resolveConfirmKeyAction(input, key);
|
|
42532
|
+
if (confirmAction.type === "yes") {
|
|
42533
|
+
ctx.runAction(confirmMode.candidate, "delete");
|
|
42534
|
+
} else {
|
|
42535
|
+
ctx.setMode({ kind: "list" });
|
|
42536
|
+
}
|
|
42537
|
+
}, handlePanelInput = (input, key, panelMode, ctx) => {
|
|
42538
|
+
const actions = availableActions(panelMode.candidate);
|
|
42539
|
+
const panelAction = resolvePanelKeyAction(input, key);
|
|
42540
|
+
switch (panelAction.type) {
|
|
42541
|
+
case "close": {
|
|
42542
|
+
ctx.setMode({ kind: "list" });
|
|
42543
|
+
break;
|
|
42544
|
+
}
|
|
42545
|
+
case "up": {
|
|
42546
|
+
ctx.setPanelIndex((current) => Math.max(0, current - 1));
|
|
42547
|
+
break;
|
|
42548
|
+
}
|
|
42549
|
+
case "down": {
|
|
42550
|
+
ctx.setPanelIndex((current) => Math.min(actions.length - 1, current + 1));
|
|
42551
|
+
break;
|
|
42552
|
+
}
|
|
42553
|
+
case "confirm": {
|
|
42554
|
+
const chosen = actions[Math.min(ctx.panelIndex, actions.length - 1)];
|
|
42555
|
+
if (chosen !== undefined) {
|
|
42556
|
+
ctx.runAction(panelMode.candidate, chosen);
|
|
42557
|
+
}
|
|
42558
|
+
break;
|
|
42559
|
+
}
|
|
42560
|
+
case "letter": {
|
|
42561
|
+
const chosen = actions.find((action) => ACTION_LETTERS[action] === panelAction.char);
|
|
42562
|
+
if (chosen !== undefined) {
|
|
42563
|
+
ctx.runAction(panelMode.candidate, chosen);
|
|
42564
|
+
}
|
|
42565
|
+
break;
|
|
42566
|
+
}
|
|
42567
|
+
case "ignore": {
|
|
42568
|
+
break;
|
|
42569
|
+
}
|
|
42570
|
+
}
|
|
42571
|
+
}, handleListInput = (input, key, ctx) => {
|
|
42572
|
+
const action = resolvePickerKeyAction(input, key);
|
|
42573
|
+
switch (action.type) {
|
|
42574
|
+
case "cancel": {
|
|
42575
|
+
ctx.onCancel(action.reason);
|
|
42576
|
+
break;
|
|
42577
|
+
}
|
|
42578
|
+
case "select": {
|
|
42579
|
+
if (ctx.selectedCandidate !== undefined) {
|
|
42580
|
+
ctx.runAction(ctx.selectedCandidate, "cd");
|
|
42581
|
+
}
|
|
42582
|
+
break;
|
|
42583
|
+
}
|
|
42584
|
+
case "up": {
|
|
42585
|
+
ctx.setIndex((current) => Math.max(0, current - 1));
|
|
42586
|
+
break;
|
|
42587
|
+
}
|
|
42588
|
+
case "down": {
|
|
42589
|
+
ctx.setIndex((current) => Math.min(ctx.filteredLength - 1, current + 1));
|
|
42590
|
+
break;
|
|
42591
|
+
}
|
|
42592
|
+
case "clear": {
|
|
42593
|
+
ctx.setQuery(() => "");
|
|
42594
|
+
ctx.setIndex(() => 0);
|
|
42595
|
+
break;
|
|
42596
|
+
}
|
|
42597
|
+
case "backspace": {
|
|
42598
|
+
ctx.setQuery((current) => current.slice(0, -1));
|
|
42599
|
+
ctx.setIndex(() => 0);
|
|
42600
|
+
break;
|
|
42601
|
+
}
|
|
42602
|
+
case "char": {
|
|
42603
|
+
ctx.setQuery((current) => current + action.char);
|
|
42604
|
+
ctx.setIndex(() => 0);
|
|
42605
|
+
break;
|
|
42606
|
+
}
|
|
42607
|
+
case "openPanel": {
|
|
42608
|
+
if (ctx.selectedCandidate !== undefined) {
|
|
42609
|
+
ctx.setPanelIndex(0);
|
|
42610
|
+
ctx.setMode({
|
|
42611
|
+
kind: "panel",
|
|
42612
|
+
candidate: ctx.selectedCandidate,
|
|
42613
|
+
error: null
|
|
42614
|
+
});
|
|
42615
|
+
}
|
|
42616
|
+
break;
|
|
42617
|
+
}
|
|
42618
|
+
case "deleteShortcut": {
|
|
42619
|
+
if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("delete")) {
|
|
42620
|
+
ctx.setMode({
|
|
42621
|
+
kind: "confirmDelete",
|
|
42622
|
+
candidate: ctx.selectedCandidate,
|
|
42623
|
+
error: null
|
|
42624
|
+
});
|
|
42625
|
+
}
|
|
42626
|
+
break;
|
|
42627
|
+
}
|
|
42628
|
+
case "rootSwitchShortcut": {
|
|
42629
|
+
if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("switchRoot")) {
|
|
42630
|
+
ctx.runAction(ctx.selectedCandidate, "switchRoot");
|
|
42631
|
+
}
|
|
42632
|
+
break;
|
|
42633
|
+
}
|
|
42634
|
+
case "ignore": {
|
|
42635
|
+
break;
|
|
42636
|
+
}
|
|
42637
|
+
}
|
|
42638
|
+
};
|
|
42639
|
+
var init_picker_input = __esm(async () => {
|
|
42640
|
+
init_actions();
|
|
42641
|
+
await init_side_panel();
|
|
42642
|
+
init_picker_keys();
|
|
42643
|
+
});
|
|
42644
|
+
|
|
42645
|
+
// src/ui/picker-controller.ts
|
|
42646
|
+
var import_react34, matchesQuery = (candidate, query) => query.length === 0 || candidateBranchLabel(candidate).toLowerCase().includes(query.toLowerCase()), panelErrorTransition = (candidate, error) => ({
|
|
42647
|
+
mode: { kind: "panel", candidate, error },
|
|
42648
|
+
panelIndex: 0
|
|
42649
|
+
}), usePickerController = (initialCandidates, callbacks, onExit, onCancel) => {
|
|
42650
|
+
const [candidates, setCandidates] = import_react34.useState(initialCandidates);
|
|
42223
42651
|
const [query, setQuery] = import_react34.useState("");
|
|
42224
42652
|
const [index, setIndex] = import_react34.useState(0);
|
|
42225
|
-
const
|
|
42653
|
+
const [mode, setMode] = import_react34.useState({ kind: "list" });
|
|
42654
|
+
const [panelIndex, setPanelIndex] = import_react34.useState(0);
|
|
42655
|
+
const [busy, setBusy] = import_react34.useState(false);
|
|
42656
|
+
const filtered = import_react34.useMemo(() => sortCandidatesForDisplay(candidates.filter((candidate) => matchesQuery(candidate, query))), [candidates, query]);
|
|
42226
42657
|
const clampedIndex = Math.min(index, Math.max(filtered.length - 1, 0));
|
|
42227
|
-
|
|
42228
|
-
|
|
42229
|
-
|
|
42658
|
+
const selectedCandidate = filtered[clampedIndex];
|
|
42659
|
+
const applyMutation = async (candidate, action) => {
|
|
42660
|
+
if (action === "switchRoot") {
|
|
42661
|
+
const result2 = await callbacks.switchRootHere(candidate);
|
|
42662
|
+
if (!result2.ok || result2.path === undefined) {
|
|
42663
|
+
const transition = panelErrorTransition(candidate, result2.message ?? "Failed to switch root.");
|
|
42664
|
+
setPanelIndex(transition.panelIndex);
|
|
42665
|
+
setMode(transition.mode);
|
|
42666
|
+
return;
|
|
42667
|
+
}
|
|
42668
|
+
onExit({ type: "path", path: result2.path });
|
|
42230
42669
|
return;
|
|
42231
42670
|
}
|
|
42232
|
-
|
|
42233
|
-
|
|
42234
|
-
|
|
42235
|
-
|
|
42236
|
-
|
|
42671
|
+
const result = await callbacks.deleteWorktree(candidate);
|
|
42672
|
+
if (!result.ok) {
|
|
42673
|
+
const transition = panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.");
|
|
42674
|
+
setPanelIndex(transition.panelIndex);
|
|
42675
|
+
setMode(transition.mode);
|
|
42676
|
+
return;
|
|
42677
|
+
}
|
|
42678
|
+
const fresh = await callbacks.reloadCandidates();
|
|
42679
|
+
setCandidates(fresh);
|
|
42680
|
+
setMode({ kind: "list" });
|
|
42681
|
+
setIndex(0);
|
|
42682
|
+
};
|
|
42683
|
+
const runAction = (candidate, action) => {
|
|
42684
|
+
if (action === "cd") {
|
|
42685
|
+
onExit({ type: "cd", candidate });
|
|
42237
42686
|
return;
|
|
42238
42687
|
}
|
|
42239
|
-
if (
|
|
42240
|
-
setIndex((current) => Math.max(0, current - 1));
|
|
42688
|
+
if (busy) {
|
|
42241
42689
|
return;
|
|
42242
42690
|
}
|
|
42243
|
-
|
|
42244
|
-
|
|
42691
|
+
setBusy(true);
|
|
42692
|
+
(async () => {
|
|
42693
|
+
await applyMutation(candidate, action);
|
|
42694
|
+
setBusy(false);
|
|
42695
|
+
})();
|
|
42696
|
+
};
|
|
42697
|
+
const handleInput = (input, key) => {
|
|
42698
|
+
if (busy) {
|
|
42245
42699
|
return;
|
|
42246
42700
|
}
|
|
42247
|
-
if (
|
|
42248
|
-
|
|
42249
|
-
setIndex(0);
|
|
42701
|
+
if (mode.kind === "confirmDelete") {
|
|
42702
|
+
handleConfirmDeleteInput(input, key, mode, { runAction, setMode });
|
|
42250
42703
|
return;
|
|
42251
42704
|
}
|
|
42252
|
-
if (
|
|
42253
|
-
|
|
42254
|
-
|
|
42705
|
+
if (mode.kind === "panel") {
|
|
42706
|
+
handlePanelInput(input, key, mode, {
|
|
42707
|
+
panelIndex,
|
|
42708
|
+
runAction,
|
|
42709
|
+
setPanelIndex,
|
|
42710
|
+
setMode
|
|
42711
|
+
});
|
|
42712
|
+
return;
|
|
42255
42713
|
}
|
|
42256
|
-
|
|
42714
|
+
handleListInput(input, key, {
|
|
42715
|
+
selectedCandidate,
|
|
42716
|
+
filteredLength: filtered.length,
|
|
42717
|
+
runAction,
|
|
42718
|
+
onCancel,
|
|
42719
|
+
setIndex,
|
|
42720
|
+
setQuery,
|
|
42721
|
+
setPanelIndex: (value) => setPanelIndex(value),
|
|
42722
|
+
setMode
|
|
42723
|
+
});
|
|
42724
|
+
};
|
|
42725
|
+
return { query, filtered, clampedIndex, mode, panelIndex, busy, handleInput };
|
|
42726
|
+
};
|
|
42727
|
+
var init_picker_controller = __esm(async () => {
|
|
42728
|
+
import_react34 = __toESM(require_react(), 1);
|
|
42729
|
+
init_picker_layout();
|
|
42730
|
+
await init_picker_input();
|
|
42731
|
+
});
|
|
42732
|
+
|
|
42733
|
+
// src/ui/use-terminal-width.ts
|
|
42734
|
+
var import_react35, DEFAULT_TERMINAL_WIDTH = 80, useTerminalWidth = (stream) => {
|
|
42735
|
+
const [width, setWidth] = import_react35.useState(() => stream.columns ?? DEFAULT_TERMINAL_WIDTH);
|
|
42736
|
+
import_react35.useEffect(() => {
|
|
42737
|
+
const handleResize = () => {
|
|
42738
|
+
setWidth(stream.columns ?? DEFAULT_TERMINAL_WIDTH);
|
|
42739
|
+
};
|
|
42740
|
+
stream.on("resize", handleResize);
|
|
42741
|
+
return () => {
|
|
42742
|
+
stream.off("resize", handleResize);
|
|
42743
|
+
};
|
|
42744
|
+
}, [stream]);
|
|
42745
|
+
return width;
|
|
42746
|
+
};
|
|
42747
|
+
var init_use_terminal_width = __esm(() => {
|
|
42748
|
+
import_react35 = __toESM(require_react(), 1);
|
|
42749
|
+
});
|
|
42750
|
+
|
|
42751
|
+
// src/ui/picker.tsx
|
|
42752
|
+
var exports_picker = {};
|
|
42753
|
+
__export(exports_picker, {
|
|
42754
|
+
runPicker: () => runPicker
|
|
42755
|
+
});
|
|
42756
|
+
import { homedir } from "node:os";
|
|
42757
|
+
var jsx_dev_runtime2, MAX_VISIBLE_ROWS = 15, LIST_FOOTER_HINT = "Tab/→/Ctrl+L actions · Ctrl+X delete · Ctrl+R switch root · ↑↓/Ctrl+P,N,K,J move · Enter cd · Esc cancel", PANEL_FOOTER_HINT = "↑↓/Ctrl+P,N,K,J move · Enter run · c/d/r shortcuts · Esc/Tab/←/Ctrl+H close", PickerList = ({
|
|
42758
|
+
query,
|
|
42759
|
+
rows,
|
|
42760
|
+
clampedIndex,
|
|
42761
|
+
hiddenCount,
|
|
42762
|
+
footerHint,
|
|
42763
|
+
marginRight
|
|
42764
|
+
}) => /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
|
|
42765
|
+
flexDirection: "column",
|
|
42766
|
+
marginRight,
|
|
42767
|
+
children: [
|
|
42768
|
+
/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42769
|
+
children: [
|
|
42770
|
+
"hop: ",
|
|
42771
|
+
/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42772
|
+
color: "cyan",
|
|
42773
|
+
children: query
|
|
42774
|
+
}, undefined, false, undefined, this),
|
|
42775
|
+
/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42776
|
+
dimColor: true,
|
|
42777
|
+
children: query.length === 0 ? " (type to filter)" : ""
|
|
42778
|
+
}, undefined, false, undefined, this)
|
|
42779
|
+
]
|
|
42780
|
+
}, undefined, true, undefined, this),
|
|
42781
|
+
rows.length === 0 && /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42782
|
+
dimColor: true,
|
|
42783
|
+
children: "No matches."
|
|
42784
|
+
}, undefined, false, undefined, this),
|
|
42785
|
+
rows.map((row) => {
|
|
42786
|
+
if (row.kind === "header") {
|
|
42787
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42788
|
+
bold: true,
|
|
42789
|
+
dimColor: true,
|
|
42790
|
+
children: row.label
|
|
42791
|
+
}, displayRowKey(row), false, undefined, this);
|
|
42792
|
+
}
|
|
42793
|
+
const selected = row.index === clampedIndex;
|
|
42794
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42795
|
+
inverse: selected,
|
|
42796
|
+
dimColor: !selected && row.section === "branch",
|
|
42797
|
+
children: `${selected ? "❯ " : " "}${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`
|
|
42798
|
+
}, displayRowKey(row), false, undefined, this);
|
|
42799
|
+
}),
|
|
42800
|
+
hiddenCount > 0 && /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42801
|
+
dimColor: true,
|
|
42802
|
+
children: [
|
|
42803
|
+
"... and ",
|
|
42804
|
+
hiddenCount,
|
|
42805
|
+
" more (keep typing to narrow down)"
|
|
42806
|
+
]
|
|
42807
|
+
}, undefined, true, undefined, this),
|
|
42808
|
+
/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42809
|
+
dimColor: true,
|
|
42810
|
+
children: [
|
|
42811
|
+
"(",
|
|
42812
|
+
LEGEND_TEXT,
|
|
42813
|
+
")"
|
|
42814
|
+
]
|
|
42815
|
+
}, undefined, true, undefined, this),
|
|
42816
|
+
/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Text, {
|
|
42817
|
+
dimColor: true,
|
|
42818
|
+
children: footerHint
|
|
42819
|
+
}, undefined, false, undefined, this)
|
|
42820
|
+
]
|
|
42821
|
+
}, undefined, true, undefined, this), renderSidePanel = (mode, panelIndex, busy) => {
|
|
42822
|
+
if (mode.kind === "panel") {
|
|
42823
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(ActionPanel, {
|
|
42824
|
+
candidate: mode.candidate,
|
|
42825
|
+
panelIndex,
|
|
42826
|
+
error: mode.error,
|
|
42827
|
+
busy
|
|
42828
|
+
}, undefined, false, undefined, this);
|
|
42829
|
+
}
|
|
42830
|
+
if (mode.kind === "confirmDelete") {
|
|
42831
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(ConfirmDeletePanel, {
|
|
42832
|
+
candidate: mode.candidate
|
|
42833
|
+
}, undefined, false, undefined, this);
|
|
42834
|
+
}
|
|
42835
|
+
return null;
|
|
42836
|
+
}, Picker = ({ candidates, callbacks, onExit, onCancel }) => {
|
|
42837
|
+
const { query, filtered, clampedIndex, mode, panelIndex, busy, handleInput } = usePickerController(candidates, callbacks, onExit, onCancel);
|
|
42838
|
+
use_input_default(handleInput);
|
|
42839
|
+
const width = useTerminalWidth(process.stderr);
|
|
42840
|
+
const narrow = isNarrowTerminal(width);
|
|
42257
42841
|
const visible = filtered.slice(0, MAX_VISIBLE_ROWS);
|
|
42258
|
-
|
|
42259
|
-
|
|
42842
|
+
const hiddenCount = filtered.length - visible.length;
|
|
42843
|
+
const rows = buildDisplayRows(visible, homedir());
|
|
42844
|
+
const sidePanel = renderSidePanel(mode, panelIndex, busy);
|
|
42845
|
+
const list = /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(PickerList, {
|
|
42846
|
+
query,
|
|
42847
|
+
rows,
|
|
42848
|
+
clampedIndex,
|
|
42849
|
+
hiddenCount,
|
|
42850
|
+
footerHint: sidePanel === null ? LIST_FOOTER_HINT : PANEL_FOOTER_HINT,
|
|
42851
|
+
marginRight: sidePanel === null || narrow ? 0 : 1
|
|
42852
|
+
}, undefined, false, undefined, this);
|
|
42853
|
+
if (sidePanel === null) {
|
|
42854
|
+
return list;
|
|
42855
|
+
}
|
|
42856
|
+
if (narrow) {
|
|
42857
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
|
|
42858
|
+
flexDirection: "column",
|
|
42859
|
+
children: [
|
|
42860
|
+
list,
|
|
42861
|
+
sidePanel
|
|
42862
|
+
]
|
|
42863
|
+
}, undefined, true, undefined, this);
|
|
42864
|
+
}
|
|
42865
|
+
return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
|
|
42866
|
+
flexDirection: "row",
|
|
42260
42867
|
children: [
|
|
42261
|
-
|
|
42262
|
-
|
|
42263
|
-
"hop: ",
|
|
42264
|
-
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42265
|
-
color: "cyan",
|
|
42266
|
-
children: query
|
|
42267
|
-
}, undefined, false, undefined, this),
|
|
42268
|
-
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42269
|
-
dimColor: true,
|
|
42270
|
-
children: query.length === 0 ? " (type to filter)" : ""
|
|
42271
|
-
}, undefined, false, undefined, this)
|
|
42272
|
-
]
|
|
42273
|
-
}, undefined, true, undefined, this),
|
|
42274
|
-
visible.length === 0 && /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42275
|
-
dimColor: true,
|
|
42276
|
-
children: "No matches."
|
|
42277
|
-
}, undefined, false, undefined, this),
|
|
42278
|
-
visible.map((candidate, rowIndex) => {
|
|
42279
|
-
const selected = rowIndex === clampedIndex;
|
|
42280
|
-
const label = candidateBranchLabel(candidate);
|
|
42281
|
-
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42282
|
-
inverse: selected,
|
|
42283
|
-
children: `${selected ? "> " : " "}[${kindTag(candidate)}${dirtyTag(candidate)}] ${label} ${pathTag(candidate)}`
|
|
42284
|
-
}, candidateRowKey(candidate, rowIndex), false, undefined, this);
|
|
42285
|
-
}),
|
|
42286
|
-
filtered.length > MAX_VISIBLE_ROWS && /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
|
|
42287
|
-
dimColor: true,
|
|
42288
|
-
children: [
|
|
42289
|
-
"... and ",
|
|
42290
|
-
filtered.length - MAX_VISIBLE_ROWS,
|
|
42291
|
-
" more (keep typing to narrow down)"
|
|
42292
|
-
]
|
|
42293
|
-
}, undefined, true, undefined, this)
|
|
42868
|
+
list,
|
|
42869
|
+
sidePanel
|
|
42294
42870
|
]
|
|
42295
42871
|
}, undefined, true, undefined, this);
|
|
42296
|
-
}, runPicker = (candidates) =>
|
|
42297
|
-
|
|
42298
|
-
|
|
42299
|
-
|
|
42300
|
-
|
|
42301
|
-
|
|
42302
|
-
},
|
|
42303
|
-
onCancel: () => {
|
|
42304
|
-
instance.unmount();
|
|
42305
|
-
resolve(null);
|
|
42306
|
-
}
|
|
42307
|
-
}, undefined, false, undefined, this), { stdout: process.stderr });
|
|
42308
|
-
});
|
|
42872
|
+
}, runPicker = (candidates, callbacks) => runInAltScreenSession((finish) => render_default(/* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Picker, {
|
|
42873
|
+
candidates,
|
|
42874
|
+
callbacks,
|
|
42875
|
+
onExit: (outcome) => finish(outcome),
|
|
42876
|
+
onCancel: (reason) => finish({ type: "cancelled", reason })
|
|
42877
|
+
}, undefined, false, undefined, this), { stdout: process.stderr }));
|
|
42309
42878
|
var init_picker = __esm(async () => {
|
|
42310
42879
|
await init_build2();
|
|
42311
|
-
|
|
42312
|
-
|
|
42880
|
+
init_alt_screen_session();
|
|
42881
|
+
await init_picker_controller();
|
|
42882
|
+
init_picker_layout();
|
|
42883
|
+
await init_side_panel();
|
|
42884
|
+
init_use_terminal_width();
|
|
42885
|
+
jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
|
|
42313
42886
|
});
|
|
42314
42887
|
|
|
42315
42888
|
// src/ui/simple-picker.ts
|
|
@@ -42317,7 +42890,7 @@ var exports_simple_picker = {};
|
|
|
42317
42890
|
__export(exports_simple_picker, {
|
|
42318
42891
|
runSimplePicker: () => runSimplePicker
|
|
42319
42892
|
});
|
|
42320
|
-
import { createInterface
|
|
42893
|
+
import { createInterface } from "node:readline";
|
|
42321
42894
|
var runSimplePicker = (candidates) => new Promise((resolve) => {
|
|
42322
42895
|
if (candidates.length === 0) {
|
|
42323
42896
|
process.stderr.write(`No candidates.
|
|
@@ -42332,7 +42905,7 @@ var runSimplePicker = (candidates) => new Promise((resolve) => {
|
|
|
42332
42905
|
`);
|
|
42333
42906
|
});
|
|
42334
42907
|
process.stderr.write("Select a number (empty to cancel): ");
|
|
42335
|
-
const rl =
|
|
42908
|
+
const rl = createInterface({
|
|
42336
42909
|
input: process.stdin,
|
|
42337
42910
|
output: process.stderr,
|
|
42338
42911
|
terminal: false
|
|
@@ -43628,139 +44201,22 @@ async function runCommand(cmd, opts) {
|
|
|
43628
44201
|
return { result };
|
|
43629
44202
|
}
|
|
43630
44203
|
|
|
43631
|
-
// src/
|
|
43632
|
-
var
|
|
43633
|
-
|
|
43634
|
-
|
|
43635
|
-
|
|
43636
|
-
|
|
43637
|
-
|
|
43638
|
-
|
|
43639
|
-
if (input.mergedIntoDefault === true) {
|
|
43640
|
-
return "merged";
|
|
43641
|
-
}
|
|
43642
|
-
if (input.upstreamGone && input.allCommitsReachableFromDefault === true) {
|
|
43643
|
-
return "gone";
|
|
43644
|
-
}
|
|
43645
|
-
return null;
|
|
43646
|
-
};
|
|
43647
|
-
|
|
43648
|
-
// src/domain/result.ts
|
|
43649
|
-
var EXIT_SUCCESS = 0;
|
|
43650
|
-
var EXIT_GENERAL_ERROR = 1;
|
|
43651
|
-
var EXIT_USAGE_ERROR = 2;
|
|
43652
|
-
var EXIT_SAFE_REJECTION = 3;
|
|
43653
|
-
var EXIT_CANCELLED = 130;
|
|
43654
|
-
var ok = (fields = {}) => ({
|
|
43655
|
-
ok: true,
|
|
43656
|
-
exitCode: EXIT_SUCCESS,
|
|
43657
|
-
...fields
|
|
43658
|
-
});
|
|
43659
|
-
var fail = (exitCode, errorMessage) => ({
|
|
43660
|
-
ok: false,
|
|
43661
|
-
exitCode,
|
|
43662
|
-
errorMessage
|
|
43663
|
-
});
|
|
43664
|
-
|
|
43665
|
-
// src/infra/lock.ts
|
|
43666
|
-
import { randomUUID } from "node:crypto";
|
|
43667
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
43668
|
-
import { join } from "node:path";
|
|
43669
|
-
|
|
43670
|
-
// src/domain/lock-policy.ts
|
|
43671
|
-
var canReclaimLock = (input) => {
|
|
43672
|
-
if (input.processAlive !== false) {
|
|
43673
|
-
return false;
|
|
43674
|
-
}
|
|
43675
|
-
return input.nowMs - input.startedAtMs > input.ttlMs;
|
|
43676
|
-
};
|
|
43677
|
-
|
|
43678
|
-
// src/infra/lock.ts
|
|
43679
|
-
var LOCK_DIR_NAME = "nuthatch-lock";
|
|
43680
|
-
var LOCK_INFO_FILE = "info.json";
|
|
43681
|
-
var DEFAULT_TTL_MS = 30000;
|
|
43682
|
-
var HEARTBEAT_INTERVAL_MS = 5000;
|
|
43683
|
-
|
|
43684
|
-
class LockHeldError extends Error {
|
|
43685
|
-
info;
|
|
43686
|
-
constructor(info) {
|
|
43687
|
-
super(`Repository is locked by another nuthatch process (pid ${info.pid})`);
|
|
43688
|
-
this.name = "LockHeldError";
|
|
43689
|
-
this.info = info;
|
|
43690
|
-
}
|
|
43691
|
-
}
|
|
43692
|
-
var isProcessAlive = (pid) => {
|
|
43693
|
-
try {
|
|
43694
|
-
process.kill(pid, 0);
|
|
43695
|
-
return true;
|
|
43696
|
-
} catch (error) {
|
|
43697
|
-
const { code } = error;
|
|
43698
|
-
if (code === "ESRCH") {
|
|
43699
|
-
return false;
|
|
43700
|
-
}
|
|
43701
|
-
if (code === "EPERM") {
|
|
43702
|
-
return true;
|
|
43703
|
-
}
|
|
43704
|
-
return "unknown";
|
|
43705
|
-
}
|
|
43706
|
-
};
|
|
43707
|
-
var readLockInfo = async (lockDir) => {
|
|
43708
|
-
try {
|
|
43709
|
-
const raw = await readFile(join(lockDir, LOCK_INFO_FILE), "utf8");
|
|
43710
|
-
return JSON.parse(raw);
|
|
43711
|
-
} catch {
|
|
43712
|
-
return null;
|
|
44204
|
+
// src/cli-dispatch.ts
|
|
44205
|
+
var normalizeCliArgs = (rawArgs, argv0, stdoutIsTTY) => stdoutIsTTY && rawArgs.length === 1 && rawArgs[0] === argv0 ? [] : rawArgs;
|
|
44206
|
+
var HELP_FLAGS = new Set(["--help", "-h", "help"]);
|
|
44207
|
+
var isHelpRequest = (rawArgs) => rawArgs.length > 0 && HELP_FLAGS.has(rawArgs[0] ?? "");
|
|
44208
|
+
var dispatchCliArgs = (rawArgs, reservedNames) => {
|
|
44209
|
+
const [first, ...rest] = rawArgs;
|
|
44210
|
+
if (first === "--") {
|
|
44211
|
+
return { kind: "jump", args: rest };
|
|
43713
44212
|
}
|
|
43714
|
-
|
|
43715
|
-
|
|
43716
|
-
await writeFile(join(lockDir, LOCK_INFO_FILE), JSON.stringify(info), "utf8");
|
|
43717
|
-
};
|
|
43718
|
-
var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
|
|
43719
|
-
const lockDir = join(commonDir, LOCK_DIR_NAME);
|
|
43720
|
-
for (;; ) {
|
|
43721
|
-
try {
|
|
43722
|
-
await mkdir(lockDir);
|
|
43723
|
-
break;
|
|
43724
|
-
} catch (error) {
|
|
43725
|
-
if (error.code !== "EEXIST") {
|
|
43726
|
-
throw error;
|
|
43727
|
-
}
|
|
43728
|
-
const existing = await readLockInfo(lockDir);
|
|
43729
|
-
if (existing === null) {
|
|
43730
|
-
throw new LockHeldError({ pid: -1, startedAtMs: 0, token: "unknown" });
|
|
43731
|
-
}
|
|
43732
|
-
const reclaimable = canReclaimLock({
|
|
43733
|
-
processAlive: isProcessAlive(existing.pid),
|
|
43734
|
-
startedAtMs: existing.startedAtMs,
|
|
43735
|
-
nowMs: Date.now(),
|
|
43736
|
-
ttlMs
|
|
43737
|
-
});
|
|
43738
|
-
if (!reclaimable) {
|
|
43739
|
-
throw new LockHeldError(existing);
|
|
43740
|
-
}
|
|
43741
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
43742
|
-
}
|
|
44213
|
+
if (first !== undefined && reservedNames.includes(first)) {
|
|
44214
|
+
return { kind: "reserved", name: first, args: rest };
|
|
43743
44215
|
}
|
|
43744
|
-
|
|
43745
|
-
pid: process.pid,
|
|
43746
|
-
startedAtMs: Date.now(),
|
|
43747
|
-
token: randomUUID()
|
|
43748
|
-
};
|
|
43749
|
-
await writeLockInfo(lockDir, info);
|
|
43750
|
-
const heartbeat = setInterval(() => {
|
|
43751
|
-
writeLockInfo(lockDir, { ...info, startedAtMs: Date.now() });
|
|
43752
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
43753
|
-
heartbeat.unref();
|
|
43754
|
-
return {
|
|
43755
|
-
async release() {
|
|
43756
|
-
clearInterval(heartbeat);
|
|
43757
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
43758
|
-
}
|
|
43759
|
-
};
|
|
44216
|
+
return { kind: "jump", args: rawArgs };
|
|
43760
44217
|
};
|
|
43761
|
-
|
|
43762
44218
|
// src/infra/repo.ts
|
|
43763
|
-
import { basename, dirname, join
|
|
44219
|
+
import { basename, dirname, join } from "node:path";
|
|
43764
44220
|
|
|
43765
44221
|
// src/domain/classify.ts
|
|
43766
44222
|
var isWithin = (parent, child) => {
|
|
@@ -43877,7 +44333,7 @@ var loadRepoContext = async (git, fs, cwd) => {
|
|
|
43877
44333
|
throw new Error("git worktree list returned no entries");
|
|
43878
44334
|
}
|
|
43879
44335
|
const rootPath = await realpathOrRaw(fs, firstEntry.path);
|
|
43880
|
-
const managedRoot =
|
|
44336
|
+
const managedRoot = join(dirname(rootPath), "_worktree", basename(rootPath));
|
|
43881
44337
|
const worktrees = await Promise.all(parsed.map(async (entry) => {
|
|
43882
44338
|
const resolvedPath = await realpathOrRaw(fs, entry.path);
|
|
43883
44339
|
return {
|
|
@@ -43896,9 +44352,378 @@ var realpathOrRaw = async (fs, path) => {
|
|
|
43896
44352
|
}
|
|
43897
44353
|
};
|
|
43898
44354
|
|
|
43899
|
-
// src/commands/
|
|
43900
|
-
var
|
|
44355
|
+
// src/commands/pick.ts
|
|
44356
|
+
var pick = async (git, fs, options) => {
|
|
43901
44357
|
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44358
|
+
const [localBranches, remoteBranches, dirtyEntries] = await Promise.all([
|
|
44359
|
+
git.listBranches(context.rootPath),
|
|
44360
|
+
git.listRemoteBranches(context.rootPath),
|
|
44361
|
+
Promise.all(context.worktrees.map(async (worktree) => [
|
|
44362
|
+
worktree.path,
|
|
44363
|
+
worktree.bare ? null : await git.isDirty(worktree.path)
|
|
44364
|
+
]))
|
|
44365
|
+
]);
|
|
44366
|
+
const dirtyByPath = new Map(dirtyEntries);
|
|
44367
|
+
const candidates = buildPickCandidates(context.worktrees, dirtyByPath, localBranches, remoteBranches);
|
|
44368
|
+
return ok({ data: { candidates } });
|
|
44369
|
+
};
|
|
44370
|
+
// src/infra/lock.ts
|
|
44371
|
+
import { randomUUID } from "node:crypto";
|
|
44372
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
44373
|
+
import { join as join2 } from "node:path";
|
|
44374
|
+
|
|
44375
|
+
// src/domain/lock-policy.ts
|
|
44376
|
+
var canReclaimLock = (input) => {
|
|
44377
|
+
if (input.processAlive !== false) {
|
|
44378
|
+
return false;
|
|
44379
|
+
}
|
|
44380
|
+
return input.nowMs - input.startedAtMs > input.ttlMs;
|
|
44381
|
+
};
|
|
44382
|
+
|
|
44383
|
+
// src/infra/lock.ts
|
|
44384
|
+
var LOCK_DIR_NAME = "nuthatch-lock";
|
|
44385
|
+
var LOCK_INFO_FILE = "info.json";
|
|
44386
|
+
var DEFAULT_TTL_MS = 30000;
|
|
44387
|
+
var HEARTBEAT_INTERVAL_MS = 5000;
|
|
44388
|
+
|
|
44389
|
+
class LockHeldError extends Error {
|
|
44390
|
+
info;
|
|
44391
|
+
constructor(info) {
|
|
44392
|
+
super(`Repository is locked by another nuthatch process (pid ${info.pid})`);
|
|
44393
|
+
this.name = "LockHeldError";
|
|
44394
|
+
this.info = info;
|
|
44395
|
+
}
|
|
44396
|
+
}
|
|
44397
|
+
var isProcessAlive = (pid) => {
|
|
44398
|
+
try {
|
|
44399
|
+
process.kill(pid, 0);
|
|
44400
|
+
return true;
|
|
44401
|
+
} catch (error) {
|
|
44402
|
+
const { code } = error;
|
|
44403
|
+
if (code === "ESRCH") {
|
|
44404
|
+
return false;
|
|
44405
|
+
}
|
|
44406
|
+
if (code === "EPERM") {
|
|
44407
|
+
return true;
|
|
44408
|
+
}
|
|
44409
|
+
return "unknown";
|
|
44410
|
+
}
|
|
44411
|
+
};
|
|
44412
|
+
var readLockInfo = async (lockDir) => {
|
|
44413
|
+
try {
|
|
44414
|
+
const raw = await readFile(join2(lockDir, LOCK_INFO_FILE), "utf8");
|
|
44415
|
+
return JSON.parse(raw);
|
|
44416
|
+
} catch {
|
|
44417
|
+
return null;
|
|
44418
|
+
}
|
|
44419
|
+
};
|
|
44420
|
+
var writeLockInfo = async (lockDir, info) => {
|
|
44421
|
+
await writeFile(join2(lockDir, LOCK_INFO_FILE), JSON.stringify(info), "utf8");
|
|
44422
|
+
};
|
|
44423
|
+
var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
|
|
44424
|
+
const lockDir = join2(commonDir, LOCK_DIR_NAME);
|
|
44425
|
+
for (;; ) {
|
|
44426
|
+
try {
|
|
44427
|
+
await mkdir(lockDir);
|
|
44428
|
+
break;
|
|
44429
|
+
} catch (error) {
|
|
44430
|
+
if (error.code !== "EEXIST") {
|
|
44431
|
+
throw error;
|
|
44432
|
+
}
|
|
44433
|
+
const existing = await readLockInfo(lockDir);
|
|
44434
|
+
if (existing === null) {
|
|
44435
|
+
throw new LockHeldError({ pid: -1, startedAtMs: 0, token: "unknown" });
|
|
44436
|
+
}
|
|
44437
|
+
const reclaimable = canReclaimLock({
|
|
44438
|
+
processAlive: isProcessAlive(existing.pid),
|
|
44439
|
+
startedAtMs: existing.startedAtMs,
|
|
44440
|
+
nowMs: Date.now(),
|
|
44441
|
+
ttlMs
|
|
44442
|
+
});
|
|
44443
|
+
if (!reclaimable) {
|
|
44444
|
+
throw new LockHeldError(existing);
|
|
44445
|
+
}
|
|
44446
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
44447
|
+
}
|
|
44448
|
+
}
|
|
44449
|
+
const info = {
|
|
44450
|
+
pid: process.pid,
|
|
44451
|
+
startedAtMs: Date.now(),
|
|
44452
|
+
token: randomUUID()
|
|
44453
|
+
};
|
|
44454
|
+
await writeLockInfo(lockDir, info);
|
|
44455
|
+
const heartbeat = setInterval(() => {
|
|
44456
|
+
writeLockInfo(lockDir, { ...info, startedAtMs: Date.now() });
|
|
44457
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
44458
|
+
heartbeat.unref();
|
|
44459
|
+
return {
|
|
44460
|
+
async release() {
|
|
44461
|
+
clearInterval(heartbeat);
|
|
44462
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
44463
|
+
}
|
|
44464
|
+
};
|
|
44465
|
+
};
|
|
44466
|
+
|
|
44467
|
+
// src/commands/rm.ts
|
|
44468
|
+
var rm2 = async (git, fs, options) => {
|
|
44469
|
+
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44470
|
+
const target = context.worktrees.find((wt) => wt.branch === options.branch);
|
|
44471
|
+
if (target === undefined) {
|
|
44472
|
+
return fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`);
|
|
44473
|
+
}
|
|
44474
|
+
if (target.kind === "root") {
|
|
44475
|
+
return fail(EXIT_USAGE_ERROR, "Cannot remove the root clone.");
|
|
44476
|
+
}
|
|
44477
|
+
if (target.kind === "external" && !(options.ext && options.force)) {
|
|
44478
|
+
return fail(EXIT_SAFE_REJECTION, `"${options.branch}" is an external worktree not managed by nuthatch. Removal requires both --ext and --force.`);
|
|
44479
|
+
}
|
|
44480
|
+
if (!options.force) {
|
|
44481
|
+
const dirty = await git.isDirty(target.path);
|
|
44482
|
+
if (dirty) {
|
|
44483
|
+
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
44484
|
+
}
|
|
44485
|
+
}
|
|
44486
|
+
const lock = await acquireRepoLock(context.commonDir);
|
|
44487
|
+
try {
|
|
44488
|
+
const fresh = await loadRepoContext(git, fs, options.cwd);
|
|
44489
|
+
const freshTarget = fresh.worktrees.find((wt) => wt.branch === options.branch);
|
|
44490
|
+
if (freshTarget === undefined) {
|
|
44491
|
+
return fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`);
|
|
44492
|
+
}
|
|
44493
|
+
if (!options.force) {
|
|
44494
|
+
const stillDirty = await git.isDirty(freshTarget.path);
|
|
44495
|
+
if (stillDirty) {
|
|
44496
|
+
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
44497
|
+
}
|
|
44498
|
+
}
|
|
44499
|
+
await git.removeWorktree(context.rootPath, freshTarget.path, options.force);
|
|
44500
|
+
return ok({ data: { branch: options.branch, path: freshTarget.path } });
|
|
44501
|
+
} catch (error) {
|
|
44502
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to remove worktree: ${error.message}`);
|
|
44503
|
+
} finally {
|
|
44504
|
+
await lock.release();
|
|
44505
|
+
}
|
|
44506
|
+
};
|
|
44507
|
+
// src/commands/root.ts
|
|
44508
|
+
var root = async (git, fs, options) => {
|
|
44509
|
+
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44510
|
+
const rootWorktree = context.worktrees.find((wt) => wt.kind === "root");
|
|
44511
|
+
if (options.target === undefined) {
|
|
44512
|
+
return ok({
|
|
44513
|
+
path: context.rootPath,
|
|
44514
|
+
data: { branch: rootWorktree?.branch ?? null, switched: false }
|
|
44515
|
+
});
|
|
44516
|
+
}
|
|
44517
|
+
const dirty = await git.isDirty(context.rootPath);
|
|
44518
|
+
if (dirty) {
|
|
44519
|
+
return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
|
|
44520
|
+
}
|
|
44521
|
+
const previousBranch = rootWorktree?.branch ?? null;
|
|
44522
|
+
if (options.target === "-") {
|
|
44523
|
+
return switchAndReport({
|
|
44524
|
+
git,
|
|
44525
|
+
fs,
|
|
44526
|
+
context,
|
|
44527
|
+
target: "-",
|
|
44528
|
+
switchOptions: {},
|
|
44529
|
+
previousBranch
|
|
44530
|
+
});
|
|
44531
|
+
}
|
|
44532
|
+
const holder = context.worktrees.find((wt) => wt.branch === options.target && wt.kind !== "root");
|
|
44533
|
+
if (holder !== undefined) {
|
|
44534
|
+
return fail(EXIT_SAFE_REJECTION, `Branch "${options.target}" is already checked out at ${holder.path}. Not swapping — cd there instead of using hop root.`);
|
|
44535
|
+
}
|
|
44536
|
+
const localBranches = await git.listBranches(context.rootPath);
|
|
44537
|
+
const branchExistsLocally = localBranches.includes(options.target);
|
|
44538
|
+
const { track: initialTrack } = options;
|
|
44539
|
+
let track = initialTrack;
|
|
44540
|
+
if (!branchExistsLocally && track === undefined) {
|
|
44541
|
+
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
44542
|
+
const [firstRemote] = remotes;
|
|
44543
|
+
if (remotes.includes("origin")) {
|
|
44544
|
+
track = `origin/${options.target}`;
|
|
44545
|
+
} else if (remotes.length === 1 && firstRemote !== undefined) {
|
|
44546
|
+
track = `${firstRemote}/${options.target}`;
|
|
44547
|
+
} else if (remotes.length > 1) {
|
|
44548
|
+
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
44549
|
+
}
|
|
44550
|
+
}
|
|
44551
|
+
return switchAndReport({
|
|
44552
|
+
git,
|
|
44553
|
+
fs,
|
|
44554
|
+
context,
|
|
44555
|
+
target: options.target,
|
|
44556
|
+
switchOptions: {
|
|
44557
|
+
createBranch: !branchExistsLocally,
|
|
44558
|
+
...track === undefined ? {} : { track }
|
|
44559
|
+
},
|
|
44560
|
+
previousBranch
|
|
44561
|
+
});
|
|
44562
|
+
};
|
|
44563
|
+
var switchAndReport = async ({
|
|
44564
|
+
git,
|
|
44565
|
+
fs,
|
|
44566
|
+
context,
|
|
44567
|
+
target,
|
|
44568
|
+
switchOptions,
|
|
44569
|
+
previousBranch
|
|
44570
|
+
}) => {
|
|
44571
|
+
const lock = await acquireRepoLock(context.commonDir);
|
|
44572
|
+
try {
|
|
44573
|
+
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
44574
|
+
const stillDirty = await git.isDirty(fresh.rootPath);
|
|
44575
|
+
if (stillDirty) {
|
|
44576
|
+
return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
|
|
44577
|
+
}
|
|
44578
|
+
if (target !== "-") {
|
|
44579
|
+
const freshHolder = fresh.worktrees.find((wt) => wt.branch === target && wt.kind !== "root");
|
|
44580
|
+
if (freshHolder !== undefined) {
|
|
44581
|
+
return fail(EXIT_SAFE_REJECTION, `Branch "${target}" is already checked out at ${freshHolder.path}. Not swapping — cd there instead of using hop root.`);
|
|
44582
|
+
}
|
|
44583
|
+
}
|
|
44584
|
+
await git.switchBranch(fresh.rootPath, target, switchOptions);
|
|
44585
|
+
return ok({
|
|
44586
|
+
path: fresh.rootPath,
|
|
44587
|
+
data: { branch: target === "-" ? null : target, switched: true }
|
|
44588
|
+
});
|
|
44589
|
+
} catch (error) {
|
|
44590
|
+
if (previousBranch !== null) {
|
|
44591
|
+
try {
|
|
44592
|
+
await git.switchBranch(context.rootPath, previousBranch, {});
|
|
44593
|
+
} catch {}
|
|
44594
|
+
}
|
|
44595
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${error.message}`);
|
|
44596
|
+
} finally {
|
|
44597
|
+
await lock.release();
|
|
44598
|
+
}
|
|
44599
|
+
};
|
|
44600
|
+
// src/render.ts
|
|
44601
|
+
var PLAIN_JSON_INDENT = 2;
|
|
44602
|
+
var render = (command, result, json) => {
|
|
44603
|
+
if (json) {
|
|
44604
|
+
const envelope = {
|
|
44605
|
+
schemaVersion: 1,
|
|
44606
|
+
command,
|
|
44607
|
+
data: result.ok ? result.data : undefined,
|
|
44608
|
+
warnings: result.warnings ?? []
|
|
44609
|
+
};
|
|
44610
|
+
process.stdout.write(`${JSON.stringify(envelope)}
|
|
44611
|
+
`);
|
|
44612
|
+
if (!result.ok && result.errorMessage !== undefined) {
|
|
44613
|
+
process.stderr.write(`${result.errorMessage}
|
|
44614
|
+
`);
|
|
44615
|
+
}
|
|
44616
|
+
return;
|
|
44617
|
+
}
|
|
44618
|
+
if (result.path !== undefined) {
|
|
44619
|
+
process.stdout.write(`${result.path}
|
|
44620
|
+
`);
|
|
44621
|
+
} else if (result.ok && result.data !== undefined) {
|
|
44622
|
+
process.stdout.write(`${JSON.stringify(result.data, null, PLAIN_JSON_INDENT)}
|
|
44623
|
+
`);
|
|
44624
|
+
}
|
|
44625
|
+
for (const warning of result.warnings ?? []) {
|
|
44626
|
+
process.stderr.write(`warning: ${warning}
|
|
44627
|
+
`);
|
|
44628
|
+
}
|
|
44629
|
+
if (!result.ok && result.errorMessage !== undefined) {
|
|
44630
|
+
process.stderr.write(`${result.errorMessage}
|
|
44631
|
+
`);
|
|
44632
|
+
}
|
|
44633
|
+
};
|
|
44634
|
+
|
|
44635
|
+
// src/cli-pick.ts
|
|
44636
|
+
var loadPickCandidates = async (git, fs3, json) => {
|
|
44637
|
+
const pickResult = await pick(git, fs3, { cwd: process.cwd() });
|
|
44638
|
+
if (!pickResult.ok) {
|
|
44639
|
+
render("pick", pickResult, json);
|
|
44640
|
+
process.exitCode = pickResult.exitCode;
|
|
44641
|
+
return null;
|
|
44642
|
+
}
|
|
44643
|
+
return pickResult.data?.candidates ?? [];
|
|
44644
|
+
};
|
|
44645
|
+
var deleteWorktree = async (git, fs3, candidate) => {
|
|
44646
|
+
const branch = candidateBranchName(candidate);
|
|
44647
|
+
if (branch === null) {
|
|
44648
|
+
return { ok: false, message: "This candidate has no branch to remove." };
|
|
44649
|
+
}
|
|
44650
|
+
const result = await rm2(git, fs3, {
|
|
44651
|
+
cwd: process.cwd(),
|
|
44652
|
+
branch,
|
|
44653
|
+
force: false,
|
|
44654
|
+
ext: false
|
|
44655
|
+
});
|
|
44656
|
+
if (!result.ok) {
|
|
44657
|
+
return {
|
|
44658
|
+
ok: false,
|
|
44659
|
+
message: result.errorMessage ?? "Failed to remove worktree."
|
|
44660
|
+
};
|
|
44661
|
+
}
|
|
44662
|
+
return { ok: true };
|
|
44663
|
+
};
|
|
44664
|
+
var createPickerCallbacks = (git, fs3, json, onSwitchedBranch) => ({
|
|
44665
|
+
deleteWorktree: (candidate) => deleteWorktree(git, fs3, candidate),
|
|
44666
|
+
switchRootHere: async (candidate) => {
|
|
44667
|
+
const branch = candidateBranchName(candidate);
|
|
44668
|
+
if (branch === null) {
|
|
44669
|
+
return {
|
|
44670
|
+
ok: false,
|
|
44671
|
+
message: "This candidate has no branch to switch to."
|
|
44672
|
+
};
|
|
44673
|
+
}
|
|
44674
|
+
const result = await root(git, fs3, { cwd: process.cwd(), target: branch });
|
|
44675
|
+
if (!result.ok) {
|
|
44676
|
+
return {
|
|
44677
|
+
ok: false,
|
|
44678
|
+
message: result.errorMessage ?? "Failed to switch root."
|
|
44679
|
+
};
|
|
44680
|
+
}
|
|
44681
|
+
onSwitchedBranch(branch);
|
|
44682
|
+
return {
|
|
44683
|
+
ok: true,
|
|
44684
|
+
...result.path === undefined ? {} : { path: result.path }
|
|
44685
|
+
};
|
|
44686
|
+
},
|
|
44687
|
+
reloadCandidates: async () => await loadPickCandidates(git, fs3, json) ?? []
|
|
44688
|
+
});
|
|
44689
|
+
var runInteractivePicker = async (candidates, callbacks) => {
|
|
44690
|
+
try {
|
|
44691
|
+
const { runPicker: runPicker2 } = await init_picker().then(() => exports_picker);
|
|
44692
|
+
return await runPicker2(candidates, callbacks);
|
|
44693
|
+
} catch {
|
|
44694
|
+
const { runSimplePicker: runSimplePicker2 } = await Promise.resolve().then(() => (init_simple_picker(), exports_simple_picker));
|
|
44695
|
+
const selected = await runSimplePicker2(candidates);
|
|
44696
|
+
return selected === null ? { type: "cancelled", reason: "esc" } : { type: "cd", candidate: selected };
|
|
44697
|
+
}
|
|
44698
|
+
};
|
|
44699
|
+
var renderSwitchRootOutcome = (path, branch, json) => {
|
|
44700
|
+
if (json) {
|
|
44701
|
+
render("root", ok({ path, data: { branch, switched: true } }), true);
|
|
44702
|
+
return;
|
|
44703
|
+
}
|
|
44704
|
+
process.stdout.write(`${path}
|
|
44705
|
+
`);
|
|
44706
|
+
};
|
|
44707
|
+
|
|
44708
|
+
// src/domain/garbage.ts
|
|
44709
|
+
var classifyGarbage = (input) => {
|
|
44710
|
+
if (input.prunable) {
|
|
44711
|
+
return "prunable";
|
|
44712
|
+
}
|
|
44713
|
+
if (!input.clean) {
|
|
44714
|
+
return null;
|
|
44715
|
+
}
|
|
44716
|
+
if (input.mergedIntoDefault === true) {
|
|
44717
|
+
return "merged";
|
|
44718
|
+
}
|
|
44719
|
+
if (input.upstreamGone && input.allCommitsReachableFromDefault === true) {
|
|
44720
|
+
return "gone";
|
|
44721
|
+
}
|
|
44722
|
+
return null;
|
|
44723
|
+
};
|
|
44724
|
+
// src/commands/clean.ts
|
|
44725
|
+
var clean = async (git, fs3, term, options) => {
|
|
44726
|
+
const context = await loadRepoContext(git, fs3, options.cwd);
|
|
43902
44727
|
const candidates = await buildCleanCandidates(git, context, options.ext);
|
|
43903
44728
|
if (options.dryRun) {
|
|
43904
44729
|
return ok({ data: { candidates } });
|
|
@@ -43920,7 +44745,7 @@ var clean = async (git, fs, term, options) => {
|
|
|
43920
44745
|
}
|
|
43921
44746
|
const execution = await executeClean({
|
|
43922
44747
|
git,
|
|
43923
|
-
fs,
|
|
44748
|
+
fs: fs3,
|
|
43924
44749
|
context,
|
|
43925
44750
|
candidates,
|
|
43926
44751
|
cleanOptions: options
|
|
@@ -44001,14 +44826,14 @@ var canDeletePrunableBranch = async (git, rootPath, candidate, defaultRef) => {
|
|
|
44001
44826
|
};
|
|
44002
44827
|
var executeClean = async ({
|
|
44003
44828
|
git,
|
|
44004
|
-
fs,
|
|
44829
|
+
fs: fs3,
|
|
44005
44830
|
context,
|
|
44006
44831
|
candidates,
|
|
44007
44832
|
cleanOptions
|
|
44008
44833
|
}) => {
|
|
44009
44834
|
const lock = await acquireRepoLock(context.commonDir);
|
|
44010
44835
|
try {
|
|
44011
|
-
const fresh = await loadRepoContext(git,
|
|
44836
|
+
const fresh = await loadRepoContext(git, fs3, context.rootPath);
|
|
44012
44837
|
const freshCandidates = await buildCleanCandidates(git, fresh, cleanOptions.ext);
|
|
44013
44838
|
const freshByPath = new Map(freshCandidates.map((candidate) => [candidate.path, candidate]));
|
|
44014
44839
|
const stillValid = candidates.flatMap((candidate) => {
|
|
@@ -44039,19 +44864,6 @@ var executeClean = async ({
|
|
|
44039
44864
|
}
|
|
44040
44865
|
};
|
|
44041
44866
|
|
|
44042
|
-
// src/cli-dispatch.ts
|
|
44043
|
-
var normalizeCliArgs = (rawArgs, argv0, stdoutIsTTY) => stdoutIsTTY && rawArgs.length === 1 && rawArgs[0] === argv0 ? [] : rawArgs;
|
|
44044
|
-
var dispatchCliArgs = (rawArgs, reservedNames) => {
|
|
44045
|
-
const [first, ...rest] = rawArgs;
|
|
44046
|
-
if (first === "--") {
|
|
44047
|
-
return { kind: "jump", args: rest };
|
|
44048
|
-
}
|
|
44049
|
-
if (first !== undefined && reservedNames.includes(first)) {
|
|
44050
|
-
return { kind: "reserved", name: first, args: rest };
|
|
44051
|
-
}
|
|
44052
|
-
return { kind: "jump", args: rawArgs };
|
|
44053
|
-
};
|
|
44054
|
-
|
|
44055
44867
|
// src/commands/init.ts
|
|
44056
44868
|
var ZSH_TEMPLATE = `# nuthatch shell integration for zsh.
|
|
44057
44869
|
# Usage: eval "$(hop init zsh)"
|
|
@@ -44129,23 +44941,23 @@ var shortHash = (input) => {
|
|
|
44129
44941
|
var baseName = (branch) => branch.replaceAll("/", "__");
|
|
44130
44942
|
var truncate = (name, maxLength) => name.length > maxLength ? name.slice(0, maxLength) : name;
|
|
44131
44943
|
var sanitizeBranchName = (branch, isTaken) => {
|
|
44132
|
-
const
|
|
44133
|
-
if (!isTaken(
|
|
44134
|
-
return
|
|
44944
|
+
const base2 = truncate(baseName(branch), MAX_DIR_NAME_LENGTH);
|
|
44945
|
+
if (!isTaken(base2)) {
|
|
44946
|
+
return base2;
|
|
44135
44947
|
}
|
|
44136
44948
|
const hash = shortHash(branch);
|
|
44137
|
-
let candidate = `${truncate(
|
|
44949
|
+
let candidate = `${truncate(base2, MAX_DIR_NAME_LENGTH - hash.length - 1)}-${hash}`;
|
|
44138
44950
|
let suffix = 1;
|
|
44139
44951
|
while (isTaken(candidate)) {
|
|
44140
44952
|
const withSuffix = `${hash}-${suffix}`;
|
|
44141
|
-
candidate = `${truncate(
|
|
44953
|
+
candidate = `${truncate(base2, MAX_DIR_NAME_LENGTH - withSuffix.length - 1)}-${withSuffix}`;
|
|
44142
44954
|
suffix++;
|
|
44143
44955
|
}
|
|
44144
44956
|
return candidate;
|
|
44145
44957
|
};
|
|
44146
44958
|
|
|
44147
44959
|
// src/commands/jump.ts
|
|
44148
|
-
var jump = async (git,
|
|
44960
|
+
var jump = async (git, fs3, term, options) => {
|
|
44149
44961
|
if (options.target === "-") {
|
|
44150
44962
|
const previous = process.env.OLDPWD;
|
|
44151
44963
|
if (previous === undefined || previous.length === 0) {
|
|
@@ -44156,7 +44968,7 @@ var jump = async (git, fs, term, options) => {
|
|
|
44156
44968
|
data: { branch: options.target, created: false }
|
|
44157
44969
|
});
|
|
44158
44970
|
}
|
|
44159
|
-
const context = await loadRepoContext(git,
|
|
44971
|
+
const context = await loadRepoContext(git, fs3, options.cwd);
|
|
44160
44972
|
const existing = context.worktrees.find((wt) => wt.branch === options.target);
|
|
44161
44973
|
if (existing !== undefined) {
|
|
44162
44974
|
return ok({
|
|
@@ -44183,13 +44995,13 @@ var jump = async (git, fs, term, options) => {
|
|
|
44183
44995
|
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
44184
44996
|
}
|
|
44185
44997
|
}
|
|
44186
|
-
const managedDirNames = await
|
|
44998
|
+
const managedDirNames = await fs3.listDirNames(context.managedRoot);
|
|
44187
44999
|
const existingDirNames = new Set(managedDirNames.map((name) => name.toLowerCase()));
|
|
44188
45000
|
const dirName = sanitizeBranchName(options.target, (candidate) => existingDirNames.has(candidate.toLowerCase()));
|
|
44189
45001
|
const targetPath = join3(context.managedRoot, dirName);
|
|
44190
45002
|
const lock = await acquireRepoLock(context.commonDir);
|
|
44191
45003
|
try {
|
|
44192
|
-
const fresh = await loadRepoContext(git,
|
|
45004
|
+
const fresh = await loadRepoContext(git, fs3, options.cwd);
|
|
44193
45005
|
const racedExisting = fresh.worktrees.find((wt) => wt.branch === options.target);
|
|
44194
45006
|
if (racedExisting !== undefined) {
|
|
44195
45007
|
return ok({
|
|
@@ -44197,7 +45009,7 @@ var jump = async (git, fs, term, options) => {
|
|
|
44197
45009
|
data: { branch: options.target, created: false }
|
|
44198
45010
|
});
|
|
44199
45011
|
}
|
|
44200
|
-
await
|
|
45012
|
+
await fs3.mkdir(context.managedRoot);
|
|
44201
45013
|
await git.addWorktree(context.rootPath, targetPath, options.target, {
|
|
44202
45014
|
createBranch: !branchExistsLocally,
|
|
44203
45015
|
...track === undefined ? {} : { track }
|
|
@@ -44212,10 +45024,9 @@ var jump = async (git, fs, term, options) => {
|
|
|
44212
45024
|
data: { branch: options.target, created: true }
|
|
44213
45025
|
});
|
|
44214
45026
|
};
|
|
44215
|
-
|
|
44216
45027
|
// src/commands/ls.ts
|
|
44217
|
-
var ls = async (git,
|
|
44218
|
-
const context = await loadRepoContext(git,
|
|
45028
|
+
var ls = async (git, fs3, options) => {
|
|
45029
|
+
const context = await loadRepoContext(git, fs3, options.cwd);
|
|
44219
45030
|
const entries = await Promise.all(context.worktrees.map(async (wt) => {
|
|
44220
45031
|
const [dirty, aheadBehind] = await Promise.all([
|
|
44221
45032
|
wt.bare ? Promise.resolve(false) : git.isDirty(wt.path),
|
|
@@ -44230,157 +45041,6 @@ var ls = async (git, fs, options) => {
|
|
|
44230
45041
|
}));
|
|
44231
45042
|
return ok({ data: entries });
|
|
44232
45043
|
};
|
|
44233
|
-
// src/commands/pick.ts
|
|
44234
|
-
var pick = async (git, fs, options) => {
|
|
44235
|
-
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44236
|
-
const [localBranches, remoteBranches, dirtyEntries] = await Promise.all([
|
|
44237
|
-
git.listBranches(context.rootPath),
|
|
44238
|
-
git.listRemoteBranches(context.rootPath),
|
|
44239
|
-
Promise.all(context.worktrees.map(async (worktree) => [
|
|
44240
|
-
worktree.path,
|
|
44241
|
-
worktree.bare ? null : await git.isDirty(worktree.path)
|
|
44242
|
-
]))
|
|
44243
|
-
]);
|
|
44244
|
-
const dirtyByPath = new Map(dirtyEntries);
|
|
44245
|
-
const candidates = buildPickCandidates(context.worktrees, dirtyByPath, localBranches, remoteBranches);
|
|
44246
|
-
return ok({ data: { candidates } });
|
|
44247
|
-
};
|
|
44248
|
-
|
|
44249
|
-
// src/commands/rm.ts
|
|
44250
|
-
var rm2 = async (git, fs, options) => {
|
|
44251
|
-
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44252
|
-
const target = context.worktrees.find((wt) => wt.branch === options.branch);
|
|
44253
|
-
if (target === undefined) {
|
|
44254
|
-
return fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`);
|
|
44255
|
-
}
|
|
44256
|
-
if (target.kind === "root") {
|
|
44257
|
-
return fail(EXIT_USAGE_ERROR, "Cannot remove the root clone.");
|
|
44258
|
-
}
|
|
44259
|
-
if (target.kind === "external" && !(options.ext && options.force)) {
|
|
44260
|
-
return fail(EXIT_SAFE_REJECTION, `"${options.branch}" is an external worktree not managed by nuthatch. Removal requires both --ext and --force.`);
|
|
44261
|
-
}
|
|
44262
|
-
if (!options.force) {
|
|
44263
|
-
const dirty = await git.isDirty(target.path);
|
|
44264
|
-
if (dirty) {
|
|
44265
|
-
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
44266
|
-
}
|
|
44267
|
-
}
|
|
44268
|
-
const lock = await acquireRepoLock(context.commonDir);
|
|
44269
|
-
try {
|
|
44270
|
-
const fresh = await loadRepoContext(git, fs, options.cwd);
|
|
44271
|
-
const freshTarget = fresh.worktrees.find((wt) => wt.branch === options.branch);
|
|
44272
|
-
if (freshTarget === undefined) {
|
|
44273
|
-
return fail(EXIT_GENERAL_ERROR, `No worktree found for branch "${options.branch}".`);
|
|
44274
|
-
}
|
|
44275
|
-
if (!options.force) {
|
|
44276
|
-
const stillDirty = await git.isDirty(freshTarget.path);
|
|
44277
|
-
if (stillDirty) {
|
|
44278
|
-
return fail(EXIT_SAFE_REJECTION, `Worktree for "${options.branch}" has uncommitted or untracked changes. Use --force to remove anyway.`);
|
|
44279
|
-
}
|
|
44280
|
-
}
|
|
44281
|
-
await git.removeWorktree(context.rootPath, freshTarget.path, options.force);
|
|
44282
|
-
return ok({ data: { branch: options.branch, path: freshTarget.path } });
|
|
44283
|
-
} catch (error) {
|
|
44284
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to remove worktree: ${error.message}`);
|
|
44285
|
-
} finally {
|
|
44286
|
-
await lock.release();
|
|
44287
|
-
}
|
|
44288
|
-
};
|
|
44289
|
-
|
|
44290
|
-
// src/commands/root.ts
|
|
44291
|
-
var root = async (git, fs, options) => {
|
|
44292
|
-
const context = await loadRepoContext(git, fs, options.cwd);
|
|
44293
|
-
const rootWorktree = context.worktrees.find((wt) => wt.kind === "root");
|
|
44294
|
-
if (options.target === undefined) {
|
|
44295
|
-
return ok({
|
|
44296
|
-
path: context.rootPath,
|
|
44297
|
-
data: { branch: rootWorktree?.branch ?? null, switched: false }
|
|
44298
|
-
});
|
|
44299
|
-
}
|
|
44300
|
-
const dirty = await git.isDirty(context.rootPath);
|
|
44301
|
-
if (dirty) {
|
|
44302
|
-
return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
|
|
44303
|
-
}
|
|
44304
|
-
const previousBranch = rootWorktree?.branch ?? null;
|
|
44305
|
-
if (options.target === "-") {
|
|
44306
|
-
return switchAndReport({
|
|
44307
|
-
git,
|
|
44308
|
-
fs,
|
|
44309
|
-
context,
|
|
44310
|
-
target: "-",
|
|
44311
|
-
switchOptions: {},
|
|
44312
|
-
previousBranch
|
|
44313
|
-
});
|
|
44314
|
-
}
|
|
44315
|
-
const holder = context.worktrees.find((wt) => wt.branch === options.target && wt.kind !== "root");
|
|
44316
|
-
if (holder !== undefined) {
|
|
44317
|
-
return fail(EXIT_SAFE_REJECTION, `Branch "${options.target}" is already checked out at ${holder.path}. Not swapping — cd there instead of using hop root.`);
|
|
44318
|
-
}
|
|
44319
|
-
const localBranches = await git.listBranches(context.rootPath);
|
|
44320
|
-
const branchExistsLocally = localBranches.includes(options.target);
|
|
44321
|
-
const { track: initialTrack } = options;
|
|
44322
|
-
let track = initialTrack;
|
|
44323
|
-
if (!branchExistsLocally && track === undefined) {
|
|
44324
|
-
const remotes = await git.remotesWithBranch(context.rootPath, options.target);
|
|
44325
|
-
const [firstRemote] = remotes;
|
|
44326
|
-
if (remotes.includes("origin")) {
|
|
44327
|
-
track = `origin/${options.target}`;
|
|
44328
|
-
} else if (remotes.length === 1 && firstRemote !== undefined) {
|
|
44329
|
-
track = `${firstRemote}/${options.target}`;
|
|
44330
|
-
} else if (remotes.length > 1) {
|
|
44331
|
-
return fail(EXIT_USAGE_ERROR, `Branch "${options.target}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`);
|
|
44332
|
-
}
|
|
44333
|
-
}
|
|
44334
|
-
return switchAndReport({
|
|
44335
|
-
git,
|
|
44336
|
-
fs,
|
|
44337
|
-
context,
|
|
44338
|
-
target: options.target,
|
|
44339
|
-
switchOptions: {
|
|
44340
|
-
createBranch: !branchExistsLocally,
|
|
44341
|
-
...track === undefined ? {} : { track }
|
|
44342
|
-
},
|
|
44343
|
-
previousBranch
|
|
44344
|
-
});
|
|
44345
|
-
};
|
|
44346
|
-
var switchAndReport = async ({
|
|
44347
|
-
git,
|
|
44348
|
-
fs,
|
|
44349
|
-
context,
|
|
44350
|
-
target,
|
|
44351
|
-
switchOptions,
|
|
44352
|
-
previousBranch
|
|
44353
|
-
}) => {
|
|
44354
|
-
const lock = await acquireRepoLock(context.commonDir);
|
|
44355
|
-
try {
|
|
44356
|
-
const fresh = await loadRepoContext(git, fs, context.rootPath);
|
|
44357
|
-
const stillDirty = await git.isDirty(fresh.rootPath);
|
|
44358
|
-
if (stillDirty) {
|
|
44359
|
-
return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
|
|
44360
|
-
}
|
|
44361
|
-
if (target !== "-") {
|
|
44362
|
-
const freshHolder = fresh.worktrees.find((wt) => wt.branch === target && wt.kind !== "root");
|
|
44363
|
-
if (freshHolder !== undefined) {
|
|
44364
|
-
return fail(EXIT_SAFE_REJECTION, `Branch "${target}" is already checked out at ${freshHolder.path}. Not swapping — cd there instead of using hop root.`);
|
|
44365
|
-
}
|
|
44366
|
-
}
|
|
44367
|
-
await git.switchBranch(fresh.rootPath, target, switchOptions);
|
|
44368
|
-
return ok({
|
|
44369
|
-
path: fresh.rootPath,
|
|
44370
|
-
data: { branch: target === "-" ? null : target, switched: true }
|
|
44371
|
-
});
|
|
44372
|
-
} catch (error) {
|
|
44373
|
-
if (previousBranch !== null) {
|
|
44374
|
-
try {
|
|
44375
|
-
await git.switchBranch(context.rootPath, previousBranch, {});
|
|
44376
|
-
} catch {}
|
|
44377
|
-
}
|
|
44378
|
-
return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${error.message}`);
|
|
44379
|
-
} finally {
|
|
44380
|
-
await lock.release();
|
|
44381
|
-
}
|
|
44382
|
-
};
|
|
44383
|
-
|
|
44384
45044
|
// src/infra/fs.ts
|
|
44385
45045
|
import { mkdir as mkdirFs, readdir, realpath as realpathFs } from "node:fs/promises";
|
|
44386
45046
|
var createFsPort = () => ({
|
|
@@ -44414,9 +45074,9 @@ import { promisify } from "node:util";
|
|
|
44414
45074
|
var execFile = promisify(execFileCb);
|
|
44415
45075
|
var MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
44416
45076
|
var GIT_ANCESTOR_EXIT_CODE = 1;
|
|
44417
|
-
var run = async (
|
|
45077
|
+
var run = async (cwd2, args) => {
|
|
44418
45078
|
const { stdout: stdout2 } = await execFile("git", [...args], {
|
|
44419
|
-
cwd,
|
|
45079
|
+
cwd: cwd2,
|
|
44420
45080
|
maxBuffer: MAX_BUFFER_BYTES
|
|
44421
45081
|
});
|
|
44422
45082
|
return stdout2;
|
|
@@ -44437,18 +45097,18 @@ var parseRemoteRefs = (out) => {
|
|
|
44437
45097
|
return parsed;
|
|
44438
45098
|
};
|
|
44439
45099
|
var createWorktreeMethods = () => ({
|
|
44440
|
-
listWorktreesPorcelain(
|
|
44441
|
-
return run(
|
|
45100
|
+
listWorktreesPorcelain(cwd2) {
|
|
45101
|
+
return run(cwd2, ["worktree", "list", "--porcelain", "-z"]);
|
|
44442
45102
|
},
|
|
44443
|
-
async commonDir(
|
|
44444
|
-
const out = await run(
|
|
45103
|
+
async commonDir(cwd2) {
|
|
45104
|
+
const out = await run(cwd2, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
44445
45105
|
return out.trim();
|
|
44446
45106
|
},
|
|
44447
45107
|
async isDirty(path) {
|
|
44448
45108
|
const status = await run(path, ["status", "--porcelain", "--untracked-files=all"]);
|
|
44449
45109
|
return status.trim().length > 0;
|
|
44450
45110
|
},
|
|
44451
|
-
async addWorktree(
|
|
45111
|
+
async addWorktree(cwd2, path, branch, options) {
|
|
44452
45112
|
const args = ["worktree", "add"];
|
|
44453
45113
|
if (options.createBranch) {
|
|
44454
45114
|
if (options.track !== undefined) {
|
|
@@ -44462,19 +45122,19 @@ var createWorktreeMethods = () => ({
|
|
|
44462
45122
|
} else if (!options.createBranch) {
|
|
44463
45123
|
args.push(branch);
|
|
44464
45124
|
}
|
|
44465
|
-
await run(
|
|
45125
|
+
await run(cwd2, args);
|
|
44466
45126
|
},
|
|
44467
|
-
async removeWorktree(
|
|
45127
|
+
async removeWorktree(cwd2, path, force) {
|
|
44468
45128
|
const args = ["worktree", "remove"];
|
|
44469
45129
|
if (force) {
|
|
44470
45130
|
args.push("--force");
|
|
44471
45131
|
}
|
|
44472
45132
|
args.push(path);
|
|
44473
|
-
await run(
|
|
45133
|
+
await run(cwd2, args);
|
|
44474
45134
|
},
|
|
44475
|
-
async aheadBehind(
|
|
45135
|
+
async aheadBehind(cwd2, branch) {
|
|
44476
45136
|
try {
|
|
44477
|
-
const out = await run(
|
|
45137
|
+
const out = await run(cwd2, [
|
|
44478
45138
|
"rev-list",
|
|
44479
45139
|
"--left-right",
|
|
44480
45140
|
"--count",
|
|
@@ -44491,13 +45151,13 @@ var createWorktreeMethods = () => ({
|
|
|
44491
45151
|
}
|
|
44492
45152
|
});
|
|
44493
45153
|
var createBranchMethods = () => ({
|
|
44494
|
-
async listBranches(
|
|
44495
|
-
const out = await run(
|
|
45154
|
+
async listBranches(cwd2) {
|
|
45155
|
+
const out = await run(cwd2, ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]);
|
|
44496
45156
|
return out.split(`
|
|
44497
45157
|
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
44498
45158
|
},
|
|
44499
|
-
async remotesWithBranch(
|
|
44500
|
-
const out = await run(
|
|
45159
|
+
async remotesWithBranch(cwd2, branch) {
|
|
45160
|
+
const out = await run(cwd2, ["for-each-ref", "--format=%(refname)", "refs/remotes/"]);
|
|
44501
45161
|
const remotes = new Set;
|
|
44502
45162
|
for (const { remote, branchName } of parseRemoteRefs(out)) {
|
|
44503
45163
|
if (branchName === branch) {
|
|
@@ -44506,8 +45166,8 @@ var createBranchMethods = () => ({
|
|
|
44506
45166
|
}
|
|
44507
45167
|
return [...remotes];
|
|
44508
45168
|
},
|
|
44509
|
-
async listRemoteBranches(
|
|
44510
|
-
const out = await run(
|
|
45169
|
+
async listRemoteBranches(cwd2) {
|
|
45170
|
+
const out = await run(cwd2, ["for-each-ref", "--format=%(refname)", "refs/remotes/"]);
|
|
44511
45171
|
const branches = new Set;
|
|
44512
45172
|
for (const { branchName } of parseRemoteRefs(out)) {
|
|
44513
45173
|
if (branchName !== "HEAD") {
|
|
@@ -44516,7 +45176,7 @@ var createBranchMethods = () => ({
|
|
|
44516
45176
|
}
|
|
44517
45177
|
return [...branches];
|
|
44518
45178
|
},
|
|
44519
|
-
async switchBranch(
|
|
45179
|
+
async switchBranch(cwd2, ref, options = {}) {
|
|
44520
45180
|
const args = ["switch"];
|
|
44521
45181
|
if (options.createBranch) {
|
|
44522
45182
|
if (options.track !== undefined) {
|
|
@@ -44529,35 +45189,35 @@ var createBranchMethods = () => ({
|
|
|
44529
45189
|
} else {
|
|
44530
45190
|
args.push(ref);
|
|
44531
45191
|
}
|
|
44532
|
-
await run(
|
|
45192
|
+
await run(cwd2, args);
|
|
44533
45193
|
},
|
|
44534
|
-
async deleteBranch(
|
|
44535
|
-
await run(
|
|
45194
|
+
async deleteBranch(cwd2, branch) {
|
|
45195
|
+
await run(cwd2, ["branch", "-D", branch]);
|
|
44536
45196
|
}
|
|
44537
45197
|
});
|
|
44538
45198
|
var createGarbagePolicyMethods = () => ({
|
|
44539
|
-
async resolveDefaultBranchRef(
|
|
45199
|
+
async resolveDefaultBranchRef(cwd2) {
|
|
44540
45200
|
try {
|
|
44541
|
-
const out = await run(
|
|
45201
|
+
const out = await run(cwd2, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
44542
45202
|
const ref = out.trim();
|
|
44543
45203
|
if (ref.length > 0) {
|
|
44544
45204
|
return ref;
|
|
44545
45205
|
}
|
|
44546
45206
|
} catch {}
|
|
44547
45207
|
try {
|
|
44548
|
-
await run(
|
|
45208
|
+
await run(cwd2, ["rev-parse", "--verify", "--quiet", "main"]);
|
|
44549
45209
|
return "main";
|
|
44550
45210
|
} catch {}
|
|
44551
45211
|
try {
|
|
44552
|
-
await run(
|
|
45212
|
+
await run(cwd2, ["rev-parse", "--verify", "--quiet", "master"]);
|
|
44553
45213
|
return "master";
|
|
44554
45214
|
} catch {
|
|
44555
45215
|
return null;
|
|
44556
45216
|
}
|
|
44557
45217
|
},
|
|
44558
|
-
async isAncestor(
|
|
45218
|
+
async isAncestor(cwd2, branch, ref) {
|
|
44559
45219
|
try {
|
|
44560
|
-
await run(
|
|
45220
|
+
await run(cwd2, ["merge-base", "--is-ancestor", branch, ref]);
|
|
44561
45221
|
return true;
|
|
44562
45222
|
} catch (error) {
|
|
44563
45223
|
if (failedWithExitCode(error, GIT_ANCESTOR_EXIT_CODE)) {
|
|
@@ -44566,18 +45226,18 @@ var createGarbagePolicyMethods = () => ({
|
|
|
44566
45226
|
return "unknown";
|
|
44567
45227
|
}
|
|
44568
45228
|
},
|
|
44569
|
-
async hasEquivalentCommits(
|
|
45229
|
+
async hasEquivalentCommits(cwd2, branch, ref) {
|
|
44570
45230
|
try {
|
|
44571
|
-
const out = await run(
|
|
45231
|
+
const out = await run(cwd2, ["cherry", ref, branch]);
|
|
44572
45232
|
return !out.split(`
|
|
44573
45233
|
`).some((line) => line.startsWith("+"));
|
|
44574
45234
|
} catch {
|
|
44575
45235
|
return "unknown";
|
|
44576
45236
|
}
|
|
44577
45237
|
},
|
|
44578
|
-
async isUpstreamGone(
|
|
45238
|
+
async isUpstreamGone(cwd2, branch) {
|
|
44579
45239
|
try {
|
|
44580
|
-
const out = await run(
|
|
45240
|
+
const out = await run(cwd2, [
|
|
44581
45241
|
"for-each-ref",
|
|
44582
45242
|
"--format=%(upstream:track)",
|
|
44583
45243
|
`refs/heads/${branch}`
|
|
@@ -44595,7 +45255,7 @@ var createGitPort = () => ({
|
|
|
44595
45255
|
});
|
|
44596
45256
|
|
|
44597
45257
|
// src/infra/term.ts
|
|
44598
|
-
import { createInterface } from "node:readline";
|
|
45258
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
44599
45259
|
var createTermPort = () => ({
|
|
44600
45260
|
isTTY() {
|
|
44601
45261
|
return process.stderr.isTTY === true && process.stdin.isTTY === true;
|
|
@@ -44606,7 +45266,7 @@ var createTermPort = () => ({
|
|
|
44606
45266
|
},
|
|
44607
45267
|
confirm(message) {
|
|
44608
45268
|
return new Promise((resolve) => {
|
|
44609
|
-
const rl =
|
|
45269
|
+
const rl = createInterface2({
|
|
44610
45270
|
input: process.stdin,
|
|
44611
45271
|
output: process.stderr,
|
|
44612
45272
|
terminal: false
|
|
@@ -44622,40 +45282,44 @@ var createTermPort = () => ({
|
|
|
44622
45282
|
}
|
|
44623
45283
|
});
|
|
44624
45284
|
|
|
44625
|
-
// src/
|
|
44626
|
-
var
|
|
44627
|
-
|
|
44628
|
-
|
|
44629
|
-
|
|
44630
|
-
|
|
44631
|
-
|
|
44632
|
-
|
|
44633
|
-
|
|
44634
|
-
|
|
44635
|
-
|
|
44636
|
-
|
|
44637
|
-
|
|
44638
|
-
|
|
44639
|
-
|
|
44640
|
-
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
|
|
44644
|
-
|
|
44645
|
-
|
|
44646
|
-
|
|
44647
|
-
|
|
44648
|
-
|
|
44649
|
-
|
|
44650
|
-
|
|
44651
|
-
|
|
44652
|
-
|
|
44653
|
-
|
|
44654
|
-
|
|
44655
|
-
|
|
44656
|
-
|
|
44657
|
-
|
|
44658
|
-
|
|
45285
|
+
// src/usage.ts
|
|
45286
|
+
var USAGE = `Usage: hop [command] [options]
|
|
45287
|
+
|
|
45288
|
+
hop Pick a worktree/branch interactively and cd into it (TTY); lists worktrees otherwise
|
|
45289
|
+
hop <branch> Create-or-jump: cd into <branch>'s worktree, creating it on demand
|
|
45290
|
+
hop root cd into the root clone
|
|
45291
|
+
hop - cd back to the previous worktree
|
|
45292
|
+
hop -- <branch> Escape a branch name that collides with a reserved command (ls/rm/clean/root/init/help)
|
|
45293
|
+
|
|
45294
|
+
hop ls [--json] List worktrees (dirty, ahead/behind, kind)
|
|
45295
|
+
hop rm <branch> Remove a worktree, keeping the branch
|
|
45296
|
+
hop clean Auto-detect and remove garbage worktrees
|
|
45297
|
+
hop root <branch> Temporarily switch the root clone (for verification)
|
|
45298
|
+
hop root - Switch the root clone back
|
|
45299
|
+
hop init zsh Print the zsh shell integration (eval "$(hop init zsh)")
|
|
45300
|
+
|
|
45301
|
+
Interactive picker keys:
|
|
45302
|
+
Enter cd into the selected candidate
|
|
45303
|
+
Tab, →, Ctrl+L, Ctrl+F Open the action panel, as a column beside the list
|
|
45304
|
+
(stacks below it instead on narrow terminals)
|
|
45305
|
+
Ctrl+X Delete the selected worktree (y/N confirmation)
|
|
45306
|
+
Ctrl+R Switch the root clone to the selected branch, immediately
|
|
45307
|
+
↑/↓, Ctrl+P/N, Ctrl+K/J Move the selection (arrow, emacs, and vim keys all work)
|
|
45308
|
+
Esc Cancel (exit 0, no output)
|
|
45309
|
+
Ctrl+C Cancel like an interrupt (exit 130, same as SIGINT)
|
|
45310
|
+
In the action panel: same up/down movement keys, Enter to run the highlighted
|
|
45311
|
+
action, c/d/r to run cd/delete/switchRoot directly, Esc/Tab/←/Ctrl+H to close
|
|
45312
|
+
|
|
45313
|
+
Options:
|
|
45314
|
+
--create Create the worktree when jumping to a branch without one (required outside a TTY)
|
|
45315
|
+
--json Output JSON instead of plain text
|
|
45316
|
+
--force Force removal even if the worktree is dirty (hop rm)
|
|
45317
|
+
--ext Allow operating on external worktrees (hop rm / hop clean)
|
|
45318
|
+
--yes Skip confirmation and execute (hop clean)
|
|
45319
|
+
--dry-run Only report candidates as JSON, without deleting (hop clean)
|
|
45320
|
+
--with-branch Also delete the branch when cleaning (hop clean)
|
|
45321
|
+
-h, --help Show this help and exit
|
|
45322
|
+
`;
|
|
44659
45323
|
|
|
44660
45324
|
// src/cli.ts
|
|
44661
45325
|
var git = createGitPort();
|
|
@@ -44806,26 +45470,24 @@ var jumpArgsSchema = {
|
|
|
44806
45470
|
json: { type: "boolean", description: "Output JSON" }
|
|
44807
45471
|
};
|
|
44808
45472
|
var runInteractivePick = async (json) => {
|
|
44809
|
-
const
|
|
44810
|
-
if (
|
|
44811
|
-
render("pick", pickResult, json);
|
|
44812
|
-
applyExitCode(pickResult);
|
|
45473
|
+
const candidates = await loadPickCandidates(git, fs3, json);
|
|
45474
|
+
if (candidates === null) {
|
|
44813
45475
|
return;
|
|
44814
45476
|
}
|
|
44815
|
-
|
|
44816
|
-
const
|
|
44817
|
-
|
|
44818
|
-
|
|
44819
|
-
|
|
44820
|
-
|
|
44821
|
-
|
|
44822
|
-
|
|
44823
|
-
|
|
44824
|
-
|
|
44825
|
-
|
|
44826
|
-
process.exitCode = EXIT_CANCELLED;
|
|
45477
|
+
let lastSwitchedBranch = null;
|
|
45478
|
+
const callbacks = createPickerCallbacks(git, fs3, json, (branch) => {
|
|
45479
|
+
lastSwitchedBranch = branch;
|
|
45480
|
+
});
|
|
45481
|
+
const outcome = await runInteractivePicker(candidates, callbacks);
|
|
45482
|
+
if (outcome.type === "cancelled") {
|
|
45483
|
+
process.exitCode = outcome.reason === "esc" ? EXIT_SUCCESS : EXIT_CANCELLED;
|
|
45484
|
+
return;
|
|
45485
|
+
}
|
|
45486
|
+
if (outcome.type === "path") {
|
|
45487
|
+
renderSwitchRootOutcome(outcome.path, lastSwitchedBranch, json);
|
|
44827
45488
|
return;
|
|
44828
45489
|
}
|
|
45490
|
+
const selected = outcome.candidate;
|
|
44829
45491
|
if (selected.kind === "worktree") {
|
|
44830
45492
|
const result = ok({
|
|
44831
45493
|
path: selected.worktree.path,
|
|
@@ -44861,12 +45523,17 @@ var runJumpFromArgs = async (rawArgs) => {
|
|
|
44861
45523
|
};
|
|
44862
45524
|
var ARGV_USER_ARGS_START = 2;
|
|
44863
45525
|
var rawArgs = normalizeCliArgs(process.argv.slice(ARGV_USER_ARGS_START), process.argv0, process.stdout.isTTY === true);
|
|
44864
|
-
|
|
44865
|
-
|
|
44866
|
-
|
|
44867
|
-
await runCommand(command, {
|
|
44868
|
-
rawArgs: [...dispatch.args]
|
|
44869
|
-
});
|
|
45526
|
+
if (isHelpRequest(rawArgs)) {
|
|
45527
|
+
process.stderr.write(USAGE);
|
|
45528
|
+
process.exitCode = 0;
|
|
44870
45529
|
} else {
|
|
44871
|
-
|
|
45530
|
+
const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
|
|
45531
|
+
if (dispatch.kind === "reserved") {
|
|
45532
|
+
const command = RESERVED_COMMANDS[dispatch.name];
|
|
45533
|
+
await runCommand(command, {
|
|
45534
|
+
rawArgs: [...dispatch.args]
|
|
45535
|
+
});
|
|
45536
|
+
} else {
|
|
45537
|
+
await runJumpFromArgs(dispatch.args);
|
|
45538
|
+
}
|
|
44872
45539
|
}
|