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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/api-client.js +10 -23
  3. package/src/github-device-flow.js +1 -1
  4. package/src/github-oauth-client-id.js +11 -0
  5. package/src/github-pr-social.js +42 -0
  6. package/src/github-rest.js +149 -6
  7. package/src/nugit-config.js +84 -0
  8. package/src/nugit-stack.js +40 -257
  9. package/src/nugit.js +104 -647
  10. package/src/review-hub/review-autoapprove.js +95 -0
  11. package/src/review-hub/review-hub-back.js +10 -0
  12. package/src/review-hub/review-hub-ink.js +169 -0
  13. package/src/review-hub/run-review-hub.js +131 -0
  14. package/src/services/repo-branches.js +151 -0
  15. package/src/services/stack-inference.js +90 -0
  16. package/src/split-view/run-split.js +14 -76
  17. package/src/split-view/split-ink.js +2 -2
  18. package/src/stack-infer-from-prs.js +71 -0
  19. package/src/stack-view/diff-line-map.js +62 -0
  20. package/src/stack-view/fetch-pr-data.js +104 -4
  21. package/src/stack-view/infer-chains-to-pick-stacks.js +80 -0
  22. package/src/stack-view/ink-app.js +3 -421
  23. package/src/stack-view/loader.js +19 -93
  24. package/src/stack-view/loading-ink.js +2 -0
  25. package/src/stack-view/merge-alternate-pick-stacks.js +245 -0
  26. package/src/stack-view/patch-preview-merge.js +108 -0
  27. package/src/stack-view/remote-infer-doc.js +76 -0
  28. package/src/stack-view/repo-picker-back.js +10 -0
  29. package/src/stack-view/run-stack-view.js +508 -150
  30. package/src/stack-view/run-view-entry.js +115 -0
  31. package/src/stack-view/sgr-mouse.js +56 -0
  32. package/src/stack-view/stack-branch-graph.js +95 -0
  33. package/src/stack-view/stack-pick-graph.js +93 -0
  34. package/src/stack-view/stack-pick-ink.js +308 -0
  35. package/src/stack-view/stack-pick-layout.js +19 -0
  36. package/src/stack-view/stack-pick-sort.js +188 -0
  37. package/src/stack-view/stack-picker-graph-pane.js +118 -0
  38. package/src/stack-view/terminal-fullscreen.js +7 -0
  39. package/src/stack-view/tree-ascii.js +73 -0
  40. package/src/stack-view/view-md-plain.js +23 -0
  41. package/src/stack-view/view-repo-picker-ink.js +293 -0
  42. package/src/stack-view/view-tui-sequential.js +126 -0
  43. package/src/tui/pages/home.js +122 -0
  44. package/src/tui/pages/repo-actions.js +81 -0
  45. package/src/tui/pages/repo-branches.js +259 -0
  46. package/src/tui/pages/viewer.js +2129 -0
  47. package/src/tui/router.js +40 -0
  48. package/src/tui/run-tui.js +281 -0
  49. package/src/utilities/loading.js +37 -0
  50. package/src/utilities/terminal.js +31 -0
  51. package/src/cli-output.js +0 -228
  52. package/src/nugit-start.js +0 -211
  53. package/src/stack-discover.js +0 -284
  54. package/src/stack-discovery-config.js +0 -91
  55. package/src/stack-extra-commands.js +0 -353
  56. package/src/stack-graph.js +0 -214
  57. package/src/stack-helpers.js +0 -58
  58. package/src/stack-propagate.js +0 -422
