nugit-cli 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/api-client.js +0 -12
- package/src/github-rest.js +35 -0
- package/src/nugit-config.js +84 -0
- package/src/nugit-stack.js +29 -266
- package/src/nugit.js +103 -661
- package/src/review-hub/review-hub-ink.js +6 -3
- package/src/review-hub/run-review-hub.js +34 -91
- package/src/services/repo-branches.js +151 -0
- package/src/services/stack-inference.js +90 -0
- package/src/split-view/run-split.js +14 -89
- package/src/stack-view/infer-chains-to-pick-stacks.js +10 -0
- package/src/stack-view/ink-app.js +3 -2118
- package/src/stack-view/loader.js +19 -93
- package/src/stack-view/loading-ink.js +2 -44
- package/src/stack-view/merge-alternate-pick-stacks.js +23 -1
- package/src/stack-view/remote-infer-doc.js +28 -45
- package/src/stack-view/run-stack-view.js +249 -526
- package/src/stack-view/run-view-entry.js +14 -18
- package/src/stack-view/stack-pick-ink.js +169 -131
- package/src/stack-view/stack-picker-graph-pane.js +118 -0
- package/src/stack-view/terminal-fullscreen.js +7 -45
- package/src/tui/pages/home.js +122 -0
- package/src/tui/pages/repo-actions.js +81 -0
- package/src/tui/pages/repo-branches.js +259 -0
- package/src/tui/pages/viewer.js +2129 -0
- package/src/tui/router.js +40 -0
- package/src/tui/run-tui.js +281 -0
- package/src/utilities/loading.js +37 -0
- package/src/utilities/terminal.js +31 -0
- package/src/cli-output.js +0 -228
- package/src/nugit-start.js +0 -211
- package/src/stack-discover.js +0 -292
- package/src/stack-discovery-config.js +0 -91
- package/src/stack-extra-commands.js +0 -353
- package/src/stack-graph.js +0 -214
- package/src/stack-helpers.js +0 -58
- package/src/stack-propagate.js +0 -422
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home page — the first screen shown when `nugit` is run bare on a TTY.
|
|
3
|
+
*
|
|
4
|
+
* Keys:
|
|
5
|
+
* Enter / c → current directory (view stack in current git repo)
|
|
6
|
+
* s / / → review hub (repos with pending reviews)
|
|
7
|
+
* o → open remote (search GitHub for a repo)
|
|
8
|
+
* q / Esc → quit
|
|
9
|
+
*/
|
|
10
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
11
|
+
import { Box, Text, useApp, useInput, render, useStdout } from "ink";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import {
|
|
14
|
+
parseSgrMouse,
|
|
15
|
+
enableSgrMouse,
|
|
16
|
+
disableSgrMouse
|
|
17
|
+
} from "../../stack-view/sgr-mouse.js";
|
|
18
|
+
import { getRepoFullNameFromGitRoot } from "../../git-info.js";
|
|
19
|
+
import { findGitRoot } from "../../nugit-stack.js";
|
|
20
|
+
import { clearInkScreen } from "../../utilities/terminal.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {"cwd" | "review" | "search" | "quit"} HomeAction
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render the home page and resolve with the user's choice.
|
|
28
|
+
* @returns {Promise<HomeAction>}
|
|
29
|
+
*/
|
|
30
|
+
export async function runHomePage() {
|
|
31
|
+
clearInkScreen();
|
|
32
|
+
const result = { action: /** @type {HomeAction} */ ("quit") };
|
|
33
|
+
const { waitUntilExit } = render(React.createElement(HomeApp, { result }));
|
|
34
|
+
await waitUntilExit();
|
|
35
|
+
return result.action;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{ result: { action: HomeAction } }} props
|
|
40
|
+
*/
|
|
41
|
+
function HomeApp({ result }) {
|
|
42
|
+
const { exit } = useApp();
|
|
43
|
+
const { stdout } = useStdout();
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
enableSgrMouse(stdout);
|
|
47
|
+
return () => disableSgrMouse(stdout);
|
|
48
|
+
}, [stdout]);
|
|
49
|
+
|
|
50
|
+
const root = findGitRoot();
|
|
51
|
+
let cwdRepo = null;
|
|
52
|
+
if (root) {
|
|
53
|
+
try { cwdRepo = getRepoFullNameFromGitRoot(root); } catch { cwdRepo = null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const finish = (action) => {
|
|
57
|
+
result.action = action;
|
|
58
|
+
exit();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
const mouse = parseSgrMouse(input);
|
|
63
|
+
if (mouse) return;
|
|
64
|
+
|
|
65
|
+
if (key.escape || input === "q" || key.backspace || key.delete) {
|
|
66
|
+
finish("quit");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if ((key.return || input === "c") && cwdRepo) {
|
|
70
|
+
finish("cwd");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (input === "s" || input === "/") {
|
|
74
|
+
finish("review");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (input === "o") {
|
|
78
|
+
finish("search");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return React.createElement(
|
|
84
|
+
Box,
|
|
85
|
+
{ flexDirection: "column", padding: 1, gap: 0 },
|
|
86
|
+
React.createElement(
|
|
87
|
+
Text,
|
|
88
|
+
{ color: "cyan", bold: true },
|
|
89
|
+
"nugit"
|
|
90
|
+
),
|
|
91
|
+
React.createElement(Text, null, ""),
|
|
92
|
+
cwdRepo
|
|
93
|
+
? React.createElement(
|
|
94
|
+
Text,
|
|
95
|
+
null,
|
|
96
|
+
chalk.whiteBright("Enter"),
|
|
97
|
+
` view this directory: ${chalk.bold(cwdRepo)}`
|
|
98
|
+
)
|
|
99
|
+
: React.createElement(
|
|
100
|
+
Text,
|
|
101
|
+
{ dimColor: true },
|
|
102
|
+
"Enter (no github.com remote — navigate to a repo with [o])"
|
|
103
|
+
),
|
|
104
|
+
React.createElement(
|
|
105
|
+
Text,
|
|
106
|
+
null,
|
|
107
|
+
chalk.whiteBright("[s]"),
|
|
108
|
+
" review hub — repos with pending reviews"
|
|
109
|
+
),
|
|
110
|
+
React.createElement(
|
|
111
|
+
Text,
|
|
112
|
+
null,
|
|
113
|
+
chalk.whiteBright("[o]"),
|
|
114
|
+
" open remote — search GitHub for any repo"
|
|
115
|
+
),
|
|
116
|
+
React.createElement(
|
|
117
|
+
Text,
|
|
118
|
+
{ dimColor: true },
|
|
119
|
+
"[q] quit"
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Actions page — shown after a repo has been chosen, before entering the viewer.
|
|
3
|
+
*
|
|
4
|
+
* Keys:
|
|
5
|
+
* Enter / s → Review stacks (stack picker + viewer)
|
|
6
|
+
* b → Branches / PRs
|
|
7
|
+
* Backspace → go back
|
|
8
|
+
* q / Esc → go back
|
|
9
|
+
*/
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { Box, Text, useApp, useInput, render } from "ink";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { clearInkScreen } from "../../utilities/terminal.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {"stacks" | "branches" | "back"} RepoAction
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ repo: string }} opts
|
|
21
|
+
* @returns {Promise<RepoAction>}
|
|
22
|
+
*/
|
|
23
|
+
export async function runRepoActionsPage({ repo }) {
|
|
24
|
+
clearInkScreen();
|
|
25
|
+
const result = { action: /** @type {RepoAction} */ ("back") };
|
|
26
|
+
const { waitUntilExit } = render(
|
|
27
|
+
React.createElement(RepoActionsApp, { repo, result })
|
|
28
|
+
);
|
|
29
|
+
await waitUntilExit();
|
|
30
|
+
return result.action;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ repo: string, result: { action: RepoAction } }} props
|
|
35
|
+
*/
|
|
36
|
+
function RepoActionsApp({ repo, result }) {
|
|
37
|
+
const { exit } = useApp();
|
|
38
|
+
|
|
39
|
+
const finish = (/** @type {RepoAction} */ action) => {
|
|
40
|
+
result.action = action;
|
|
41
|
+
exit();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
useInput((input, key) => {
|
|
45
|
+
const backKey = key.backspace || key.delete || input === "\x7f" || input === "\b";
|
|
46
|
+
if (backKey || input === "q" || key.escape) {
|
|
47
|
+
finish("back");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.return || input === "s") {
|
|
51
|
+
finish("stacks");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (input === "b") {
|
|
55
|
+
finish("branches");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return React.createElement(
|
|
61
|
+
Box,
|
|
62
|
+
{ flexDirection: "column", padding: 1, gap: 0 },
|
|
63
|
+
React.createElement(Text, { color: "cyan", bold: true }, "nugit"),
|
|
64
|
+
React.createElement(Text, { dimColor: true }, repo),
|
|
65
|
+
React.createElement(Text, {}, ""),
|
|
66
|
+
React.createElement(
|
|
67
|
+
Text,
|
|
68
|
+
{},
|
|
69
|
+
chalk.whiteBright("Enter"),
|
|
70
|
+
" Review stacks — browse and approve stacked PRs"
|
|
71
|
+
),
|
|
72
|
+
React.createElement(
|
|
73
|
+
Text,
|
|
74
|
+
{},
|
|
75
|
+
chalk.whiteBright("[b]"),
|
|
76
|
+
" Branches / PRs — list, push, and open PRs for branches"
|
|
77
|
+
),
|
|
78
|
+
React.createElement(Text, {}, ""),
|
|
79
|
+
React.createElement(Text, { dimColor: true }, "Backspace / [q] back")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branches / PRs page — list local + remote branches for a repo, with actions:
|
|
3
|
+
* Enter / p → push local branch to remote (when local_ahead or diverged)
|
|
4
|
+
* Enter / c → create PR (opens gh pr create or browser compare URL)
|
|
5
|
+
* o → open compare URL in browser
|
|
6
|
+
* j / k → move cursor
|
|
7
|
+
* Backspace / q → go back
|
|
8
|
+
*/
|
|
9
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
10
|
+
import { Box, Text, useApp, useInput, render } from "ink";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
13
|
+
import { clearInkScreen } from "../../utilities/terminal.js";
|
|
14
|
+
import { openUrl } from "../../stack-view/open-url.js";
|
|
15
|
+
import {
|
|
16
|
+
listLocalBranches,
|
|
17
|
+
listRemoteBranches,
|
|
18
|
+
mergeBranchModel
|
|
19
|
+
} from "../../services/repo-branches.js";
|
|
20
|
+
import { findGitRoot } from "../../nugit-stack.js";
|
|
21
|
+
import { getRepoFullNameFromGitRoot } from "../../git-info.js";
|
|
22
|
+
import { parseRepoFullName } from "../../nugit-stack.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ repo: string }} opts
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
export async function runRepoBranchesPage({ repo }) {
|
|
29
|
+
clearInkScreen();
|
|
30
|
+
const { waitUntilExit } = render(
|
|
31
|
+
React.createElement(RepoBranchesApp, { repo })
|
|
32
|
+
);
|
|
33
|
+
await waitUntilExit();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ repo: string }} props
|
|
38
|
+
*/
|
|
39
|
+
function RepoBranchesApp({ repo }) {
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
const { owner, repo: repoName } = parseRepoFullName(repo);
|
|
42
|
+
|
|
43
|
+
const [loading, setLoading] = useState(true);
|
|
44
|
+
const [err, setErr] = useState(/** @type {string | null} */ (null));
|
|
45
|
+
const [rows, setRows] = useState(/** @type {import("../../services/repo-branches.js").BranchRow[]} */ ([]));
|
|
46
|
+
const [cursor, setCursor] = useState(0);
|
|
47
|
+
const [action, setAction] = useState(/** @type {string | null} */ (null));
|
|
48
|
+
const [actionErr, setActionErr] = useState(/** @type {string | null} */ (null));
|
|
49
|
+
|
|
50
|
+
// Resolve local git root (may not match repo if user launched from a different dir)
|
|
51
|
+
const localRoot = (() => {
|
|
52
|
+
const root = findGitRoot();
|
|
53
|
+
if (!root) return null;
|
|
54
|
+
try {
|
|
55
|
+
const remoteRepo = getRepoFullNameFromGitRoot(root);
|
|
56
|
+
return remoteRepo === repo ? root : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
const rowsRef = useRef(rows);
|
|
63
|
+
rowsRef.current = rows;
|
|
64
|
+
|
|
65
|
+
const loadBranches = () => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
setErr(null);
|
|
68
|
+
setActionErr(null);
|
|
69
|
+
|
|
70
|
+
const local = localRoot ? listLocalBranches(localRoot) : [];
|
|
71
|
+
listRemoteBranches(owner, repoName)
|
|
72
|
+
.then((remote) => mergeBranchModel(local, remote, localRoot))
|
|
73
|
+
.then((merged) => {
|
|
74
|
+
setRows(merged);
|
|
75
|
+
setCursor(0);
|
|
76
|
+
})
|
|
77
|
+
.catch((e) => setErr(String(e?.message || e)))
|
|
78
|
+
.finally(() => setLoading(false));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
loadBranches();
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
useInput((input, key) => {
|
|
87
|
+
if (loading) return;
|
|
88
|
+
const backKey = key.backspace || key.delete || input === "\x7f" || input === "\b";
|
|
89
|
+
if (backKey || input === "q" || key.escape) {
|
|
90
|
+
exit();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (input === "j" || key.downArrow) {
|
|
94
|
+
setCursor((c) => Math.min(c + 1, Math.max(0, rowsRef.current.length - 1)));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (input === "k" || key.upArrow) {
|
|
98
|
+
setCursor((c) => Math.max(c - 1, 0));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (input === "r") {
|
|
102
|
+
loadBranches();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const row = rowsRef.current[cursor];
|
|
107
|
+
if (!row) return;
|
|
108
|
+
|
|
109
|
+
// Push local → remote
|
|
110
|
+
if (input === "p" && localRoot && (row.status === "local_ahead" || row.status === "local_only" || row.status === "diverged")) {
|
|
111
|
+
setAction(`Pushing ${row.name}…`);
|
|
112
|
+
setActionErr(null);
|
|
113
|
+
try {
|
|
114
|
+
spawnSync("git", ["push", "origin", row.name], {
|
|
115
|
+
cwd: localRoot,
|
|
116
|
+
stdio: "pipe"
|
|
117
|
+
});
|
|
118
|
+
setAction(null);
|
|
119
|
+
loadBranches();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
setActionErr(String(/** @type {{ message?: string }} */ (e)?.message || e));
|
|
122
|
+
setAction(null);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create PR via gh CLI or browser
|
|
128
|
+
if (key.return || input === "c") {
|
|
129
|
+
if (row.status === "remote_only" || row.status === "in_sync" || row.status === "remote_ahead") {
|
|
130
|
+
// Branch is on remote — open compare URL
|
|
131
|
+
const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
|
|
132
|
+
openUrl(compareUrl);
|
|
133
|
+
setAction(`Opened compare URL for ${row.name}`);
|
|
134
|
+
setTimeout(() => setAction(null), 2000);
|
|
135
|
+
} else if (row.status === "local_only" || row.status === "local_ahead") {
|
|
136
|
+
if (!localRoot) {
|
|
137
|
+
setActionErr("No local clone found for push/create PR.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Try gh pr create, fall back to browser
|
|
141
|
+
const ghResult = spawnSync("gh", ["pr", "create", "--head", row.name, "--fill"], {
|
|
142
|
+
cwd: localRoot,
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
stdio: "pipe"
|
|
145
|
+
});
|
|
146
|
+
if (ghResult.status === 0) {
|
|
147
|
+
setAction(`PR created via gh for ${row.name}`);
|
|
148
|
+
setTimeout(() => { setAction(null); loadBranches(); }, 2000);
|
|
149
|
+
} else {
|
|
150
|
+
const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
|
|
151
|
+
openUrl(compareUrl);
|
|
152
|
+
setAction(`Opened compare URL for ${row.name} (gh not available)`);
|
|
153
|
+
setTimeout(() => setAction(null), 2000);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Open compare URL in browser
|
|
160
|
+
if (input === "o") {
|
|
161
|
+
const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
|
|
162
|
+
openUrl(compareUrl);
|
|
163
|
+
setAction(`Opened compare URL for ${row.name}`);
|
|
164
|
+
setTimeout(() => setAction(null), 2000);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const safeRow = rows.length ? rows[Math.min(cursor, rows.length - 1)] : null;
|
|
170
|
+
|
|
171
|
+
/** @param {import("../../services/repo-branches.js").BranchStatus} status */
|
|
172
|
+
function statusBadge(status) {
|
|
173
|
+
switch (status) {
|
|
174
|
+
case "local_ahead": return chalk.green("↑ local ahead");
|
|
175
|
+
case "remote_ahead": return chalk.yellow("↓ remote ahead");
|
|
176
|
+
case "diverged": return chalk.red("⇅ diverged");
|
|
177
|
+
case "local_only": return chalk.cyan("local only");
|
|
178
|
+
case "remote_only": return chalk.gray("remote only");
|
|
179
|
+
case "in_sync": return chalk.dim("in sync");
|
|
180
|
+
default: return "";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const VIEWPORT = 14;
|
|
185
|
+
const n = rows.length;
|
|
186
|
+
const viewStart =
|
|
187
|
+
n <= VIEWPORT ? 0 : Math.max(0, Math.min(cursor - Math.floor(VIEWPORT / 2), n - VIEWPORT));
|
|
188
|
+
const viewSlice = rows.slice(viewStart, viewStart + VIEWPORT);
|
|
189
|
+
|
|
190
|
+
/** @param {import("../../services/repo-branches.js").BranchRow} row */
|
|
191
|
+
function rowActions(row) {
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (
|
|
194
|
+
localRoot &&
|
|
195
|
+
(row.status === "local_ahead" || row.status === "local_only" || row.status === "diverged")
|
|
196
|
+
) {
|
|
197
|
+
parts.push("[p] push");
|
|
198
|
+
}
|
|
199
|
+
if (row.status !== "in_sync") {
|
|
200
|
+
parts.push("[Enter/c] create PR");
|
|
201
|
+
}
|
|
202
|
+
parts.push("[o] compare");
|
|
203
|
+
return chalk.dim(parts.join(" · "));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return React.createElement(
|
|
207
|
+
Box,
|
|
208
|
+
{ flexDirection: "column", padding: 1 },
|
|
209
|
+
React.createElement(
|
|
210
|
+
Text,
|
|
211
|
+
{ color: "cyan", bold: true },
|
|
212
|
+
`Branches / PRs — ${repo}`
|
|
213
|
+
),
|
|
214
|
+
React.createElement(
|
|
215
|
+
Text,
|
|
216
|
+
{ dimColor: true },
|
|
217
|
+
`j/k navigate · [r] refresh · Backspace/q back${localRoot ? " · [p] push" : " (no local clone)"}${" · [Enter/c] create PR · [o] compare"}`
|
|
218
|
+
),
|
|
219
|
+
loading
|
|
220
|
+
? React.createElement(Text, { color: "yellow" }, "Loading branches…")
|
|
221
|
+
: err
|
|
222
|
+
? React.createElement(Text, { color: "red" }, err)
|
|
223
|
+
: React.createElement(
|
|
224
|
+
Box,
|
|
225
|
+
{ flexDirection: "column", marginTop: 1 },
|
|
226
|
+
n === 0
|
|
227
|
+
? React.createElement(Text, { dimColor: true }, "No branches found.")
|
|
228
|
+
: viewSlice.map((row, localI) => {
|
|
229
|
+
const i = viewStart + localI;
|
|
230
|
+
const sel = i === cursor;
|
|
231
|
+
const mark = sel ? "▶ " : " ";
|
|
232
|
+
const nameColor = sel ? chalk.yellowBright : chalk.white;
|
|
233
|
+
const aheadBehind =
|
|
234
|
+
row.ahead || row.behind
|
|
235
|
+
? chalk.dim(` (${row.ahead}↑ ${row.behind}↓)`)
|
|
236
|
+
: "";
|
|
237
|
+
|
|
238
|
+
return React.createElement(
|
|
239
|
+
Box,
|
|
240
|
+
{ key: row.name, flexDirection: "column", marginBottom: sel ? 1 : 0 },
|
|
241
|
+
React.createElement(
|
|
242
|
+
Text,
|
|
243
|
+
{},
|
|
244
|
+
mark,
|
|
245
|
+
nameColor(row.name),
|
|
246
|
+
" ",
|
|
247
|
+
statusBadge(row.status),
|
|
248
|
+
aheadBehind
|
|
249
|
+
),
|
|
250
|
+
sel
|
|
251
|
+
? React.createElement(Text, {}, " ", rowActions(row))
|
|
252
|
+
: null
|
|
253
|
+
);
|
|
254
|
+
})
|
|
255
|
+
),
|
|
256
|
+
action ? React.createElement(Text, { color: "green" }, action) : null,
|
|
257
|
+
actionErr ? React.createElement(Text, { color: "red" }, actionErr) : null
|
|
258
|
+
);
|
|
259
|
+
}
|