@profullstack/g1tz 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Profullstack, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # g1tz
2
+
3
+ A git TUI that shows you the repository, not a menu of git commands.
4
+
5
+ ```
6
+ bunx @profullstack/g1tz # the repository you are standing in
7
+ bunx @profullstack/g1tz ~/proj # somewhere else
8
+ npm i -g @profullstack/g1tz # then just g1tz
9
+ ```
10
+
11
+ The package is scoped because npm refuses the bare name ("too similar to got, gts"); the command it installs is still `g1tz`.
12
+
13
+ ```
14
+ g1tz main origin/main ↓1 ~/hqtui Tab panes Space stage q quit
15
+ ╭─ Files (3) ───────────────────────────╮ ╭─ Diff ─────────────────────────────────────────────────────────╮
16
+ │ M src/a.ts │ │ @@ -0,0 +1,8 @@ │
17
+ │ M src/b.ts │ │ +export interface Span { │
18
+ │ ?? new.ts │ │ + text: string; │
19
+ ╰───────────────────────────────────────╯ │ +} │
20
+ ╭─ Branches (39) ───────────────────────╮ │ const style: Style = { fg: options.fg }; │
21
+ │ * main ↓1 │ │ │
22
+ │ spans-richtext │ │ │
23
+ ╰───────────────────────────────────────╯ │ │
24
+ ╭─ Log (78) ────────────────────────────╮ │ │
25
+ │ e28fec7 Finish widget parity 2h │ │ │
26
+ ╰───────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────╯
27
+ Tab files Space Stage ↑↓ Move r Reload q Quit
28
+ ```
29
+
30
+ Files, branches and log down the left. The diff on the right follows whatever you have selected — a file's changes, or a commit's patch.
31
+
32
+ ## Keys
33
+
34
+ | Key | Does |
35
+ |---|---|
36
+ | `Tab` | Cycle files → branches → log |
37
+ | `↑` `↓` `PgUp` `PgDn` | Move the selection |
38
+ | `Space` | Stage the selected file, or unstage it if it is fully staged |
39
+ | `←` `→` | Scroll the diff |
40
+ | `r` | Re-read the repository |
41
+ | `p` | Pulse: what moved over a period (below) |
42
+ | `q` | Quit |
43
+
44
+ ## Pulse
45
+
46
+ `p` flips to Pulse: what moved in this repository over a period, the way GitHub's Insights > Pulse tab shows it, but for the repository you are standing in and without leaving the terminal.
47
+
48
+ ```
49
+ bunx @profullstack/g1tz pulse # start on the Pulse screen, last week
50
+ bunx @profullstack/g1tz pulse --range month # day, week, month, quarter, year, all
51
+ ```
52
+
53
+ Three sources, each optional beyond the first:
54
+
55
+ - **git**, always, offline: commits and authors on the branch and across every branch (merges counted separately, as GitHub does), the net change to the tree since the period began (`git diff` from the first parent of the oldest commit in range, with `-M` so a rename is a rename), commits per hour, day, week or month, the branches that saw commits and the tags that were created.
56
+ - **GitHub**, when `origin` is on github.com and `gh` is logged in: pull requests opened, merged and closed, issues opened and closed, releases, and new stars for the period. Pages are walked newest first under a small budget; when the budget runs out the counts are floors and the status bar says so. Nothing here sees a token; `gh` holds the login.
57
+ - **Traffic**, when a [gh-pulse](https://github.com/profullstack/cli-tools) report exists on the machine (`~/.local/share/gh-pulse`, or `GH_PULSE_DATA`): views and clones for the last fourteen days, referrers and popular paths. GitHub only serves those through its traffic API and only for fourteen days; gh-pulse keeps them.
58
+
59
+ | Key | Does |
60
+ |---|---|
61
+ | `d` `w` `m` `q` `y` `a` | Pick the range: day, week, month, quarter, year, all time. The range buttons are clickable too. |
62
+ | `↑` `↓` `PgUp` `PgDn` | Scroll the files |
63
+ | `r` | Read it again |
64
+ | `p` `Esc` | Back to the repository, where `q` quits |
65
+ | `Ctrl+C` | Quit |
66
+
67
+ ## How it reads git
68
+
69
+ By shelling out, using porcelain formats only, with `-z` wherever a path could contain a newline. Parsing output meant for humans is how a TUI ends up corrupting someone's working tree.
70
+
71
+ `git` runs with `GIT_PAGER=cat`, `GIT_EDITOR=true` and `GIT_OPTIONAL_LOCKS=0`, so nothing it invokes can take the terminal away from the interface or fight the editor you already have open.
72
+
73
+ Untracked files are listed with `--untracked-files=all`. The default collapses a new directory to a single `? sub/` entry, and you cannot stage or diff a directory.
74
+
75
+ ## Status
76
+
77
+ Early, and deliberately read-mostly. It reads the repository and stages files. It does **not** yet commit, push, pull, branch, rebase, stash, or discard — the destructive half is the half worth getting right slowly.
78
+
79
+ Known rough edges:
80
+
81
+ - The diff is coloured a line at a time, one `text()` call per line, because per-line styling is all the library offers today. Styled spans land in `@profullstack/hqtui` 0.3.0 ([hqtui#60](https://github.com/profullstack/hqtui/issues/60)) and will make word-level diff highlighting possible.
82
+ - No hunk staging. That wants an interactive diff pane.
83
+
84
+ ## Built with
85
+
86
+ [hqtui](https://hqtui.com) — the terminal UI library. g1tz exists partly to keep hqtui honest: a real application finds the gaps a widget gallery does not.
87
+
88
+ ## Licence
89
+
90
+ MIT
package/bin/g1tz.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/main.js";
package/dist/diff.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Diff rendering, as spans.
3
+ *
4
+ * A whole line coloured green tells you it changed. Colouring only the words
5
+ * that actually differ tells you *what* changed, which is the thing you were
6
+ * squinting at the line to work out.
7
+ */
8
+ import type { SpanLine } from "@profullstack/hqtui";
9
+ export interface DiffPalette {
10
+ add: number;
11
+ remove: number;
12
+ hunk: number;
13
+ meta: number;
14
+ context: number;
15
+ /** Background emphasis for the words that actually differ. */
16
+ addEmphasis: number;
17
+ removeEmphasis: number;
18
+ }
19
+ /** Split into words and the separators between them, both kept. */
20
+ export declare function words(line: string): string[];
21
+ export interface Segment {
22
+ text: string;
23
+ changed: boolean;
24
+ }
25
+ /**
26
+ * The differing middle of two lines, found by trimming the common prefix and
27
+ * suffix. Not a full Myers diff: for the single-line case a prefix/suffix trim
28
+ * is what people actually read, and it cannot produce the confetti that a
29
+ * token-level LCS gives you on a reformatted line.
30
+ */
31
+ export declare function intraLine(before: string, after: string): {
32
+ before: Segment[];
33
+ after: Segment[];
34
+ };
35
+ /**
36
+ * Pair each removed line with the added line that replaced it.
37
+ *
38
+ * Only runs of equal length are paired. An unequal run is a genuine insertion
39
+ * or deletion rather than an edit, and pretending otherwise produces word
40
+ * highlighting that points at the wrong thing.
41
+ */
42
+ export declare function pairRuns(lines: string[]): Map<number, number>;
43
+ /** A whole diff as span lines, with the changed words emphasised. */
44
+ export declare function highlightDiff(lines: string[], palette: DiffPalette): SpanLine[];
package/dist/diff.js ADDED
@@ -0,0 +1,112 @@
1
+ import { diffLineKind } from "./git.js";
2
+ /** Split into words and the separators between them, both kept. */
3
+ export function words(line) {
4
+ return line.split(/(\W)/).filter((part) => part !== "");
5
+ }
6
+ /**
7
+ * The differing middle of two lines, found by trimming the common prefix and
8
+ * suffix. Not a full Myers diff: for the single-line case a prefix/suffix trim
9
+ * is what people actually read, and it cannot produce the confetti that a
10
+ * token-level LCS gives you on a reformatted line.
11
+ */
12
+ export function intraLine(before, after) {
13
+ const a = words(before);
14
+ const b = words(after);
15
+ let head = 0;
16
+ while (head < a.length && head < b.length && a[head] === b[head])
17
+ head++;
18
+ let tail = 0;
19
+ while (tail < a.length - head &&
20
+ tail < b.length - head &&
21
+ a[a.length - 1 - tail] === b[b.length - 1 - tail])
22
+ tail++;
23
+ const build = (parts) => {
24
+ const middle = parts.slice(head, parts.length - tail).join("");
25
+ const out = [];
26
+ const prefix = parts.slice(0, head).join("");
27
+ const suffix = parts.slice(parts.length - tail).join("");
28
+ if (prefix)
29
+ out.push({ text: prefix, changed: false });
30
+ if (middle)
31
+ out.push({ text: middle, changed: true });
32
+ if (suffix)
33
+ out.push({ text: suffix, changed: false });
34
+ return out;
35
+ };
36
+ return { before: build(a), after: build(b) };
37
+ }
38
+ /**
39
+ * Pair each removed line with the added line that replaced it.
40
+ *
41
+ * Only runs of equal length are paired. An unequal run is a genuine insertion
42
+ * or deletion rather than an edit, and pretending otherwise produces word
43
+ * highlighting that points at the wrong thing.
44
+ */
45
+ export function pairRuns(lines) {
46
+ const pairs = new Map();
47
+ let i = 0;
48
+ while (i < lines.length) {
49
+ if (!isRemoval(lines[i])) {
50
+ i++;
51
+ continue;
52
+ }
53
+ let removals = 0;
54
+ while (i + removals < lines.length && isRemoval(lines[i + removals]))
55
+ removals++;
56
+ let additions = 0;
57
+ while (i + removals + additions < lines.length &&
58
+ isAddition(lines[i + removals + additions]))
59
+ additions++;
60
+ if (removals === additions && removals > 0) {
61
+ for (let k = 0; k < removals; k++)
62
+ pairs.set(i + k, i + removals + k);
63
+ }
64
+ i += removals + additions;
65
+ }
66
+ return pairs;
67
+ }
68
+ function isRemoval(line) {
69
+ return line.startsWith("-") && !line.startsWith("---");
70
+ }
71
+ function isAddition(line) {
72
+ return line.startsWith("+") && !line.startsWith("+++");
73
+ }
74
+ /** A whole diff as span lines, with the changed words emphasised. */
75
+ export function highlightDiff(lines, palette) {
76
+ const pairs = pairRuns(lines);
77
+ const partnerOf = new Map();
78
+ for (const [from, to] of pairs)
79
+ partnerOf.set(to, from);
80
+ const colorFor = (line) => {
81
+ switch (diffLineKind(line)) {
82
+ case "success": return palette.add;
83
+ case "danger": return palette.remove;
84
+ case "accent": return palette.hunk;
85
+ case "muted": return palette.meta;
86
+ default: return palette.context;
87
+ }
88
+ };
89
+ return lines.map((line, index) => {
90
+ const base = colorFor(line);
91
+ const partner = pairs.has(index)
92
+ ? lines[pairs.get(index)]
93
+ : partnerOf.has(index)
94
+ ? lines[partnerOf.get(index)]
95
+ : undefined;
96
+ if (partner === undefined) {
97
+ return line === "" ? [] : [{ text: line, fg: base }];
98
+ }
99
+ const removed = isRemoval(line);
100
+ const { before, after } = intraLine((removed ? line : partner).slice(1), (removed ? partner : line).slice(1));
101
+ const segments = removed ? before : after;
102
+ const emphasis = removed ? palette.removeEmphasis : palette.addEmphasis;
103
+ return [
104
+ { text: line.slice(0, 1), fg: base },
105
+ ...segments.map((segment) => ({
106
+ text: segment.text,
107
+ fg: base,
108
+ ...(segment.changed ? { bg: emphasis, bold: true } : {}),
109
+ })),
110
+ ];
111
+ });
112
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ export interface FileChange {
2
+ /** Index status, as git reports it: M, A, D, R, C, U or space. */
3
+ index: string;
4
+ /** Worktree status. */
5
+ work: string;
6
+ path: string;
7
+ /** Original path for a rename. */
8
+ from?: string;
9
+ staged: boolean;
10
+ unstaged: boolean;
11
+ untracked: boolean;
12
+ conflicted: boolean;
13
+ }
14
+ export interface Commit {
15
+ hash: string;
16
+ short: string;
17
+ subject: string;
18
+ author: string;
19
+ when: string;
20
+ refs: string;
21
+ }
22
+ export interface Branch {
23
+ name: string;
24
+ current: boolean;
25
+ upstream: string;
26
+ ahead: number;
27
+ behind: number;
28
+ }
29
+ export interface GitError {
30
+ command: string;
31
+ message: string;
32
+ }
33
+ export interface Repo {
34
+ root: string;
35
+ branch: string;
36
+ upstream: string;
37
+ ahead: number;
38
+ behind: number;
39
+ files: FileChange[];
40
+ commits: Commit[];
41
+ branches: Branch[];
42
+ errors: GitError[];
43
+ }
44
+ /** Run git and return stdout, or null when it fails. Never throws. */
45
+ export declare function git(cwd: string, args: string[]): string | null;
46
+ export declare function repoRoot(cwd: string): string | null;
47
+ /**
48
+ * `git status --porcelain=v2 -z`. v2 rather than v1 because it reports the
49
+ * branch, the upstream and the ahead/behind counts in the same call, and -z
50
+ * because a path may contain anything but NUL.
51
+ */
52
+ export declare function parseStatus(out: string): Pick<Repo, "branch" | "upstream" | "ahead" | "behind" | "files">;
53
+ export declare function parseLog(out: string): Commit[];
54
+ export declare function parseBranches(out: string): Branch[];
55
+ export declare function readRepo(cwd: string, logLimit?: number): Repo | null;
56
+ /** The diff for one path, staged or not. Empty string when there is none. */
57
+ export declare function fileDiff(root: string, file: FileChange, staged: boolean): string;
58
+ /** Colour family for a diff line, as a theme key. */
59
+ export declare function diffLineKind(line: string): "success" | "danger" | "accent" | "muted" | "foreground";
60
+ export declare function stage(root: string, file: FileChange): boolean;
61
+ export declare function unstage(root: string, file: FileChange): boolean;
package/dist/git.js ADDED
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Everything g1tz knows about a repository, read by shelling out to git.
3
+ *
4
+ * Porcelain formats only, with explicit -z where a filename could contain a
5
+ * newline. Parsing `git status` output meant for humans is how a TUI ends up
6
+ * corrupting someone's working tree.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ /** Run git and return stdout, or null when it fails. Never throws. */
10
+ export function git(cwd, args) {
11
+ const result = spawnSync("git", args, {
12
+ cwd,
13
+ encoding: "utf8",
14
+ maxBuffer: 32 * 1024 * 1024,
15
+ // A pager or an editor would take the terminal away from the TUI.
16
+ env: { ...process.env, GIT_PAGER: "cat", GIT_EDITOR: "true", GIT_OPTIONAL_LOCKS: "0" },
17
+ });
18
+ if (result.error || result.status !== 0)
19
+ return null;
20
+ return result.stdout;
21
+ }
22
+ export function repoRoot(cwd) {
23
+ return git(cwd, ["rev-parse", "--show-toplevel"])?.trim() || null;
24
+ }
25
+ /**
26
+ * `git status --porcelain=v2 -z`. v2 rather than v1 because it reports the
27
+ * branch, the upstream and the ahead/behind counts in the same call, and -z
28
+ * because a path may contain anything but NUL.
29
+ */
30
+ export function parseStatus(out) {
31
+ const result = { branch: "", upstream: "", ahead: 0, behind: 0, files: [] };
32
+ const records = out.split("\0");
33
+ for (let i = 0; i < records.length; i++) {
34
+ const line = records[i];
35
+ if (line === "")
36
+ continue;
37
+ if (line.startsWith("# branch.head ")) {
38
+ result.branch = line.slice("# branch.head ".length);
39
+ }
40
+ else if (line.startsWith("# branch.upstream ")) {
41
+ result.upstream = line.slice("# branch.upstream ".length);
42
+ }
43
+ else if (line.startsWith("# branch.ab ")) {
44
+ const m = /\+(-?\d+) -(-?\d+)/.exec(line);
45
+ if (m) {
46
+ result.ahead = Number(m[1]);
47
+ result.behind = Number(m[2]);
48
+ }
49
+ }
50
+ else if (line.startsWith("1 ") || line.startsWith("2 ")) {
51
+ const rename = line.startsWith("2 ");
52
+ const parts = line.split(" ");
53
+ const xy = parts[1] ?? "..";
54
+ const index = xy[0] ?? ".";
55
+ const work = xy[1] ?? ".";
56
+ // Fields are fixed up to the path, which is the rest of the record.
57
+ const pathStart = rename ? 9 : 8;
58
+ const path = parts.slice(pathStart).join(" ");
59
+ // A rename's original path is the *next* NUL-separated record.
60
+ const from = rename ? records[++i] : undefined;
61
+ result.files.push({
62
+ index, work, path, from,
63
+ staged: index !== "." && index !== " ",
64
+ unstaged: work !== "." && work !== " ",
65
+ untracked: false,
66
+ conflicted: false,
67
+ });
68
+ }
69
+ else if (line.startsWith("u ")) {
70
+ const parts = line.split(" ");
71
+ result.files.push({
72
+ index: "U", work: "U",
73
+ path: parts.slice(10).join(" "),
74
+ staged: false, unstaged: true, untracked: false, conflicted: true,
75
+ });
76
+ }
77
+ else if (line.startsWith("? ")) {
78
+ result.files.push({
79
+ index: "?", work: "?", path: line.slice(2),
80
+ staged: false, unstaged: true, untracked: true, conflicted: false,
81
+ });
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+ const LOG_FORMAT = "%H%x1f%h%x1f%s%x1f%an%x1f%ar%x1f%D";
87
+ export function parseLog(out) {
88
+ const commits = [];
89
+ for (const raw of out.split("\0")) {
90
+ // `--pretty=format:` puts a newline BETWEEN records, after the NUL, so
91
+ // every record but the first starts with one. Left in, it rode along on
92
+ // the hash and `git show` failed for every commit but the first.
93
+ const line = raw.replace(/^\n/, "");
94
+ if (line === "")
95
+ continue;
96
+ const [hash, short, subject, author, when, refs] = line.split("\x1f");
97
+ if (!hash)
98
+ continue;
99
+ commits.push({
100
+ hash, short: short ?? "", subject: subject ?? "",
101
+ author: author ?? "", when: when ?? "", refs: refs ?? "",
102
+ });
103
+ }
104
+ return commits;
105
+ }
106
+ const BRANCH_FORMAT = "%(HEAD)%1f%(refname:short)%1f%(upstream:short)%1f%(upstream:track)";
107
+ export function parseBranches(out) {
108
+ const branches = [];
109
+ for (const line of out.split("\n")) {
110
+ if (line.trim() === "")
111
+ continue;
112
+ const [head, name, upstream, track] = line.split("\x1f");
113
+ if (!name)
114
+ continue;
115
+ const ahead = /ahead (\d+)/.exec(track ?? "");
116
+ const behind = /behind (\d+)/.exec(track ?? "");
117
+ branches.push({
118
+ name,
119
+ current: head === "*",
120
+ upstream: upstream ?? "",
121
+ ahead: ahead ? Number(ahead[1]) : 0,
122
+ behind: behind ? Number(behind[1]) : 0,
123
+ });
124
+ }
125
+ return branches;
126
+ }
127
+ export function readRepo(cwd, logLimit = 200) {
128
+ const root = repoRoot(cwd);
129
+ if (!root)
130
+ return null;
131
+ const errors = [];
132
+ const run = (args) => {
133
+ const out = git(root, args);
134
+ if (out === null) {
135
+ errors.push({ command: `git ${args[0]}`, message: "failed" });
136
+ return "";
137
+ }
138
+ return out;
139
+ };
140
+ // --untracked-files=all, because the default collapses a new directory to a
141
+ // single "? sub/" entry and you cannot stage or diff a directory here.
142
+ const status = parseStatus(run(["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]));
143
+ // --no-show-signature: with log.showSignature set, git writes the
144
+ // verification text on stdout ahead of every record, glued to the hash.
145
+ const commits = parseLog(run(["log", "--no-show-signature", `--pretty=format:${LOG_FORMAT}%x00`, "-n", String(logLimit)]));
146
+ const branches = parseBranches(run(["for-each-ref", "--sort=-committerdate",
147
+ `--format=${BRANCH_FORMAT}`, "refs/heads"]));
148
+ return { root, ...status, commits, branches, errors };
149
+ }
150
+ /** The diff for one path, staged or not. Empty string when there is none. */
151
+ export function fileDiff(root, file, staged) {
152
+ if (file.untracked) {
153
+ const out = git(root, ["diff", "--no-color", "--no-index", "/dev/null", file.path]);
154
+ // --no-index exits 1 when the files differ, which is always here.
155
+ return out ?? git(root, ["show", `:${file.path}`]) ?? "(untracked)";
156
+ }
157
+ const args = ["diff", "--no-color"];
158
+ if (staged)
159
+ args.push("--cached");
160
+ args.push("--", file.path);
161
+ return git(root, args) ?? "";
162
+ }
163
+ /** Colour family for a diff line, as a theme key. */
164
+ export function diffLineKind(line) {
165
+ if (line.startsWith("+++") || line.startsWith("---"))
166
+ return "muted";
167
+ if (line.startsWith("@@"))
168
+ return "accent";
169
+ if (line.startsWith("+"))
170
+ return "success";
171
+ if (line.startsWith("-"))
172
+ return "danger";
173
+ if (line.startsWith("diff ") || line.startsWith("index "))
174
+ return "muted";
175
+ return "foreground";
176
+ }
177
+ export function stage(root, file) {
178
+ return git(root, ["add", "--", file.path]) !== null;
179
+ }
180
+ export function unstage(root, file) {
181
+ return git(root, ["restore", "--staged", "--", file.path]) !== null;
182
+ }
@@ -0,0 +1,63 @@
1
+ export interface GitHubRepo {
2
+ owner: string;
3
+ name: string;
4
+ }
5
+ export interface GitHubItem {
6
+ number: number;
7
+ title: string;
8
+ user: string;
9
+ /** ISO date of the event this item is listed for: opened, merged or closed. */
10
+ at: string;
11
+ }
12
+ export interface GitHubRelease {
13
+ tag: string;
14
+ name: string;
15
+ at: string;
16
+ }
17
+ export interface GitHubPulse {
18
+ repo: string;
19
+ url: string;
20
+ description: string;
21
+ stars: number;
22
+ forks: number;
23
+ /** GitHub's open issue count, which includes open pull requests. */
24
+ openIssues: number;
25
+ prsOpened: GitHubItem[];
26
+ prsMerged: GitHubItem[];
27
+ /** Closed without merging. */
28
+ prsClosed: GitHubItem[];
29
+ issuesOpened: GitHubItem[];
30
+ issuesClosed: GitHubItem[];
31
+ releases: GitHubRelease[];
32
+ newStars: number;
33
+ /** A page budget ran out somewhere, so a count is a floor rather than a total. */
34
+ partial: boolean;
35
+ }
36
+ /**
37
+ * https, ssh and scp-style GitHub remotes, with or without .git, including
38
+ * GitHub's "SSH over the HTTPS port" form (ssh.github.com:443) and an explicit
39
+ * port. Anything else is null.
40
+ */
41
+ export declare function parseGitHubRemote(url: string): GitHubRepo | null;
42
+ export declare function githubRemote(root: string): GitHubRepo | null;
43
+ export declare const repoName: (repo: GitHubRepo) => string;
44
+ /** One GitHub API call: a path relative to the API root, parsed JSON back. */
45
+ export type Fetch = (path: string, headers?: Record<string, string>) => Promise<unknown>;
46
+ /** Whether gh is on the PATH. Asked once per process. */
47
+ export declare function ghInstalled(): boolean;
48
+ /** What to tell the user when gh could not answer. */
49
+ export declare function ghErrorMessage(error: {
50
+ code?: string | number | null | undefined;
51
+ signal?: string | null | undefined;
52
+ killed?: boolean | undefined;
53
+ message: string;
54
+ }, stderr: string): string;
55
+ export declare function ghFetch(path: string, headers?: Record<string, string>): Promise<unknown>;
56
+ export interface PageBudget {
57
+ pulls: number;
58
+ issues: number;
59
+ releases: number;
60
+ stars: number;
61
+ }
62
+ export declare const PAGE_BUDGET: PageBudget;
63
+ export declare function readGitHubPulse(repo: GitHubRepo, since: Date | null, fetch?: Fetch, budget?: PageBudget): Promise<GitHubPulse>;