@n-seiji/nuthatch 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,16 +8,18 @@ import {
8
8
  dispatchCliArgs,
9
9
  wantsJson,
10
10
  reportFatalError,
11
- firstExecutable,
12
11
  VERSION
13
- } from "./cli-3ya15je0.js";
12
+ } from "./cli-sre95k9x.js";
14
13
  import {
15
14
  GIT_EXECUTABLE_OVERRIDE_ENV,
16
15
  GIT_NOT_FOUND_ERROR_CODE,
17
16
  gitExecutableCandidates,
18
17
  gitNotFoundMessage,
19
18
  describeFatalError
20
- } from "./cli-qrxgkskr.js";
19
+ } from "./cli-ph0gp56k.js";
20
+ import {
21
+ createTermPort
22
+ } from "./cli-26d2drt6.js";
21
23
  import {
22
24
  render,
23
25
  reportResult
@@ -25,16 +27,16 @@ import {
25
27
  import {
26
28
  UPSTREAM_FORMAT,
27
29
  parseUpstreamStates
28
- } from "./cli-pqd2qe7v.js";
29
- import {
30
- isWithin
31
- } from "./cli-g72ra4e5.js";
30
+ } from "./cli-btqjpf1q.js";
32
31
  import {
33
32
  EXIT_SUCCESS,
34
33
  EXIT_USAGE_ERROR,
35
34
  EXIT_CANCELLED,
36
35
  ok
37
- } from "./cli-2bhb9p0x.js";
36
+ } from "./cli-77yanq37.js";
37
+ import {
38
+ isWithinOrSelf
39
+ } from "./cli-a56b2ar0.js";
38
40
 
39
41
  // src/cli-args.ts
40
42
  var DASH = "-";
@@ -144,7 +146,7 @@ var filterOutNestedWorktreePaths = (relativePaths, targetPath, otherWorktreePath
144
146
  }
145
147
  return relativePaths.filter((relativePath) => {
146
148
  const absolutePath = stripTrailingSlash(`${target}/${relativePath}`);
147
- return !others.some((other) => absolutePath === other || isWithin(other, absolutePath));
149
+ return !others.some((other) => isWithinOrSelf(other, absolutePath));
148
150
  });
149
151
  };
