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,8 +1,8 @@
1
- import fs from "fs";
2
- import path from "path";
3
1
  import { execSync } from "child_process";
4
2
 
5
- const STACK_REL = path.join(".nugit", "stack.json");
3
+ /**
4
+ * Core stack document helpers — no file I/O, no .nugit paths.
5
+ */
6
6
 
7
7
  export function findGitRoot(cwd = process.cwd()) {
8
8
  try {
@@ -17,203 +17,6 @@ export function findGitRoot(cwd = process.cwd()) {
17
17
  }
18
18
  }
19
19
 
20
- export function stackJsonPath(root) {
21
- return path.join(root, STACK_REL);
22
- }
23
-
24
- export function readStackFile(root) {
25
- const p = stackJsonPath(root);
26
- if (!fs.existsSync(p)) {
27
- return null;
28
- }
29
- const raw = fs.readFileSync(p, "utf8");
30
- return JSON.parse(raw);
31
- }
32
-
33
- export function validateStackDoc(doc) {
34
- if (!doc || typeof doc !== "object") {
35
- throw new Error("Invalid stack document");
36
- }
37
- if (doc.version !== 1) {
38
- throw new Error("version must be 1");
39
- }
40
- if (!doc.repo_full_name || typeof doc.repo_full_name !== "string") {
41
- throw new Error("repo_full_name is required");
42
- }
43
- if (!doc.created_by || typeof doc.created_by !== "string") {
44
- throw new Error("created_by is required");
45
- }
46
- if (!Array.isArray(doc.prs)) {
47
- throw new Error("prs must be an array");
48
- }
49
- const seenN = new Set();
50
- const seenP = new Set();
51
- for (let i = 0; i < doc.prs.length; i++) {
52
- const pr = doc.prs[i];
53
- if (!pr || typeof pr !== "object") {
54
- throw new Error(`prs[${i}] invalid`);
55
- }
56
- const n = pr.pr_number;
57
- const pos = pr.position;
58
- if (typeof n !== "number" || typeof pos !== "number") {
59
- throw new Error(`prs[${i}]: pr_number and position required`);
60
- }
61
- if (seenN.has(n)) {
62
- throw new Error(`duplicate pr_number ${n}`);
63
- }
64
- if (seenP.has(pos)) {
65
- throw new Error(`duplicate position ${pos}`);
66
- }
67
- seenN.add(n);
68
- seenP.add(pos);
69
- }
70
- validateOptionalLayer(doc);
71
- return true;
72
- }
73
-
74
- /**
75
- * @param {unknown} p
76
- * @param {string} label
77
- * @param {boolean} allowBranch
78
- */
79
- function validateLayerPointer(p, label, allowBranch) {
80
- if (p == null || typeof p !== "object") {
81
- throw new Error(`${label} must be an object`);
82
- }
83
- const o = /** @type {Record<string, unknown>} */ (p);
84
- const t = o.type;
85
- if (t === "branch") {
86
- if (!allowBranch) {
87
- throw new Error(`${label}: type "branch" not allowed here`);
88
- }
89
- if (typeof o.ref !== "string") {
90
- throw new Error(`${label}: branch ref must be a string`);
91
- }
92
- return;
93
- }
94
- if (t === "stack_pr") {
95
- if (typeof o.pr_number !== "number" || !Number.isInteger(o.pr_number) || o.pr_number < 1) {
96
- throw new Error(`${label}: stack_pr pr_number invalid`);
97
- }
98
- if (typeof o.head_branch !== "string") {
99
- throw new Error(`${label}: stack_pr head_branch must be a string`);
100
- }
101
- return;
102
- }
103
- throw new Error(`${label}: type must be "branch" or "stack_pr"`);
104
- }
105
-
106
- /**
107
- * @param {unknown} tip
108
- */
109
- function validateLayerTip(tip) {
110
- if (tip == null || typeof tip !== "object") {
111
- throw new Error("layer.tip must be an object when present");
112
- }
113
- const t = /** @type {Record<string, unknown>} */ (tip);
114
- if (typeof t.pr_number !== "number" || !Number.isInteger(t.pr_number) || t.pr_number < 1) {
115
- throw new Error("layer.tip.pr_number must be a positive integer");
116
- }
117
- if (typeof t.head_branch !== "string") {
118
- throw new Error("layer.tip.head_branch must be a string");
119
- }
120
- }
121
-
122
- /**
123
- * @param {unknown} layer
124
- * @param {number} prsLength
125
- * @param {unknown[]} [prs] entries for prefix / position checks
126
- */
127
- export function validateLayerShape(layer, prsLength, prs) {
128
- if (layer == null || typeof layer !== "object") {
129
- throw new Error("layer must be an object");
130
- }
131
- const L = /** @type {Record<string, unknown>} */ (layer);
132
- if (typeof L.position !== "number" || !Number.isInteger(L.position) || L.position < 0) {
133
- throw new Error("layer.position must be a non-negative integer");
134
- }
135
- if (typeof L.stack_size !== "number" || !Number.isInteger(L.stack_size) || L.stack_size < 1) {
136
- throw new Error("layer.stack_size must be a positive integer");
137
- }
138
- const hasTip = "tip" in L && L.tip !== undefined && L.tip !== null;
139
- if (hasTip) {
140
- validateLayerTip(L.tip);
141
- if (L.stack_size < prsLength) {
142
- throw new Error(`layer.stack_size (${L.stack_size}) must be >= prs.length (${prsLength})`);
143
- }
144
- if (prsLength !== L.position + 1) {
145
- throw new Error(
146
- `with layer.tip, prs must be a bottom-up prefix: prs.length (${prsLength}) === layer.position + 1 (${L.position + 1})`
147
- );
148
- }
149
- if (prs && Array.isArray(prs) && prs.length > 0) {
150
- const sorted = [...prs].sort(
151
- (a, b) =>
152
- (/** @type {{ position?: number }} */ (a).position ?? 0) -
153
- (/** @type {{ position?: number }} */ (b).position ?? 0)
154
- );
155
- const last = sorted[sorted.length - 1];
156
- const lp = last && typeof last === "object" ? /** @type {{ position?: number }} */ (last).position : undefined;
157
- if (lp !== L.position) {
158
- throw new Error("last pr in prs must have position === layer.position");
159
- }
160
- const want = new Set();
161
- for (let i = 0; i <= L.position; i++) {
162
- want.add(i);
163
- }
164
- const have = new Set(sorted.map((p) => (p && typeof p === "object" ? p.position : -1)));
165
- if (want.size !== have.size || [...want].some((x) => !have.has(x))) {
166
- throw new Error("prs positions must be contiguous 0..layer.position");
167
- }
168
- }
169
- } else if (L.stack_size !== prsLength) {
170
- throw new Error(
171
- `without layer.tip, layer.stack_size (${L.stack_size}) must equal prs.length (${prsLength})`
172
- );
173
- }
174
- validateLayerPointer(L.below, "layer.below", true);
175
- if (L.above !== null) {
176
- validateLayerPointer(L.above, "layer.above", false);
177
- }
178
- }
179
-
180
- /**
181
- * @param {Record<string, unknown>} doc
182
- */
183
- export function validateOptionalLayer(doc) {
184
- if (!("layer" in doc) || doc.layer === undefined) {
185
- return;
186
- }
187
- if (!Array.isArray(doc.prs)) {
188
- return;
189
- }
190
- validateLayerShape(doc.layer, doc.prs.length, doc.prs);
191
- const L = /** @type {Record<string, unknown>} */ (doc.layer);
192
- const positions = new Set(
193
- doc.prs.map((p) => (p && typeof p === "object" ? /** @type {{ position?: number }} */ (p).position : undefined))
194
- );
195
- if (!positions.has(L.position)) {
196
- throw new Error("layer.position must match one of prs[].position values");
197
- }
198
- }
199
-
200
- export function writeStackFile(root, doc) {
201
- validateStackDoc(doc);
202
- const dir = path.join(root, ".nugit");
203
- fs.mkdirSync(dir, { recursive: true });
204
- fs.writeFileSync(stackJsonPath(root), JSON.stringify(doc, null, 2) + "\n");
205
- }
206
-
207
- export function createInitialStackDoc(repoFullName, createdBy) {
208
- return {
209
- version: 1,
210
- repo_full_name: repoFullName,
211
- created_by: createdBy,
212
- prs: [],
213
- resolution_contexts: []
214
- };
215
- }
216
-
217
20
  export function parseRepoFullName(s) {
218
21
  const parts = s.split("/").filter(Boolean);
219
22
  if (parts.length !== 2) {
@@ -222,68 +25,48 @@ export function parseRepoFullName(s) {
222
25
  return { owner: parts[0], repo: parts[1] };
223
26
  }
224
27
 
225
- /** Next position after current max, or 0 if empty. */
226
- export function nextStackPosition(prs) {
227
- if (!prs || !prs.length) {
228
- return 0;
229
- }
230
- return Math.max(...prs.map((p) => p.position)) + 1;
231
- }
232
-
233
28
  /**
234
- * Normalize `nugit stack add --pr` values (variadic and/or comma-separated).
235
- * @param {string | string[] | undefined} optsPr
236
- * @returns {number[]}
29
+ * Minimal stack doc for viewer-only flows (inferred stacks, no .nugit on disk).
30
+ * @param {string} repoFullName
31
+ * @param {string} createdBy
32
+ * @param {number[]} prNumbersBottomToTop
237
33
  */
238
- export function parseStackAddPrNumbers(optsPr) {
239
- const raw = optsPr == null ? [] : Array.isArray(optsPr) ? optsPr : [optsPr];
240
- const tokens = [];
241
- for (const r of raw) {
242
- for (const part of String(r).split(/[\s,]+/)) {
243
- const t = part.trim();
244
- if (t) {
245
- tokens.push(t);
246
- }
247
- }
248
- }
249
- if (tokens.length === 0) {
250
- throw new Error("Pass at least one PR number: --pr <n> [n...]");
251
- }
252
- const nums = tokens.map((t) => Number.parseInt(t, 10));
253
- for (let i = 0; i < nums.length; i++) {
254
- if (!Number.isFinite(nums[i]) || nums[i] < 1) {
255
- throw new Error(`Invalid PR number: ${tokens[i]}`);
256
- }
257
- }
258
- const seen = new Set();
259
- for (const n of nums) {
260
- if (seen.has(n)) {
261
- throw new Error(`Duplicate PR #${n} in --pr list`);
262
- }
263
- seen.add(n);
264
- }
265
- return nums;
34
+ export function createInferredStackDoc(repoFullName, createdBy, prNumbersBottomToTop) {
35
+ const prs = prNumbersBottomToTop.map((n, i) => ({
36
+ pr_number: n,
37
+ position: i + 1
38
+ }));
39
+ return {
40
+ version: 1,
41
+ repo_full_name: repoFullName,
42
+ created_by: createdBy,
43
+ prs,
44
+ inferred: true
45
+ };
266
46
  }
267
47
 
268
48
  /**
269
- * Build a stack PR entry from GitHub GET /pulls/{n} JSON.
270
- * @param {Record<string, unknown>} pull
271
- * @param {number} position
49
+ * Basic validation for a stack document (viewer-side; not strict .nugit format).
50
+ * @param {unknown} doc
272
51
  */
273
- export function stackEntryFromGithubPull(pull, position) {
274
- const head = pull.head && typeof pull.head === "object" ? pull.head : {};
275
- const base = pull.base && typeof pull.base === "object" ? pull.base : {};
276
- let status = "open";
277
- if (pull.state === "closed") {
278
- status = pull.merged ? "merged" : "closed";
52
+ export function validateStackDoc(doc) {
53
+ if (!doc || typeof doc !== "object") throw new Error("Invalid stack document");
54
+ if (doc.version !== 1) throw new Error("version must be 1");
55
+ if (!doc.repo_full_name || typeof doc.repo_full_name !== "string") throw new Error("repo_full_name is required");
56
+ if (!doc.created_by || typeof doc.created_by !== "string") throw new Error("created_by is required");
57
+ if (!Array.isArray(doc.prs)) throw new Error("prs must be an array");
58
+ const seenN = new Set();
59
+ const seenP = new Set();
60
+ for (let i = 0; i < doc.prs.length; i++) {
61
+ const pr = doc.prs[i];
62
+ if (!pr || typeof pr !== "object") throw new Error(`prs[${i}] invalid`);
63
+ const n = pr.pr_number;
64
+ const pos = pr.position;
65
+ if (typeof n !== "number" || typeof pos !== "number") throw new Error(`prs[${i}]: pr_number and position required`);
66
+ if (seenN.has(n)) throw new Error(`duplicate pr_number ${n}`);
67
+ if (seenP.has(pos)) throw new Error(`duplicate position ${pos}`);
68
+ seenN.add(n);
69
+ seenP.add(pos);
279
70
  }
280
- return {
281
- pr_number: pull.number,
282
- position,
283
- head_branch: head.ref || "",
284
- base_branch: base.ref || "",
285
- head_sha: head.sha || "",
286
- base_sha: base.sha || "",
287
- status
288
- };
71
+ return true;
289
72
  }