@@ -1,211 +0,0 @@
1
- import { spawn } from "child_process";
2
- import fs from "fs";
3
- import path from "path";
4
- import {
5
- buildStartEnv,
6
- expandUserPath,
7
- getConfigPath,
8
- inferMonorepoRootFromCli,
9
- loadEnvFile,
10
- mergeNugitPath,
11
- readUserConfig,
12
- writeUserConfig
13
- } from "./user-config.js";
14
- import { findGitRoot, parseRepoFullName } from "./nugit-stack.js";
15
- import { getRepoFullNameFromGitRoot } from "./git-info.js";
16
- import { questionLine } from "./stack-view/prompt-line.js";
17
-
18
- /**
19
- * @param {object} opts
20
- * @param {string} [opts.installRoot]
21
- * @param {string} [opts.envFile]
22
- * @param {string} [opts.workingDirectory]
23
- */
24
- export function runConfigInit(opts) {
25
- const root = path.resolve(
26
- expandUserPath(opts.installRoot || inferMonorepoRootFromCli())
27
- );
28
- const defaultEnv = path.join(root, ".env");
29
- const envFile = expandUserPath(opts.envFile || defaultEnv);
30
- const cfg = { installRoot: root, envFile };
31
- if (opts.workingDirectory) {
32
- cfg.workingDirectory = expandUserPath(opts.workingDirectory);
33
- }
34
- writeUserConfig(cfg);
35
- console.error(`Wrote ${getConfigPath()}`);
36
- console.error(` installRoot: ${root}`);
37
- console.error(` envFile: ${envFile}`);
38
- if (!fs.existsSync(envFile)) {
39
- console.error(` (env file does not exist yet — create it or run: nugit config set env-file <path>)`);
40
- }
41
- if (cfg.workingDirectory) {
42
- console.error(` workingDirectory: ${cfg.workingDirectory}`);
43
- }
44
- }
45
-
46
- export function runConfigShow() {
47
- const c = readUserConfig();
48
- console.log(JSON.stringify(c, null, 2));
49
- }
50
-
51
- /**
52
- * @param {string} key
53
- * @param {string} value
54
- */
55
- export function runConfigSet(key, value) {
56
- const c = readUserConfig();
57
- const k = key.toLowerCase().replace(/_/g, "-");
58
- if (k === "install-root") {
59
- c.installRoot = path.resolve(expandUserPath(value));
60
- } else if (k === "env-file") {
61
- c.envFile = expandUserPath(value);
62
- } else if (k === "working-directory" || k === "cwd") {
63
- c.workingDirectory = expandUserPath(value);
64
- } else if (k === "stack-discovery-mode") {
65
- c.stackDiscovery = { ...(c.stackDiscovery || {}), mode: value };
66
- } else if (k === "stack-discovery-max-open-prs") {
67
- c.stackDiscovery = {
68
- ...(c.stackDiscovery || {}),
69
- maxOpenPrs: Number.parseInt(value, 10)
70
- };
71
- } else if (k === "stack-discovery-fetch-concurrency") {
72
- c.stackDiscovery = {
73
- ...(c.stackDiscovery || {}),
74
- fetchConcurrency: Number.parseInt(value, 10)
75
- };
76
- } else if (k === "stack-discovery-background") {
77
- c.stackDiscovery = {
78
- ...(c.stackDiscovery || {}),
79
- background: value === "true" || value === "1"
80
- };
81
- } else if (k === "stack-discovery-lazy-first-pass-max-prs") {
82
- c.stackDiscovery = {
83
- ...(c.stackDiscovery || {}),
84
- lazyFirstPassMaxPrs: Number.parseInt(value, 10)
85
- };
86
- } else {
87
- throw new Error(
88
- `Unknown key "${key}". Use: install-root | env-file | working-directory | stack-discovery-mode | stack-discovery-max-open-prs | stack-discovery-fetch-concurrency | stack-discovery-background | stack-discovery-lazy-first-pass-max-prs`
89
- );
90
- }
91
- writeUserConfig(c);
92
- console.error(`Updated ${getConfigPath()}`);
93
- }
94
-
95
- /**
96
- * Shell-escape for single-quoted POSIX strings.
97
- * @param {string} s
98
- */
99
- function shellQuoteExport(s) {
100
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
101
- }
102
-
103
- /**
104
- * Print eval-able export lines (bash/zsh).
105
- * @param {'bash' | 'fish'} style
106
- */
107
- export function runEnvExport(style = "bash") {
108
- const cfg = readUserConfig();
109
- if (!cfg.installRoot || !cfg.envFile) {
110
- throw new Error("Run `nugit config init` first (or set install-root and env-file).");
111
- }
112
- const root = path.resolve(expandUserPath(cfg.installRoot));
113
- const { vars, pathUsed } = loadEnvFile(cfg.envFile);
114
- const merged = mergeNugitPath(
115
- {
116
- ...process.env,
117
- ...vars,
118
- NUGIT_MONOREPO_ROOT: root,
119
- NUGIT_ENV_FILE: pathUsed
120
- },
121
- root
122
- );
123
-
124
- if (style === "fish") {
125
- for (const [k, v] of Object.entries(vars)) {
126
- console.log(`set -gx ${k} ${JSON.stringify(v)}`);
127
- }
128
- console.log(`set -gx NUGIT_MONOREPO_ROOT ${JSON.stringify(root)}`);
129
- console.log(`set -gx NUGIT_ENV_FILE ${JSON.stringify(pathUsed)}`);
130
- if (merged.PATH && merged.PATH !== process.env.PATH) {
131
- console.log(`set -gx PATH ${JSON.stringify(merged.PATH)}`);
132
- }
133
- return;
134
- }
135
-
136
- for (const [k, v] of Object.entries(vars)) {
137
- console.log(`export ${k}=${shellQuoteExport(v)}`);
138
- }
139
- console.log(`export NUGIT_MONOREPO_ROOT=${shellQuoteExport(root)}`);
140
- console.log(`export NUGIT_ENV_FILE=${shellQuoteExport(pathUsed)}`);
141
- if (merged.PATH && merged.PATH !== process.env.PATH) {
142
- console.log(`export PATH=${shellQuoteExport(merged.PATH)}`);
143
- }
144
- }
145
-
146
- /**
147
- * @param {object} opts
148
- * @param {string} [opts.command] shell command string for -lc
149
- */
150
- export function runStart(opts) {
151
- const cfg = readUserConfig();
152
- if (!cfg.installRoot || !cfg.envFile) {
153
- throw new Error(
154
- "No saved config. Run:\n nugit config init\n # then: nugit start"
155
- );
156
- }
157
- const env = buildStartEnv(cfg);
158
- const cwd = cfg.workingDirectory
159
- ? expandUserPath(cfg.workingDirectory)
160
- : process.cwd();
161
- const shell = process.env.SHELL || "/bin/bash";
162
- const cmd = opts.command;
163
- const args = cmd ? ["-lc", cmd] : ["-i"];
164
- const child = spawn(shell, args, {
165
- env,
166
- cwd,
167
- stdio: "inherit"
168
- });
169
- child.on("exit", (code, signal) => {
170
- if (signal) {
171
- process.exit(1);
172
- }
173
- process.exit(code ?? 0);
174
- });
175
- }
176
-
177
- /**
178
- * Interactive menu when stdin/stdout are TTY. Options 1–2 run then exit; 3 spawns the shell (same as plain `nugit start` without a menu).
179
- */
180
- export async function runStartHub() {
181
- console.error("nugit start — choose:");
182
- console.error(" 1) Stack view");
183
- console.error(" 2) Split a PR");
184
- console.error(" 3) Open shell");
185
- const ans = (await questionLine("Enter 1–3 [3]: ")).trim();
186
- const choice = ans || "3";
187
- if (choice === "1") {
188
- const { runStackViewCommand } = await import("./stack-view/run-stack-view.js");
189
- await runStackViewCommand({});
190
- process.exit(0);
191
- }
192
- if (choice === "2") {
193
- const raw = (await questionLine("PR number to split: ")).trim();
194
- const n = Number.parseInt(raw, 10);
195
- if (!Number.isFinite(n) || n < 1) {
196
- console.error("Invalid PR number.");
197
- process.exit(1);
198
- }
199
- const root = findGitRoot();
200
- if (!root) {
201
- console.error("Not inside a git repository.");
202
- process.exit(1);
203
- }
204
- const repoFull = getRepoFullNameFromGitRoot(root);
205
- const { owner, repo: repoName } = parseRepoFullName(repoFull);
206
- const { runSplitCommand } = await import("./split-view/run-split.js");
207
- await runSplitCommand({ root, owner, repo: repoName, prNumber: n });
208
- process.exit(0);
209
- }
210
- runStart({});
211
- }
@@ -1,284 +0,0 @@
1
- import { githubListOpenPulls } from "./github-rest.js";
2
- import { getGithubContents, decodeGithubFileContent, getPull } from "./api-client.js";
3
- import { validateStackDoc } from "./nugit-stack.js";
4
-
5
- /**
6
- * Stack tip PR # from layer.tip, else top entry by position in doc.prs.
7
- * @param {Record<string, unknown>} doc
8
- * @returns {number | null}
9
- */
10
- export function stackTipPrNumber(doc) {
11
- const layer = doc.layer;
12
- if (layer && typeof layer === "object") {
13
- const tip = /** @type {{ tip?: { pr_number?: number } }} */ (layer).tip;
14
- if (tip && typeof tip === "object" && typeof tip.pr_number === "number" && tip.pr_number >= 1) {
15
- return tip.pr_number;
16
- }
17
- }
18
- const prs = Array.isArray(doc.prs) ? doc.prs : [];
19
- if (!prs.length) {
20
- return null;
21
- }
22
- const sorted = [...prs].sort(
23
- (a, b) =>
24
- (/** @type {{ position?: number }} */ (a).position ?? 0) -
25
- (/** @type {{ position?: number }} */ (b).position ?? 0)
26
- );
27
- const top = sorted[sorted.length - 1];
28
- const n = top && typeof top === "object" ? /** @type {{ pr_number?: number }} */ (top).pr_number : undefined;
29
- return typeof n === "number" && n >= 1 ? n : null;
30
- }
31
-
32
- /**
33
- * @param {Record<string, unknown>} doc
34
- * @param {string} owner
35
- * @param {string} repo
36
- */
37
- export function docRepoMatches(doc, owner, repo) {
38
- const full = String(doc.repo_full_name || "").toLowerCase();
39
- return full === `${owner}/${repo}`.toLowerCase();
40
- }
41
-
42
- /**
43
- * @template T
44
- * @param {T[]} items
45
- * @param {number} concurrency
46
- * @param {(item: T, index: number) => Promise<void>} fn
47
- */
48
- async function forEachPool(items, concurrency, fn) {
49
- let idx = 0;
50
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
51
- for (;;) {
52
- const i = idx++;
53
- if (i >= items.length) {
54
- return;
55
- }
56
- await fn(items[i], i);
57
- }
58
- });
59
- await Promise.all(workers);
60
- }
61
-
62
- /**
63
- * @param {string} owner
64
- * @param {string} repo
65
- * @param {string} ref
66
- * @returns {Promise<Record<string, unknown> | null>}
67
- */
68
- async function tryLoadStackDocAtRef(owner, repo, ref) {
69
- try {
70
- const item = await getGithubContents(owner, repo, ".nugit/stack.json", ref);
71
- const text = decodeGithubFileContent(item);
72
- if (!text) {
73
- return null;
74
- }
75
- const doc = JSON.parse(text);
76
- validateStackDoc(doc);
77
- if (!docRepoMatches(doc, owner, repo)) {
78
- return null;
79
- }
80
- return /** @type {Record<string, unknown>} */ (doc);
81
- } catch {
82
- return null;
83
- }
84
- }
85
-
86
- /**
87
- * If this is a propagated prefix, try to load the full tip stack doc.
88
- * @param {string} owner
89
- * @param {string} repo
90
- * @param {Record<string, unknown>} doc
91
- */
92
- async function maybeExpandPrefixDoc(owner, repo, doc) {
93
- const layer = doc.layer && typeof doc.layer === "object" ? /** @type {Record<string, unknown>} */ (doc.layer) : null;
94
- if (!layer) {
95
- return doc;
96
- }
97
- const stackSize = layer.stack_size;
98
- const prs = Array.isArray(doc.prs) ? doc.prs : [];
99
- const tip = layer.tip && typeof layer.tip === "object" ? /** @type {Record<string, unknown>} */ (layer.tip) : null;
100
- const tipHead = tip && typeof tip.head_branch === "string" ? tip.head_branch.trim() : "";
101
- if (
102
- typeof stackSize !== "number" ||
103
- !Number.isFinite(stackSize) ||
104
- stackSize <= prs.length ||
105
- !tipHead
106
- ) {
107
- return doc;
108
- }
109
- const full = await tryLoadStackDocAtRef(owner, repo, tipHead);
110
- if (!full) {
111
- return doc;
112
- }
113
- const fullPrs = Array.isArray(full.prs) ? full.prs : [];
114
- return fullPrs.length > prs.length ? full : doc;
115
- }
116
-
117
- /**
118
- * Scan open PRs; any head with committed `.nugit/stack.json` counts. Deduplicate by stack tip PR # (layer.tip or top of prs).
119
- *
120
- * @param {string} owner
121
- * @param {string} repo
122
- * @param {{
123
- * maxOpenPrs?: number,
124
- * listPerPage?: number,
125
- * enrich?: boolean,
126
- * fetchConcurrency?: number,
127
- * onProgress?: (msg: string) => void
128
- * }} [opts]
129
- */
130
- export async function discoverStacksInRepo(owner, repo, opts = {}) {
131
- const maxOpenPrs = opts.maxOpenPrs ?? 500;
132
- const listPerPage = Math.min(100, Math.max(1, opts.listPerPage ?? 100));
133
- const fetchConc = Math.max(1, Math.min(32, opts.fetchConcurrency ?? 8));
134
- const enrich = opts.enrich !== false;
135
- const onProgress = typeof opts.onProgress === "function" ? opts.onProgress : null;
136
-
137
- /** @type {unknown[]} */
138
- const allPulls = [];
139
- let page = 1;
140
- let truncated = false;
141
- for (;;) {
142
- if (maxOpenPrs > 0 && allPulls.length >= maxOpenPrs) {
143
- truncated = true;
144
- break;
145
- }
146
- const pulls = await githubListOpenPulls(owner, repo, page, listPerPage);
147
- if (!Array.isArray(pulls) || pulls.length === 0) {
148
- break;
149
- }
150
- for (const p of pulls) {
151
- if (maxOpenPrs > 0 && allPulls.length >= maxOpenPrs) {
152
- truncated = true;
153
- break;
154
- }
155
- allPulls.push(p);
156
- }
157
- if (truncated) {
158
- break;
159
- }
160
- if (onProgress) {
161
- onProgress(`listed open PRs: ${allPulls.length}`);
162
- }
163
- if (pulls.length < listPerPage) {
164
- break;
165
- }
166
- page += 1;
167
- }
168
-
169
- /** @type {({ doc: Record<string, unknown>, discoveredFromPr: number, headRef: string } | null)[]} */
170
- const rowSlots = Array(allPulls.length).fill(null);
171
-
172
- let checkedHeads = 0;
173
- await forEachPool(allPulls, fetchConc, async (pull, i) => {
174
- const p = pull && typeof pull === "object" ? /** @type {Record<string, unknown>} */ (pull) : {};
175
- const head = p.head && typeof p.head === "object" ? /** @type {Record<string, unknown>} */ (p.head) : {};
176
- const ref = typeof head.ref === "string" ? head.ref : "";
177
- const num = p.number;
178
- if (!ref || typeof num !== "number") {
179
- return;
180
- }
181
- const doc = await tryLoadStackDocAtRef(owner, repo, ref);
182
- checkedHeads += 1;
183
- if (onProgress && (checkedHeads % 10 === 0 || checkedHeads === allPulls.length)) {
184
- onProgress(`checked stack.json on PR heads: ${checkedHeads}/${allPulls.length}`);
185
- }
186
- if (!doc) {
187
- return;
188
- }
189
- rowSlots[i] = { doc, discoveredFromPr: num, headRef: ref };
190
- });
191
-
192
- const found = rowSlots.filter(Boolean);
193
- /** @type {Map<number, { doc: Record<string, unknown>, discoveredFromPr: number, headRef: string }>} */
194
- const byTip = new Map();
195
-
196
- for (const row of found) {
197
- if (!row) {
198
- continue;
199
- }
200
- const expandedDoc = await maybeExpandPrefixDoc(owner, repo, row.doc);
201
- const tip = stackTipPrNumber(expandedDoc);
202
- if (tip == null) {
203
- continue;
204
- }
205
- const prev = byTip.get(tip);
206
- const score = Array.isArray(expandedDoc.prs) ? expandedDoc.prs.length : 0;
207
- const prevScore = prev ? (Array.isArray(prev.doc.prs) ? prev.doc.prs.length : 0) : -1;
208
- if (!prev || score > prevScore || (score === prevScore && row.discoveredFromPr === tip)) {
209
- byTip.set(tip, {
210
- doc: expandedDoc,
211
- discoveredFromPr: row.discoveredFromPr,
212
- headRef: row.headRef
213
- });
214
- }
215
- }
216
-
217
- const repoFull = `${owner}/${repo}`;
218
- /** @type {import("./stack-discover.js").DiscoveredStack[]} */
219
- const stacks = [];
220
-
221
- for (const [tipPr, meta] of [...byTip.entries()].sort((a, b) => a[0] - b[0])) {
222
- const doc = meta.doc;
223
- const prs = Array.isArray(doc.prs) ? doc.prs : [];
224
- const sorted = [...prs].sort(
225
- (a, b) =>
226
- (/** @type {{ position?: number }} */ (a).position ?? 0) -
227
- (/** @type {{ position?: number }} */ (b).position ?? 0)
228
- );
229
-
230
- /** @type {{ pr_number: number, position: number, title?: string, html_url?: string, head_branch?: string }[]} */
231
- let prRows = sorted.map((entry) => {
232
- const e = entry && typeof entry === "object" ? /** @type {Record<string, unknown>} */ (entry) : {};
233
- return {
234
- pr_number: /** @type {number} */ (e.pr_number),
235
- position: /** @type {number} */ (e.position),
236
- head_branch: typeof e.head_branch === "string" ? e.head_branch : undefined
237
- };
238
- });
239
-
240
- if (enrich) {
241
- if (onProgress) {
242
- onProgress(`loading PR titles for stack tip #${tipPr}`);
243
- }
244
- await forEachPool(prRows, fetchConc, async (row) => {
245
- try {
246
- const g = await getPull(owner, repo, row.pr_number);
247
- row.title = typeof g.title === "string" ? g.title : undefined;
248
- row.html_url = typeof g.html_url === "string" ? g.html_url : undefined;
249
- } catch {
250
- /* keep without title */
251
- }
252
- });
253
- }
254
-
255
- const tipEntry = sorted.find((e) => {
256
- const o = e && typeof e === "object" ? /** @type {{ pr_number?: number }} */ (e) : {};
257
- return o.pr_number === tipPr;
258
- });
259
- const tipObj = tipEntry && typeof tipEntry === "object" ? /** @type {Record<string, unknown>} */ (tipEntry) : {};
260
- const tipHeadBranch =
261
- typeof tipObj.head_branch === "string"
262
- ? tipObj.head_branch
263
- : meta.headRef;
264
-
265
- stacks.push({
266
- tip_pr_number: tipPr,
267
- created_by: String(doc.created_by || ""),
268
- discovered_from_pr: meta.discoveredFromPr,
269
- pr_count: prRows.length,
270
- prs: prRows,
271
- tip_head_branch: tipHeadBranch,
272
- fetch_command: `nugit stack fetch --repo ${repoFull} --ref ${tipHeadBranch}`,
273
- view_command: `nugit stack view --repo ${repoFull} --ref ${tipHeadBranch}`
274
- });
275
- }
276
-
277
- return {
278
- repo_full_name: repoFull,
279
- scanned_open_prs: allPulls.length,
280
- open_prs_truncated: truncated,
281
- stacks_found: stacks.length,
282
- stacks
283
- };
284
- }
@@ -1,91 +0,0 @@
1
- import { readUserConfig } from "./user-config.js";
2
-
3
- /**
4
- * @typedef {{
5
- * mode: 'eager' | 'lazy' | 'manual',
6
- * maxOpenPrs: number,
7
- * fetchConcurrency: number,
8
- * background: boolean,
9
- * lazyFirstPassMaxPrs: number
10
- * }} StackDiscoveryResolved
11
- */
12
-
13
- /**
14
- * Resolve stack discovery options from ~/.config/nugit/config.json and env.
15
- * Env overrides: NUGIT_STACK_DISCOVERY_MODE, NUGIT_STACK_DISCOVERY_MAX_OPEN_PRS, NUGIT_STACK_DISCOVERY_CONCURRENCY, NUGIT_STACK_DISCOVERY_BACKGROUND
16
- * @param {Partial<StackDiscoveryResolved>} [cliOverrides] CLI flags override file/env for that invocation
17
- * @returns {StackDiscoveryResolved}
18
- */
19
- export function getStackDiscoveryOpts(cliOverrides = {}) {
20
- const cfg = readUserConfig();
21
- const sd =
22
- cfg.stackDiscovery && typeof cfg.stackDiscovery === "object"
23
- ? /** @type {Record<string, unknown>} */ (cfg.stackDiscovery)
24
- : {};
25
-
26
- const envMode = process.env.NUGIT_STACK_DISCOVERY_MODE;
27
- const modeRaw =
28
- cliOverrides.mode ??
29
- envMode ??
30
- (typeof sd.mode === "string" ? sd.mode : "eager");
31
- const mode =
32
- modeRaw === "lazy" || modeRaw === "manual" || modeRaw === "eager" ? modeRaw : "eager";
33
-
34
- const maxFromEnv = process.env.NUGIT_STACK_DISCOVERY_MAX_OPEN_PRS;
35
- const maxOpenPrs =
36
- cliOverrides.maxOpenPrs ??
37
- (maxFromEnv != null && maxFromEnv !== ""
38
- ? Number.parseInt(maxFromEnv, 10)
39
- : typeof sd.maxOpenPrs === "number"
40
- ? sd.maxOpenPrs
41
- : mode === "lazy"
42
- ? 100
43
- : 500);
44
-
45
- const concFromEnv = process.env.NUGIT_STACK_DISCOVERY_CONCURRENCY;
46
- const fetchConcurrency =
47
- cliOverrides.fetchConcurrency ??
48
- (concFromEnv != null && concFromEnv !== ""
49
- ? Number.parseInt(concFromEnv, 10)
50
- : typeof sd.fetchConcurrency === "number"
51
- ? sd.fetchConcurrency
52
- : 8);
53
-
54
- const bgEnv = process.env.NUGIT_STACK_DISCOVERY_BACKGROUND;
55
- const background =
56
- cliOverrides.background ??
57
- (bgEnv === "1" || bgEnv === "true"
58
- ? true
59
- : bgEnv === "0" || bgEnv === "false"
60
- ? false
61
- : typeof sd.background === "boolean"
62
- ? sd.background
63
- : false);
64
-
65
- const lazyFirst =
66
- typeof sd.lazyFirstPassMaxPrs === "number" ? sd.lazyFirstPassMaxPrs : Math.min(50, maxOpenPrs || 50);
67
-
68
- return {
69
- mode,
70
- maxOpenPrs: Number.isFinite(maxOpenPrs) && maxOpenPrs >= 0 ? maxOpenPrs : 500,
71
- fetchConcurrency: Math.max(1, Math.min(32, Number.isFinite(fetchConcurrency) ? fetchConcurrency : 8)),
72
- background,
73
- lazyFirstPassMaxPrs: Math.max(1, lazyFirst)
74
- };
75
- }
76
-
77
- /**
78
- * For lazy mode: first pass cap (smaller scan before optional full refresh).
79
- * @param {StackDiscoveryResolved} opts
80
- * @param {boolean} full If true, use maxOpenPrs
81
- * @returns {number}
82
- */
83
- export function effectiveMaxOpenPrs(opts, full) {
84
- if (full || opts.mode === "eager") {
85
- return opts.maxOpenPrs;
86
- }
87
- if (opts.mode === "manual") {
88
- return opts.maxOpenPrs;
89
- }
90
- return Math.min(opts.lazyFirstPassMaxPrs, opts.maxOpenPrs || 100);
91
- }