@xuhaojun/githunk 0.2.0 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +23 -2
  2. package/dist/githunk.js +192 -34
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -5,19 +5,40 @@ A review-first Git TUI that mixes [lazygit](https://github.com/jesseduffield/laz
5
5
 
6
6
  ## Install
7
7
 
8
+ No Node.js required — githunk ships as a standalone binary:
9
+
10
+ ```sh
11
+ curl -fsSL https://raw.githubusercontent.com/XuHaoJun/githunk/main/install.sh | sh
12
+ ```
13
+
14
+ This installs the newest release into `~/.local/bin` (or `$XDG_BIN_HOME`, or `$GITHUNK_INSTALL_DIR`), verifying its checksum first. Pin a version with `sh -s -- 0.2.0`, or pass `--no-modify-path` to skip shell-startup PATH wiring. macOS and Linux only.
15
+
16
+ Alternative via npm (needs Node.js 26.1.0 or newer just to install):
17
+
8
18
  ```sh
9
19
  npm install --global @xuhaojun/githunk
10
20
  ```
11
21
 
12
22
  ## Requirements
13
23
 
14
- - Node.js 26.1.0 or newer
15
24
  - Git
16
25
  - A terminal with interactive TUI support
17
26
 
18
27
  `gh` is optional. When it is installed and authenticated, githunk can show GitHub pull-request status; local Git review works without it.
19
28
 
20
- The installed launcher enables Node's experimental FFI support automatically for OpenTUI.
29
+ The launcher prefers the prebuilt binary. On platforms without one it falls back to the Node.js bundle, which needs Node.js 26.1.0 or newer; the launcher enables Node's experimental FFI support automatically for OpenTUI.
30
+
31
+ ## Update
32
+
33
+ ```sh
34
+ githunk update # newest release
35
+ githunk update 0.3.0 # a specific version
36
+ githunk update --check # report without installing
37
+ ```
38
+
39
+ Standalone installs replace their own binary after checksum verification. npm installs update through npm instead: `npm update --global @xuhaojun/githunk`.
40
+
41
+ Installer knobs: `GITHUNK_VERSION` pins the version, `GITHUNK_INSTALL_DIR` overrides the install directory, `GITHUNK_NO_MODIFY_PATH=1` skips shell-startup PATH wiring.
21
42
 
22
43
  ## Usage
23
44
 
package/dist/githunk.js CHANGED
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { chmodSync, cpSync, mkdtempSync, renameSync, rmSync } from "node:fs";
6
+ import { writeFile } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join as join6 } from "node:path";
9
+
3
10
  // src/cli/args.ts
4
11
  import { Command, CommanderError } from "commander";
5
12
  // package.json
