@indigoai-us/hq-cli 5.267.0 → 5.269.0
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/CHANGELOG.md +23 -0
- package/dist/command-catalog.generated.d.ts +73 -0
- package/dist/command-catalog.generated.js +94 -0
- package/dist/commands/lanes-hold.d.ts +2 -0
- package/dist/commands/lanes-hold.js +122 -310
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-review.d.ts +10 -0
- package/dist/commands/lanes-review.js +56 -0
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +26 -1
- package/dist/lib/checkpoint-ontology/multi-writer.d.ts +1 -1
- package/dist/lib/checkpoint-ontology/multi-writer.js +3 -0
- package/dist/lib/lanes/blocker-freshness.js +2 -1
- package/dist/lib/lanes/keep-alive.js +49 -20
- package/dist/lib/lanes/kr-freshness.js +3 -2
- package/dist/lib/lanes/lane-company.d.ts +3 -2
- package/dist/lib/lanes/lane-company.js +21 -4
- package/dist/lib/lanes/manager-status-gate.js +5 -3
- package/dist/lib/lanes/merge-command.d.ts +14 -0
- package/dist/lib/lanes/merge-command.js +363 -0
- package/dist/lib/lanes/reviews.d.ts +38 -0
- package/dist/lib/lanes/reviews.js +395 -0
- package/dist/lib/lanes/watcher.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
function fail(error, message, details = {}) {
|
|
3
|
+
throw new LanesError(error, message, details);
|
|
4
|
+
}
|
|
5
|
+
export function canonicalRepo(raw) {
|
|
6
|
+
const value = raw.trim();
|
|
7
|
+
const parts = value.split("/");
|
|
8
|
+
if (parts.length !== 2) {
|
|
9
|
+
return fail("repo_hold_repo_invalid", "Repository must be a safe owner/repo pair", { field: "repo" });
|
|
10
|
+
}
|
|
11
|
+
const [owner, repository] = parts;
|
|
12
|
+
if (!owner || !repository ||
|
|
13
|
+
!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(owner) ||
|
|
14
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(repository) ||
|
|
15
|
+
owner === "." || owner === ".." || repository === "." || repository === "..") {
|
|
16
|
+
return fail("repo_hold_repo_invalid", "Repository must be a safe owner/repo pair", { field: "repo" });
|
|
17
|
+
}
|
|
18
|
+
return `${owner.toLowerCase()}/${repository.toLowerCase()}`;
|
|
19
|
+
}
|
|
20
|
+
function splitCommandChain(command) {
|
|
21
|
+
const segments = [];
|
|
22
|
+
let current = "";
|
|
23
|
+
let quote = null;
|
|
24
|
+
let escaped = false;
|
|
25
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
26
|
+
const character = command[index] ?? "";
|
|
27
|
+
if (escaped) {
|
|
28
|
+
current += character;
|
|
29
|
+
escaped = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (character === "\\" && quote !== "'") {
|
|
33
|
+
current += character;
|
|
34
|
+
escaped = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (quote) {
|
|
38
|
+
current += character;
|
|
39
|
+
if (character === quote)
|
|
40
|
+
quote = null;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (character === "'" || character === "\"") {
|
|
44
|
+
quote = character;
|
|
45
|
+
current += character;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (character === ";" || character === "|") {
|
|
49
|
+
segments.push(current.trim());
|
|
50
|
+
current = "";
|
|
51
|
+
if (character === "|" && command[index + 1] === "|")
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (character === "&" && command[index + 1] === "&") {
|
|
56
|
+
segments.push(current.trim());
|
|
57
|
+
current = "";
|
|
58
|
+
index += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
current += character;
|
|
62
|
+
}
|
|
63
|
+
if (quote !== null || escaped) {
|
|
64
|
+
return fail("repo_hold_command_invalid", "Command has an unclosed quote or escape");
|
|
65
|
+
}
|
|
66
|
+
segments.push(current.trim());
|
|
67
|
+
return segments.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
function shellWords(segment) {
|
|
70
|
+
const words = [];
|
|
71
|
+
let current = "";
|
|
72
|
+
let quote = null;
|
|
73
|
+
let escaped = false;
|
|
74
|
+
for (const character of segment) {
|
|
75
|
+
if (escaped) {
|
|
76
|
+
current += character;
|
|
77
|
+
escaped = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (character === "\\" && quote !== "'") {
|
|
81
|
+
escaped = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (quote) {
|
|
85
|
+
if (character === quote)
|
|
86
|
+
quote = null;
|
|
87
|
+
else
|
|
88
|
+
current += character;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === "'" || character === "\"") {
|
|
92
|
+
quote = character;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (/\s/.test(character)) {
|
|
96
|
+
if (current)
|
|
97
|
+
words.push(current);
|
|
98
|
+
current = "";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
current += character;
|
|
102
|
+
}
|
|
103
|
+
if (escaped)
|
|
104
|
+
current += "\\";
|
|
105
|
+
if (current)
|
|
106
|
+
words.push(current);
|
|
107
|
+
return words;
|
|
108
|
+
}
|
|
109
|
+
function optionRepo(words) {
|
|
110
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
111
|
+
const word = words[index] ?? "";
|
|
112
|
+
if ((word === "-R" || word === "--repo") && words[index + 1])
|
|
113
|
+
return words[index + 1] ?? null;
|
|
114
|
+
if (word.startsWith("--repo="))
|
|
115
|
+
return word.slice("--repo=".length);
|
|
116
|
+
if (word.startsWith("-R") && word.length > 2)
|
|
117
|
+
return word.slice(2);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function ghPrSubcommand(words) {
|
|
122
|
+
const groupIndex = words.indexOf("pr", 1);
|
|
123
|
+
if (groupIndex < 0)
|
|
124
|
+
return null;
|
|
125
|
+
for (let index = groupIndex + 1; index < words.length; index += 1) {
|
|
126
|
+
const word = words[index] ?? "";
|
|
127
|
+
if (word === "-R" || word === "--repo" || word === "--hostname") {
|
|
128
|
+
index += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (word.startsWith("--repo=") || (word.startsWith("-R") && word.length > 2))
|
|
132
|
+
continue;
|
|
133
|
+
if (word.startsWith("-"))
|
|
134
|
+
continue;
|
|
135
|
+
return { subcommandIndex: index, subcommand: word };
|
|
136
|
+
}
|
|
137
|
+
return { subcommandIndex: words.length, subcommand: null };
|
|
138
|
+
}
|
|
139
|
+
const SHELL_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
140
|
+
const PR_MERGE_VALUE_OPTIONS = new Set([
|
|
141
|
+
"-A", "--author-email", "-b", "--body", "-F", "--body-file",
|
|
142
|
+
"--match-head-commit", "-t", "--subject",
|
|
143
|
+
]);
|
|
144
|
+
function skipEnvOptions(words, start) {
|
|
145
|
+
let index = start + 1;
|
|
146
|
+
while (index < words.length) {
|
|
147
|
+
const word = words[index] ?? "";
|
|
148
|
+
if (SHELL_ASSIGNMENT.test(word)) {
|
|
149
|
+
index += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (word === "-i" || word === "--ignore-environment") {
|
|
153
|
+
index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (word === "-u" || word === "--unset" || word === "-C" || word === "--chdir") {
|
|
157
|
+
index += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (word.startsWith("--unset=") || word.startsWith("--chdir=")) {
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (word === "--")
|
|
165
|
+
return index + 1;
|
|
166
|
+
if (word.startsWith("-")) {
|
|
167
|
+
index += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
function skipTimeoutOptions(words, start) {
|
|
175
|
+
let index = start + 1;
|
|
176
|
+
while (index < words.length) {
|
|
177
|
+
const word = words[index] ?? "";
|
|
178
|
+
if (word === "--") {
|
|
179
|
+
index += 1;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
if (word === "-k" || word === "--kill-after" || word === "-s" || word === "--signal") {
|
|
183
|
+
index += 2;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (word.startsWith("--kill-after=") || word.startsWith("--signal=")) {
|
|
187
|
+
index += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (word === "--preserve-status" || word === "--foreground" || word === "--verbose" || word === "-v") {
|
|
191
|
+
index += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (word.startsWith("-")) {
|
|
195
|
+
index += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return Math.min(index + 1, words.length);
|
|
201
|
+
}
|
|
202
|
+
function unwrapCommand(words) {
|
|
203
|
+
let index = 0;
|
|
204
|
+
while (index < words.length) {
|
|
205
|
+
if (SHELL_ASSIGNMENT.test(words[index] ?? "")) {
|
|
206
|
+
index += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (words[index] === "env") {
|
|
210
|
+
index = skipEnvOptions(words, index);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (words[index] === "timeout") {
|
|
214
|
+
index = skipTimeoutOptions(words, index);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
return words.slice(index);
|
|
220
|
+
}
|
|
221
|
+
function parsePullUrl(raw) {
|
|
222
|
+
if (!/^https?:\/\//i.test(raw))
|
|
223
|
+
return null;
|
|
224
|
+
let url;
|
|
225
|
+
try {
|
|
226
|
+
url = new URL(raw);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const match = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/.exec(url.pathname);
|
|
232
|
+
if (!match)
|
|
233
|
+
return null;
|
|
234
|
+
try {
|
|
235
|
+
const pr = Number(match[3]);
|
|
236
|
+
if (!Number.isSafeInteger(pr) || pr <= 0)
|
|
237
|
+
return null;
|
|
238
|
+
return { repo: canonicalRepo(`${decodeURIComponent(match[1] ?? "")}/${decodeURIComponent(match[2] ?? "")}`), pr };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof LanesError && error.error === "repo_hold_repo_invalid") {
|
|
242
|
+
return fail("repo_hold_repo_invalid", "Pull request URL has an invalid repository", { field: "repo" });
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function pinnedHead(words, start = 0) {
|
|
248
|
+
for (let index = start; index < words.length; index += 1) {
|
|
249
|
+
const word = words[index] ?? "";
|
|
250
|
+
if (word === "--match-head-commit")
|
|
251
|
+
return words[index + 1]?.trim().toLowerCase() || null;
|
|
252
|
+
if (word.startsWith("--match-head-commit="))
|
|
253
|
+
return word.slice("--match-head-commit=".length).trim().toLowerCase() || null;
|
|
254
|
+
if (word === "-f" || word === "-F" || word === "--field" || word === "--raw-field") {
|
|
255
|
+
const value = words[index + 1] ?? "";
|
|
256
|
+
if (value.startsWith("sha="))
|
|
257
|
+
return value.slice(4).trim().toLowerCase() || null;
|
|
258
|
+
index += 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (const prefix of ["-fsha=", "-Fsha=", "--field=sha=", "--raw-field=sha="]) {
|
|
262
|
+
if (word.startsWith(prefix))
|
|
263
|
+
return word.slice(prefix.length).trim().toLowerCase() || null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
function prMergeTarget(words, subcommandIndex) {
|
|
269
|
+
const rawRepo = optionRepo(words);
|
|
270
|
+
let rawNumber = null;
|
|
271
|
+
let urlTarget = null;
|
|
272
|
+
const start = subcommandIndex + 1;
|
|
273
|
+
for (let index = start; index < words.length; index += 1) {
|
|
274
|
+
const word = words[index] ?? "";
|
|
275
|
+
if (PR_MERGE_VALUE_OPTIONS.has(word)) {
|
|
276
|
+
index += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (["-R", "--repo", "--hostname"].includes(word)) {
|
|
280
|
+
index += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (word.startsWith("--repo=") || (word.startsWith("-R") && word.length > 2) || word.startsWith("--match-head-commit="))
|
|
284
|
+
continue;
|
|
285
|
+
if (word.startsWith("-"))
|
|
286
|
+
continue;
|
|
287
|
+
urlTarget = parsePullUrl(word);
|
|
288
|
+
if (urlTarget)
|
|
289
|
+
break;
|
|
290
|
+
rawNumber = word;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
const repo = rawRepo ? canonicalRepo(rawRepo) : urlTarget?.repo ?? null;
|
|
294
|
+
const pr = urlTarget?.pr ?? (rawNumber && /^\d+$/.test(rawNumber) && Number.isSafeInteger(Number(rawNumber)) && Number(rawNumber) > 0
|
|
295
|
+
? Number(rawNumber)
|
|
296
|
+
: null);
|
|
297
|
+
const reason = repo === null
|
|
298
|
+
? "repository could not be resolved without a pull request URL or --repo"
|
|
299
|
+
: pr === null
|
|
300
|
+
? "pull request number could not be resolved"
|
|
301
|
+
: null;
|
|
302
|
+
return { kind: "pr", repo, pr, pinnedHead: pinnedHead(words, start), unresolvedReason: reason };
|
|
303
|
+
}
|
|
304
|
+
function apiMergeTarget(words, apiIndex) {
|
|
305
|
+
const rest = words.slice(apiIndex + 1);
|
|
306
|
+
const graphql = rest.includes("graphql");
|
|
307
|
+
if (graphql && rest.some((word) => word.includes("mergePullRequest"))) {
|
|
308
|
+
return {
|
|
309
|
+
kind: "graphql",
|
|
310
|
+
repo: null,
|
|
311
|
+
pr: null,
|
|
312
|
+
pinnedHead: pinnedHead(rest),
|
|
313
|
+
unresolvedReason: "GraphQL mergePullRequest identifies a node id, not a pull request number",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
for (const word of rest) {
|
|
317
|
+
const match = /^(?:https?:\/\/api\.github\.com)?\/?repos\/([^/]+)\/([^/]+)\/pulls\/(\d+)\/merge\/?$/.exec(word);
|
|
318
|
+
if (!match)
|
|
319
|
+
continue;
|
|
320
|
+
const pr = Number(match[3]);
|
|
321
|
+
if (!Number.isSafeInteger(pr) || pr <= 0)
|
|
322
|
+
break;
|
|
323
|
+
return {
|
|
324
|
+
kind: "api",
|
|
325
|
+
repo: canonicalRepo(`${match[1]}/${match[2]}`),
|
|
326
|
+
pr,
|
|
327
|
+
pinnedHead: pinnedHead(rest),
|
|
328
|
+
unresolvedReason: null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function parseSegment(segment) {
|
|
334
|
+
const words = unwrapCommand(shellWords(segment));
|
|
335
|
+
if (words[0] === "git" && words.slice(1).includes("merge")) {
|
|
336
|
+
return {
|
|
337
|
+
kind: "git",
|
|
338
|
+
repo: null,
|
|
339
|
+
pr: null,
|
|
340
|
+
pinnedHead: null,
|
|
341
|
+
unresolvedReason: "git merge does not identify a pull request",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (words[0] !== "gh")
|
|
345
|
+
return null;
|
|
346
|
+
const pr = ghPrSubcommand(words);
|
|
347
|
+
if (pr?.subcommand === "merge")
|
|
348
|
+
return prMergeTarget(words, pr.subcommandIndex);
|
|
349
|
+
const apiIndex = words.indexOf("api", 1);
|
|
350
|
+
if (apiIndex < 0)
|
|
351
|
+
return null;
|
|
352
|
+
return apiMergeTarget(words, apiIndex);
|
|
353
|
+
}
|
|
354
|
+
export function parseMergeCommands(command) {
|
|
355
|
+
const targets = splitCommandChain(command)
|
|
356
|
+
.map(parseSegment)
|
|
357
|
+
.filter((target) => target !== null);
|
|
358
|
+
return {
|
|
359
|
+
targets,
|
|
360
|
+
unresolved: targets.some((target) => target.unresolvedReason !== null),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=merge-command.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const LANE_REVIEWS_REL: string;
|
|
2
|
+
export declare const LANE_REVIEW_SCHEMA_VERSION = "hq-lanes-review.v1";
|
|
3
|
+
export declare const LANE_REVIEW_HISTORY_CAP = 50;
|
|
4
|
+
export interface LaneReviewHistoryEntry {
|
|
5
|
+
repo: string;
|
|
6
|
+
pr: number;
|
|
7
|
+
head_sha: string;
|
|
8
|
+
reviewer_lane: string | null;
|
|
9
|
+
reviewer_session: string | null;
|
|
10
|
+
worker_lane: string | null;
|
|
11
|
+
note: string | null;
|
|
12
|
+
recorded_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LaneReview extends LaneReviewHistoryEntry {
|
|
15
|
+
schema_version: typeof LANE_REVIEW_SCHEMA_VERSION;
|
|
16
|
+
history: LaneReviewHistoryEntry[];
|
|
17
|
+
}
|
|
18
|
+
export interface LaneReviewListing {
|
|
19
|
+
reviews: LaneReview[];
|
|
20
|
+
skipped: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RecordLaneReviewInput {
|
|
23
|
+
repo: string;
|
|
24
|
+
pr: string | number;
|
|
25
|
+
headSha: string;
|
|
26
|
+
reviewerLane: string | null;
|
|
27
|
+
reviewerSession: string | null;
|
|
28
|
+
workerLane?: string;
|
|
29
|
+
note?: string;
|
|
30
|
+
recordedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
export declare function canonicalLaneReviewRepo(raw: string): string;
|
|
33
|
+
export declare function parseLaneReviewPr(raw: string | number): number;
|
|
34
|
+
export declare function laneReviewPath(hqRoot: string, repo: string, pr: number): string;
|
|
35
|
+
export declare function readLaneReview(hqRoot: string, repo: string, pr: number): LaneReview | null;
|
|
36
|
+
export declare function listLaneReviews(hqRoot: string, reviewerLane?: string): LaneReviewListing;
|
|
37
|
+
export declare function recordLaneReview(hqRoot: string, input: RecordLaneReviewInput): Promise<LaneReview>;
|
|
38
|
+
//# sourceMappingURL=reviews.d.ts.map
|