@n-seiji/nuthatch 0.1.5 → 0.1.7

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.
@@ -0,0 +1,35 @@
1
+ import {
2
+ REFS_HEADS_PREFIX
3
+ } from "./cli-g72ra4e5.js";
4
+
5
+ // src/domain/upstream.ts
6
+ var NONE = { kind: "none" };
7
+ var FIELD_SEPARATOR = "\x00";
8
+ var UPSTREAM_FORMAT = "%(refname)%00%(upstream)%00%(upstream:track,nobracket)";
9
+ var countOf = (track, word) => {
10
+ const match = new RegExp(`${word} (?<count>\\d+)`, "u").exec(track);
11
+ return match?.groups?.count === undefined ? 0 : Number(match.groups.count);
12
+ };
13
+ var parseTrack = (upstream, track) => {
14
+ if (upstream.length === 0) {
15
+ return NONE;
16
+ }
17
+ if (track === "gone") {
18
+ return { kind: "gone" };
19
+ }
20
+ return { kind: "tracking", ahead: countOf(track, "ahead"), behind: countOf(track, "behind") };
21
+ };
22
+ var parseUpstreamStates = (output) => {
23
+ const states = new Map;
24
+ for (const line of output.split(`
25
+ `)) {
26
+ const [refname = "", upstream = "", track = ""] = line.split(FIELD_SEPARATOR);
27
+ if (refname.startsWith(REFS_HEADS_PREFIX)) {
28
+ states.set(refname.slice(REFS_HEADS_PREFIX.length), parseTrack(upstream, track.trim()));
29
+ }
30
+ }
31
+ return states;
32
+ };
33
+ var upstreamStateOf = (states, branch) => states.get(branch) ?? NONE;
34
+
35
+ export { UPSTREAM_FORMAT, parseUpstreamStates, upstreamStateOf };
@@ -0,0 +1,69 @@
1
+ import {
2
+ EXIT_GENERAL_ERROR,
3
+ EXIT_SAFE_REJECTION
4
+ } from "./cli-2bhb9p0x.js";
5
+
6
+ // src/domain/posix-path.ts
7
+ var withoutTrailingSlash = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
8
+ var parentDir = (path) => {
9
+ const trimmed = withoutTrailingSlash(path);
10
+ const index = trimmed.lastIndexOf("/");
11
+ if (index === -1) {
12
+ return ".";
13
+ }
14
+ return index === 0 ? "/" : trimmed.slice(0, index);
15
+ };
16
+ var baseName = (path) => {
17
+ const trimmed = withoutTrailingSlash(path);
18
+ return trimmed.slice(trimmed.lastIndexOf("/") + 1);
19
+ };
20
+ var absoluteCandidates = (dirs, name) => [
21
+ ...new Set(dirs.map((entry) => entry.trim()).filter((entry) => entry.startsWith("/")).map((dir) => `${dir.endsWith("/") ? dir.slice(0, -1) : dir}/${name}`))
22
+ ];
23
+ var splitPathEnv = (pathEnv) => (pathEnv ?? "").split(":");
24
+
25
+ // src/domain/git-executable.ts
26
+ var GIT_EXECUTABLE_OVERRIDE_ENV = "HOP_GIT";
27
+ var GIT_NOT_FOUND_ERROR_CODE = "HOP_GIT_NOT_FOUND";
28
+ var GIT_NOT_FOUND_HINT = `Install git, or set ${GIT_EXECUTABLE_OVERRIDE_ENV} to its absolute path.`;
29
+ var GIT_FALLBACK_BIN_DIRS = [
30
+ "/opt/homebrew/bin",
31
+ "/usr/local/bin",
32
+ "/usr/bin",
33
+ "/bin"
34
+ ];
35
+ var gitExecutableCandidates = (input) => {
36
+ const override = input.override?.trim() ?? "";
37
+ if (override.length > 0) {
38
+ return [override];
39
+ }
40
+ return absoluteCandidates([...splitPathEnv(input.path), ...GIT_FALLBACK_BIN_DIRS], "git");
41
+ };
42
+ var gitNotFoundMessage = (candidates) => `git executable not found. Looked in: ${candidates.join(", ")}. ${GIT_NOT_FOUND_HINT}`;
43
+
44
+ // src/domain/fatal-error.ts
45
+ var fieldsOf = (error) => typeof error === "object" && error !== null ? error : {};
46
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
47
+ var trimmedStringOf = (value) => typeof value === "string" ? value.trim() : "";
48
+ var isSpawnFailure = (fields) => fields.code === "ENOENT" && typeof fields.syscall === "string" && fields.syscall.startsWith("spawn");
49
+ var isGitFailure = (fields) => typeof fields.code === "number" || typeof fields.signal === "string" && fields.signal !== "";
50
+ var describeFatalError = (error) => {
51
+ const fields = fieldsOf(error);
52
+ if (isSpawnFailure(fields)) {
53
+ const binary = trimmedStringOf(fields.path) || "git";
54
+ return {
55
+ message: `failed to run ${binary}: no such executable. ${GIT_NOT_FOUND_HINT}`,
56
+ exitCode: EXIT_GENERAL_ERROR
57
+ };
58
+ }
59
+ if (isGitFailure(fields)) {
60
+ const stderr = trimmedStringOf(fields.stderr);
61
+ return {
62
+ message: stderr.length > 0 ? stderr : messageOf(error),
63
+ exitCode: EXIT_SAFE_REJECTION
64
+ };
65
+ }
66
+ return { message: messageOf(error), exitCode: EXIT_GENERAL_ERROR };
67
+ };
68
+
69
+ export { parentDir, baseName, absoluteCandidates, splitPathEnv, GIT_EXECUTABLE_OVERRIDE_ENV, GIT_NOT_FOUND_ERROR_CODE, gitExecutableCandidates, gitNotFoundMessage, messageOf, describeFatalError };
@@ -0,0 +1,124 @@
1
+ import {
2
+ EXIT_SAFE_REJECTION,
3
+ fail
4
+ } from "./cli-2bhb9p0x.js";
5
+
6
+ // src/infra/lock.ts
7
+ import { randomUUID } from "node:crypto";
8
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+
11
+ // src/domain/lock-policy.ts
12
+ var canReclaimLock = (input) => {
13
+ if (input.processAlive !== false) {
14
+ return false;
15
+ }
16
+ return input.nowMs - input.startedAtMs > input.ttlMs;
17
+ };
18
+
19
+ // src/infra/lock.ts
20
+ var LOCK_DIR_NAME = "nuthatch-lock";
21
+ var LOCK_INFO_FILE = "info.json";
22
+ var DEFAULT_TTL_MS = 30000;
23
+ var HEARTBEAT_INTERVAL_MS = 5000;
24
+
25
+ class LockHeldError extends Error {
26
+ info;
27
+ constructor(info) {
28
+ super(`Repository is locked by another nuthatch process (pid ${info.pid})`);
29
+ this.name = "LockHeldError";
30
+ this.info = info;
31
+ }
32
+ }
33
+ var isProcessAlive = (pid) => {
34
+ try {
35
+ process.kill(pid, 0);
36
+ return true;
37
+ } catch (error) {
38
+ const { code } = error;
39
+ if (code === "ESRCH") {
40
+ return false;
41
+ }
42
+ if (code === "EPERM") {
43
+ return true;
44
+ }
45
+ return "unknown";
46
+ }
47
+ };
48
+ var readLockInfo = async (lockDir) => {
49
+ try {
50
+ const raw = await readFile(join(lockDir, LOCK_INFO_FILE), "utf8");
51
+ return JSON.parse(raw);
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
56
+ var writeLockInfo = async (lockDir, info) => {
57
+ await writeFile(join(lockDir, LOCK_INFO_FILE), JSON.stringify(info), "utf8");
58
+ };
59
+ var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
60
+ const lockDir = join(commonDir, LOCK_DIR_NAME);
61
+ for (;; ) {
62
+ try {
63
+ await mkdir(lockDir);
64
+ break;
65
+ } catch (error) {
66
+ if (error.code !== "EEXIST") {
67
+ throw error;
68
+ }
69
+ const existing = await readLockInfo(lockDir);
70
+ if (existing === null) {
71
+ throw new LockHeldError({ pid: -1, startedAtMs: 0, token: "unknown" });
72
+ }
73
+ const reclaimable = canReclaimLock({
74
+ processAlive: isProcessAlive(existing.pid),
75
+ startedAtMs: existing.startedAtMs,
76
+ nowMs: Date.now(),
77
+ ttlMs
78
+ });
79
+ if (!reclaimable) {
80
+ throw new LockHeldError(existing);
81
+ }
82
+ await rm(lockDir, { recursive: true, force: true });
83
+ }
84
+ }
85
+ const info = {
86
+ pid: process.pid,
87
+ startedAtMs: Date.now(),
88
+ token: randomUUID()
89
+ };
90
+ await writeLockInfo(lockDir, info);
91
+ const heartbeat = setInterval(() => {
92
+ writeLockInfo(lockDir, { ...info, startedAtMs: Date.now() });
93
+ }, HEARTBEAT_INTERVAL_MS);
94
+ heartbeat.unref();
95
+ return {
96
+ async release() {
97
+ clearInterval(heartbeat);
98
+ await rm(lockDir, { recursive: true, force: true });
99
+ }
100
+ };
101
+ };
102
+ var acquireUnlessHeld = async (commonDir) => {
103
+ try {
104
+ return await acquireRepoLock(commonDir);
105
+ } catch (error) {
106
+ if (error instanceof LockHeldError) {
107
+ return error;
108
+ }
109
+ throw error;
110
+ }
111
+ };
112
+ var withRepoLock = async (commonDir, mutate) => {
113
+ const lock = await acquireUnlessHeld(commonDir);
114
+ if (lock instanceof LockHeldError) {
115
+ return fail(EXIT_SAFE_REJECTION, lock.message);
116
+ }
117
+ try {
118
+ return await mutate();
119
+ } finally {
120
+ await lock.release();
121
+ }
122
+ };
123
+
124
+ export { withRepoLock };