nugit-cli 0.0.1 → 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 +10 -23
- package/src/github-device-flow.js +1 -1
- package/src/github-oauth-client-id.js +11 -0
- package/src/github-pr-social.js +42 -0
- package/src/github-rest.js +149 -6
- package/src/nugit-config.js +84 -0
- package/src/nugit-stack.js +40 -257
- package/src/nugit.js +104 -647
- package/src/review-hub/review-autoapprove.js +95 -0
- package/src/review-hub/review-hub-back.js +10 -0
- package/src/review-hub/review-hub-ink.js +169 -0
- package/src/review-hub/run-review-hub.js +131 -0
- package/src/services/repo-branches.js +151 -0
- package/src/services/stack-inference.js +90 -0
- package/src/split-view/run-split.js +14 -76
- package/src/split-view/split-ink.js +2 -2
- package/src/stack-infer-from-prs.js +71 -0
- package/src/stack-view/diff-line-map.js +62 -0
- package/src/stack-view/fetch-pr-data.js +104 -4
- package/src/stack-view/infer-chains-to-pick-stacks.js +80 -0
- package/src/stack-view/ink-app.js +3 -421
- package/src/stack-view/loader.js +19 -93
- package/src/stack-view/loading-ink.js +2 -0
- package/src/stack-view/merge-alternate-pick-stacks.js +245 -0
- package/src/stack-view/patch-preview-merge.js +108 -0
- package/src/stack-view/remote-infer-doc.js +76 -0
- package/src/stack-view/repo-picker-back.js +10 -0
- package/src/stack-view/run-stack-view.js +508 -150
- package/src/stack-view/run-view-entry.js +115 -0
- package/src/stack-view/sgr-mouse.js +56 -0
- package/src/stack-view/stack-branch-graph.js +95 -0
- package/src/stack-view/stack-pick-graph.js +93 -0
- package/src/stack-view/stack-pick-ink.js +308 -0
- package/src/stack-view/stack-pick-layout.js +19 -0
- package/src/stack-view/stack-pick-sort.js +188 -0
- package/src/stack-view/stack-picker-graph-pane.js +118 -0
- package/src/stack-view/terminal-fullscreen.js +7 -0
- package/src/stack-view/tree-ascii.js +73 -0
- package/src/stack-view/view-md-plain.js +23 -0
- package/src/stack-view/view-repo-picker-ink.js +293 -0
- package/src/stack-view/view-tui-sequential.js +126 -0
- 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 -284
- 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
|
@@ -1,421 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { openUrl } from "./open-url.js";
|
|
5
|
-
|
|
6
|
-
/** @returns {{ next: null | Record<string, unknown> }} */
|
|
7
|
-
export function createExitPayload() {
|
|
8
|
-
return { next: null };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {object} props
|
|
13
|
-
* @param {Awaited<ReturnType<import('./fetch-pr-data.js').fetchStackPrDetails>>} props.rows
|
|
14
|
-
* @param {{ next: null | Record<string, unknown> }} props.exitPayload
|
|
15
|
-
*/
|
|
16
|
-
export function StackInkApp({ rows, exitPayload }) {
|
|
17
|
-
const { exit } = useApp();
|
|
18
|
-
const [prIndex, setPrIndex] = useState(0);
|
|
19
|
-
const [tab, setTab] = useState(0);
|
|
20
|
-
const [lineIndex, setLineIndex] = useState(0);
|
|
21
|
-
const [fileIndex, setFileIndex] = useState(0);
|
|
22
|
-
const [filePatchOffset, setFilePatchOffset] = useState(0);
|
|
23
|
-
const [fileCommentIndex, setFileCommentIndex] = useState(0);
|
|
24
|
-
|
|
25
|
-
const len = rows?.length ?? 0;
|
|
26
|
-
const safePr = len === 0 ? 0 : Math.min(prIndex, len - 1);
|
|
27
|
-
const row = len ? rows[safePr] : null;
|
|
28
|
-
const issueList = row?.issueComments || [];
|
|
29
|
-
const reviewList = row?.reviewComments || [];
|
|
30
|
-
const fileList = row?.files || [];
|
|
31
|
-
|
|
32
|
-
const listLen = useMemo(() => {
|
|
33
|
-
if (tab === 1) {
|
|
34
|
-
return issueList.length;
|
|
35
|
-
}
|
|
36
|
-
if (tab === 2) {
|
|
37
|
-
return reviewList.length;
|
|
38
|
-
}
|
|
39
|
-
if (tab === 3) {
|
|
40
|
-
return fileList.length;
|
|
41
|
-
}
|
|
42
|
-
return 0;
|
|
43
|
-
}, [tab, issueList.length, reviewList.length, fileList.length]);
|
|
44
|
-
|
|
45
|
-
const safeLine = Math.min(lineIndex, Math.max(0, listLen - 1));
|
|
46
|
-
const safeFile = fileList.length === 0 ? 0 : Math.min(fileIndex, fileList.length - 1);
|
|
47
|
-
const selectedFile = fileList[safeFile] || null;
|
|
48
|
-
const patchLines =
|
|
49
|
-
selectedFile && typeof selectedFile.patch === "string"
|
|
50
|
-
? String(selectedFile.patch).split("\n")
|
|
51
|
-
: [];
|
|
52
|
-
const patchPageSize = 12;
|
|
53
|
-
const maxPatchOffset = Math.max(0, patchLines.length - patchPageSize);
|
|
54
|
-
const fileComments = useMemo(() => {
|
|
55
|
-
if (!selectedFile) return [];
|
|
56
|
-
const fileName = String(selectedFile.filename || "");
|
|
57
|
-
return reviewList.filter((c) => String(c?.path || "") === fileName);
|
|
58
|
-
}, [selectedFile, reviewList]);
|
|
59
|
-
const safeFileComment = fileComments.length
|
|
60
|
-
? Math.min(fileCommentIndex, fileComments.length - 1)
|
|
61
|
-
: 0;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Best-effort patch scroll target for a code line.
|
|
65
|
-
* @param {string[]} lines
|
|
66
|
-
* @param {number} lineNo
|
|
67
|
-
*/
|
|
68
|
-
const patchOffsetForLine = (lines, lineNo) => {
|
|
69
|
-
if (!lineNo || !Number.isInteger(lineNo) || lineNo < 1) return 0;
|
|
70
|
-
let newLine = 0;
|
|
71
|
-
for (let i = 0; i < lines.length; i++) {
|
|
72
|
-
const t = lines[i];
|
|
73
|
-
if (t.startsWith("@@")) {
|
|
74
|
-
const m = t.match(/\+(\d+)/);
|
|
75
|
-
if (m) newLine = Number.parseInt(m[1], 10) - 1;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (t.startsWith("+") || t.startsWith(" ")) {
|
|
79
|
-
newLine += 1;
|
|
80
|
-
}
|
|
81
|
-
if (newLine >= lineNo) {
|
|
82
|
-
return Math.max(0, i - 2);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return 0;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
useInput((input, key) => {
|
|
89
|
-
if (input === "q" || key.escape) {
|
|
90
|
-
exitPayload.next = { type: "quit" };
|
|
91
|
-
exit();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (input === "u") {
|
|
95
|
-
exitPayload.next = { type: "refresh" };
|
|
96
|
-
exit();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (key.tab) {
|
|
100
|
-
setTab((t) => (t + 1) % 4);
|
|
101
|
-
setLineIndex(0);
|
|
102
|
-
setFilePatchOffset(0);
|
|
103
|
-
setFileCommentIndex(0);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (input === "[") {
|
|
107
|
-
setTab((t) => (t + 3) % 4);
|
|
108
|
-
setLineIndex(0);
|
|
109
|
-
setFilePatchOffset(0);
|
|
110
|
-
setFileCommentIndex(0);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (input === "]") {
|
|
114
|
-
setTab((t) => (t + 1) % 4);
|
|
115
|
-
setLineIndex(0);
|
|
116
|
-
setFilePatchOffset(0);
|
|
117
|
-
setFileCommentIndex(0);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (input === "j" || key.downArrow) {
|
|
122
|
-
if (tab === 0 && len > 0) {
|
|
123
|
-
setPrIndex((i) => Math.min(i + 1, len - 1));
|
|
124
|
-
} else if (tab === 3) {
|
|
125
|
-
setFilePatchOffset((i) => Math.min(i + 1, maxPatchOffset));
|
|
126
|
-
} else {
|
|
127
|
-
setLineIndex((i) => Math.min(i + 1, Math.max(0, listLen - 1)));
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (input === "k" || key.upArrow) {
|
|
132
|
-
if (tab === 0 && len > 0) {
|
|
133
|
-
setPrIndex((i) => Math.max(i - 1, 0));
|
|
134
|
-
} else if (tab === 3) {
|
|
135
|
-
setFilePatchOffset((i) => Math.max(i - 1, 0));
|
|
136
|
-
} else {
|
|
137
|
-
setLineIndex((i) => Math.max(i - 1, 0));
|
|
138
|
-
}
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (input === "o" && row && !row.error) {
|
|
143
|
-
setTab(3);
|
|
144
|
-
setFileIndex(0);
|
|
145
|
-
setFilePatchOffset(0);
|
|
146
|
-
setFileCommentIndex(0);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (tab === 3 && input === "n") {
|
|
151
|
-
setFileIndex((i) => Math.min(i + 1, Math.max(0, fileList.length - 1)));
|
|
152
|
-
setFilePatchOffset(0);
|
|
153
|
-
setFileCommentIndex(0);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (tab === 3 && input === "p") {
|
|
157
|
-
setFileIndex((i) => Math.max(i - 1, 0));
|
|
158
|
-
setFilePatchOffset(0);
|
|
159
|
-
setFileCommentIndex(0);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (tab === 3 && input === "m" && fileComments.length) {
|
|
163
|
-
setFileCommentIndex((i) => (i + 1) % fileComments.length);
|
|
164
|
-
const c = fileComments[(safeFileComment + 1) % fileComments.length];
|
|
165
|
-
const lineNo = c?.line ?? c?.original_line ?? 0;
|
|
166
|
-
setFilePatchOffset(Math.min(maxPatchOffset, patchOffsetForLine(patchLines, Number(lineNo) || 0)));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (tab === 3 && input === "M" && fileComments.length) {
|
|
170
|
-
const next = (safeFileComment - 1 + fileComments.length) % fileComments.length;
|
|
171
|
-
setFileCommentIndex(next);
|
|
172
|
-
const c = fileComments[next];
|
|
173
|
-
const lineNo = c?.line ?? c?.original_line ?? 0;
|
|
174
|
-
setFilePatchOffset(Math.min(maxPatchOffset, patchOffsetForLine(patchLines, Number(lineNo) || 0)));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (input === "l" && tab === 2) {
|
|
179
|
-
const c = reviewList[safeLine];
|
|
180
|
-
if (c?.html_url) {
|
|
181
|
-
openUrl(c.html_url);
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (input === "g" && tab === 2 && row && !row.error) {
|
|
186
|
-
const c = reviewList[safeLine];
|
|
187
|
-
const targetPath = String(c?.path || "");
|
|
188
|
-
if (targetPath) {
|
|
189
|
-
const idx = fileList.findIndex((f) => String(f?.filename || "") === targetPath);
|
|
190
|
-
if (idx >= 0) {
|
|
191
|
-
setTab(3);
|
|
192
|
-
setFileIndex(idx);
|
|
193
|
-
const selected = fileList[idx];
|
|
194
|
-
const lines = typeof selected?.patch === "string" ? String(selected.patch).split("\n") : [];
|
|
195
|
-
const lineNo = c?.line ?? c?.original_line ?? 0;
|
|
196
|
-
const off = patchOffsetForLine(lines, Number(lineNo) || 0);
|
|
197
|
-
setFilePatchOffset(off);
|
|
198
|
-
const fcIdx = reviewList
|
|
199
|
-
.filter((rc) => String(rc?.path || "") === targetPath)
|
|
200
|
-
.findIndex((rc) => rc?.id === c?.id);
|
|
201
|
-
setFileCommentIndex(fcIdx >= 0 ? fcIdx : 0);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (input === "S" && tab === 0 && row && !row.error) {
|
|
208
|
-
exitPayload.next = {
|
|
209
|
-
type: "split",
|
|
210
|
-
prNumber: row.entry.pr_number
|
|
211
|
-
};
|
|
212
|
-
exit();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (input === "r" && row && !row.error) {
|
|
217
|
-
exitPayload.next = {
|
|
218
|
-
type: "issue_comment",
|
|
219
|
-
prNumber: row.entry.pr_number
|
|
220
|
-
};
|
|
221
|
-
exit();
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (input === "R" && row && !row.error) {
|
|
226
|
-
exitPayload.next = {
|
|
227
|
-
type: "request_reviewers",
|
|
228
|
-
prNumber: row.entry.pr_number
|
|
229
|
-
};
|
|
230
|
-
exit();
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (input === "t" && tab === 2 && row && !row.error) {
|
|
235
|
-
const c = reviewList[safeLine];
|
|
236
|
-
if (c?.id != null) {
|
|
237
|
-
exitPayload.next = {
|
|
238
|
-
type: "review_reply",
|
|
239
|
-
commentId: c.id
|
|
240
|
-
};
|
|
241
|
-
exit();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (len === 0) {
|
|
247
|
-
return React.createElement(
|
|
248
|
-
Box,
|
|
249
|
-
{ flexDirection: "column", padding: 1 },
|
|
250
|
-
React.createElement(Text, { color: "red" }, "No PRs in stack."),
|
|
251
|
-
React.createElement(Text, { dimColor: true }, "Press q to quit.")
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const ladder = rows.map((r, i) => {
|
|
256
|
-
const mark = i === safePr ? "▶" : " ";
|
|
257
|
-
const err = r.error ? ` ${r.error}` : "";
|
|
258
|
-
const title = r.pull?.title || err || "(loading)";
|
|
259
|
-
const num = r.entry.pr_number;
|
|
260
|
-
const st = r.pull?.draft ? "draft" : r.pull?.state || "?";
|
|
261
|
-
const reviewState = String(r.reviewSummary || "none");
|
|
262
|
-
const badge =
|
|
263
|
-
reviewState === "approved"
|
|
264
|
-
? chalk.green("A")
|
|
265
|
-
: reviewState === "changes_requested"
|
|
266
|
-
? chalk.red("CR")
|
|
267
|
-
: reviewState === "commented" || (r.reviewComments?.length || 0) > 0
|
|
268
|
-
? chalk.yellow("C")
|
|
269
|
-
: chalk.dim("-");
|
|
270
|
-
return `${mark} #${num} [${st}] ${badge} ${title.slice(0, 56)}`;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const tabName = ["overview", "conversation", "review", "files"][tab];
|
|
274
|
-
let bodyLines = [];
|
|
275
|
-
if (row?.error) {
|
|
276
|
-
bodyLines = [row.error];
|
|
277
|
-
} else if (tab === 0 && row?.pull) {
|
|
278
|
-
const p = row.pull;
|
|
279
|
-
bodyLines = [
|
|
280
|
-
`Title: ${p.title || ""}`,
|
|
281
|
-
`Head: ${p.head?.ref || ""} Base: ${p.base?.ref || ""}`,
|
|
282
|
-
`Comments: ${issueList.length} conv / ${reviewList.length} review (line)`
|
|
283
|
-
];
|
|
284
|
-
const rs = String(row.reviewSummary || "none");
|
|
285
|
-
bodyLines.push(
|
|
286
|
-
`Review: ${
|
|
287
|
-
rs === "approved"
|
|
288
|
-
? chalk.green("approved")
|
|
289
|
-
: rs === "changes_requested"
|
|
290
|
-
? chalk.red("changes requested")
|
|
291
|
-
: rs === "commented"
|
|
292
|
-
? chalk.yellow("commented")
|
|
293
|
-
: chalk.dim("no review state")
|
|
294
|
-
}`
|
|
295
|
-
);
|
|
296
|
-
} else if (tab === 1) {
|
|
297
|
-
issueList.forEach((c, i) => {
|
|
298
|
-
const mark = i === safeLine ? ">" : " ";
|
|
299
|
-
const who = c.user?.login || "?";
|
|
300
|
-
const one = (c.body || "").split("\n")[0].slice(0, 70);
|
|
301
|
-
bodyLines.push(`${mark} @${who}: ${one}`);
|
|
302
|
-
});
|
|
303
|
-
if (bodyLines.length === 0) {
|
|
304
|
-
bodyLines.push("(no issue comments)");
|
|
305
|
-
}
|
|
306
|
-
} else if (tab === 2) {
|
|
307
|
-
reviewList.forEach((c, i) => {
|
|
308
|
-
const mark = i === safeLine ? ">" : " ";
|
|
309
|
-
const path = c.path || "?";
|
|
310
|
-
const ln = c.line ?? c.original_line ?? "?";
|
|
311
|
-
const one = (c.body || "").split("\n")[0].slice(0, 50);
|
|
312
|
-
bodyLines.push(`${mark} ${path}:${ln} ${one}`);
|
|
313
|
-
});
|
|
314
|
-
if (bodyLines.length === 0) {
|
|
315
|
-
bodyLines.push("(no review comments)");
|
|
316
|
-
bodyLines.push(chalk.dim("Tip: press g on a review comment to jump to file diff"));
|
|
317
|
-
}
|
|
318
|
-
} else if (tab === 3) {
|
|
319
|
-
const start = Math.max(0, safeFile - 2);
|
|
320
|
-
const end = Math.min(fileList.length, start + 5);
|
|
321
|
-
for (let i = start; i < end; i++) {
|
|
322
|
-
const f = fileList[i];
|
|
323
|
-
const mark = i === safeFile ? ">" : " ";
|
|
324
|
-
const name = String(f.filename || "?");
|
|
325
|
-
const st = String(f.status || "?");
|
|
326
|
-
const statusColor =
|
|
327
|
-
st === "added"
|
|
328
|
-
? chalk.green
|
|
329
|
-
: st === "removed"
|
|
330
|
-
? chalk.red
|
|
331
|
-
: st === "renamed"
|
|
332
|
-
? chalk.yellow
|
|
333
|
-
: chalk.cyan;
|
|
334
|
-
const ch = `${chalk.green("+" + String(f.additions ?? 0))} ${chalk.red("-" + String(f.deletions ?? 0))}`;
|
|
335
|
-
bodyLines.push(`${mark} ${statusColor(name)} ${chalk.dim("[" + st + "]")} ${ch}`);
|
|
336
|
-
}
|
|
337
|
-
if (fileList.length === 0) {
|
|
338
|
-
bodyLines.push("(no changed files)");
|
|
339
|
-
} else if (selectedFile) {
|
|
340
|
-
bodyLines.push("");
|
|
341
|
-
bodyLines.push(
|
|
342
|
-
`${chalk.bold("Patch:")} ${String(selectedFile.filename || "?")} ` +
|
|
343
|
-
chalk.dim(`(${filePatchOffset + 1}-${Math.min(filePatchOffset + patchPageSize, patchLines.length)} / ${patchLines.length || 0})`)
|
|
344
|
-
);
|
|
345
|
-
const patch = typeof selectedFile.patch === "string" ? selectedFile.patch : "";
|
|
346
|
-
if (!patch) {
|
|
347
|
-
bodyLines.push("(patch not available from GitHub API for this file)");
|
|
348
|
-
} else {
|
|
349
|
-
const page = patchLines.slice(filePatchOffset, filePatchOffset + patchPageSize);
|
|
350
|
-
for (const p of page) {
|
|
351
|
-
let line = p;
|
|
352
|
-
if (line.startsWith("+++ ") || line.startsWith("--- ")) {
|
|
353
|
-
line = chalk.bold(line);
|
|
354
|
-
} else if (line.startsWith("@@")) {
|
|
355
|
-
line = chalk.cyan(line);
|
|
356
|
-
} else if (line.startsWith("+")) {
|
|
357
|
-
line = chalk.green(line);
|
|
358
|
-
} else if (line.startsWith("-")) {
|
|
359
|
-
line = chalk.red(line);
|
|
360
|
-
} else {
|
|
361
|
-
line = chalk.dim(line);
|
|
362
|
-
}
|
|
363
|
-
bodyLines.push(line.slice(0, 130));
|
|
364
|
-
}
|
|
365
|
-
if (filePatchOffset + patchPageSize < patchLines.length) {
|
|
366
|
-
bodyLines.push(chalk.dim("... more below (j/k scroll)"));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (fileComments.length) {
|
|
370
|
-
bodyLines.push("");
|
|
371
|
-
const c = fileComments[safeFileComment];
|
|
372
|
-
bodyLines.push(
|
|
373
|
-
`${chalk.bold("Comment")} ${safeFileComment + 1}/${fileComments.length} ` +
|
|
374
|
-
chalk.dim(
|
|
375
|
-
`line ${c?.line ?? c?.original_line ?? "?"} by @${c?.user?.login || "?"}`
|
|
376
|
-
)
|
|
377
|
-
);
|
|
378
|
-
bodyLines.push(String(c?.body || "").split("\n")[0].slice(0, 100));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const selectedReview = tab === 2 ? reviewList[safeLine] : null;
|
|
384
|
-
const helpParts = ["Tab/[] tabs", "o files view"];
|
|
385
|
-
if (tab === 0 || tab === 1 || tab === 2) {
|
|
386
|
-
helpParts.unshift("j/k PR or line");
|
|
387
|
-
}
|
|
388
|
-
if (tab === 3) {
|
|
389
|
-
helpParts.unshift("j/k scroll patch", "n/p file", "m/M comment");
|
|
390
|
-
}
|
|
391
|
-
if (row && !row.error) {
|
|
392
|
-
if (tab === 0) {
|
|
393
|
-
helpParts.push("S split PR");
|
|
394
|
-
}
|
|
395
|
-
helpParts.push("r comment", "R Assign Reviewers");
|
|
396
|
-
}
|
|
397
|
-
if (tab === 2 && selectedReview?.html_url) {
|
|
398
|
-
helpParts.push("l open line");
|
|
399
|
-
}
|
|
400
|
-
if (tab === 2 && selectedReview?.id != null && row && !row.error) {
|
|
401
|
-
helpParts.push("t reply thread", "g jump to file");
|
|
402
|
-
}
|
|
403
|
-
helpParts.push("u refresh", "q quit");
|
|
404
|
-
const help = helpParts.join(" | ");
|
|
405
|
-
|
|
406
|
-
return React.createElement(
|
|
407
|
-
Box,
|
|
408
|
-
{ flexDirection: "column", padding: 1 },
|
|
409
|
-
React.createElement(Text, { color: "cyan", bold: true }, "nugit stack view"),
|
|
410
|
-
React.createElement(Text, { dimColor: true }, ladder.join("\n")),
|
|
411
|
-
React.createElement(Text, { color: "magenta" }, `Tab: ${tabName}`),
|
|
412
|
-
React.createElement(
|
|
413
|
-
Box,
|
|
414
|
-
{ marginTop: 1, flexDirection: "column" },
|
|
415
|
-
...bodyLines.slice(0, 14).map((line, idx) =>
|
|
416
|
-
React.createElement(Text, { key: String(idx) }, line)
|
|
417
|
-
)
|
|
418
|
-
),
|
|
419
|
-
React.createElement(Text, { dimColor: true, marginTop: 1 }, help)
|
|
420
|
-
);
|
|
421
|
-
}
|
|
1
|
+
// Viewer page component lives in cli/src/tui/pages/viewer.js.
|
|
2
|
+
// This file is kept for backward-compatible imports within stack-view/.
|
|
3
|
+
export { StackInkApp, createExitPayload } from "../tui/pages/viewer.js";
|
package/src/stack-view/loader.js
CHANGED
|
@@ -1,101 +1,27 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { decodeGithubFileContent } from "../api-client.js";
|
|
3
|
-
import { githubGetContents } from "../github-rest.js";
|
|
4
|
-
import {
|
|
5
|
-
findGitRoot,
|
|
6
|
-
parseRepoFullName,
|
|
7
|
-
readStackFile,
|
|
8
|
-
stackJsonPath,
|
|
9
|
-
validateStackDoc
|
|
10
|
-
} from "../nugit-stack.js";
|
|
11
|
-
|
|
12
1
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param {string} ref branch or sha
|
|
2
|
+
* Stack loader — inference-only. All stacks are discovered from open PR chains.
|
|
3
|
+
* .nugit/stack.json is no longer read or written.
|
|
16
4
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const item = await githubGetContents(owner, repo, ".nugit/stack.json", ref);
|
|
20
|
-
const text = decodeGithubFileContent(item);
|
|
21
|
-
if (!text) {
|
|
22
|
-
throw new Error("Could not decode .nugit/stack.json from GitHub");
|
|
23
|
-
}
|
|
24
|
-
const doc = JSON.parse(text);
|
|
25
|
-
validateStackDoc(doc);
|
|
26
|
-
return doc;
|
|
27
|
-
}
|
|
5
|
+
import { buildInferredViewerDoc } from "../services/stack-inference.js";
|
|
6
|
+
import { parseRepoFullName } from "../nugit-stack.js";
|
|
28
7
|
|
|
29
|
-
|
|
30
|
-
* If document is a propagated prefix, fetch full stack from layer.tip.head_branch.
|
|
31
|
-
* @param {Record<string, unknown>} doc
|
|
32
|
-
*/
|
|
33
|
-
export async function expandStackDocIfPrefix(doc) {
|
|
34
|
-
const layer = doc.layer;
|
|
35
|
-
if (!layer || typeof layer !== "object" || !layer.tip || typeof layer.tip !== "object") {
|
|
36
|
-
return doc;
|
|
37
|
-
}
|
|
38
|
-
const tip = /** @type {{ head_branch?: string }} */ (layer.tip);
|
|
39
|
-
const stackSize = layer.stack_size;
|
|
40
|
-
const prs = doc.prs;
|
|
41
|
-
if (
|
|
42
|
-
typeof stackSize !== "number" ||
|
|
43
|
-
!Array.isArray(prs) ||
|
|
44
|
-
prs.length >= stackSize ||
|
|
45
|
-
typeof tip.head_branch !== "string" ||
|
|
46
|
-
!tip.head_branch.trim()
|
|
47
|
-
) {
|
|
48
|
-
return doc;
|
|
49
|
-
}
|
|
50
|
-
const { owner, repo } = parseRepoFullName(doc.repo_full_name);
|
|
51
|
-
const item = await githubGetContents(
|
|
52
|
-
owner,
|
|
53
|
-
repo,
|
|
54
|
-
".nugit/stack.json",
|
|
55
|
-
tip.head_branch.trim()
|
|
56
|
-
);
|
|
57
|
-
const text = decodeGithubFileContent(item);
|
|
58
|
-
if (!text) {
|
|
59
|
-
return doc;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const full = JSON.parse(text);
|
|
63
|
-
validateStackDoc(full);
|
|
64
|
-
return full;
|
|
65
|
-
} catch {
|
|
66
|
-
return doc;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
8
|
+
export { isGithubNotFoundError } from "./remote-infer-doc.js";
|
|
69
9
|
|
|
70
10
|
/**
|
|
71
|
-
*
|
|
11
|
+
* Build a viewer-ready stack doc for the given repo via open-PR inference.
|
|
12
|
+
*
|
|
13
|
+
* @param {{
|
|
14
|
+
* repo: string,
|
|
15
|
+
* tuiChainPick?: (chains: number[][], pulls: unknown[]) => Promise<number>,
|
|
16
|
+
* preselectedChainIndex?: number
|
|
17
|
+
* }} opts
|
|
72
18
|
*/
|
|
73
19
|
export async function loadStackDocForView(opts) {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} else if (opts.repo && opts.ref) {
|
|
82
|
-
doc = await fetchStackDocFromGithub(opts.repo, opts.ref);
|
|
83
|
-
} else if (root) {
|
|
84
|
-
const p = stackJsonPath(root);
|
|
85
|
-
if (!fs.existsSync(p)) {
|
|
86
|
-
throw new Error(`No ${p}; run nugit init or pass --repo OWNER/REPO --ref BRANCH`);
|
|
87
|
-
}
|
|
88
|
-
doc = readStackFile(root);
|
|
89
|
-
if (!doc) {
|
|
90
|
-
throw new Error("Empty stack file");
|
|
91
|
-
}
|
|
92
|
-
validateStackDoc(doc);
|
|
93
|
-
} else {
|
|
94
|
-
throw new Error(
|
|
95
|
-
"Not in a git repo: pass --file path/to/stack.json or --repo owner/repo --ref branch"
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
doc = await expandStackDocIfPrefix(doc);
|
|
100
|
-
return { doc, root: root || null };
|
|
20
|
+
const { owner, repo } = parseRepoFullName(opts.repo);
|
|
21
|
+
const repoFull = `${owner}/${repo}`;
|
|
22
|
+
const { doc, viewerLogin, pulls } = await buildInferredViewerDoc(repoFull, {
|
|
23
|
+
preselectedChainIndex: opts.preselectedChainIndex,
|
|
24
|
+
tuiChainPick: opts.tuiChainPick
|
|
25
|
+
});
|
|
26
|
+
return { doc, viewerLogin, pulls };
|
|
101
27
|
}
|