150
152
  var isDirtyFromStatus = (statusOutput, targetPath, otherWorktreePaths) => {
@@ -173,6 +175,32 @@ var parseStatusPaths = (statusOutput) => {
173
175
  return paths;
174
176
  };
175
177
 
178
+ // src/infra/executable.ts
179
+ import { constants } from "node:fs";
180
+ import { access, stat } from "node:fs/promises";
181
+ var isExecutableFile = async (path) => {
182
+ try {
183
+ const stats = await stat(path);
184
+ if (!stats.isFile()) {
185
+ return false;
186
+ }
187
+ await access(path, constants.X_OK);
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ };
193
+ var firstExecutable = async (candidates) => {
194
+ const probes = candidates.map(async (candidate) => await isExecutableFile(candidate) ? candidate : null);
195
+ for (const probe of probes) {
196
+ const found = await probe;
197
+ if (found !== null) {
198
+ return found;
199
+ }
200
+ }
201
+ return null;
202
+ };
203
+
176
204
  // src/infra/git-executable.ts
177
205
  var resolveGitExecutable = async (env) => {
178
206
  const candidates = gitExecutableCandidates({
@@ -304,12 +332,12 @@ var createBranchMethods = () => ({
304
332
  args.push(options.track);
305
333
  }
306
334
  } else {
307
- args.push(ref);
335
+ args.push("--end-of-options", ref);
308
336
  }
309
337
  await run(cwd, args);
310
338
  },
311
339
  async deleteBranch(cwd, branch) {
312
- await run(cwd, ["branch", "-D", branch]);
340
+ await run(cwd, ["branch", "-D", "--end-of-options", branch]);
313
341
  },
314
342
  async detachHead(cwd) {
315
343
  await run(cwd, ["switch", "--detach", "HEAD"]);
@@ -337,7 +365,7 @@ var createGarbagePolicyMethods = () => ({
337
365
  },
338
366
  async isAncestor(cwd, branch, ref) {
339
367
  try {
340
- await run(cwd, ["merge-base", "--is-ancestor", branch, ref]);
368
+ await run(cwd, ["merge-base", "--is-ancestor", "--end-of-options", branch, ref]);
341
369
  return true;
342
370
  } catch (error) {
343
371
  if (failedWithExitCode(error, GIT_ANCESTOR_EXIT_CODE)) {
@@ -348,7 +376,7 @@ var createGarbagePolicyMethods = () => ({
348
376
  },
349
377
  async hasEquivalentCommits(cwd, branch, ref) {
350
378
  try {
351
- const out = await run(cwd, ["cherry", ref, branch]);
379
+ const out = await run(cwd, ["cherry", "--end-of-options", ref, branch]);
352
380
  return !out.split(`
353
381
  `).some((line) => line.startsWith("+"));
354
382
  } catch {
@@ -362,34 +390,6 @@ var createGitPort = () => ({
362
390
  ...createGarbagePolicyMethods()
363
391
  });
364
392
 
365
- // src/infra/term.ts
366
- import { createInterface } from "node:readline";
367
- var createTermPort = () => ({
368
- isTTY() {
369
- return process.stderr.isTTY === true && process.stdin.isTTY === true;
370
- },
371
- logStderr(message) {
372
- process.stderr.write(`${message}
373
- `);
374
- },
375
- confirm(message) {
376
- return new Promise((resolve) => {
377
- const rl = createInterface({
378
- input: process.stdin,
379
- output: process.stderr,
380
- terminal: false
381
- });
382
- rl.question(`${message} [y/N] `, (answer) => {
383
- rl.close();
384
- resolve(answer.trim().toLowerCase() === "y");
385
- });
386
- rl.once("close", () => {
387
- resolve(false);
388
- });
389
- });
390
- }
391
- });
392
-
393
393
  // src/usage.ts
394
394
  var USAGE = `Usage: hop [command] [options]
395
395
 
@@ -406,7 +406,7 @@ var USAGE = `Usage: hop [command] [options]
406
406
  hop root - Switch the root clone back
407
407
  hop init zsh Print the zsh shell integration (eval "$(hop init zsh)")
408
408
 
409
- hop --update Update hop to the latest release, the way it was installed
409
+ hop --update Replace a standalone binary with the latest release (else: how to update)
410
410
  hop --update --check Only report whether an update is available; change nothing
411
411
  hop --version Print hop's version
412
412
 
@@ -416,7 +416,7 @@ Interactive picker keys:
416
416
  (stacks below it instead on narrow terminals)
417
417
  Ctrl+X Delete the selected worktree (y/N confirmation)
418
418
  Ctrl+R Switch the root clone to the selected branch (y/N first for an external worktree)
419
- ↑/↓, Ctrl+P/N, Ctrl+K/J Move the selection (arrow, emacs, and vim keys all work)
419
+ ↑/↓, Ctrl+P/N, Ctrl+K Move the selection (arrow, emacs, and vim keys all work)
420
420
  Esc Cancel (exit 0, no output)
421
421
  Ctrl+C Cancel like an interrupt (exit 130, same as SIGINT)
422
422
  In the action panel: same up/down movement keys, Enter to run the highlighted
@@ -426,8 +426,7 @@ Options:
426
426
  --create Create the worktree when jumping to a branch without one (always required, TTY or not)
427
427
  --json Output JSON instead of plain text
428
428
  --force Force removal even if the worktree is dirty (hop rm)
429
- --ext [deprecated, no-op for hop rm] hop rm no longer requires it to
430
- remove external worktrees; still gates hop clean's auto-cleanup scope
429
+ --ext Also include external worktrees in the auto-cleanup scope (hop clean)
431
430
  --yes Skip confirmation and execute (hop clean)
432
431
  --dry-run Only report candidates as JSON, without deleting (hop clean)
433
432
  --with-branch Also delete the branch when cleaning (hop clean)
@@ -443,14 +442,14 @@ var getPorts = () => {
443
442
  var JSON_FLAG = "json";
444
443
  var runLs = async (json) => {
445
444
  const [{ ls }, { reportLs }] = await Promise.all([
446
- import("./ls-kvz4wmc9.js"),
447
- import("./render-ls-6r3vv7y0.js")
445
+ import("./ls-3hyjf60d.js"),
446
+ import("./render-ls-x4hpnsvy.js")
448
447
  ]);
449
448
  const { git, fs } = getPorts();
450
449
  reportLs(await ls(git, fs, { cwd: process.cwd() }), json);
451
450
  };
452
451
  var runJump = async (target, options) => {
453
- const { jump } = await import("./jump-nvdkeega.js");
452
+ const { jump } = await import("./jump-1zkraat1.js");
454
453
  const { git, fs } = getPorts();
455
454
  const result = await jump(git, fs, {
456
455
  cwd: process.cwd(),
@@ -466,7 +465,7 @@ var runInteractivePick = async (json) => {
466
465
  loadPickCandidates,
467
466
  renderSwitchRootOutcome,
468
467
  runInteractivePicker
469
- } = await import("./cli-pick-w9qk71fy.js");
468
+ } = await import("./cli-pick-yjd4qpd9.js");
470
469
  const { git, fs } = getPorts();
471
470
  const candidates = await loadPickCandidates(git, fs, json);
472
471
  if (candidates === null) {
@@ -523,15 +522,14 @@ var runJumpFromArgs = async (tokens) => {
523
522
  var runRm = async (tokens) => {
524
523
  const args = parseCliArgs(tokens, {
525
524
  positionals: [{ name: "branch", required: true }],
526
- booleans: ["force", "ext", JSON_FLAG]
525
+ booleans: ["force", JSON_FLAG]
527
526
  });
528
- const { rm } = await import("./rm-231qjgd8.js");
527
+ const { rm } = await import("./rm-eg3j8ae1.js");
529
528
  const { git, fs } = getPorts();
530
529
  const result = await rm(git, fs, {
531
530
  cwd: process.cwd(),
532
531
  branch: args.positionals.branch ?? "",
533
- force: args.booleans.force ?? false,
534
- ext: args.booleans.ext ?? false
532
+ force: args.booleans.force ?? false
535
533
  });
536
534
  reportResult("rm", result, args.booleans.json ?? false);
537
535
  };
@@ -539,7 +537,7 @@ var runClean = async (tokens) => {
539
537
  const args = parseCliArgs(tokens, {
540
538
  booleans: ["ext", "withBranch", "dryRun", "yes", JSON_FLAG]
541
539
  });
542
- const { clean } = await import("./clean-n2f9z8n3.js");
540
+ const { clean } = await import("./clean-rn2476my.js");
543
541
  const { git, fs, term } = getPorts();
544
542
  const result = await clean(git, fs, term, {
545
543
  cwd: process.cwd(),
@@ -556,7 +554,7 @@ var runRoot = async (tokens) => {
556
554
  booleans: [JSON_FLAG],
557
555
  strings: ["track"]
558
556
  });
559
- const { root } = await import("./root-mmyazwc3.js");
557
+ const { root } = await import("./root-49vcb8x2.js");
560
558
  const { git, fs } = getPorts();
561
559
  const { branch } = args.positionals;
562
560
  const { track } = args.strings;
@@ -578,8 +576,8 @@ var runInit = async (tokens) => {
578
576
  process.exitCode = EXIT_USAGE_ERROR;
579
577
  return;
580
578
  }
581
- const { renderInit } = await import("./init-s0dafa83.js");
582
- process.stdout.write(renderInit({ shell: "zsh" }));
579
+ const { renderInit } = await import("./init-167d1bbs.js");
580
+ process.stdout.write(renderInit());
583
581
  };
584
582
  var RESERVED_COMMANDS = {
585
583
  ls: (tokens) => runLs(parseCliArgs(tokens, { booleans: [JSON_FLAG] }).booleans.json ?? false),
@@ -594,7 +592,7 @@ var dispatchAndRun = async (dispatch) => {
594
592
  await (command === undefined ? runJumpFromArgs(dispatch.args) : command(dispatch.args));
595
593
  };
596
594
  var runUpdate = async (rawArgs) => {
597
- const update = await import("./cli-update-7exha1em.js");
595
+ const update = await import("./cli-update-x0r4b4xk.js");
598
596
  await update.runUpdate(rawArgs, getPorts().term);
599
597
  };
600
598
  var main = async () => {
@@ -2,6 +2,14 @@
2
2
  var ZSH_TEMPLATE = `# nuthatch shell integration for zsh.
3
3
  # Usage: eval "$(hop init zsh)"
4
4
  hop() {
5
+ # stdout is not a terminal (e.g. inside p=$(hop ...)): a cd here would only
6
+ # affect the capturing subshell and leave the caller with an empty string,
7
+ # so just print the path.
8
+ if [[ ! -t 1 ]]; then
9
+ command hop "$@"
10
+ return $?
11
+ fi
12
+
5
13
  if [[ "$1" == "-" ]]; then
6
14
  cd - > /dev/null || return $?
7
15
  return 0
@@ -50,16 +58,7 @@ hop() {
50
58
  return $rc
51
59
  }
52
60
  `;
53
- var renderInit = (options) => {
54
- switch (options.shell) {
55
- case "zsh": {
56
- return ZSH_TEMPLATE;
57
- }
58
- default: {
59
- return ZSH_TEMPLATE;
60
- }
61
- }
62
- };
61
+ var renderInit = () => ZSH_TEMPLATE;
63
62
  export {
64
63
  renderInit
65
64
  };
@@ -1,19 +1,20 @@
1
1
  import {
2
2
  messageOf
3
- } from "./cli-qrxgkskr.js";
3
+ } from "./cli-ph0gp56k.js";
4
4
  import {
5
5
  withRepoLock
6
- } from "./cli-rcw5adqd.js";
6
+ } from "./cli-wcnyhk4w.js";
7
7
  import {
8
+ refArgumentRejection,
8
9
  loadRepoContext,
9
10
  resolveBranchCheckout
10
- } from "./cli-7jg5qqs9.js";
11
+ } from "./cli-y8vsqre6.js";
11
12
  import {
12
13
  EXIT_GENERAL_ERROR,
13
14
  EXIT_SAFE_REJECTION,
14
15
  ok,
15
16
  fail
16
- } from "./cli-2bhb9p0x.js";
17
+ } from "./cli-77yanq37.js";
17
18
 
18
19
  // src/commands/jump.ts
19
20
  import { join } from "node:path";
@@ -64,6 +65,10 @@ var jump = async (git, fs, options) => {
64
65
  data: { branch: options.target, created: false }
65
66
  });
66
67
  }
68
+ const refRejection = refArgumentRejection(options.target, options.track);
69
+ if (refRejection !== null) {
70
+ return refRejection;
71
+ }
67
72
  const context = await loadRepoContext(git, fs, options.cwd);
68
73
  const existing = context.worktrees.find((wt) => wt.branch === options.target);
69
74
  if (existing !== undefined) {
@@ -88,7 +93,7 @@ var jump = async (git, fs, options) => {
88
93
  return existingWorktreeResult(options.target, racedExisting.path);
89
94
  }
90
95
  await fs.mkdir(context.managedRoot);
91
- await git.addWorktree(context.rootPath, targetPath, options.target, checkout.options);
96
+ await git.addWorktree(context.rootPath, targetPath, options.target, checkout.value);
92
97
  return ok({
93
98
  path: targetPath,
94
99
  data: { branch: options.target, created: true }
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  upstreamStateOf
3
- } from "./cli-pqd2qe7v.js";
3
+ } from "./cli-btqjpf1q.js";
4
4
  import {
5
5
  loadRepoContext,
6
6
  worktreeDirtyState
7
- } from "./cli-7jg5qqs9.js";
7
+ } from "./cli-y8vsqre6.js";
8
8
  import {
9
9
  ok
10
- } from "./cli-2bhb9p0x.js";
10
+ } from "./cli-77yanq37.js";
11
11
 
12
12
  // src/commands/ls.ts
13
13
  var ls = async (git, fs, options) => {
@@ -2,26 +2,26 @@ import {
2
2
  displayWidth,
3
3
  padToWidth,
4
4
  truncateToWidth,
5
- truncateToWidthKeepingTail
6
- } from "./cli-71jgs14h.js";
5
+ truncateToWidthKeepingTail,
6
+ SGR,
7
+ SGR_RESET
8
+ } from "./cli-fhta3tr8.js";
9
+ import {
10
+ colorEnabled
11
+ } from "./cli-26d2drt6.js";
7
12
  import {
8
13
  reportResult
9
14
  } from "./cli-dbzx9nzg.js";
15
+ import {
16
+ shortenHome
17
+ } from "./cli-s5qcjcwk.js";
18
+ import {
19
+ isWithinOrSelf
20
+ } from "./cli-a56b2ar0.js";
10
21
 
11
22
  // src/render-ls.ts
12
- var ESC = "\x1B";
13
- var SGR = {
14
- plain: "",
15
- bold: `${ESC}[1m`,
16
- dim: `${ESC}[2m`,
17
- green: `${ESC}[32m`,
18
- yellow: `${ESC}[33m`,
19
- red: `${ESC}[31m`,
20
- magenta: `${ESC}[35m`,
21
- cyan: `${ESC}[36m`
22
- };
23
23
  var cell = (text, tone = "plain") => ({ text, tone });
24
- var paint = (value, color) => !color || value.tone === "plain" ? value.text : `${SGR[value.tone]}${value.text}${ESC}[0m`;
24
+ var paint = (value, color) => !color || value.tone === "plain" ? value.text : `${SGR[value.tone]}${value.text}${SGR_RESET}`;
25
25
  var SHORT_SHA_LENGTH = 7;
26
26
  var MIN_PATH_WIDTH = 12;
27
27
  var MAX_BRANCH_WIDTH = 32;
@@ -78,17 +78,7 @@ var syncCells = (entry) => {
78
78
  }
79
79
  return cells;
80
80
  };
81
- var shortenHome = (path, home) => {
82
- if (home === "" || home === "/") {
83
- return path;
84
- }
85
- if (path === home) {
86
- return "~";
87
- }
88
- return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
89
- };
90
- var isInside = (cwd, path) => cwd === path || cwd.startsWith(path.endsWith("/") ? path : `${path}/`);
91
- var currentPath = (entries, cwd) => entries.filter((entry) => isInside(cwd, entry.path)).reduce((best, entry) => best === null || entry.path.length > best.length ? entry.path : best, null);
81
+ var currentPath = (entries, cwd) => entries.filter((entry) => isWithinOrSelf(entry.path, cwd)).reduce((best, entry) => best === null || entry.path.length > best.length ? entry.path : best, null);
92
82
  var summaryLine = (entries) => {
93
83
  const parts = [`${entries.length} worktree${entries.length === 1 ? "" : "s"}`];
94
84
  const counts = [
@@ -136,13 +126,12 @@ var formatLsTable = (entries, options) => {
136
126
  `)}
137
127
  `;
138
128
  };
139
- var stdoutColor = () => process.stdout.isTTY === true && (process.env["NO_COLOR"] ?? "") === "";
140
129
  var reportLs = (result, json) => {
141
130
  if (json || !result.ok || result.data === undefined) {
142
131
  reportResult("ls", result, json);
143
132
  return;
144
133
  }
145
- const color = stdoutColor();
134
+ const color = colorEnabled(process.stdout);
146
135
  process.stdout.write(formatLsTable(result.data, {
147
136
  color,
148
137
  width: process.stdout.isTTY === true ? process.stdout.columns ?? null : null,
@@ -0,0 +1,7 @@
1
+ import {
2
+ rm2
3
+ } from "./cli-9n04gsq1.js";
4
+ import"./cli-ph0gp56k.js";
5
+ export {
6
+ rm2 as rm
7
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ root2
3
+ } from "./cli-hz0r7zw8.js";
4
+ import"./cli-ph0gp56k.js";
5
+ export {
6
+ root2 as root
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n-seiji/nuthatch",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "description": "git worktree manager (hop)",
5
5
  "homepage": "https://github.com/n-seiji/nuthatch#readme",
6
6
  "bugs": {
@@ -15,8 +15,7 @@
15
15
  "hop": "dist/cli.js"
16
16
  },
17
17
  "files": [
18
- "dist",
19
- "shell"
18
+ "dist"
20
19
  ],
21
20
  "type": "module",
22
21
  "publishConfig": {
@@ -36,7 +35,7 @@
36
35
  },
37
36
  "devDependencies": {
38
37
  "@types/bun": "latest",
39
- "oxfmt": "^0.66.0",
38
+ "oxfmt": "^0.72.0",
40
39
  "oxlint": "^1.81.0",
41
40
  "typescript": "^5.6.0"
42
41
  },
@@ -1,7 +0,0 @@
1
- import {
2
- rm2
3
- } from "./cli-23z98dw1.js";
4
- import"./cli-qrxgkskr.js";
5
- export {
6
- rm2 as rm
7
- };
@@ -1,7 +0,0 @@
1
- import {
2
- root2
3
- } from "./cli-ygt2w4e1.js";
4
- import"./cli-qrxgkskr.js";
5
- export {
6
- root2 as root
7
- };
package/shell/init.zsh DELETED
@@ -1,53 +0,0 @@
1
- # nuthatch shell integration for zsh.
2
- # Usage: eval "$(hop init zsh)"
3
- #
4
- # This file is a reference copy of the template embedded in
5
- # src/commands/init.ts (renderInit). Keep the two in sync.
6
- hop() {
7
- if [[ "$1" == "-" ]]; then
8
- cd - > /dev/null || return $?
9
- return 0
10
- fi
11
-
12
- local should_cd=1
13
- case "$1" in
14
- ls|rm|clean|init) should_cd=0 ;;
15
- --) ;; # "hop -- <branch>" escapes a reserved word; still a jump, so cd.
16
- "")
17
- # Bare "hop": picker on a real terminal (cd on success), ls fallback
18
- # otherwise (prints a listing, not a path — don't cd). fd 2/0 are what
19
- # the child inherits unchanged; fd 1 is about to become the capture
20
- # pipe below, so it can't be checked here — this mirrors hop's own
21
- # isTTY() check (stderr + stdin), not stdout.
22
- if [[ -t 2 && -t 0 ]]; then
23
- should_cd=1
24
- else
25
- should_cd=0
26
- fi
27
- ;;
28
- --*) should_cd=0 ;;
29
- esac
30
-
31
- # Nothing to cd into: run hop with the terminal as its stdout, so a listing
32
- # keeps its colors (hop only colors stdout when it is a terminal).
33
- if [[ $should_cd -eq 0 ]]; then
34
- command hop "$@"
35
- return $?
36
- fi
37
-
38
- # "status" is a zsh read-only special parameter (mirrors $?); using it as a
39
- # local name fails with "read-only variable: status". Use "rc" instead, and
40
- # declare it before the command substitution so the exit code isn't
41
- # clobbered by "local"'s own exit status.
42
- local out rc
43
- out="$(command hop "$@")"
44
- rc=$?
45
-
46
- if [[ $rc -eq 0 && $should_cd -eq 1 && -n "$out" ]]; then
47
- cd -- "$out" || return $?
48
- elif [[ -n "$out" ]]; then
49
- print -r -- "$out"
50
- fi
51
-
52
- return $rc
53
- }