@tarcisiopgs/lisa 1.26.2 → 1.28.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/README.md +21 -5
- package/dist/chunk-2TW2MJXF.js +116 -0
- package/dist/{chunk-CDI22S63.js → chunk-3EOEDL3T.js} +2 -2
- package/dist/{chunk-AGYOJQBR.js → chunk-42NKASE3.js} +8 -106
- package/dist/{chunk-235DMOXJ.js → chunk-5N4BWHIT.js} +186 -116
- package/dist/chunk-FCEUJ7VK.js +407 -0
- package/dist/chunk-UXVSQQID.js +3924 -0
- package/dist/chunk-W73XGHD4.js +3059 -0
- package/dist/{detection-OWEDBW7B.js → detection-JT7HSKSX.js} +2 -1
- package/dist/{guardrails-6IG2C4KJ.js → guardrails-IX3VVUO5.js} +1 -1
- package/dist/index.js +1474 -7340
- package/dist/{kanban-PSOTFEQI.js → kanban-2WMA5VPQ.js} +551 -116
- package/dist/loop-KNJIRK7T.js +21 -0
- package/dist/tui-bridge-DCC4JAPM.js +164 -0
- package/package.json +5 -4
|
@@ -3,27 +3,24 @@ import {
|
|
|
3
3
|
notify
|
|
4
4
|
} from "./chunk-72CYGBT4.js";
|
|
5
5
|
|
|
6
|
-
// src/ui/state.ts
|
|
7
|
-
import { EventEmitter } from "events";
|
|
8
|
-
import { useEffect, useState } from "react";
|
|
9
|
-
|
|
10
|
-
// src/sources/github-issues.ts
|
|
11
|
-
import { execa } from "execa";
|
|
12
|
-
|
|
13
6
|
// src/output/logger.ts
|
|
14
7
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
15
8
|
import { dirname } from "path";
|
|
16
9
|
import pc from "picocolors";
|
|
17
10
|
var logFilePath = null;
|
|
18
11
|
var outputMode = "default";
|
|
12
|
+
var logLevel = "default";
|
|
19
13
|
function setOutputMode(mode) {
|
|
20
14
|
outputMode = mode;
|
|
21
15
|
}
|
|
22
16
|
function getOutputMode() {
|
|
23
17
|
return outputMode;
|
|
24
18
|
}
|
|
19
|
+
function setLogLevel(level) {
|
|
20
|
+
logLevel = level;
|
|
21
|
+
}
|
|
25
22
|
function shouldPrintToConsole() {
|
|
26
|
-
return outputMode !== "tui";
|
|
23
|
+
return outputMode !== "tui" && logLevel !== "quiet";
|
|
27
24
|
}
|
|
28
25
|
function initLogFile(path) {
|
|
29
26
|
const dir = dirname(path);
|
|
@@ -45,7 +42,7 @@ function writeToFile(level, message) {
|
|
|
45
42
|
}
|
|
46
43
|
function log(message) {
|
|
47
44
|
if (shouldPrintToConsole()) {
|
|
48
|
-
console.
|
|
45
|
+
console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
|
|
49
46
|
}
|
|
50
47
|
writeToFile("info", message);
|
|
51
48
|
}
|
|
@@ -63,7 +60,7 @@ function error(message) {
|
|
|
63
60
|
}
|
|
64
61
|
function ok(message) {
|
|
65
62
|
if (shouldPrintToConsole()) {
|
|
66
|
-
console.
|
|
63
|
+
console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
|
|
67
64
|
}
|
|
68
65
|
writeToFile("ok", message);
|
|
69
66
|
}
|
|
@@ -71,34 +68,90 @@ function divider(session) {
|
|
|
71
68
|
log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
|
|
72
69
|
}
|
|
73
70
|
function banner() {
|
|
74
|
-
if (outputMode !== "default") return;
|
|
71
|
+
if (outputMode !== "default" || logLevel === "quiet") return;
|
|
75
72
|
const title = " lisa \u266A autonomous issue resolver ";
|
|
76
73
|
const border = "\u2500".repeat(title.length);
|
|
77
|
-
console.
|
|
74
|
+
console.error(pc.yellow(`
|
|
78
75
|
\u250C${border}\u2510`));
|
|
79
|
-
console.
|
|
80
|
-
console.
|
|
76
|
+
console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
|
|
77
|
+
console.error(pc.yellow(` \u2514${border}\u2518
|
|
81
78
|
`));
|
|
82
79
|
}
|
|
83
80
|
function updateNotice(update) {
|
|
84
|
-
if (outputMode !== "default") return;
|
|
81
|
+
if (outputMode !== "default" || logLevel === "quiet") return;
|
|
85
82
|
const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
|
|
86
83
|
const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
|
|
87
84
|
const lines = [msg, cmd];
|
|
88
85
|
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
89
86
|
const maxLen = Math.max(...lines.map((l) => strip(l).length));
|
|
90
87
|
const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
|
|
91
|
-
console.
|
|
88
|
+
console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
|
|
92
89
|
for (const line of lines) {
|
|
93
|
-
console.
|
|
90
|
+
console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
|
|
94
91
|
}
|
|
95
|
-
console.
|
|
92
|
+
console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
|
|
96
93
|
`));
|
|
97
94
|
}
|
|
98
95
|
|
|
96
|
+
// src/ui/state.ts
|
|
97
|
+
import { EventEmitter } from "events";
|
|
98
|
+
import { useEffect, useState } from "react";
|
|
99
|
+
|
|
99
100
|
// src/sources/github-issues.ts
|
|
100
|
-
|
|
101
|
+
import { execa } from "execa";
|
|
102
|
+
|
|
103
|
+
// src/sources/base.ts
|
|
101
104
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
105
|
+
function normalizeLabels(config) {
|
|
106
|
+
return Array.isArray(config.label) ? config.label : config.label ? [config.label] : [];
|
|
107
|
+
}
|
|
108
|
+
function createApiClient(baseUrl, getHeaders, name) {
|
|
109
|
+
async function request(method, path, body) {
|
|
110
|
+
const url = `${baseUrl}${path}`;
|
|
111
|
+
const headers = {
|
|
112
|
+
...await getHeaders(),
|
|
113
|
+
"Content-Type": "application/json"
|
|
114
|
+
};
|
|
115
|
+
const res = await fetch(url, {
|
|
116
|
+
method,
|
|
117
|
+
headers,
|
|
118
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
119
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const text = await res.text();
|
|
123
|
+
throw new Error(`${name} API error (${res.status}): ${text}`);
|
|
124
|
+
}
|
|
125
|
+
if (method === "DELETE" || res.status === 204) return void 0;
|
|
126
|
+
return await res.json();
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
get: (path) => request("GET", path),
|
|
130
|
+
post: (path, body) => request("POST", path, body),
|
|
131
|
+
put: (path, body) => request("PUT", path, body),
|
|
132
|
+
patch: (path, body) => request("PATCH", path, body),
|
|
133
|
+
delete: (path) => request("DELETE", path),
|
|
134
|
+
/** Raw request for non-JSON bodies (e.g. Trello form-encoded). */
|
|
135
|
+
raw: async (method, path, init) => {
|
|
136
|
+
const url = `${baseUrl}${path}`;
|
|
137
|
+
const headers = await getHeaders();
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
method,
|
|
140
|
+
headers: { ...headers, ...init?.headers },
|
|
141
|
+
body: init?.body,
|
|
142
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const text = await res.text();
|
|
146
|
+
throw new Error(`${name} API error (${res.status}): ${text}`);
|
|
147
|
+
}
|
|
148
|
+
if (method === "DELETE" || res.status === 204) return void 0;
|
|
149
|
+
return await res.json();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/sources/github-issues.ts
|
|
102
155
|
var PRIORITY_LABELS = ["p1", "p2", "p3"];
|
|
103
156
|
var DEPENDENCY_PATTERN = /(?:depends\s+on|blocked\s+by)\s+#(\d+)/gi;
|
|
104
157
|
async function getToken() {
|
|
@@ -118,36 +171,12 @@ async function getAuthHeaders() {
|
|
|
118
171
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
119
172
|
};
|
|
120
173
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"Content-Type": "application/json"
|
|
126
|
-
};
|
|
127
|
-
const res = await fetch(url, {
|
|
128
|
-
method,
|
|
129
|
-
headers,
|
|
130
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
131
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
132
|
-
});
|
|
133
|
-
if (!res.ok) {
|
|
134
|
-
const text = await res.text();
|
|
135
|
-
throw new Error(`GitHub API error (${res.status}): ${text}`);
|
|
174
|
+
var _api;
|
|
175
|
+
function api() {
|
|
176
|
+
if (!_api) {
|
|
177
|
+
_api = createApiClient("https://api.github.com", getAuthHeaders, "GitHub");
|
|
136
178
|
}
|
|
137
|
-
|
|
138
|
-
return await res.json();
|
|
139
|
-
}
|
|
140
|
-
async function githubGet(path) {
|
|
141
|
-
return githubFetch("GET", path);
|
|
142
|
-
}
|
|
143
|
-
async function githubPost(path, body) {
|
|
144
|
-
return githubFetch("POST", path, body);
|
|
145
|
-
}
|
|
146
|
-
async function githubPatch(path, body) {
|
|
147
|
-
return githubFetch("PATCH", path, body);
|
|
148
|
-
}
|
|
149
|
-
async function githubDelete(path) {
|
|
150
|
-
await githubFetch("DELETE", path);
|
|
179
|
+
return _api;
|
|
151
180
|
}
|
|
152
181
|
function parseGitHubPrUrl(url) {
|
|
153
182
|
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
@@ -160,7 +189,7 @@ async function checkPrMerged(prUrl) {
|
|
|
160
189
|
const parsed = parseGitHubPrUrl(prUrl);
|
|
161
190
|
if (!parsed) return false;
|
|
162
191
|
try {
|
|
163
|
-
const pr = await
|
|
192
|
+
const pr = await api().get(
|
|
164
193
|
`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`
|
|
165
194
|
);
|
|
166
195
|
return pr.merged === true;
|
|
@@ -216,10 +245,10 @@ var GitHubIssuesSource = class {
|
|
|
216
245
|
const { owner, repo } = parseOwnerRepo(config.scope);
|
|
217
246
|
const validStates = ["open", "closed", "all"];
|
|
218
247
|
const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
|
|
219
|
-
const filterLabels = isOrphanDetection ? [config.pick_from] :
|
|
248
|
+
const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
|
|
220
249
|
const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
|
|
221
250
|
const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
|
|
222
|
-
const issues = (await
|
|
251
|
+
const issues = (await api().get(path)).filter((i) => !i.pull_request);
|
|
223
252
|
if (issues.length === 0) return null;
|
|
224
253
|
const unblocked = [];
|
|
225
254
|
const blocked = [];
|
|
@@ -234,7 +263,7 @@ var GitHubIssuesSource = class {
|
|
|
234
263
|
const closedBlockers = [];
|
|
235
264
|
for (const depNum of depNumbers) {
|
|
236
265
|
try {
|
|
237
|
-
const dep = await
|
|
266
|
+
const dep = await api().get(`/repos/${owner}/${repo}/issues/${depNum}`);
|
|
238
267
|
if (!dep.state || dep.state === "open") {
|
|
239
268
|
activeBlockers.push(depNum);
|
|
240
269
|
} else {
|
|
@@ -284,14 +313,15 @@ var GitHubIssuesSource = class {
|
|
|
284
313
|
async fetchIssueById(id) {
|
|
285
314
|
const ref = parseGitHubIssueNumber(id);
|
|
286
315
|
try {
|
|
287
|
-
const issue = await
|
|
316
|
+
const issue = await api().get(
|
|
288
317
|
`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`
|
|
289
318
|
);
|
|
290
319
|
return {
|
|
291
320
|
id: makeIssueId(ref.owner, ref.repo, issue.number),
|
|
292
321
|
title: issue.title,
|
|
293
322
|
description: issue.body ?? "",
|
|
294
|
-
url: issue.html_url
|
|
323
|
+
url: issue.html_url,
|
|
324
|
+
status: issue.state
|
|
295
325
|
};
|
|
296
326
|
} catch {
|
|
297
327
|
return null;
|
|
@@ -300,15 +330,15 @@ var GitHubIssuesSource = class {
|
|
|
300
330
|
async updateStatus(issueId, labelToAdd, config) {
|
|
301
331
|
const ref = parseGitHubIssueNumber(issueId);
|
|
302
332
|
if (config && config.in_progress !== config.pick_from) {
|
|
303
|
-
const filterLabels =
|
|
333
|
+
const filterLabels = normalizeLabels(config);
|
|
304
334
|
const isMovingToInProgress = labelToAdd === config.in_progress;
|
|
305
335
|
if (isMovingToInProgress) {
|
|
306
|
-
await
|
|
336
|
+
await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
|
|
307
337
|
labels: [labelToAdd]
|
|
308
338
|
});
|
|
309
339
|
for (const label of filterLabels) {
|
|
310
340
|
try {
|
|
311
|
-
await
|
|
341
|
+
await api().delete(
|
|
312
342
|
`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(label)}`
|
|
313
343
|
);
|
|
314
344
|
} catch {
|
|
@@ -316,30 +346,30 @@ var GitHubIssuesSource = class {
|
|
|
316
346
|
}
|
|
317
347
|
return;
|
|
318
348
|
}
|
|
319
|
-
await
|
|
349
|
+
await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
|
|
320
350
|
labels: filterLabels
|
|
321
351
|
});
|
|
322
352
|
try {
|
|
323
|
-
await
|
|
353
|
+
await api().delete(
|
|
324
354
|
`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
|
|
325
355
|
);
|
|
326
356
|
} catch {
|
|
327
357
|
}
|
|
328
358
|
return;
|
|
329
359
|
}
|
|
330
|
-
await
|
|
360
|
+
await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
|
|
331
361
|
labels: [labelToAdd]
|
|
332
362
|
});
|
|
333
363
|
}
|
|
334
364
|
async attachPullRequest(issueId, prUrl) {
|
|
335
365
|
const ref = parseGitHubIssueNumber(issueId);
|
|
336
|
-
await
|
|
366
|
+
await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments`, {
|
|
337
367
|
body: `Pull request: ${prUrl}`
|
|
338
368
|
});
|
|
339
369
|
}
|
|
340
370
|
async completeIssue(issueId, _status, labelToRemove, config) {
|
|
341
371
|
const ref = parseGitHubIssueNumber(issueId);
|
|
342
|
-
await
|
|
372
|
+
await api().patch(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, {
|
|
343
373
|
state: "closed"
|
|
344
374
|
});
|
|
345
375
|
if (labelToRemove) {
|
|
@@ -347,7 +377,7 @@ var GitHubIssuesSource = class {
|
|
|
347
377
|
}
|
|
348
378
|
if (config && config.in_progress !== config.pick_from) {
|
|
349
379
|
try {
|
|
350
|
-
await
|
|
380
|
+
await api().delete(
|
|
351
381
|
`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
|
|
352
382
|
);
|
|
353
383
|
} catch {
|
|
@@ -356,10 +386,10 @@ var GitHubIssuesSource = class {
|
|
|
356
386
|
}
|
|
357
387
|
async listIssues(config) {
|
|
358
388
|
const { owner, repo } = parseOwnerRepo(config.scope);
|
|
359
|
-
const labels =
|
|
389
|
+
const labels = normalizeLabels(config);
|
|
360
390
|
const label = labels.map((l) => encodeURIComponent(l)).join(",");
|
|
361
391
|
const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
|
|
362
|
-
const issues = (await
|
|
392
|
+
const issues = (await api().get(path)).filter((i) => !i.pull_request);
|
|
363
393
|
return issues.map((issue) => ({
|
|
364
394
|
id: makeIssueId(owner, repo, issue.number),
|
|
365
395
|
title: issue.title,
|
|
@@ -372,7 +402,7 @@ var GitHubIssuesSource = class {
|
|
|
372
402
|
const results = [];
|
|
373
403
|
let page = 1;
|
|
374
404
|
while (true) {
|
|
375
|
-
const labels = await
|
|
405
|
+
const labels = await api().get(
|
|
376
406
|
`/repos/${owner}/${repo}/labels?per_page=100&page=${page}`
|
|
377
407
|
);
|
|
378
408
|
for (const l of labels) {
|
|
@@ -389,17 +419,26 @@ var GitHubIssuesSource = class {
|
|
|
389
419
|
async removeLabel(issueId, labelToRemove) {
|
|
390
420
|
const ref = parseGitHubIssueNumber(issueId);
|
|
391
421
|
try {
|
|
392
|
-
await
|
|
422
|
+
await api().delete(
|
|
393
423
|
`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(labelToRemove)}`
|
|
394
424
|
);
|
|
395
425
|
} catch {
|
|
396
426
|
}
|
|
397
427
|
}
|
|
428
|
+
async createIssue(opts, config) {
|
|
429
|
+
const { owner, repo } = parseOwnerRepo(config.scope);
|
|
430
|
+
const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
|
|
431
|
+
const issue = await api().post(`/repos/${owner}/${repo}/issues`, {
|
|
432
|
+
title: opts.title,
|
|
433
|
+
body: opts.description,
|
|
434
|
+
labels
|
|
435
|
+
});
|
|
436
|
+
return makeIssueId(owner, repo, issue.number);
|
|
437
|
+
}
|
|
398
438
|
};
|
|
399
439
|
|
|
400
440
|
// src/sources/gitlab-issues.ts
|
|
401
441
|
var DEFAULT_BASE_URL = "https://gitlab.com";
|
|
402
|
-
var REQUEST_TIMEOUT_MS2 = 3e4;
|
|
403
442
|
var PRIORITY_LABELS2 = ["p1", "p2", "p3"];
|
|
404
443
|
function getBaseUrl() {
|
|
405
444
|
return (process.env.GITLAB_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
@@ -409,33 +448,12 @@ function getAuthHeaders2() {
|
|
|
409
448
|
if (!token) throw new Error("GITLAB_TOKEN must be set");
|
|
410
449
|
return { "PRIVATE-TOKEN": token };
|
|
411
450
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
"Content-Type": "application/json"
|
|
417
|
-
};
|
|
418
|
-
const res = await fetch(url, {
|
|
419
|
-
method,
|
|
420
|
-
headers,
|
|
421
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
422
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
423
|
-
});
|
|
424
|
-
if (!res.ok) {
|
|
425
|
-
const text = await res.text();
|
|
426
|
-
throw new Error(`GitLab API error (${res.status}): ${text}`);
|
|
451
|
+
var _api2;
|
|
452
|
+
function api2() {
|
|
453
|
+
if (!_api2) {
|
|
454
|
+
_api2 = createApiClient(`${getBaseUrl()}/api/v4`, getAuthHeaders2, "GitLab");
|
|
427
455
|
}
|
|
428
|
-
|
|
429
|
-
return await res.json();
|
|
430
|
-
}
|
|
431
|
-
async function gitlabGet(path) {
|
|
432
|
-
return gitlabFetch("GET", path);
|
|
433
|
-
}
|
|
434
|
-
async function gitlabPost(path, body) {
|
|
435
|
-
return gitlabFetch("POST", path, body);
|
|
436
|
-
}
|
|
437
|
-
async function gitlabPut(path, body) {
|
|
438
|
-
return gitlabFetch("PUT", path, body);
|
|
456
|
+
return _api2;
|
|
439
457
|
}
|
|
440
458
|
function parseGitLabMrUrl(url) {
|
|
441
459
|
const match = url.match(/gitlab(?:\.com|[^/]*)\/(.+?)\/-\/merge_requests\/(\d+)/);
|
|
@@ -449,7 +467,7 @@ async function checkPrMerged2(prUrl) {
|
|
|
449
467
|
if (!parsed) return false;
|
|
450
468
|
try {
|
|
451
469
|
const encodedProject = parseGitLabProject(parsed.project);
|
|
452
|
-
const mr = await
|
|
470
|
+
const mr = await api2().get(
|
|
453
471
|
`/projects/${encodedProject}/merge_requests/${parsed.iid}`
|
|
454
472
|
);
|
|
455
473
|
return mr.state === "merged";
|
|
@@ -480,15 +498,15 @@ var GitLabIssuesSource = class {
|
|
|
480
498
|
const project = parseGitLabProject(config.scope);
|
|
481
499
|
const validStates = ["opened", "closed", "all"];
|
|
482
500
|
const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
|
|
483
|
-
const filterLabels = isOrphanDetection ? [config.pick_from] :
|
|
501
|
+
const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
|
|
484
502
|
const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
|
|
485
503
|
const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
|
|
486
|
-
const issues = await
|
|
504
|
+
const issues = await api2().get(path);
|
|
487
505
|
if (issues.length === 0) return null;
|
|
488
506
|
const unblocked = [];
|
|
489
507
|
const blocked = [];
|
|
490
508
|
for (const issue2 of issues) {
|
|
491
|
-
const links = await
|
|
509
|
+
const links = await api2().get(
|
|
492
510
|
`/projects/${project}/issues/${issue2.iid}/links`
|
|
493
511
|
);
|
|
494
512
|
const activeBlockers = links.filter((link) => {
|
|
@@ -533,12 +551,13 @@ var GitLabIssuesSource = class {
|
|
|
533
551
|
const ref = parseGitLabIssueRef(id);
|
|
534
552
|
try {
|
|
535
553
|
const project = parseGitLabProject(ref.project);
|
|
536
|
-
const issue = await
|
|
554
|
+
const issue = await api2().get(`/projects/${project}/issues/${ref.iid}`);
|
|
537
555
|
return {
|
|
538
556
|
id: makeIssueId2(ref.project, issue.iid),
|
|
539
557
|
title: issue.title,
|
|
540
558
|
description: issue.description ?? "",
|
|
541
|
-
url: issue.web_url
|
|
559
|
+
url: issue.web_url,
|
|
560
|
+
status: issue.state
|
|
542
561
|
};
|
|
543
562
|
} catch {
|
|
544
563
|
return null;
|
|
@@ -547,15 +566,15 @@ var GitLabIssuesSource = class {
|
|
|
547
566
|
async updateStatus(issueId, labelToAdd, config) {
|
|
548
567
|
const { project, iid } = splitIssueId(issueId);
|
|
549
568
|
const encodedProject = parseGitLabProject(project);
|
|
550
|
-
const issue = await
|
|
569
|
+
const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
|
|
551
570
|
if (config && config.in_progress !== config.pick_from) {
|
|
552
|
-
const filterLabels =
|
|
571
|
+
const filterLabels = normalizeLabels(config);
|
|
553
572
|
const isMovingToInProgress = labelToAdd === config.in_progress;
|
|
554
573
|
if (isMovingToInProgress) {
|
|
555
574
|
const updated2 = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])].filter(
|
|
556
575
|
(l) => !filterLabels.includes(l)
|
|
557
576
|
);
|
|
558
|
-
await
|
|
577
|
+
await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
|
|
559
578
|
labels: updated2.join(",")
|
|
560
579
|
});
|
|
561
580
|
return;
|
|
@@ -563,40 +582,40 @@ var GitLabIssuesSource = class {
|
|
|
563
582
|
const updated = [.../* @__PURE__ */ new Set([...issue.labels, ...filterLabels])].filter(
|
|
564
583
|
(l) => l !== config.in_progress
|
|
565
584
|
);
|
|
566
|
-
await
|
|
585
|
+
await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
|
|
567
586
|
labels: updated.join(",")
|
|
568
587
|
});
|
|
569
588
|
return;
|
|
570
589
|
}
|
|
571
590
|
const labels = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])];
|
|
572
|
-
await
|
|
591
|
+
await api2().put(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
|
|
573
592
|
}
|
|
574
593
|
async attachPullRequest(issueId, prUrl) {
|
|
575
594
|
const { project, iid } = splitIssueId(issueId);
|
|
576
595
|
const encodedProject = parseGitLabProject(project);
|
|
577
|
-
await
|
|
596
|
+
await api2().post(`/projects/${encodedProject}/issues/${iid}/notes`, {
|
|
578
597
|
body: `Pull request: ${prUrl}`
|
|
579
598
|
});
|
|
580
599
|
}
|
|
581
600
|
async completeIssue(issueId, _status, labelToRemove, config) {
|
|
582
601
|
const { project, iid } = splitIssueId(issueId);
|
|
583
602
|
const encodedProject = parseGitLabProject(project);
|
|
584
|
-
const issue = await
|
|
603
|
+
const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
|
|
585
604
|
let labels = labelToRemove ? issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue.labels;
|
|
586
605
|
if (config && config.in_progress !== config.pick_from) {
|
|
587
606
|
labels = labels.filter((l) => l !== config.in_progress);
|
|
588
607
|
}
|
|
589
|
-
await
|
|
608
|
+
await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
|
|
590
609
|
state_event: "close",
|
|
591
610
|
labels: labels.join(",")
|
|
592
611
|
});
|
|
593
612
|
}
|
|
594
613
|
async listIssues(config) {
|
|
595
614
|
const project = parseGitLabProject(config.scope);
|
|
596
|
-
const labelsArr =
|
|
615
|
+
const labelsArr = normalizeLabels(config);
|
|
597
616
|
const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
|
|
598
617
|
const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
|
|
599
|
-
const issues = await
|
|
618
|
+
const issues = await api2().get(path);
|
|
600
619
|
return issues.map((issue) => ({
|
|
601
620
|
id: makeIssueId2(config.scope, issue.iid),
|
|
602
621
|
title: issue.title,
|
|
@@ -609,7 +628,7 @@ var GitLabIssuesSource = class {
|
|
|
609
628
|
const results = [];
|
|
610
629
|
let page = 1;
|
|
611
630
|
while (true) {
|
|
612
|
-
const labels = await
|
|
631
|
+
const labels = await api2().get(
|
|
613
632
|
`/projects/${project}/labels?per_page=100&page=${page}`
|
|
614
633
|
);
|
|
615
634
|
for (const l of labels) {
|
|
@@ -626,13 +645,37 @@ var GitLabIssuesSource = class {
|
|
|
626
645
|
async removeLabel(issueId, labelToRemove) {
|
|
627
646
|
const { project, iid } = splitIssueId(issueId);
|
|
628
647
|
const encodedProject = parseGitLabProject(project);
|
|
629
|
-
const issue = await
|
|
648
|
+
const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
|
|
630
649
|
const filtered = issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase());
|
|
631
650
|
if (filtered.length === issue.labels.length) return;
|
|
632
|
-
await
|
|
651
|
+
await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
|
|
633
652
|
labels: filtered.join(",")
|
|
634
653
|
});
|
|
635
654
|
}
|
|
655
|
+
async createIssue(opts, config) {
|
|
656
|
+
const encodedProject = parseGitLabProject(config.scope);
|
|
657
|
+
const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
|
|
658
|
+
const issue = await api2().post(`/projects/${encodedProject}/issues`, {
|
|
659
|
+
title: opts.title,
|
|
660
|
+
description: opts.description,
|
|
661
|
+
labels: labels.join(","),
|
|
662
|
+
...opts.order !== void 0 && { weight: opts.order }
|
|
663
|
+
});
|
|
664
|
+
return makeIssueId2(config.scope, issue.iid);
|
|
665
|
+
}
|
|
666
|
+
async linkDependency(issueId, dependsOnId) {
|
|
667
|
+
const source = splitIssueId(issueId);
|
|
668
|
+
const target = splitIssueId(dependsOnId);
|
|
669
|
+
const encodedProject = parseGitLabProject(source.project);
|
|
670
|
+
const projectInfo = await api2().get(
|
|
671
|
+
`/projects/${parseGitLabProject(target.project)}`
|
|
672
|
+
);
|
|
673
|
+
await api2().post(`/projects/${encodedProject}/issues/${source.iid}/links`, {
|
|
674
|
+
target_project_id: projectInfo.id,
|
|
675
|
+
target_issue_iid: Number(target.iid),
|
|
676
|
+
link_type: "is_blocked_by"
|
|
677
|
+
});
|
|
678
|
+
}
|
|
636
679
|
};
|
|
637
680
|
function parseGitLabProject(input) {
|
|
638
681
|
if (/^\d+$/.test(input)) return input;
|
|
@@ -819,11 +862,18 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
819
862
|
setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, logFile } : c));
|
|
820
863
|
};
|
|
821
864
|
const MAX_OUTPUT_SIZE = 2e5;
|
|
822
|
-
const
|
|
865
|
+
const outputBuffer = /* @__PURE__ */ new Map();
|
|
866
|
+
let flushTimer = null;
|
|
867
|
+
const flushOutputBuffer = () => {
|
|
868
|
+
flushTimer = null;
|
|
869
|
+
if (outputBuffer.size === 0) return;
|
|
870
|
+
const buffered = new Map(outputBuffer);
|
|
871
|
+
outputBuffer.clear();
|
|
823
872
|
setCards(
|
|
824
873
|
(prev) => prev.map((c) => {
|
|
825
|
-
|
|
826
|
-
|
|
874
|
+
const chunk = buffered.get(c.id);
|
|
875
|
+
if (chunk === void 0) return c;
|
|
876
|
+
let newLog = c.outputLog + chunk;
|
|
827
877
|
if (newLog.length > MAX_OUTPUT_SIZE) {
|
|
828
878
|
const trimAt = newLog.indexOf("\n", newLog.length - MAX_OUTPUT_SIZE);
|
|
829
879
|
newLog = trimAt !== -1 ? newLog.slice(trimAt + 1) : newLog.slice(-MAX_OUTPUT_SIZE);
|
|
@@ -832,7 +882,15 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
832
882
|
})
|
|
833
883
|
);
|
|
834
884
|
};
|
|
885
|
+
const onOutput = (issueId, text) => {
|
|
886
|
+
const existing = outputBuffer.get(issueId);
|
|
887
|
+
outputBuffer.set(issueId, existing !== void 0 ? existing + text : text);
|
|
888
|
+
if (flushTimer === null) {
|
|
889
|
+
flushTimer = setTimeout(flushOutputBuffer, 100);
|
|
890
|
+
}
|
|
891
|
+
};
|
|
835
892
|
const onReconcileRemove = (issueId) => {
|
|
893
|
+
outputBuffer.delete(issueId);
|
|
836
894
|
setCards((prev) => prev.filter((c) => c.id !== issueId));
|
|
837
895
|
};
|
|
838
896
|
kanbanEmitter.on("issue:queued", onQueued);
|
|
@@ -850,6 +908,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
850
908
|
const onModelChanged = (model) => setModelInUse(model);
|
|
851
909
|
kanbanEmitter.on("provider:model-changed", onModelChanged);
|
|
852
910
|
const onEmpty = () => setIsEmpty(true);
|
|
911
|
+
const onResumed = () => setIsEmpty(false);
|
|
853
912
|
const onComplete = (data) => setWorkComplete(data);
|
|
854
913
|
const onWatching = () => setIsWatching(true);
|
|
855
914
|
const onWatchResume = () => setIsWatching(false);
|
|
@@ -859,6 +918,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
859
918
|
};
|
|
860
919
|
const onWatchPromptResolved = () => setIsWatchPrompt(false);
|
|
861
920
|
kanbanEmitter.on("work:empty", onEmpty);
|
|
921
|
+
kanbanEmitter.on("work:resumed", onResumed);
|
|
862
922
|
kanbanEmitter.on("work:complete", onComplete);
|
|
863
923
|
kanbanEmitter.on("work:watching", onWatching);
|
|
864
924
|
kanbanEmitter.on("work:watch-resume", onWatchResume);
|
|
@@ -866,6 +926,11 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
866
926
|
kanbanEmitter.on("work:watch-prompt-resolved", onWatchPromptResolved);
|
|
867
927
|
const cleanupBell = registerBellListeners(bellEnabled);
|
|
868
928
|
return () => {
|
|
929
|
+
if (flushTimer !== null) {
|
|
930
|
+
clearTimeout(flushTimer);
|
|
931
|
+
flushTimer = null;
|
|
932
|
+
}
|
|
933
|
+
outputBuffer.clear();
|
|
869
934
|
kanbanEmitter.off("issue:queued", onQueued);
|
|
870
935
|
kanbanEmitter.off("issue:started", onStarted);
|
|
871
936
|
kanbanEmitter.off("issue:done", onDone);
|
|
@@ -880,6 +945,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
880
945
|
kanbanEmitter.off("issue:output", onOutput);
|
|
881
946
|
kanbanEmitter.off("provider:model-changed", onModelChanged);
|
|
882
947
|
kanbanEmitter.off("work:empty", onEmpty);
|
|
948
|
+
kanbanEmitter.off("work:resumed", onResumed);
|
|
883
949
|
kanbanEmitter.off("work:complete", onComplete);
|
|
884
950
|
kanbanEmitter.off("work:watching", onWatching);
|
|
885
951
|
kanbanEmitter.off("work:watch-resume", onWatchResume);
|
|
@@ -906,6 +972,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
|
|
|
906
972
|
export {
|
|
907
973
|
setOutputMode,
|
|
908
974
|
getOutputMode,
|
|
975
|
+
setLogLevel,
|
|
909
976
|
initLogFile,
|
|
910
977
|
log,
|
|
911
978
|
warn,
|
|
@@ -914,6 +981,9 @@ export {
|
|
|
914
981
|
divider,
|
|
915
982
|
banner,
|
|
916
983
|
updateNotice,
|
|
984
|
+
REQUEST_TIMEOUT_MS,
|
|
985
|
+
normalizeLabels,
|
|
986
|
+
createApiClient,
|
|
917
987
|
GitHubIssuesSource,
|
|
918
988
|
GitLabIssuesSource,
|
|
919
989
|
kanbanEmitter,
|