6
13
  var package_default = {
7
14
  name: "@xuhaojun/githunk",
8
- version: "0.2.0",
15
+ version: "0.3.0",
9
16
  description: "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
10
17
  type: "module",
11
18
  bin: {
@@ -80,8 +87,9 @@ var cliVersion = typeof package_default.version === "string" && package_default.
80
87
  function parseCliArgs(argv) {
81
88
  let stdout = "";
82
89
  let stderr = "";
90
+ let update;
83
91
  const program = new Command;
84
- program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().configureOutput({
92
+ program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().action(() => {}).configureOutput({
85
93
  writeOut: (text) => {
86
94
  stdout += text;
87
95
  },
@@ -89,6 +97,12 @@ function parseCliArgs(argv) {
89
97
  stderr += text;
90
98
  }
91
99
  });
100
+ program.command("update").description("update githunk to the newest (or a given) release").argument("[version]", "version to install; the newest release when omitted").option("--check", "report the installed and available versions without installing").action((version, options2) => {
101
+ update = {
102
+ ...version === undefined ? {} : { version },
103
+ check: options2.check ?? false
104
+ };
105
+ });
92
106
  try {
93
107
  program.parse([...argv], { from: "user" });
94
108
  } catch (error) {
@@ -102,12 +116,91 @@ function parseCliArgs(argv) {
102
116
  }
103
117
  throw error;
104
118
  }
119
+ if (update !== undefined)
120
+ return { kind: "update", ...update };
105
121
  const options = program.opts();
106
122
  const positional = program.args[0];
107
123
  const startDirectory = options.path ?? positional;
108
124
  return startDirectory === undefined ? { kind: "start" } : { kind: "start", startDirectory };
109
125
  }
110
126
 
127
+ // src/cli/update.ts
128
+ import { createHash } from "node:crypto";
129
+ import { basename, join } from "node:path";
130
+ function assetNameFor(platform, arch) {
131
+ const osToken = platform === "linux" ? "linux" : platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : null;
132
+ const archToken = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : null;
133
+ if (osToken === null || archToken === null)
134
+ return null;
135
+ if (osToken === "windows" && archToken !== "x64")
136
+ return null;
137
+ return `githunk-${osToken}-${archToken}.tar.gz`;
138
+ }
139
+ function normalizeVersion(version) {
140
+ return version.trim().replace(/^v/, "");
141
+ }
142
+ function compareVersions(left, right) {
143
+ const parts = (version) => {
144
+ const [major = "0", minor = "0", patch = "0"] = normalizeVersion(version).split(".");
145
+ return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
146
+ };
147
+ const [aMajor, aMinor, aPatch] = parts(left);
148
+ const [bMajor, bMinor, bPatch] = parts(right);
149
+ return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
150
+ }
151
+ function isSelfManagedBinary(executablePath) {
152
+ const base = basename(executablePath);
153
+ return base === "githunk" || base === "githunk.exe";
154
+ }
155
+ function verifyChecksum(tarball, checksums, asset) {
156
+ const actual = createHash("sha256").update(tarball).digest("hex");
157
+ const line = checksums.split(`
158
+ `).map((entry) => entry.trim().split(/\s+/)).find((fields) => fields[fields.length - 1] === asset);
159
+ const expected = line?.[0];
160
+ if (expected === undefined || expected === "" || expected.toLowerCase() !== actual) {
161
+ throw new Error(`checksum mismatch for ${asset}`);
162
+ }
163
+ }
164
+ async function applyUpdate(target, asset, env) {
165
+ await env.withTempDir(async (dir) => {
166
+ const { tarball, checksums } = await env.fetchAsset(`v${target}`, asset);
167
+ verifyChecksum(tarball, checksums, asset);
168
+ const archivePath = join(dir, asset);
169
+ await env.writeFile(archivePath, tarball);
170
+ await env.extractTarball(archivePath, dir);
171
+ await env.writeBinary(env.stagedBinary(dir), env.executablePath);
172
+ });
173
+ }
174
+ async function runUpdate(request, env) {
175
+ try {
176
+ if (!isSelfManagedBinary(env.executablePath)) {
177
+ return {
178
+ exitCode: 1,
179
+ message: "githunk was installed via npm — update it with `npm update --global @xuhaojun/githunk`"
180
+ };
181
+ }
182
+ const asset = assetNameFor(env.platform, env.arch);
183
+ if (asset === null) {
184
+ return {
185
+ exitCode: 1,
186
+ message: `no prebuilt githunk binary ships for ${env.platform}-${env.arch} — install with \`npm install -g @xuhaojun/githunk\``
187
+ };
188
+ }
189
+ const current = normalizeVersion(env.installedVersion());
190
+ const target = normalizeVersion(request.version ?? normalizeVersion(await env.fetchReleaseTag()));
191
+ if (compareVersions(target, current) === 0) {
192
+ return { exitCode: 0, message: `githunk ${current} is already up to date` };
193
+ }
194
+ if (request.check) {
195
+ return { exitCode: 0, message: `update available: ${current} -> ${target}` };
196
+ }
197
+ await applyUpdate(target, asset, env);
198
+ return { exitCode: 0, message: `updated githunk ${current} -> ${target}` };
199
+ } catch (error) {
200
+ return { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
201
+ }
202
+ }
203
+
111
204
  // src/main.ts
112
205
  import { resolve as resolve4 } from "node:path";
113
206
  import { createCliRenderer } from "@opentui/core";
@@ -991,12 +1084,12 @@ function reviewStateFor(record, currentFingerprint) {
991
1084
  }
992
1085
 
993
1086
  // src/review/working-tree-fingerprint.ts
994
- import { createHash } from "node:crypto";
1087
+ import { createHash as createHash2 } from "node:crypto";
995
1088
  function utf8(value) {
996
1089
  return new TextEncoder().encode(value);
997
1090
  }
998
1091
  function sha256Tuple(parts) {
999
- const hash = createHash("sha256");
1092
+ const hash = createHash2("sha256");
1000
1093
  for (const part of parts) {
1001
1094
  const bytes = utf8(part);
1002
1095
  const length = Buffer.allocUnsafe(4);
@@ -1029,7 +1122,7 @@ function fingerprintWorkingTreeFile(target, filePatch) {
1029
1122
 
1030
1123
  // src/storage/local-state-file.ts
1031
1124
  import { mkdir, open, rename, stat, unlink, lstat, link, readFile } from "node:fs/promises";
1032
- import { dirname, isAbsolute, join, resolve } from "node:path";
1125
+ import { dirname, isAbsolute, join as join2, resolve } from "node:path";
1033
1126
  import { randomUUID } from "node:crypto";
1034
1127
  async function assertNoSymlinkInPath(path, pathKind) {
1035
1128
  const absolute = resolve(path);
@@ -1059,7 +1152,7 @@ class LocalStateFile {
1059
1152
  this.pathKind = options.pathKind ?? "state";
1060
1153
  }
1061
1154
  get path() {
1062
- return this.resolvedPath ?? join(this.runner.cwd, ".git", this.relativePath);
1155
+ return this.resolvedPath ?? join2(this.runner.cwd, ".git", this.relativePath);
1063
1156
  }
1064
1157
  async resolvePath() {
1065
1158
  if (this.resolvedPath !== undefined)
@@ -1067,7 +1160,7 @@ class LocalStateFile {
1067
1160
  const output = (await this.runner.run(["rev-parse", "--git-path", this.relativePath], { readOnly: true })).stdout.trim();
1068
1161
  if (output.length === 0)
1069
1162
  throw new Error(`git returned an empty path for ${this.relativePath}`);
1070
- this.resolvedPath = isAbsolute(output) ? output : join(this.runner.cwd, output);
1163
+ this.resolvedPath = isAbsolute(output) ? output : join2(this.runner.cwd, output);
1071
1164
  return this.resolvedPath;
1072
1165
  }
1073
1166
  async readText() {
@@ -1976,7 +2069,7 @@ function validateUpstream(upstream) {
1976
2069
  throw new Error("invalid upstream choice");
1977
2070
  }
1978
2071
  }
1979
- async function fetch(runner, remote, options = {}) {
2072
+ async function fetch2(runner, remote, options = {}) {
1980
2073
  await runner.run(remote === undefined ? ["fetch"] : ["fetch", remote], options.background === true ? { dontLog: true } : { streamOutput: true });
1981
2074
  }
1982
2075
  async function pull(runner, options = {}) {
@@ -2185,7 +2278,7 @@ async function listReflog(runner, options = {}) {
2185
2278
 
2186
2279
  // src/git/worktrees.ts
2187
2280
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
2188
- import { dirname as dirname2, join as join2 } from "node:path";
2281
+ import { dirname as dirname2, join as join3 } from "node:path";
2189
2282
  function finalizeEntry(entry) {
2190
2283
  return {
2191
2284
  path: entry.path,
@@ -2320,11 +2413,11 @@ async function readTrimmedFile(path) {
2320
2413
  }
2321
2414
  async function inProgressBranch(gitDir) {
2322
2415
  for (const directory of ["rebase-merge", "rebase-apply"]) {
2323
- const headName = await readTrimmedFile(join2(gitDir, directory, "head-name"));
2416
+ const headName = await readTrimmedFile(join3(gitDir, directory, "head-name"));
2324
2417
  if (headName !== undefined)
2325
2418
  return headName.replace(/^refs\/heads\//, "");
2326
2419
  }
2327
- return await readTrimmedFile(join2(gitDir, "BISECT_START"));
2420
+ return await readTrimmedFile(join3(gitDir, "BISECT_START"));
2328
2421
  }
2329
2422
  async function listWorktrees(runner) {
2330
2423
  const repositoryPaths = await resolveRepositoryPaths(runner);
@@ -2391,7 +2484,7 @@ function worktreeRemovalRequiresForce(error) {
2391
2484
 
2392
2485
  // src/git/submodules.ts
2393
2486
  import { readFile as readFile3 } from "node:fs/promises";
2394
- import { join as join3, resolve as resolve2 } from "node:path";
2487
+ import { join as join4, resolve as resolve2 } from "node:path";
2395
2488
 
2396
2489
  // src/domain/submodule.ts
2397
2490
  function submoduleFullName(submodule) {
@@ -2446,7 +2539,7 @@ function parseGitModules(raw) {
2446
2539
  }
2447
2540
  async function readGitModules(directory) {
2448
2541
  try {
2449
- return await readFile3(join3(directory, ".gitmodules"), "utf8");
2542
+ return await readFile3(join4(directory, ".gitmodules"), "utf8");
2450
2543
  } catch (error) {
2451
2544
  if (error instanceof Error && "code" in error) {
2452
2545
  const code = error.code;
@@ -2457,7 +2550,7 @@ async function readGitModules(directory) {
2457
2550
  }
2458
2551
  }
2459
2552
  async function collectSubmodules(worktreePath, parentModule, visited) {
2460
- const directory = parentModule === undefined ? worktreePath : join3(worktreePath, submoduleFullPath(parentModule));
2553
+ const directory = parentModule === undefined ? worktreePath : join4(worktreePath, submoduleFullPath(parentModule));
2461
2554
  const resolved = resolve2(directory);
2462
2555
  if (visited.has(resolved))
2463
2556
  return [];
@@ -2941,7 +3034,7 @@ class AppController {
2941
3034
  return;
2942
3035
  if (options.background !== true)
2943
3036
  this.logAction(LOG_ACTIONS.fetch);
2944
- await this.runMutation(() => this.requireRunnerOperation((runner) => fetch(runner, remote, options)));
3037
+ await this.runMutation(() => this.requireRunnerOperation((runner) => fetch2(runner, remote, options)));
2945
3038
  }
2946
3039
  async pull(options = {}) {
2947
3040
  if (!this.ensureWorkingTreeMutation())
@@ -5935,7 +6028,7 @@ function commitGraphRows(commits, getColor) {
5935
6028
  }
5936
6029
 
5937
6030
  // src/ui/author-style.ts
5938
- import { createHash as createHash2 } from "node:crypto";
6031
+ import { createHash as createHash3 } from "node:crypto";
5939
6032
  var initialsCache = new Map;
5940
6033
  var colorCache = new Map;
5941
6034
  function randInt(bytes, max) {
@@ -5976,7 +6069,7 @@ function authorColor(authorName) {
5976
6069
  const cached = colorCache.get(authorName);
5977
6070
  if (cached !== undefined)
5978
6071
  return cached;
5979
- const hash = new Uint8Array(createHash2("md5").update(authorName).digest());
6072
+ const hash = new Uint8Array(createHash3("md5").update(authorName).digest());
5980
6073
  const color = hslToHex(randFloat(hash.slice(0, 4)) * 360, 0.6 + 0.4 * randFloat(hash.slice(4, 8)), 0.4 + 0.2 * randFloat(hash.slice(8, 12)));
5981
6074
  colorCache.set(authorName, color);
5982
6075
  return color;
@@ -15469,7 +15562,7 @@ async function loadRefsSnapshot(runner) {
15469
15562
  }
15470
15563
 
15471
15564
  // src/git/editor.ts
15472
- import { join as join4, basename } from "node:path";
15565
+ import { join as join5, basename as basename2 } from "node:path";
15473
15566
  function standardTerminalPreset(editor) {
15474
15567
  return {
15475
15568
  edit: `${editor} -- {{filename}}`,
@@ -15562,13 +15655,13 @@ function resolvePlaceholders(template, values) {
15562
15655
  }
15563
15656
  function guessEditorBase(env, gitEditor) {
15564
15657
  if (env.GITHUNK_EDITOR !== undefined && env.GITHUNK_EDITOR.trim().length > 0)
15565
- return basename(env.GITHUNK_EDITOR.split(" ")[0].trim());
15658
+ return basename2(env.GITHUNK_EDITOR.split(" ")[0].trim());
15566
15659
  if (gitEditor !== undefined && gitEditor.trim().length > 0)
15567
- return basename(gitEditor.split(" ")[0].trim());
15660
+ return basename2(gitEditor.split(" ")[0].trim());
15568
15661
  for (const key of ["GIT_EDITOR", "VISUAL", "EDITOR"]) {
15569
15662
  const value = env[key];
15570
15663
  if (value !== undefined && value.trim().length > 0)
15571
- return basename(value.split(" ")[0].trim());
15664
+ return basename2(value.split(" ")[0].trim());
15572
15665
  }
15573
15666
  return "vi";
15574
15667
  }
@@ -15607,7 +15700,7 @@ async function resolveEditCommand(files, options = {}) {
15607
15700
  function absolutePath(repoRoot, relativePath) {
15608
15701
  if (relativePath.startsWith("/"))
15609
15702
  return relativePath;
15610
- return join4(repoRoot, relativePath);
15703
+ return join5(repoRoot, relativePath);
15611
15704
  }
15612
15705
 
15613
15706
  // src/app/create-app.ts
@@ -15615,7 +15708,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
15615
15708
 
15616
15709
  // src/app/index-watcher.ts
15617
15710
  import { watch, statSync } from "node:fs";
15618
- import { basename as basename2, dirname as dirname3 } from "node:path";
15711
+ import { basename as basename3, dirname as dirname3 } from "node:path";
15619
15712
  var DEFAULT_INDEX_EVENT_DEBOUNCE_MS = 50;
15620
15713
  var BUSY_RETRY_MS = 50;
15621
15714
  function fingerprint(path) {
@@ -15645,7 +15738,7 @@ class IndexWatcher {
15645
15738
  stopped = true;
15646
15739
  constructor(options) {
15647
15740
  this.options = options;
15648
- this.indexName = basename2(options.indexPath);
15741
+ this.indexName = basename3(options.indexPath);
15649
15742
  this.lockName = `${this.indexName}.lock`;
15650
15743
  this.debounceMs = options.debounceMs ?? DEFAULT_INDEX_EVENT_DEBOUNCE_MS;
15651
15744
  this.baseline = undefined;
@@ -16322,7 +16415,7 @@ function reviewHelp(focus, state) {
16322
16415
  }
16323
16416
 
16324
16417
  // src/ui/review-workspace/review-sidebar.ts
16325
- import { basename as basename3, dirname as dirname4 } from "node:path/posix";
16418
+ import { basename as basename4, dirname as dirname4 } from "node:path/posix";
16326
16419
  function normalizeDiffPath(p) {
16327
16420
  return p?.replace(/[\r\n]+$/u, "");
16328
16421
  }
@@ -16351,10 +16444,10 @@ function sidebarFileName(file) {
16351
16444
  const path = formatTerminalPath(normalizeDiffPath(file.path) ?? file.path);
16352
16445
  const previousPath = file.previousPath ? formatTerminalPath(normalizeDiffPath(file.previousPath) ?? file.previousPath) : undefined;
16353
16446
  if (previousPath === undefined || previousPath === path) {
16354
- return basename3(path);
16447
+ return basename4(path);
16355
16448
  }
16356
- const previousName = basename3(previousPath);
16357
- const nextName = basename3(path);
16449
+ const previousName = basename4(previousPath);
16450
+ const nextName = basename4(path);
16358
16451
  return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
16359
16452
  }
16360
16453
  function formatSidebarStat(prefix, value, truncated = false) {
@@ -17600,9 +17693,9 @@ function syntaxThemeForAppearance(appearance) {
17600
17693
  import { parsePatchFiles as parsePatchFiles2 } from "@pierre/diffs";
17601
17694
 
17602
17695
  // src/review/core/identity.ts
17603
- import { createHash as createHash3 } from "node:crypto";
17696
+ import { createHash as createHash4 } from "node:crypto";
17604
17697
  function sha256Tuple2(parts) {
17605
- const hash = createHash3("sha256");
17698
+ const hash = createHash4("sha256");
17606
17699
  for (const part of parts) {
17607
17700
  const bytes = new TextEncoder().encode(part);
17608
17701
  const length = Buffer.alloc(4);
@@ -21924,7 +22017,7 @@ class AppScreenController {
21924
22017
  }
21925
22018
 
21926
22019
  // src/ui/review-workspace/controller.ts
21927
- import { createHash as createHash5 } from "node:crypto";
22020
+ import { createHash as createHash6 } from "node:crypto";
21928
22021
 
21929
22022
  // src/review/core/state.ts
21930
22023
  function createInitialReviewState(document) {
@@ -23354,12 +23447,12 @@ function persistedFromReviewState(state) {
23354
23447
  }
23355
23448
 
23356
23449
  // src/review/storage/review-artifact-store.ts
23357
- import { createHash as createHash4 } from "node:crypto";
23450
+ import { createHash as createHash5 } from "node:crypto";
23358
23451
  function artifactRelativePath(reviewId, artifactId) {
23359
23452
  return `githunk/reviews/${reviewId}/${artifactId}.json`;
23360
23453
  }
23361
23454
  function artifactDigest(text) {
23362
- return createHash4("sha256").update(text, "utf8").digest("hex");
23455
+ return createHash5("sha256").update(text, "utf8").digest("hex");
23363
23456
  }
23364
23457
  function artifactText(artifact) {
23365
23458
  return serializeReviewArtifactV1(artifact) + `
@@ -23868,7 +23961,7 @@ class ReviewWorkspaceController {
23868
23961
  artifactIdFromMarker = marker.artifactId;
23869
23962
  const raw = await this.artifactStore.readRaw(reviewId, marker.artifactId);
23870
23963
  if (raw !== undefined) {
23871
- const digest = createHash5("sha256").update(raw, "utf8").digest("hex");
23964
+ const digest = createHash6("sha256").update(raw, "utf8").digest("hex");
23872
23965
  if (digest === marker.digest) {
23873
23966
  const parsed = JSON.parse(raw);
23874
23967
  const res = parseReviewArtifactV1(parsed);
@@ -24892,6 +24985,64 @@ ${detail}
24892
24985
  if (false) {}
24893
24986
 
24894
24987
  // src/cli.ts
24988
+ var RELEASES_API = "https://api.github.com/repos/XuHaoJun/githunk/releases/latest";
24989
+ var DOWNLOAD_BASE = "https://github.com/XuHaoJun/githunk/releases/download";
24990
+ async function fetchText(url) {
24991
+ const response = await fetch(url);
24992
+ if (!response.ok)
24993
+ throw new Error(`request failed: ${url} (${response.status})`);
24994
+ return response.text();
24995
+ }
24996
+ async function fetchBytes(url) {
24997
+ const response = await fetch(url);
24998
+ if (!response.ok)
24999
+ throw new Error(`request failed: ${url} (${response.status})`);
25000
+ return new Uint8Array(await response.arrayBuffer());
25001
+ }
25002
+ function productionUpdateEnv() {
25003
+ return {
25004
+ executablePath: process.execPath,
25005
+ platform: process.platform,
25006
+ arch: process.arch,
25007
+ installedVersion: () => {
25008
+ const proc = spawnSync(process.execPath, ["--version"], { encoding: "utf8" });
25009
+ if (proc.status !== 0)
25010
+ throw new Error("could not read the installed version");
25011
+ return proc.stdout.trim();
25012
+ },
25013
+ fetchReleaseTag: async () => {
25014
+ const payload = JSON.parse(await fetchText(RELEASES_API));
25015
+ const tag = typeof payload === "object" && payload !== null && "tag_name" in payload ? payload.tag_name : undefined;
25016
+ if (typeof tag !== "string" || tag === "")
25017
+ throw new Error("could not read the newest release");
25018
+ return tag;
25019
+ },
25020
+ fetchAsset: async (tag, asset) => ({
25021
+ tarball: await fetchBytes(`${DOWNLOAD_BASE}/${tag}/${asset}`),
25022
+ checksums: await fetchText(`${DOWNLOAD_BASE}/${tag}/SHA256SUMS`)
25023
+ }),
25024
+ withTempDir: async (run) => {
25025
+ const dir = mkdtempSync(join6(tmpdir(), "githunk-update-"));
25026
+ try {
25027
+ return await run(dir);
25028
+ } finally {
25029
+ rmSync(dir, { recursive: true, force: true });
25030
+ }
25031
+ },
25032
+ writeFile: (path, data) => writeFile(path, data),
25033
+ extractTarball: async (archivePath, destDir) => {
25034
+ const proc = spawnSync("tar", ["-xzf", archivePath, "-C", destDir]);
25035
+ if (proc.status !== 0)
25036
+ throw new Error("could not extract the release archive (need tar on PATH)");
25037
+ },
25038
+ stagedBinary: (dir) => join6(dir, `githunk-${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "arm64" : "x64"}`, process.platform === "win32" ? "githunk.exe" : "githunk"),
25039
+ writeBinary: async (stagedPath, destPath) => {
25040
+ cpSync(stagedPath, `${destPath}.new`);
25041
+ chmodSync(`${destPath}.new`, 493);
25042
+ renameSync(`${destPath}.new`, destPath);
25043
+ }
25044
+ };
25045
+ }
24895
25046
  var result = parseCliArgs(process.argv.slice(2));
24896
25047
  if (result.kind === "help" || result.kind === "version") {
24897
25048
  process.stdout.write(result.text.endsWith(`
@@ -24903,6 +25054,13 @@ if (result.kind === "help" || result.kind === "version") {
24903
25054
  `) ? result.message : `${result.message}
24904
25055
  `);
24905
25056
  process.exitCode = result.exitCode;
25057
+ } else if (result.kind === "update") {
25058
+ const outcome = await runUpdate({ ...result.version === undefined ? {} : { version: result.version }, check: result.check }, productionUpdateEnv());
25059
+ const stream = outcome.exitCode === 0 ? process.stdout : process.stderr;
25060
+ stream.write(outcome.message.endsWith(`
25061
+ `) ? outcome.message : `${outcome.message}
25062
+ `);
25063
+ process.exitCode = outcome.exitCode;
24906
25064
  } else {
24907
25065
  process.exitCode = await startApp(result.startDirectory === undefined ? {} : { startDirectory: result.startDirectory });
24908
25066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuhaojun/githunk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
5
5
  "bin": {
6
6
  "githunk": "bin/githunk.js"
@@ -38,11 +38,11 @@
38
38
  "zod": "^4.4.3"
39
39
  },
40
40
  "optionalDependencies": {
41
- "@xuhaojun/githunk-darwin-arm64": "0.2.0",
42
- "@xuhaojun/githunk-darwin-x64": "0.2.0",
43
- "@xuhaojun/githunk-linux-arm64": "0.2.0",
44
- "@xuhaojun/githunk-linux-x64": "0.2.0",
45
- "@xuhaojun/githunk-windows-x64": "0.2.0"
41
+ "@xuhaojun/githunk-darwin-arm64": "0.3.0",
42
+ "@xuhaojun/githunk-darwin-x64": "0.3.0",
43
+ "@xuhaojun/githunk-linux-arm64": "0.3.0",
44
+ "@xuhaojun/githunk-linux-x64": "0.3.0",
45
+ "@xuhaojun/githunk-windows-x64": "0.3.0"
46
46
  },
47
47
  "license": "MIT",
48
48
  "publishConfig": {