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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/api-client.js +0 -12
  3. package/src/github-rest.js +35 -0
  4. package/src/nugit-config.js +84 -0
  5. package/src/nugit-stack.js +29 -266
  6. package/src/nugit.js +103 -661
  7. package/src/review-hub/review-hub-ink.js +6 -3
  8. package/src/review-hub/run-review-hub.js +34 -91
  9. package/src/services/repo-branches.js +151 -0
  10. package/src/services/stack-inference.js +90 -0
  11. package/src/split-view/run-split.js +14 -89
  12. package/src/stack-view/infer-chains-to-pick-stacks.js +10 -0
  13. package/src/stack-view/ink-app.js +3 -2118
  14. package/src/stack-view/loader.js +19 -93
  15. package/src/stack-view/loading-ink.js +2 -44
  16. package/src/stack-view/merge-alternate-pick-stacks.js +23 -1
  17. package/src/stack-view/remote-infer-doc.js +28 -45
  18. package/src/stack-view/run-stack-view.js +249 -526
  19. package/src/stack-view/run-view-entry.js +14 -18
  20. package/src/stack-view/stack-pick-ink.js +169 -131
  21. package/src/stack-view/stack-picker-graph-pane.js +118 -0
  22. package/src/stack-view/terminal-fullscreen.js +7 -45
  23. package/src/tui/pages/home.js +122 -0
  24. package/src/tui/pages/repo-actions.js +81 -0
  25. package/src/tui/pages/repo-branches.js +259 -0
  26. package/src/tui/pages/viewer.js +2129 -0
  27. package/src/tui/router.js +40 -0
  28. package/src/tui/run-tui.js +281 -0
  29. package/src/utilities/loading.js +37 -0
  30. package/src/utilities/terminal.js +31 -0
  31. package/src/cli-output.js +0 -228
  32. package/src/nugit-start.js +0 -211
  33. package/src/stack-discover.js +0 -292
  34. package/src/stack-discovery-config.js +0 -91
  35. package/src/stack-extra-commands.js +0 -353
  36. package/src/stack-graph.js +0 -214
  37. package/src/stack-helpers.js +0 -58
  38. package/src/stack-propagate.js +0 -422
@@ -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
- * Load stack.json from GitHub Contents API.
14
- * @param {string} repoFull owner/repo
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
- export async function fetchStackDocFromGithub(repoFull, ref) {
18
- const { owner, repo } = parseRepoFullName(repoFull);
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
- * @param {{ root?: string | null, repo?: string, ref?: string, file?: string }} opts
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
- let doc = null;
75
- const root = opts.root ?? findGitRoot();
76
-
77
- if (opts.file) {
78
- const raw = fs.readFileSync(opts.file, "utf8");
79
- doc = JSON.parse(raw);
80
- validateStackDoc(doc);
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
- import React, { useEffect, useState } from "react";
2
- import { Box, Text, render } from "ink";
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
- import chalk from "chalk";
2
- import { authMe } from "../api-client.js";
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 { questionLine } from "./prompt-line.js";
7
+ import { authMe } from "../api-client.js";
7
8
  import { RepoPickerBackError } from "./repo-picker-back.js";
8
9
 
9
10
  /**
10
- * Open PR groups inferrable as a stack (for TUI pickers).
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
- * @param {unknown} e
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 {{ interactivePick?: boolean, preselectedChainIndex?: number, tuiChainPick?: (chains: number[][], pulls: unknown[]) => Promise<number> }} [opts]
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 .nugit/stack.json on that ref, and no inferrable same-repo open PR stack in ${repoFull}. ` +
44
- `Try a branch that contains .nugit/stack.json (often the stack tip), or use \`nugit review\`.`
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
- throw new RepoPickerBackError();
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
  }