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
package/src/nugit-stack.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import { execSync } from "child_process";
|
|
4
2
|
|
|
5
|
-
|
|
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
|
-
*
|
|
235
|
-
* @param {string
|
|
236
|
-
* @
|
|
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
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
*
|
|
270
|
-
* @param {
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
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
|
}
|