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
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
|
}
|
|
@@ -1,44 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { clearInkScreen } from "./terminal-fullscreen.js";
|
|
4
|
-
|
|
5
|
-
const FRAMES = ["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834", "\u2826", "\u2827", "\u2807", "\u280f"];
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Full-screen loading line with a small terminal spinner (Ink has no Spinner export in v6).
|
|
9
|
-
*
|
|
10
|
-
* @param {string} message
|
|
11
|
-
* @param {() => Promise<void>} work
|
|
12
|
-
*/
|
|
13
|
-
export async function withStackLoadInkScreen(message, work) {
|
|
14
|
-
clearInkScreen();
|
|
15
|
-
const LoadingLine = () => {
|
|
16
|
-
const [i, setI] = useState(0);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const t = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 80);
|
|
19
|
-
return () => clearInterval(t);
|
|
20
|
-
}, []);
|
|
21
|
-
return React.createElement(
|
|
22
|
-
Box,
|
|
23
|
-
{ flexDirection: "row", padding: 1 },
|
|
24
|
-
React.createElement(Text, { color: "cyan" }, FRAMES[i]),
|
|
25
|
-
React.createElement(Text, null, ` ${message}`)
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
const inst = render(React.createElement(LoadingLine));
|
|
29
|
-
try {
|
|
30
|
-
await new Promise((r) => setImmediate(r));
|
|
31
|
-
await work();
|
|
32
|
-
} finally {
|
|
33
|
-
try {
|
|
34
|
-
inst.unmount();
|
|
35
|
-
} catch {
|
|
36
|
-
/* ignore */
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
inst.clear();
|
|
40
|
-
} catch {
|
|
41
|
-
/* ignore */
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
// Re-exported from cli/src/utilities/loading.js — import from there for new code.
|
|
2
|
+
export { withLoadingScreen as withStackLoadInkScreen } from "../utilities/loading.js";
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { githubListAllOpenPulls } from "../github-rest.js";
|
|
2
2
|
import { inferPrChainsFromOpenPulls } from "../stack-infer-from-prs.js";
|
|
3
|
-
import { stackTipPrNumber } from "../stack-discover.js";
|
|
4
3
|
import { inferChainsToPickStacks } from "./infer-chains-to-pick-stacks.js";
|
|
5
4
|
import { matchesPickerViewingStack } from "./stack-pick-sort.js";
|
|
6
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Stack tip PR # from layer.tip, else top entry by position in doc.prs.
|
|
8
|
+
* @param {Record<string, unknown>} doc
|
|
9
|
+
* @returns {number | null}
|
|
10
|
+
*/
|
|
11
|
+
export function stackTipPrNumber(doc) {
|
|
12
|
+
const layer = doc.layer;
|
|
13
|
+
if (layer && typeof layer === "object") {
|
|
14
|
+
const tip = /** @type {{ tip?: { pr_number?: number } }} */ (layer).tip;
|
|
15
|
+
if (tip && typeof tip === "object" && typeof tip.pr_number === "number" && tip.pr_number >= 1) {
|
|
16
|
+
return tip.pr_number;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const prs = Array.isArray(doc.prs) ? doc.prs : [];
|
|
20
|
+
if (!prs.length) return null;
|
|
21
|
+
const sorted = [...prs].sort(
|
|
22
|
+
(a, b) => (/** @type {{ position?: number }} */ (a).position ?? 0) - (/** @type {{ position?: number }} */ (b).position ?? 0)
|
|
23
|
+
);
|
|
24
|
+
const top = sorted[sorted.length - 1];
|
|
25
|
+
const n = top && typeof top === "object" ? /** @type {{ pr_number?: number }} */ (top).pr_number : undefined;
|
|
26
|
+
return typeof n === "number" && n >= 1 ? n : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
/**
|
|
8
30
|
* PR numbers from GitHub open-pull payloads (see {@link githubListAllOpenPulls}).
|
|
9
31
|
*
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Inference helpers for the stack viewer — builds viewer docs from open PR chains.
|
|
3
|
+
*/
|
|
3
4
|
import { githubListAllOpenPulls } from "../github-rest.js";
|
|
4
5
|
import { parseRepoFullName, createInferredStackDoc } from "../nugit-stack.js";
|
|
5
6
|
import { inferPrChainsFromOpenPulls } from "../stack-infer-from-prs.js";
|
|
6
|
-
import {
|
|
7
|
+
import { authMe } from "../api-client.js";
|
|
7
8
|
import { RepoPickerBackError } from "./repo-picker-back.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* @param {unknown} e
|
|
12
|
+
*/
|
|
13
|
+
export function isGithubNotFoundError(e) {
|
|
14
|
+
const s = String(/** @type {{ message?: string }} */ (e)?.message || e);
|
|
15
|
+
return /not found/i.test(s) || /\b404\b/.test(s);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Open PR groups inferrable as a stack.
|
|
11
20
|
* @param {string} repoFull
|
|
12
21
|
* @returns {Promise<number[][]>}
|
|
13
22
|
*/
|
|
@@ -18,73 +27,47 @@ export async function listOpenPrChainGroups(repoFull) {
|
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
export function isGithubNotFoundError(e) {
|
|
24
|
-
const s = String(/** @type {{ message?: string }} */ (e)?.message || e);
|
|
25
|
-
return /not found/i.test(s) || /\b404\b/.test(s);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* When `.nugit/stack.json` is missing on the remote ref, build a viewer doc from open PRs (same-repo inference).
|
|
30
|
+
* Build a viewer doc from open PRs (same-repo chain inference).
|
|
31
|
+
*
|
|
30
32
|
* @param {string} repoFull
|
|
31
|
-
* @param {{
|
|
33
|
+
* @param {{
|
|
34
|
+
* interactivePick?: boolean,
|
|
35
|
+
* preselectedChainIndex?: number,
|
|
36
|
+
* tuiChainPick?: (chains: number[][], pulls: unknown[]) => Promise<number>
|
|
37
|
+
* }} [opts]
|
|
32
38
|
*/
|
|
33
39
|
export async function inferStackDocForRemoteView(repoFull, opts = {}) {
|
|
34
|
-
const interactive = opts.interactivePick !== false && process.stdin.isTTY;
|
|
35
|
-
const me = await authMe();
|
|
36
|
-
const login = me && typeof me.login === "string" ? me.login : "viewer";
|
|
37
40
|
const { owner, repo } = parseRepoFullName(repoFull);
|
|
38
41
|
const pulls = await githubListAllOpenPulls(owner, repo);
|
|
39
42
|
const chains = inferPrChainsFromOpenPulls(pulls, repoFull);
|
|
40
43
|
|
|
41
44
|
if (chains.length === 0) {
|
|
42
45
|
throw new Error(
|
|
43
|
-
`No
|
|
44
|
-
|
|
46
|
+
`No open PR stacks found in ${repoFull}. ` +
|
|
47
|
+
"Open some PRs in a stacked chain and try again, or use \`nugit review\` to browse all repos."
|
|
45
48
|
);
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
const me = await authMe();
|
|
52
|
+
const login = me && typeof me.login === "string" ? me.login : "viewer";
|
|
53
|
+
|
|
48
54
|
/** @type {number[]} */
|
|
49
55
|
let chosen;
|
|
50
56
|
if (chains.length === 1) {
|
|
51
57
|
chosen = chains[0];
|
|
52
58
|
} else if (typeof opts.preselectedChainIndex === "number" && Number.isInteger(opts.preselectedChainIndex)) {
|
|
53
59
|
const i = opts.preselectedChainIndex;
|
|
54
|
-
if (i < 0 || i >= chains.length)
|
|
55
|
-
throw new Error("Invalid preselectedChainIndex for inferred stack.");
|
|
56
|
-
}
|
|
60
|
+
if (i < 0 || i >= chains.length) throw new Error("Invalid preselectedChainIndex for inferred stack.");
|
|
57
61
|
chosen = chains[i];
|
|
58
62
|
} else if (typeof opts.tuiChainPick === "function") {
|
|
59
63
|
const idx = await opts.tuiChainPick(chains, pulls);
|
|
60
|
-
if (idx === -2)
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
if (idx === -1) {
|
|
64
|
-
throw new Error("Stack group selection cancelled.");
|
|
65
|
-
}
|
|
64
|
+
if (idx === -2) throw new RepoPickerBackError();
|
|
65
|
+
if (idx === -1) throw new Error("Stack group selection cancelled.");
|
|
66
66
|
if (typeof idx === "number" && idx >= 0 && idx < chains.length) {
|
|
67
67
|
chosen = chains[idx];
|
|
68
68
|
} else {
|
|
69
69
|
throw new Error("Invalid stack group selection.");
|
|
70
70
|
}
|
|
71
|
-
} else if (interactive) {
|
|
72
|
-
console.error(chalk.bold.cyan(`No stack.json on default ref — inferred ${chains.length} open PR group(s):`));
|
|
73
|
-
for (let i = 0; i < chains.length; i++) {
|
|
74
|
-
const c = chains[i];
|
|
75
|
-
const label = c.length > 1 ? `stack ${c.join(" → ")}` : `PR #${c[0]}`;
|
|
76
|
-
console.error(` ${chalk.yellow("[" + (i + 1) + "]")} ${label}`);
|
|
77
|
-
}
|
|
78
|
-
const ans = String(await questionLine(chalk.green("Select group (empty = use largest stack): "))).trim();
|
|
79
|
-
if (ans) {
|
|
80
|
-
const n = Number.parseInt(ans, 10);
|
|
81
|
-
if (!Number.isInteger(n) || n < 1 || n > chains.length) {
|
|
82
|
-
throw new Error("Invalid selection.");
|
|
83
|
-
}
|
|
84
|
-
chosen = chains[n - 1];
|
|
85
|
-
} else {
|
|
86
|
-
chosen = chains.reduce((a, b) => (b.length > a.length ? b : a));
|
|
87
|
-
}
|
|
88
71
|
} else {
|
|
89
72
|
chosen = chains.reduce((a, b) => (b.length > a.length ? b : a));
|
|
90
73
|
}
|