@n-seiji/nuthatch 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -137,6 +137,17 @@ install script は `hop` を `~/.local/bin` に置き (`HOP_INSTALL_DIR` で
137
137
  には `HOP_VERSION=vX.Y.Z` を使う。Linux arm64 はビルド済みバイナリが
138
138
  まだないため、npm install を使う。
139
139
 
140
+ ## トラブルシューティング
141
+
142
+ `hop: git executable not found. Looked in: …` は git のバイナリを見つけられ
143
+ なかったという意味。hop は `PATH` の絶対パスエントリを順に探し、その後
144
+ `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` を見る。git が
145
+ それ以外の場所にある場合は、直接指定する:
146
+
147
+ ```sh
148
+ export HOP_GIT=/path/to/git
149
+ ```
150
+
140
151
  ## Agent skill (Claude Code / Codex plugin)
141
152
 
142
153
  このリポジトリは plugin marketplace も兼ねており、
package/README.md CHANGED
@@ -146,6 +146,17 @@ The install script places `hop` in `~/.local/bin` (override with
146
146
  specific version with `HOP_VERSION=vX.Y.Z`. Linux arm64 has no prebuilt
147
147
  binary yet — use the npm install instead.
148
148
 
149
+ ## Troubleshooting
150
+
151
+ `hop: git executable not found. Looked in: …` means hop could not find a git
152
+ binary. hop searches every absolute entry in `PATH`, then `/opt/homebrew/bin`,
153
+ `/usr/local/bin`, `/usr/bin` and `/bin`. If your git lives somewhere else,
154
+ point hop straight at it:
155
+
156
+ ```sh
157
+ export HOP_GIT=/path/to/git
158
+ ```
159
+
149
160
  ## Agent skill (Claude Code / Codex plugin)
150
161
 
151
162
  This repo doubles as a plugin marketplace that ships the
package/dist/cli.js CHANGED
@@ -2054,6 +2054,31 @@ var dispatchCliArgs = (rawArgs, reservedNames) => {
2054
2054
  return { kind: "jump", args: rawArgs };
2055
2055
  };
2056
2056
 
2057
+ // src/domain/git-executable.ts
2058
+ var GIT_EXECUTABLE_OVERRIDE_ENV = "HOP_GIT";
2059
+ var GIT_NOT_FOUND_ERROR_CODE = "HOP_GIT_NOT_FOUND";
2060
+ var GIT_NOT_FOUND_HINT = `Install git, or set ${GIT_EXECUTABLE_OVERRIDE_ENV} to its absolute path.`;
2061
+ var GIT_FALLBACK_BIN_DIRS = [
2062
+ "/opt/homebrew/bin",
2063
+ "/usr/local/bin",
2064
+ "/usr/bin",
2065
+ "/bin"
2066
+ ];
2067
+ var PATH_SEPARATOR = ":";
2068
+ var joinBin = (dir) => {
2069
+ const base = dir.endsWith("/") ? dir.slice(0, -1) : dir;
2070
+ return `${base}/git`;
2071
+ };
2072
+ var gitExecutableCandidates = (input) => {
2073
+ const override = input.override?.trim() ?? "";
2074
+ if (override.length > 0) {
2075
+ return [override];
2076
+ }
2077
+ const dirs = (input.path ?? "").split(PATH_SEPARATOR).map((entry) => entry.trim()).filter((entry) => entry.startsWith("/"));
2078
+ return [...new Set([...dirs, ...GIT_FALLBACK_BIN_DIRS].map((dir) => joinBin(dir)))];
2079
+ };
2080
+ var gitNotFoundMessage = (candidates) => `git executable not found. Looked in: ${candidates.join(", ")}. ${GIT_NOT_FOUND_HINT}`;
2081
+
2057
2082
  // src/domain/result.ts
2058
2083
  var EXIT_SUCCESS = 0;
2059
2084
  var EXIT_GENERAL_ERROR = 1;
@@ -2072,6 +2097,75 @@ var fail = (exitCode, errorMessage, warnings) => ({
2072
2097
  ...warnings === undefined || warnings.length === 0 ? {} : { warnings }
2073
2098
  });
2074
2099
 
2100
+ // src/domain/fatal-error.ts
2101
+ var fieldsOf = (error) => typeof error === "object" && error !== null ? error : {};
2102
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
2103
+ var trimmedStringOf = (value) => typeof value === "string" ? value.trim() : "";
2104
+ var isSpawnFailure = (fields) => fields.code === "ENOENT" && typeof fields.syscall === "string" && fields.syscall.startsWith("spawn");
2105
+ var isGitFailure = (fields) => typeof fields.code === "number" || typeof fields.signal === "string" && fields.signal !== "";
2106
+ var describeFatalError = (error) => {
2107
+ const fields = fieldsOf(error);
2108
+ if (isSpawnFailure(fields)) {
2109
+ const binary = trimmedStringOf(fields.path) || "git";
2110
+ return {
2111
+ message: `failed to run ${binary}: no such executable. ${GIT_NOT_FOUND_HINT}`,
2112
+ exitCode: EXIT_GENERAL_ERROR
2113
+ };
2114
+ }
2115
+ if (isGitFailure(fields)) {
2116
+ const stderr = trimmedStringOf(fields.stderr);
2117
+ return {
2118
+ message: stderr.length > 0 ? stderr : messageOf(error),
2119
+ exitCode: EXIT_SAFE_REJECTION
2120
+ };
2121
+ }
2122
+ return { message: messageOf(error), exitCode: EXIT_GENERAL_ERROR };
2123
+ };
2124
+
2125
+ // src/render.ts
2126
+ var PLAIN_JSON_INDENT = 2;
2127
+ var render = (command, result, json) => {
2128
+ if (json) {
2129
+ const envelope = {
2130
+ schemaVersion: 1,
2131
+ command,
2132
+ data: result.ok ? result.data : undefined,
2133
+ warnings: result.warnings ?? []
2134
+ };
2135
+ process.stdout.write(`${JSON.stringify(envelope)}
2136
+ `);
2137
+ if (!result.ok && result.errorMessage !== undefined) {
2138
+ process.stderr.write(`${result.errorMessage}
2139
+ `);
2140
+ }
2141
+ return;
2142
+ }
2143
+ if (result.path !== undefined) {
2144
+ process.stdout.write(`${result.path}
2145
+ `);
2146
+ } else if (result.ok && result.data !== undefined) {
2147
+ process.stdout.write(`${JSON.stringify(result.data, null, PLAIN_JSON_INDENT)}
2148
+ `);
2149
+ }
2150
+ for (const warning of result.warnings ?? []) {
2151
+ process.stderr.write(`warning: ${warning}
2152
+ `);
2153
+ }
2154
+ if (!result.ok && result.errorMessage !== undefined) {
2155
+ process.stderr.write(`${result.errorMessage}
2156
+ `);
2157
+ }
2158
+ };
2159
+
2160
+ // src/cli-fatal.ts
2161
+ var wantsJson = (args) => args.some((arg) => arg === "--json" || arg.startsWith("--json="));
2162
+ var reportFatalError = (command, json, error) => {
2163
+ const fatal = describeFatalError(error);
2164
+ const result = fail(fatal.exitCode, `hop: ${fatal.message}`);
2165
+ render(command, result, json);
2166
+ process.exitCode = result.exitCode;
2167
+ };
2168
+
2075
2169
  // src/infra/lock.ts
2076
2170
  import { randomUUID } from "node:crypto";
2077
2171
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -2498,41 +2592,6 @@ var switchAndReport = async ({
2498
2592
  }
2499
2593
  };
2500
2594
 
2501
- // src/render.ts
2502
- var PLAIN_JSON_INDENT = 2;
2503
- var render = (command, result, json) => {
2504
- if (json) {
2505
- const envelope = {
2506
- schemaVersion: 1,
2507
- command,
2508
- data: result.ok ? result.data : undefined,
2509
- warnings: result.warnings ?? []
2510
- };
2511
- process.stdout.write(`${JSON.stringify(envelope)}
2512
- `);
2513
- if (!result.ok && result.errorMessage !== undefined) {
2514
- process.stderr.write(`${result.errorMessage}
2515
- `);
2516
- }
2517
- return;
2518
- }
2519
- if (result.path !== undefined) {
2520
- process.stdout.write(`${result.path}
2521
- `);
2522
- } else if (result.ok && result.data !== undefined) {
2523
- process.stdout.write(`${JSON.stringify(result.data, null, PLAIN_JSON_INDENT)}
2524
- `);
2525
- }
2526
- for (const warning of result.warnings ?? []) {
2527
- process.stderr.write(`warning: ${warning}
2528
- `);
2529
- }
2530
- if (!result.ok && result.errorMessage !== undefined) {
2531
- process.stderr.write(`${result.errorMessage}
2532
- `);
2533
- }
2534
- };
2535
-
2536
2595
  // src/cli-root-command.ts
2537
2596
  var ROOT_PREVIOUS_TOKEN = " hop-root-previous ";
2538
2597
  var VALUE_FLAGS = new Set(["--track"]);
@@ -4734,12 +4793,52 @@ var parseStatusPaths = (statusOutput) => {
4734
4793
  };
4735
4794
  var STATUS_CODE_WIDTH = 3;
4736
4795
 
4796
+ // src/infra/git-executable.ts
4797
+ import { constants } from "node:fs";
4798
+ import { access, stat } from "node:fs/promises";
4799
+ var isExecutableFile = async (path) => {
4800
+ try {
4801
+ const stats = await stat(path);
4802
+ if (!stats.isFile()) {
4803
+ return false;
4804
+ }
4805
+ await access(path, constants.X_OK);
4806
+ return true;
4807
+ } catch {
4808
+ return false;
4809
+ }
4810
+ };
4811
+ var resolveGitExecutable = async (env2) => {
4812
+ const candidates = gitExecutableCandidates({
4813
+ override: env2[GIT_EXECUTABLE_OVERRIDE_ENV],
4814
+ path: env2.PATH
4815
+ });
4816
+ const probes = candidates.map(async (candidate) => await isExecutableFile(candidate) ? candidate : undefined);
4817
+ for (const probe of probes) {
4818
+ const found = await probe;
4819
+ if (found !== undefined) {
4820
+ return found;
4821
+ }
4822
+ }
4823
+ throw Object.assign(new Error(gitNotFoundMessage(candidates)), {
4824
+ code: GIT_NOT_FOUND_ERROR_CODE
4825
+ });
4826
+ };
4827
+ var createGitExecutableResolver = (env2) => {
4828
+ let cached = null;
4829
+ return () => {
4830
+ cached ??= resolveGitExecutable(env2);
4831
+ return cached;
4832
+ };
4833
+ };
4834
+
4737
4835
  // src/infra/git.ts
4738
4836
  var execFile = promisify(execFileCb);
4837
+ var gitExecutable = createGitExecutableResolver(process.env);
4739
4838
  var MAX_BUFFER_BYTES = 64 * 1024 * 1024;
4740
4839
  var GIT_ANCESTOR_EXIT_CODE = 1;
4741
4840
  var run = async (cwd, args) => {
4742
- const { stdout: stdout2 } = await execFile("git", [...args], {
4841
+ const { stdout: stdout2 } = await execFile(await gitExecutable(), [...args], {
4743
4842
  cwd,
4744
4843
  maxBuffer: MAX_BUFFER_BYTES
4745
4844
  });
@@ -5164,19 +5263,37 @@ var runJumpFromArgs = async (rawArgs) => {
5164
5263
  });
5165
5264
  };
5166
5265
  var ARGV_USER_ARGS_START = 2;
5167
- var rawArgs = normalizeCliArgs(process.argv.slice(ARGV_USER_ARGS_START), process.argv0, isRunningAsCompiledBinary());
5168
- if (isHelpRequest(rawArgs)) {
5169
- process.stderr.write(USAGE);
5170
- process.exitCode = 0;
5171
- } else {
5172
- const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
5266
+ var dispatchAndRun = async (dispatch) => {
5173
5267
  if (dispatch.kind === "reserved") {
5174
5268
  const command = RESERVED_COMMANDS[dispatch.name];
5175
5269
  const commandArgs = dispatch.name === "root" ? rewriteRootPreviousToken(dispatch.args) : [...dispatch.args];
5176
5270
  await runCommand(command, {
5177
5271
  rawArgs: commandArgs
5178
5272
  });
5179
- } else {
5180
- await runJumpFromArgs(dispatch.args);
5273
+ return;
5274
+ }
5275
+ await runJumpFromArgs(dispatch.args);
5276
+ };
5277
+ var main = async () => {
5278
+ const rawArgs = normalizeCliArgs(process.argv.slice(ARGV_USER_ARGS_START), process.argv0, isRunningAsCompiledBinary());
5279
+ if (isHelpRequest(rawArgs)) {
5280
+ process.stderr.write(USAGE);
5281
+ process.exitCode = 0;
5282
+ return;
5181
5283
  }
5284
+ const dispatch = dispatchCliArgs(rawArgs, Object.keys(RESERVED_COMMANDS));
5285
+ try {
5286
+ await dispatchAndRun(dispatch);
5287
+ } catch (error) {
5288
+ const command = dispatch.kind === "reserved" ? dispatch.name : "jump";
5289
+ reportFatalError(command, wantsJson(rawArgs), error);
5290
+ }
5291
+ };
5292
+ try {
5293
+ await main();
5294
+ } catch (error) {
5295
+ const fatal = describeFatalError(error);
5296
+ process.stderr.write(`hop: ${fatal.message}
5297
+ `);
5298
+ process.exitCode = fatal.exitCode;
5182
5299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n-seiji/nuthatch",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "git worktree manager (hop)",
5
5
  "homepage": "https://github.com/n-seiji/nuthatch#readme",
6
6
  "bugs": {
@@ -42,6 +42,6 @@
42
42
  "typescript": "^5.6.0"
43
43
  },
44
44
  "engines": {
45
- "node": ">=20"
45
+ "node": ">=22"
46
46
  }
47
47
  }