@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 +11 -0
- package/README.md +11 -0
- package/dist/cli.js +161 -44
- package/package.json +2 -2
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(
|
|
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
|
|
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
|
-
|
|
5180
|
-
|
|
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
|
+
"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": ">=
|
|
45
|
+
"node": ">=22"
|
|
46
46
|
}
|
|
47
47
|
}
|