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/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -159,18 +159,6 @@ export function decodeGithubFileContent(item) {
|
|
|
159
159
|
return Buffer.from(item.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export async function fetchRemoteStackJson(repoFullName, ref) {
|
|
163
|
-
const [owner, repo] = repoFullName.split("/");
|
|
164
|
-
if (!owner || !repo) {
|
|
165
|
-
throw new Error("repoFullName must be owner/repo");
|
|
166
|
-
}
|
|
167
|
-
const item = await getGithubContents(owner, repo, ".nugit/stack.json", ref);
|
|
168
|
-
const text = decodeGithubFileContent(item);
|
|
169
|
-
if (!text) {
|
|
170
|
-
throw new Error("Could not read .nugit/stack.json from GitHub");
|
|
171
|
-
}
|
|
172
|
-
return JSON.parse(text);
|
|
173
|
-
}
|
|
174
162
|
|
|
175
163
|
export async function getPull(owner, repo, number) {
|
|
176
164
|
return githubGetPull(owner, repo, number);
|
package/src/github-rest.js
CHANGED
|
@@ -245,6 +245,41 @@ export async function githubSearchRepositories(q, perPage = 20, page = 1) {
|
|
|
245
245
|
return githubRestJson("GET", `/search/repositories?${query.toString()}`);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
/**
|
|
249
|
+
* List branches in a repository (one page).
|
|
250
|
+
* @param {string} owner
|
|
251
|
+
* @param {string} repo
|
|
252
|
+
* @param {number} [page] 1-based
|
|
253
|
+
* @param {number} [perPage] max 100
|
|
254
|
+
*/
|
|
255
|
+
export async function githubListBranches(owner, repo, page = 1, perPage = 100) {
|
|
256
|
+
const pp = Math.min(100, Math.max(1, perPage));
|
|
257
|
+
const p = Math.max(1, page);
|
|
258
|
+
return githubRestJson(
|
|
259
|
+
"GET",
|
|
260
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches?page=${p}&per_page=${pp}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} owner
|
|
266
|
+
* @param {string} repo
|
|
267
|
+
* @returns {Promise<unknown[]>}
|
|
268
|
+
*/
|
|
269
|
+
export async function githubListAllBranches(owner, repo) {
|
|
270
|
+
/** @type {unknown[]} */
|
|
271
|
+
const all = [];
|
|
272
|
+
let page = 1;
|
|
273
|
+
for (;;) {
|
|
274
|
+
const chunk = await githubListBranches(owner, repo, page, 100);
|
|
275
|
+
if (!Array.isArray(chunk) || chunk.length === 0) break;
|
|
276
|
+
all.push(...chunk);
|
|
277
|
+
if (chunk.length < 100) break;
|
|
278
|
+
page += 1;
|
|
279
|
+
}
|
|
280
|
+
return all;
|
|
281
|
+
}
|
|
282
|
+
|
|
248
283
|
export async function githubSearchIssues(q, perPage = 30, page = 1) {
|
|
249
284
|
const pp = Math.min(100, Math.max(1, perPage));
|
|
250
285
|
const p = Math.max(1, page);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-related actions for the `nugit config` and `nugit env` commands.
|
|
3
|
+
* Extracted from the former nugit-start.js.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import {
|
|
8
|
+
expandUserPath,
|
|
9
|
+
getConfigPath,
|
|
10
|
+
inferMonorepoRootFromCli,
|
|
11
|
+
readUserConfig,
|
|
12
|
+
writeUserConfig,
|
|
13
|
+
buildStartEnv
|
|
14
|
+
} from "./user-config.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} opts
|
|
18
|
+
* @param {string} [opts.installRoot]
|
|
19
|
+
* @param {string} [opts.envFile]
|
|
20
|
+
* @param {string} [opts.workingDirectory]
|
|
21
|
+
*/
|
|
22
|
+
export function runConfigInit(opts) {
|
|
23
|
+
const root = path.resolve(expandUserPath(opts.installRoot || inferMonorepoRootFromCli()));
|
|
24
|
+
const defaultEnv = path.join(root, ".env");
|
|
25
|
+
const envFile = expandUserPath(opts.envFile || defaultEnv);
|
|
26
|
+
const cfg = { installRoot: root, envFile };
|
|
27
|
+
if (opts.workingDirectory) cfg.workingDirectory = expandUserPath(opts.workingDirectory);
|
|
28
|
+
writeUserConfig(cfg);
|
|
29
|
+
console.error(`Wrote ${getConfigPath()}`);
|
|
30
|
+
console.error(` installRoot: ${root}`);
|
|
31
|
+
console.error(` envFile: ${envFile}`);
|
|
32
|
+
if (!fs.existsSync(envFile)) {
|
|
33
|
+
console.error(" (env file does not exist yet — create it or run: nugit config set env-file <path>)");
|
|
34
|
+
}
|
|
35
|
+
if (cfg.workingDirectory) console.error(` workingDirectory: ${cfg.workingDirectory}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runConfigShow() {
|
|
39
|
+
console.log(JSON.stringify(readUserConfig(), null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @param {string} value
|
|
45
|
+
*/
|
|
46
|
+
export function runConfigSet(key, value) {
|
|
47
|
+
const c = readUserConfig();
|
|
48
|
+
const k = key.toLowerCase().replace(/_/g, "-");
|
|
49
|
+
if (k === "install-root") {
|
|
50
|
+
c.installRoot = path.resolve(expandUserPath(value));
|
|
51
|
+
} else if (k === "env-file") {
|
|
52
|
+
c.envFile = expandUserPath(value);
|
|
53
|
+
} else if (k === "working-directory" || k === "cwd") {
|
|
54
|
+
c.workingDirectory = expandUserPath(value);
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error(`Unknown key "${key}". Use: install-root | env-file | working-directory`);
|
|
57
|
+
}
|
|
58
|
+
writeUserConfig(c);
|
|
59
|
+
console.error(`Updated ${getConfigPath()}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {"bash" | "fish"} shell
|
|
64
|
+
*/
|
|
65
|
+
export function runEnvExport(shell) {
|
|
66
|
+
const cfg = readUserConfig();
|
|
67
|
+
let env;
|
|
68
|
+
try {
|
|
69
|
+
env = buildStartEnv(cfg);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error(String(e?.message || e));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const entries = Object.entries(env);
|
|
75
|
+
if (shell === "fish") {
|
|
76
|
+
for (const [k, v] of entries) {
|
|
77
|
+
if (v !== undefined) console.log(`set -gx ${k} ${JSON.stringify(v)};`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
for (const [k, v] of entries) {
|
|
81
|
+
if (v !== undefined) console.log(`export ${k}=${JSON.stringify(v)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
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,191 +17,12 @@ export function findGitRoot(cwd = process.cwd()) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function
|
|
21
|
-
|
|
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");
|
|
20
|
+
export function parseRepoFullName(s) {
|
|
21
|
+
const parts = s.split("/").filter(Boolean);
|
|
22
|
+
if (parts.length !== 2) {
|
|
23
|
+
throw new Error("repo must be owner/repo");
|
|
197
24
|
}
|
|
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");
|
|
25
|
+
return { owner: parts[0], repo: parts[1] };
|
|
205
26
|
}
|
|
206
27
|
|
|
207
28
|
/**
|
|
@@ -224,86 +45,28 @@ export function createInferredStackDoc(repoFullName, createdBy, prNumbersBottomT
|
|
|
224
45
|
};
|
|
225
46
|
}
|
|
226
47
|
|
|
227
|
-
export function createInitialStackDoc(repoFullName, createdBy) {
|
|
228
|
-
return {
|
|
229
|
-
version: 1,
|
|
230
|
-
repo_full_name: repoFullName,
|
|
231
|
-
created_by: createdBy,
|
|
232
|
-
prs: [],
|
|
233
|
-
resolution_contexts: []
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function parseRepoFullName(s) {
|
|
238
|
-
const parts = s.split("/").filter(Boolean);
|
|
239
|
-
if (parts.length !== 2) {
|
|
240
|
-
throw new Error("repo must be owner/repo");
|
|
241
|
-
}
|
|
242
|
-
return { owner: parts[0], repo: parts[1] };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Next position after current max, or 0 if empty. */
|
|
246
|
-
export function nextStackPosition(prs) {
|
|
247
|
-
if (!prs || !prs.length) {
|
|
248
|
-
return 0;
|
|
249
|
-
}
|
|
250
|
-
return Math.max(...prs.map((p) => p.position)) + 1;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Normalize `nugit stack add --pr` values (variadic and/or comma-separated).
|
|
255
|
-
* @param {string | string[] | undefined} optsPr
|
|
256
|
-
* @returns {number[]}
|
|
257
|
-
*/
|
|
258
|
-
export function parseStackAddPrNumbers(optsPr) {
|
|
259
|
-
const raw = optsPr == null ? [] : Array.isArray(optsPr) ? optsPr : [optsPr];
|
|
260
|
-
const tokens = [];
|
|
261
|
-
for (const r of raw) {
|
|
262
|
-
for (const part of String(r).split(/[\s,]+/)) {
|
|
263
|
-
const t = part.trim();
|
|
264
|
-
if (t) {
|
|
265
|
-
tokens.push(t);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (tokens.length === 0) {
|
|
270
|
-
throw new Error("Pass at least one PR number: --pr <n> [n...]");
|
|
271
|
-
}
|
|
272
|
-
const nums = tokens.map((t) => Number.parseInt(t, 10));
|
|
273
|
-
for (let i = 0; i < nums.length; i++) {
|
|
274
|
-
if (!Number.isFinite(nums[i]) || nums[i] < 1) {
|
|
275
|
-
throw new Error(`Invalid PR number: ${tokens[i]}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const seen = new Set();
|
|
279
|
-
for (const n of nums) {
|
|
280
|
-
if (seen.has(n)) {
|
|
281
|
-
throw new Error(`Duplicate PR #${n} in --pr list`);
|
|
282
|
-
}
|
|
283
|
-
seen.add(n);
|
|
284
|
-
}
|
|
285
|
-
return nums;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
48
|
/**
|
|
289
|
-
*
|
|
290
|
-
* @param {
|
|
291
|
-
* @param {number} position
|
|
49
|
+
* Basic validation for a stack document (viewer-side; not strict .nugit format).
|
|
50
|
+
* @param {unknown} doc
|
|
292
51
|
*/
|
|
293
|
-
export function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
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);
|
|
299
70
|
}
|
|
300
|
-
return
|
|
301
|
-
pr_number: pull.number,
|
|
302
|
-
position,
|
|
303
|
-
head_branch: head.ref || "",
|
|
304
|
-
base_branch: base.ref || "",
|
|
305
|
-
head_sha: head.sha || "",
|
|
306
|
-
base_sha: base.sha || "",
|
|
307
|
-
status
|
|
308
|
-
};
|
|
71
|
+
return true;
|
|
309
72
|
}
|