@h-rig/standard-plugin 0.0.6-alpha.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 +1 -0
- package/dist/src/files-source.js +106 -0
- package/dist/src/github-issues-source.js +496 -0
- package/dist/src/index.js +660 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @h-rig/standard-plugin
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/standard-plugin/src/files-source.ts
|
|
3
|
+
import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { join, basename } from "path";
|
|
5
|
+
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
6
|
+
function readTaskFile(file, pattern) {
|
|
7
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
8
|
+
const inferredId = basename(file).replace(pattern, "");
|
|
9
|
+
const labels = Array.isArray(raw.labels) ? raw.labels.filter((label) => typeof label === "string") : [];
|
|
10
|
+
const scope = labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
11
|
+
const validators = labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
12
|
+
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
13
|
+
return {
|
|
14
|
+
id: raw["id"] ?? inferredId,
|
|
15
|
+
deps: raw["deps"] ?? raw["depends_on"] ?? [],
|
|
16
|
+
status: raw["status"] ?? "ready",
|
|
17
|
+
...scope.length > 0 ? { scope } : {},
|
|
18
|
+
...roleLabel ? { role: roleLabel.slice("role:".length) } : {},
|
|
19
|
+
...validators.length > 0 ? { validators, validation: validators } : {},
|
|
20
|
+
...raw
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function createFilesTaskSource(opts) {
|
|
24
|
+
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
25
|
+
const directory = opts.path ?? opts.dir;
|
|
26
|
+
if (!directory) {
|
|
27
|
+
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
28
|
+
}
|
|
29
|
+
const findTaskFile = (id) => {
|
|
30
|
+
if (!existsSync(directory))
|
|
31
|
+
return;
|
|
32
|
+
for (const name of readdirSync(directory)) {
|
|
33
|
+
if (!pattern.test(name))
|
|
34
|
+
continue;
|
|
35
|
+
const p = join(directory, name);
|
|
36
|
+
try {
|
|
37
|
+
if (!statSync(p).isFile())
|
|
38
|
+
continue;
|
|
39
|
+
if (readTaskFile(p, pattern).id === id)
|
|
40
|
+
return p;
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
};
|
|
45
|
+
const applyUpdate = (id, update) => {
|
|
46
|
+
const file = findTaskFile(id);
|
|
47
|
+
if (!file) {
|
|
48
|
+
throw new Error(`files task not found: ${id}`);
|
|
49
|
+
}
|
|
50
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
51
|
+
if (update.status)
|
|
52
|
+
raw.status = update.status;
|
|
53
|
+
if (update.title !== undefined)
|
|
54
|
+
raw.title = update.title;
|
|
55
|
+
if (update.body !== undefined)
|
|
56
|
+
raw.body = update.body;
|
|
57
|
+
if (update.comment?.trim()) {
|
|
58
|
+
const existing = Array.isArray(raw.comments) ? raw.comments : [];
|
|
59
|
+
raw.comments = [
|
|
60
|
+
...existing,
|
|
61
|
+
{
|
|
62
|
+
body: update.comment,
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
source: "rig"
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(file, `${JSON.stringify(raw, null, 2)}
|
|
69
|
+
`, "utf-8");
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
id: "std:files",
|
|
73
|
+
kind: "files",
|
|
74
|
+
async list() {
|
|
75
|
+
if (!existsSync(directory))
|
|
76
|
+
return [];
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const name of readdirSync(directory)) {
|
|
79
|
+
if (!pattern.test(name))
|
|
80
|
+
continue;
|
|
81
|
+
const p = join(directory, name);
|
|
82
|
+
try {
|
|
83
|
+
if (!statSync(p).isFile())
|
|
84
|
+
continue;
|
|
85
|
+
out.push(readTaskFile(p, pattern));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn(`[files-source] skipped ${name}: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
},
|
|
92
|
+
async get(id) {
|
|
93
|
+
const all = await this.list();
|
|
94
|
+
return all.find((t) => t.id === id);
|
|
95
|
+
},
|
|
96
|
+
async updateStatus(id, status) {
|
|
97
|
+
applyUpdate(id, { status });
|
|
98
|
+
},
|
|
99
|
+
async updateTask(id, update) {
|
|
100
|
+
applyUpdate(id, update);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
createFilesTaskSource
|
|
106
|
+
};
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/standard-plugin/src/github-issues-source.ts
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
function cleanToken(value) {
|
|
7
|
+
const trimmed = value?.trim() ?? "";
|
|
8
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
9
|
+
}
|
|
10
|
+
function createEnvGitHubCredentialProvider() {
|
|
11
|
+
return {
|
|
12
|
+
async resolveGitHubToken(input) {
|
|
13
|
+
if (input.purpose === "selected-repo") {
|
|
14
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
15
|
+
}
|
|
16
|
+
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
19
|
+
}
|
|
20
|
+
return { token, source: "host-admin-fallback" };
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function createStateGitHubCredentialProvider(options = {}) {
|
|
25
|
+
const resolveStateFile = () => {
|
|
26
|
+
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
27
|
+
if (explicitFile?.trim())
|
|
28
|
+
return resolve(explicitFile.trim());
|
|
29
|
+
const stateDir = options.stateDir ?? process.env.RIG_STATE_DIR;
|
|
30
|
+
return stateDir?.trim() ? resolve(stateDir.trim(), "github-auth.json") : null;
|
|
31
|
+
};
|
|
32
|
+
const readToken = () => {
|
|
33
|
+
const stateFile = resolveStateFile();
|
|
34
|
+
if (!stateFile || !existsSync(stateFile))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
38
|
+
return typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
async resolveGitHubToken(input) {
|
|
45
|
+
const token = readToken();
|
|
46
|
+
if (input.purpose === "selected-repo") {
|
|
47
|
+
return { token: token ?? "", source: "signed-in-user" };
|
|
48
|
+
}
|
|
49
|
+
if (token) {
|
|
50
|
+
return { token, source: "signed-in-user" };
|
|
51
|
+
}
|
|
52
|
+
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
53
|
+
if (!fallback) {
|
|
54
|
+
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
55
|
+
}
|
|
56
|
+
return { token: fallback, source: "host-admin-fallback" };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
61
|
+
var RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
|
|
62
|
+
var RIG_METADATA_START = "<!-- rig:metadata:start -->";
|
|
63
|
+
var RIG_METADATA_END = "<!-- rig:metadata:end -->";
|
|
64
|
+
var RIG_STATUS_LABELS = new Set(["rig:running", "rig:pr-open", "rig:ci-fixing", "rig:merging", "rig:done", "rig:needs-attention"]);
|
|
65
|
+
var DEFAULT_GH_TIMEOUT_MS = 15000;
|
|
66
|
+
var DEFAULT_GITHUB_ISSUE_LIST_LIMIT = 1000;
|
|
67
|
+
function statusFor(issue) {
|
|
68
|
+
const state = (issue.state ?? "").toUpperCase();
|
|
69
|
+
if (state === "CLOSED")
|
|
70
|
+
return "closed";
|
|
71
|
+
const labelNames = labelNamesFor(issue);
|
|
72
|
+
if (labelNames.includes("in-progress"))
|
|
73
|
+
return "in_progress";
|
|
74
|
+
if (labelNames.includes("blocked"))
|
|
75
|
+
return "blocked";
|
|
76
|
+
if (labelNames.includes("ready"))
|
|
77
|
+
return "ready";
|
|
78
|
+
if (labelNames.includes("under-review"))
|
|
79
|
+
return "under_review";
|
|
80
|
+
if (labelNames.includes("failed"))
|
|
81
|
+
return "failed";
|
|
82
|
+
if (labelNames.includes("cancelled"))
|
|
83
|
+
return "cancelled";
|
|
84
|
+
return "open";
|
|
85
|
+
}
|
|
86
|
+
function parseDeps(body) {
|
|
87
|
+
const match = body.match(/^depends-on:\s*([^\n]+)/im);
|
|
88
|
+
if (!match)
|
|
89
|
+
return [];
|
|
90
|
+
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
91
|
+
}
|
|
92
|
+
function parseParents(body) {
|
|
93
|
+
const match = body.match(/^parents?:\s*([^\n]+)/im);
|
|
94
|
+
if (!match)
|
|
95
|
+
return [];
|
|
96
|
+
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
97
|
+
}
|
|
98
|
+
function issueTypeFor(issue) {
|
|
99
|
+
const labels = labelNamesFor(issue);
|
|
100
|
+
const typed = labels.find((l) => l.startsWith("type:"));
|
|
101
|
+
if (typed)
|
|
102
|
+
return typed.slice("type:".length);
|
|
103
|
+
if (labels.includes("epic"))
|
|
104
|
+
return "epic";
|
|
105
|
+
return "task";
|
|
106
|
+
}
|
|
107
|
+
function issueToTask(issue, repo) {
|
|
108
|
+
const labelNames = labelNamesFor(issue);
|
|
109
|
+
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
110
|
+
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
111
|
+
const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
|
|
112
|
+
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
113
|
+
const body = issue.body ?? "";
|
|
114
|
+
return {
|
|
115
|
+
id: String(issue.number),
|
|
116
|
+
deps: parseDeps(body),
|
|
117
|
+
status: statusFor(issue),
|
|
118
|
+
title: issue.title,
|
|
119
|
+
body,
|
|
120
|
+
scope,
|
|
121
|
+
role,
|
|
122
|
+
validators,
|
|
123
|
+
url: issue.url ?? issue.html_url,
|
|
124
|
+
issueType: issueTypeFor(issue),
|
|
125
|
+
sourceIssueId: `${repo}#${issue.number}`,
|
|
126
|
+
parentChildDeps: parseParents(body),
|
|
127
|
+
labels: labelNames,
|
|
128
|
+
raw: issue
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function labelNamesFor(issue) {
|
|
132
|
+
return (issue.labels ?? []).flatMap((label) => {
|
|
133
|
+
if (typeof label === "string")
|
|
134
|
+
return [label];
|
|
135
|
+
return typeof label.name === "string" ? [label.name] : [];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function yamlScalar(value) {
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.length === 0 ? "[]" : `
|
|
141
|
+
${value.map((entry) => ` - ${String(entry)}`).join(`
|
|
142
|
+
`)}`;
|
|
143
|
+
}
|
|
144
|
+
if (value && typeof value === "object")
|
|
145
|
+
return JSON.stringify(value);
|
|
146
|
+
return String(value);
|
|
147
|
+
}
|
|
148
|
+
function updateRigOwnedMetadataBlock(body, metadata) {
|
|
149
|
+
const rendered = [
|
|
150
|
+
RIG_METADATA_START,
|
|
151
|
+
...Object.entries(metadata).map(([key, value]) => Array.isArray(value) ? `${key}:${yamlScalar(value)}` : `${key}: ${yamlScalar(value)}`),
|
|
152
|
+
RIG_METADATA_END
|
|
153
|
+
].join(`
|
|
154
|
+
`);
|
|
155
|
+
const pattern = new RegExp(`${RIG_METADATA_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\\s\\S]*?\\s*${RIG_METADATA_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
156
|
+
if (pattern.test(body))
|
|
157
|
+
return body.replace(pattern, rendered);
|
|
158
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
159
|
+
|
|
160
|
+
${rendered}
|
|
161
|
+
` : `${rendered}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
function buildRigStickyStatusComment(input) {
|
|
165
|
+
const lines = [
|
|
166
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
167
|
+
`### Rig status: ${input.status}`,
|
|
168
|
+
"",
|
|
169
|
+
input.summary
|
|
170
|
+
];
|
|
171
|
+
if (input.runId)
|
|
172
|
+
lines.push("", `- Run: ${input.runId}`);
|
|
173
|
+
if (input.prUrl)
|
|
174
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
175
|
+
for (const detail of input.details ?? [])
|
|
176
|
+
lines.push(`- ${detail}`);
|
|
177
|
+
return lines.join(`
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
180
|
+
function isRigStickyStatusComment(body) {
|
|
181
|
+
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
182
|
+
}
|
|
183
|
+
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
184
|
+
if (!extraEnv)
|
|
185
|
+
return { encoding: "utf-8", timeout: timeoutMs };
|
|
186
|
+
return { encoding: "utf-8", timeout: timeoutMs, env: { ...process.env, ...extraEnv } };
|
|
187
|
+
}
|
|
188
|
+
function credentialEnv(token) {
|
|
189
|
+
const clean = token?.trim() ?? "";
|
|
190
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
191
|
+
}
|
|
192
|
+
async function resolveCredentialEnv(opts, purpose) {
|
|
193
|
+
if (!opts.credentialProvider)
|
|
194
|
+
return;
|
|
195
|
+
const input = {
|
|
196
|
+
owner: opts.owner,
|
|
197
|
+
repo: opts.repo,
|
|
198
|
+
workspaceId: opts.workspaceId ?? `${opts.owner}/${opts.repo}`,
|
|
199
|
+
...opts.userId ? { userId: opts.userId } : {},
|
|
200
|
+
purpose
|
|
201
|
+
};
|
|
202
|
+
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
203
|
+
return credentialEnv(resolved.token);
|
|
204
|
+
}
|
|
205
|
+
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
206
|
+
const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
|
|
207
|
+
assertGhSuccess(args, res);
|
|
208
|
+
if (!res.stdout || res.stdout.trim() === "")
|
|
209
|
+
return [];
|
|
210
|
+
return JSON.parse(res.stdout);
|
|
211
|
+
}
|
|
212
|
+
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
213
|
+
const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
|
|
214
|
+
assertGhSuccess(args, res);
|
|
215
|
+
}
|
|
216
|
+
function assertGhSuccess(args, res) {
|
|
217
|
+
if (res.error) {
|
|
218
|
+
const msg = res.error.message ?? String(res.error);
|
|
219
|
+
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
220
|
+
}
|
|
221
|
+
if (res.status !== 0) {
|
|
222
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function statusLabelFor(status) {
|
|
226
|
+
switch (status) {
|
|
227
|
+
case "in_progress":
|
|
228
|
+
return "in-progress";
|
|
229
|
+
case "blocked":
|
|
230
|
+
return "blocked";
|
|
231
|
+
case "ready":
|
|
232
|
+
return "ready";
|
|
233
|
+
case "under_review":
|
|
234
|
+
return "under-review";
|
|
235
|
+
case "failed":
|
|
236
|
+
return "failed";
|
|
237
|
+
case "cancelled":
|
|
238
|
+
return "cancelled";
|
|
239
|
+
case "ci_fixing":
|
|
240
|
+
return "under-review";
|
|
241
|
+
case "merging":
|
|
242
|
+
return "under-review";
|
|
243
|
+
case "needs_attention":
|
|
244
|
+
return "blocked";
|
|
245
|
+
case "open":
|
|
246
|
+
return null;
|
|
247
|
+
default:
|
|
248
|
+
throw new Error(`unsupported status: ${status}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function rigStatusLabelFor(status) {
|
|
252
|
+
switch (status) {
|
|
253
|
+
case "in_progress":
|
|
254
|
+
return "rig:running";
|
|
255
|
+
case "under_review":
|
|
256
|
+
return "rig:pr-open";
|
|
257
|
+
case "closed":
|
|
258
|
+
return "rig:done";
|
|
259
|
+
case "ci_fixing":
|
|
260
|
+
return "rig:ci-fixing";
|
|
261
|
+
case "merging":
|
|
262
|
+
return "rig:merging";
|
|
263
|
+
case "needs_attention":
|
|
264
|
+
case "failed":
|
|
265
|
+
case "blocked":
|
|
266
|
+
return "rig:needs-attention";
|
|
267
|
+
case "ready":
|
|
268
|
+
case "cancelled":
|
|
269
|
+
case "open":
|
|
270
|
+
return null;
|
|
271
|
+
default:
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
276
|
+
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
277
|
+
const targetRigLabel = rigStatusLabelFor(status);
|
|
278
|
+
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
279
|
+
if (targetLabel !== null && l === targetLabel)
|
|
280
|
+
continue;
|
|
281
|
+
if (targetRigLabel !== null && l === targetRigLabel)
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--remove-label", l], spawnFn, extraEnv, timeoutMs);
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
for (const label of [targetLabel, targetRigLabel].filter((value) => Boolean(value))) {
|
|
288
|
+
try {
|
|
289
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
+
if (!/not found/i.test(message)) {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
296
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (status === "closed") {
|
|
300
|
+
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs) {
|
|
304
|
+
try {
|
|
305
|
+
runGhVoid(bin, [
|
|
306
|
+
"label",
|
|
307
|
+
"create",
|
|
308
|
+
label,
|
|
309
|
+
"--repo",
|
|
310
|
+
repo,
|
|
311
|
+
"--color",
|
|
312
|
+
"6f42c1",
|
|
313
|
+
"--description",
|
|
314
|
+
"Task status managed by Rig"
|
|
315
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
if (!/already exists/i.test(message)) {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function upsertRigStickyComment(bin, repo, spawnFn, id, body, extraEnv, timeoutMs) {
|
|
324
|
+
const comments = runGh(bin, ["api", `repos/${repo}/issues/${id}/comments`, "--paginate"], spawnFn, extraEnv, timeoutMs);
|
|
325
|
+
const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(RIG_STATUS_COMMENT_MARKER));
|
|
326
|
+
if (existing) {
|
|
327
|
+
runGhVoid(bin, ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
runGhVoid(bin, ["api", "-X", "POST", `repos/${repo}/issues/${id}/comments`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
331
|
+
}
|
|
332
|
+
function notifyTaskChanged(onTaskChanged, repo, id, status) {
|
|
333
|
+
onTaskChanged?.({ repo, id, ...status ? { status } : {}, reason: "github-issue-updated" });
|
|
334
|
+
}
|
|
335
|
+
function fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
336
|
+
const issue = runGh(bin, [
|
|
337
|
+
"issue",
|
|
338
|
+
"view",
|
|
339
|
+
String(id),
|
|
340
|
+
"--repo",
|
|
341
|
+
repo,
|
|
342
|
+
"--json",
|
|
343
|
+
"body"
|
|
344
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
345
|
+
return typeof issue.body === "string" ? issue.body : undefined;
|
|
346
|
+
}
|
|
347
|
+
function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs) {
|
|
348
|
+
for (const rawLabel of labels) {
|
|
349
|
+
const label = rawLabel.trim();
|
|
350
|
+
if (!label)
|
|
351
|
+
continue;
|
|
352
|
+
if (action === "--add-label") {
|
|
353
|
+
try {
|
|
354
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
357
|
+
if (!/not found/i.test(message))
|
|
358
|
+
throw error;
|
|
359
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
360
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
366
|
+
} catch {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
370
|
+
if (update.status) {
|
|
371
|
+
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
372
|
+
}
|
|
373
|
+
if (update.comment?.trim()) {
|
|
374
|
+
if (isRigStickyStatusComment(update.comment)) {
|
|
375
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
376
|
+
} else {
|
|
377
|
+
runGhVoid(bin, ["issue", "comment", String(id), "--repo", repo, "--body", update.comment], spawnFn, extraEnv, timeoutMs);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const editArgs = ["issue", "edit", String(id), "--repo", repo];
|
|
381
|
+
if (update.title?.trim()) {
|
|
382
|
+
editArgs.push("--title", update.title.trim());
|
|
383
|
+
}
|
|
384
|
+
const nextBody = update.metadata ? updateRigOwnedMetadataBlock(update.body ?? fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) ?? "", update.metadata) : update.body;
|
|
385
|
+
if (nextBody !== undefined) {
|
|
386
|
+
editArgs.push("--body", nextBody);
|
|
387
|
+
}
|
|
388
|
+
if (editArgs.length > 5) {
|
|
389
|
+
runGhVoid(bin, editArgs, spawnFn, extraEnv, timeoutMs);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function createGitHubIssuesTaskSource(opts) {
|
|
393
|
+
const bin = opts.ghBinary ?? "gh";
|
|
394
|
+
const state = opts.state ?? "open";
|
|
395
|
+
const repo = `${opts.owner}/${opts.repo}`;
|
|
396
|
+
const spawnFn = opts.spawn ?? spawnSync;
|
|
397
|
+
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
398
|
+
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
399
|
+
return {
|
|
400
|
+
id: "std:github-issues",
|
|
401
|
+
kind: "github-issues",
|
|
402
|
+
async list() {
|
|
403
|
+
const labelArg = opts.labels && opts.labels.length > 0 ? ["--label", opts.labels.join(",")] : [];
|
|
404
|
+
const assigneeArg = opts.assignee?.trim() ? ["--assignee", opts.assignee.trim()] : [];
|
|
405
|
+
const args = [
|
|
406
|
+
"issue",
|
|
407
|
+
"list",
|
|
408
|
+
"--repo",
|
|
409
|
+
repo,
|
|
410
|
+
...labelArg,
|
|
411
|
+
...assigneeArg,
|
|
412
|
+
"--state",
|
|
413
|
+
state,
|
|
414
|
+
"--limit",
|
|
415
|
+
String(listLimit),
|
|
416
|
+
"--json",
|
|
417
|
+
"number,title,body,labels,state,url,assignees"
|
|
418
|
+
];
|
|
419
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
420
|
+
const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
421
|
+
if (rawIssues.length >= listLimit) {
|
|
422
|
+
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
423
|
+
}
|
|
424
|
+
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
425
|
+
return issues.map((i) => issueToTask(i, repo));
|
|
426
|
+
},
|
|
427
|
+
async get(id) {
|
|
428
|
+
try {
|
|
429
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
430
|
+
const issue = runGh(bin, [
|
|
431
|
+
"issue",
|
|
432
|
+
"view",
|
|
433
|
+
String(id),
|
|
434
|
+
"--repo",
|
|
435
|
+
repo,
|
|
436
|
+
"--json",
|
|
437
|
+
"number,title,body,labels,state,url,assignees"
|
|
438
|
+
], spawnFn, env, timeoutMs);
|
|
439
|
+
return issueToTask(issue, repo);
|
|
440
|
+
} catch {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
async updateStatus(id, status) {
|
|
445
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
446
|
+
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
447
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
448
|
+
},
|
|
449
|
+
async updateTask(id, update) {
|
|
450
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
451
|
+
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
452
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
453
|
+
},
|
|
454
|
+
async addLabels(id, labels) {
|
|
455
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
456
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--add-label", env, timeoutMs);
|
|
457
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
458
|
+
},
|
|
459
|
+
async removeLabels(id, labels) {
|
|
460
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
461
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--remove-label", env, timeoutMs);
|
|
462
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
463
|
+
},
|
|
464
|
+
async createIssue(input) {
|
|
465
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
466
|
+
const args = [
|
|
467
|
+
"api",
|
|
468
|
+
"-X",
|
|
469
|
+
"POST",
|
|
470
|
+
`repos/${repo}/issues`,
|
|
471
|
+
"-f",
|
|
472
|
+
`title=${input.title}`,
|
|
473
|
+
"-f",
|
|
474
|
+
`body=${input.body ?? ""}`,
|
|
475
|
+
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
476
|
+
];
|
|
477
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
478
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
479
|
+
return issueToTask(issue, repo);
|
|
480
|
+
},
|
|
481
|
+
async getIssueBody(id) {
|
|
482
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
483
|
+
return fetchIssueBody(bin, repo, spawnFn, id, env, timeoutMs);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
export {
|
|
488
|
+
updateRigOwnedMetadataBlock,
|
|
489
|
+
createStateGitHubCredentialProvider,
|
|
490
|
+
createGitHubIssuesTaskSource,
|
|
491
|
+
createEnvGitHubCredentialProvider,
|
|
492
|
+
buildRigStickyStatusComment,
|
|
493
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
494
|
+
RIG_METADATA_START,
|
|
495
|
+
RIG_METADATA_END
|
|
496
|
+
};
|
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/standard-plugin/src/index.ts
|
|
3
|
+
import { definePlugin } from "@rig/core";
|
|
4
|
+
|
|
5
|
+
// packages/standard-plugin/src/github-issues-source.ts
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
function cleanToken(value) {
|
|
10
|
+
const trimmed = value?.trim() ?? "";
|
|
11
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
12
|
+
}
|
|
13
|
+
function createEnvGitHubCredentialProvider() {
|
|
14
|
+
return {
|
|
15
|
+
async resolveGitHubToken(input) {
|
|
16
|
+
if (input.purpose === "selected-repo") {
|
|
17
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
|
+
}
|
|
19
|
+
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
22
|
+
}
|
|
23
|
+
return { token, source: "host-admin-fallback" };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createStateGitHubCredentialProvider(options = {}) {
|
|
28
|
+
const resolveStateFile = () => {
|
|
29
|
+
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
30
|
+
if (explicitFile?.trim())
|
|
31
|
+
return resolve(explicitFile.trim());
|
|
32
|
+
const stateDir = options.stateDir ?? process.env.RIG_STATE_DIR;
|
|
33
|
+
return stateDir?.trim() ? resolve(stateDir.trim(), "github-auth.json") : null;
|
|
34
|
+
};
|
|
35
|
+
const readToken = () => {
|
|
36
|
+
const stateFile = resolveStateFile();
|
|
37
|
+
if (!stateFile || !existsSync(stateFile))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
41
|
+
return typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
async resolveGitHubToken(input) {
|
|
48
|
+
const token = readToken();
|
|
49
|
+
if (input.purpose === "selected-repo") {
|
|
50
|
+
return { token: token ?? "", source: "signed-in-user" };
|
|
51
|
+
}
|
|
52
|
+
if (token) {
|
|
53
|
+
return { token, source: "signed-in-user" };
|
|
54
|
+
}
|
|
55
|
+
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
56
|
+
if (!fallback) {
|
|
57
|
+
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
58
|
+
}
|
|
59
|
+
return { token: fallback, source: "host-admin-fallback" };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
64
|
+
var RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
|
|
65
|
+
var RIG_METADATA_START = "<!-- rig:metadata:start -->";
|
|
66
|
+
var RIG_METADATA_END = "<!-- rig:metadata:end -->";
|
|
67
|
+
var RIG_STATUS_LABELS = new Set(["rig:running", "rig:pr-open", "rig:ci-fixing", "rig:merging", "rig:done", "rig:needs-attention"]);
|
|
68
|
+
var DEFAULT_GH_TIMEOUT_MS = 15000;
|
|
69
|
+
var DEFAULT_GITHUB_ISSUE_LIST_LIMIT = 1000;
|
|
70
|
+
function statusFor(issue) {
|
|
71
|
+
const state = (issue.state ?? "").toUpperCase();
|
|
72
|
+
if (state === "CLOSED")
|
|
73
|
+
return "closed";
|
|
74
|
+
const labelNames = labelNamesFor(issue);
|
|
75
|
+
if (labelNames.includes("in-progress"))
|
|
76
|
+
return "in_progress";
|
|
77
|
+
if (labelNames.includes("blocked"))
|
|
78
|
+
return "blocked";
|
|
79
|
+
if (labelNames.includes("ready"))
|
|
80
|
+
return "ready";
|
|
81
|
+
if (labelNames.includes("under-review"))
|
|
82
|
+
return "under_review";
|
|
83
|
+
if (labelNames.includes("failed"))
|
|
84
|
+
return "failed";
|
|
85
|
+
if (labelNames.includes("cancelled"))
|
|
86
|
+
return "cancelled";
|
|
87
|
+
return "open";
|
|
88
|
+
}
|
|
89
|
+
function parseDeps(body) {
|
|
90
|
+
const match = body.match(/^depends-on:\s*([^\n]+)/im);
|
|
91
|
+
if (!match)
|
|
92
|
+
return [];
|
|
93
|
+
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
94
|
+
}
|
|
95
|
+
function parseParents(body) {
|
|
96
|
+
const match = body.match(/^parents?:\s*([^\n]+)/im);
|
|
97
|
+
if (!match)
|
|
98
|
+
return [];
|
|
99
|
+
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
100
|
+
}
|
|
101
|
+
function issueTypeFor(issue) {
|
|
102
|
+
const labels = labelNamesFor(issue);
|
|
103
|
+
const typed = labels.find((l) => l.startsWith("type:"));
|
|
104
|
+
if (typed)
|
|
105
|
+
return typed.slice("type:".length);
|
|
106
|
+
if (labels.includes("epic"))
|
|
107
|
+
return "epic";
|
|
108
|
+
return "task";
|
|
109
|
+
}
|
|
110
|
+
function issueToTask(issue, repo) {
|
|
111
|
+
const labelNames = labelNamesFor(issue);
|
|
112
|
+
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
113
|
+
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
114
|
+
const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
|
|
115
|
+
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
116
|
+
const body = issue.body ?? "";
|
|
117
|
+
return {
|
|
118
|
+
id: String(issue.number),
|
|
119
|
+
deps: parseDeps(body),
|
|
120
|
+
status: statusFor(issue),
|
|
121
|
+
title: issue.title,
|
|
122
|
+
body,
|
|
123
|
+
scope,
|
|
124
|
+
role,
|
|
125
|
+
validators,
|
|
126
|
+
url: issue.url ?? issue.html_url,
|
|
127
|
+
issueType: issueTypeFor(issue),
|
|
128
|
+
sourceIssueId: `${repo}#${issue.number}`,
|
|
129
|
+
parentChildDeps: parseParents(body),
|
|
130
|
+
labels: labelNames,
|
|
131
|
+
raw: issue
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function labelNamesFor(issue) {
|
|
135
|
+
return (issue.labels ?? []).flatMap((label) => {
|
|
136
|
+
if (typeof label === "string")
|
|
137
|
+
return [label];
|
|
138
|
+
return typeof label.name === "string" ? [label.name] : [];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function yamlScalar(value) {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value.length === 0 ? "[]" : `
|
|
144
|
+
${value.map((entry) => ` - ${String(entry)}`).join(`
|
|
145
|
+
`)}`;
|
|
146
|
+
}
|
|
147
|
+
if (value && typeof value === "object")
|
|
148
|
+
return JSON.stringify(value);
|
|
149
|
+
return String(value);
|
|
150
|
+
}
|
|
151
|
+
function updateRigOwnedMetadataBlock(body, metadata) {
|
|
152
|
+
const rendered = [
|
|
153
|
+
RIG_METADATA_START,
|
|
154
|
+
...Object.entries(metadata).map(([key, value]) => Array.isArray(value) ? `${key}:${yamlScalar(value)}` : `${key}: ${yamlScalar(value)}`),
|
|
155
|
+
RIG_METADATA_END
|
|
156
|
+
].join(`
|
|
157
|
+
`);
|
|
158
|
+
const pattern = new RegExp(`${RIG_METADATA_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\\s\\S]*?\\s*${RIG_METADATA_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
159
|
+
if (pattern.test(body))
|
|
160
|
+
return body.replace(pattern, rendered);
|
|
161
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
162
|
+
|
|
163
|
+
${rendered}
|
|
164
|
+
` : `${rendered}
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
function isRigStickyStatusComment(body) {
|
|
168
|
+
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
169
|
+
}
|
|
170
|
+
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
171
|
+
if (!extraEnv)
|
|
172
|
+
return { encoding: "utf-8", timeout: timeoutMs };
|
|
173
|
+
return { encoding: "utf-8", timeout: timeoutMs, env: { ...process.env, ...extraEnv } };
|
|
174
|
+
}
|
|
175
|
+
function credentialEnv(token) {
|
|
176
|
+
const clean = token?.trim() ?? "";
|
|
177
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
178
|
+
}
|
|
179
|
+
async function resolveCredentialEnv(opts, purpose) {
|
|
180
|
+
if (!opts.credentialProvider)
|
|
181
|
+
return;
|
|
182
|
+
const input = {
|
|
183
|
+
owner: opts.owner,
|
|
184
|
+
repo: opts.repo,
|
|
185
|
+
workspaceId: opts.workspaceId ?? `${opts.owner}/${opts.repo}`,
|
|
186
|
+
...opts.userId ? { userId: opts.userId } : {},
|
|
187
|
+
purpose
|
|
188
|
+
};
|
|
189
|
+
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
190
|
+
return credentialEnv(resolved.token);
|
|
191
|
+
}
|
|
192
|
+
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
193
|
+
const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
|
|
194
|
+
assertGhSuccess(args, res);
|
|
195
|
+
if (!res.stdout || res.stdout.trim() === "")
|
|
196
|
+
return [];
|
|
197
|
+
return JSON.parse(res.stdout);
|
|
198
|
+
}
|
|
199
|
+
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
200
|
+
const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
|
|
201
|
+
assertGhSuccess(args, res);
|
|
202
|
+
}
|
|
203
|
+
function assertGhSuccess(args, res) {
|
|
204
|
+
if (res.error) {
|
|
205
|
+
const msg = res.error.message ?? String(res.error);
|
|
206
|
+
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
207
|
+
}
|
|
208
|
+
if (res.status !== 0) {
|
|
209
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function statusLabelFor(status) {
|
|
213
|
+
switch (status) {
|
|
214
|
+
case "in_progress":
|
|
215
|
+
return "in-progress";
|
|
216
|
+
case "blocked":
|
|
217
|
+
return "blocked";
|
|
218
|
+
case "ready":
|
|
219
|
+
return "ready";
|
|
220
|
+
case "under_review":
|
|
221
|
+
return "under-review";
|
|
222
|
+
case "failed":
|
|
223
|
+
return "failed";
|
|
224
|
+
case "cancelled":
|
|
225
|
+
return "cancelled";
|
|
226
|
+
case "ci_fixing":
|
|
227
|
+
return "under-review";
|
|
228
|
+
case "merging":
|
|
229
|
+
return "under-review";
|
|
230
|
+
case "needs_attention":
|
|
231
|
+
return "blocked";
|
|
232
|
+
case "open":
|
|
233
|
+
return null;
|
|
234
|
+
default:
|
|
235
|
+
throw new Error(`unsupported status: ${status}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function rigStatusLabelFor(status) {
|
|
239
|
+
switch (status) {
|
|
240
|
+
case "in_progress":
|
|
241
|
+
return "rig:running";
|
|
242
|
+
case "under_review":
|
|
243
|
+
return "rig:pr-open";
|
|
244
|
+
case "closed":
|
|
245
|
+
return "rig:done";
|
|
246
|
+
case "ci_fixing":
|
|
247
|
+
return "rig:ci-fixing";
|
|
248
|
+
case "merging":
|
|
249
|
+
return "rig:merging";
|
|
250
|
+
case "needs_attention":
|
|
251
|
+
case "failed":
|
|
252
|
+
case "blocked":
|
|
253
|
+
return "rig:needs-attention";
|
|
254
|
+
case "ready":
|
|
255
|
+
case "cancelled":
|
|
256
|
+
case "open":
|
|
257
|
+
return null;
|
|
258
|
+
default:
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
263
|
+
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
264
|
+
const targetRigLabel = rigStatusLabelFor(status);
|
|
265
|
+
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
266
|
+
if (targetLabel !== null && l === targetLabel)
|
|
267
|
+
continue;
|
|
268
|
+
if (targetRigLabel !== null && l === targetRigLabel)
|
|
269
|
+
continue;
|
|
270
|
+
try {
|
|
271
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--remove-label", l], spawnFn, extraEnv, timeoutMs);
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
for (const label of [targetLabel, targetRigLabel].filter((value) => Boolean(value))) {
|
|
275
|
+
try {
|
|
276
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
279
|
+
if (!/not found/i.test(message)) {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
283
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (status === "closed") {
|
|
287
|
+
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs) {
|
|
291
|
+
try {
|
|
292
|
+
runGhVoid(bin, [
|
|
293
|
+
"label",
|
|
294
|
+
"create",
|
|
295
|
+
label,
|
|
296
|
+
"--repo",
|
|
297
|
+
repo,
|
|
298
|
+
"--color",
|
|
299
|
+
"6f42c1",
|
|
300
|
+
"--description",
|
|
301
|
+
"Task status managed by Rig"
|
|
302
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
+
if (!/already exists/i.test(message)) {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function upsertRigStickyComment(bin, repo, spawnFn, id, body, extraEnv, timeoutMs) {
|
|
311
|
+
const comments = runGh(bin, ["api", `repos/${repo}/issues/${id}/comments`, "--paginate"], spawnFn, extraEnv, timeoutMs);
|
|
312
|
+
const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(RIG_STATUS_COMMENT_MARKER));
|
|
313
|
+
if (existing) {
|
|
314
|
+
runGhVoid(bin, ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
runGhVoid(bin, ["api", "-X", "POST", `repos/${repo}/issues/${id}/comments`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
318
|
+
}
|
|
319
|
+
function notifyTaskChanged(onTaskChanged, repo, id, status) {
|
|
320
|
+
onTaskChanged?.({ repo, id, ...status ? { status } : {}, reason: "github-issue-updated" });
|
|
321
|
+
}
|
|
322
|
+
function fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
323
|
+
const issue = runGh(bin, [
|
|
324
|
+
"issue",
|
|
325
|
+
"view",
|
|
326
|
+
String(id),
|
|
327
|
+
"--repo",
|
|
328
|
+
repo,
|
|
329
|
+
"--json",
|
|
330
|
+
"body"
|
|
331
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
332
|
+
return typeof issue.body === "string" ? issue.body : undefined;
|
|
333
|
+
}
|
|
334
|
+
function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs) {
|
|
335
|
+
for (const rawLabel of labels) {
|
|
336
|
+
const label = rawLabel.trim();
|
|
337
|
+
if (!label)
|
|
338
|
+
continue;
|
|
339
|
+
if (action === "--add-label") {
|
|
340
|
+
try {
|
|
341
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
344
|
+
if (!/not found/i.test(message))
|
|
345
|
+
throw error;
|
|
346
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
347
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
357
|
+
if (update.status) {
|
|
358
|
+
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
359
|
+
}
|
|
360
|
+
if (update.comment?.trim()) {
|
|
361
|
+
if (isRigStickyStatusComment(update.comment)) {
|
|
362
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
363
|
+
} else {
|
|
364
|
+
runGhVoid(bin, ["issue", "comment", String(id), "--repo", repo, "--body", update.comment], spawnFn, extraEnv, timeoutMs);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const editArgs = ["issue", "edit", String(id), "--repo", repo];
|
|
368
|
+
if (update.title?.trim()) {
|
|
369
|
+
editArgs.push("--title", update.title.trim());
|
|
370
|
+
}
|
|
371
|
+
const nextBody = update.metadata ? updateRigOwnedMetadataBlock(update.body ?? fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) ?? "", update.metadata) : update.body;
|
|
372
|
+
if (nextBody !== undefined) {
|
|
373
|
+
editArgs.push("--body", nextBody);
|
|
374
|
+
}
|
|
375
|
+
if (editArgs.length > 5) {
|
|
376
|
+
runGhVoid(bin, editArgs, spawnFn, extraEnv, timeoutMs);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function createGitHubIssuesTaskSource(opts) {
|
|
380
|
+
const bin = opts.ghBinary ?? "gh";
|
|
381
|
+
const state = opts.state ?? "open";
|
|
382
|
+
const repo = `${opts.owner}/${opts.repo}`;
|
|
383
|
+
const spawnFn = opts.spawn ?? spawnSync;
|
|
384
|
+
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
385
|
+
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
386
|
+
return {
|
|
387
|
+
id: "std:github-issues",
|
|
388
|
+
kind: "github-issues",
|
|
389
|
+
async list() {
|
|
390
|
+
const labelArg = opts.labels && opts.labels.length > 0 ? ["--label", opts.labels.join(",")] : [];
|
|
391
|
+
const assigneeArg = opts.assignee?.trim() ? ["--assignee", opts.assignee.trim()] : [];
|
|
392
|
+
const args = [
|
|
393
|
+
"issue",
|
|
394
|
+
"list",
|
|
395
|
+
"--repo",
|
|
396
|
+
repo,
|
|
397
|
+
...labelArg,
|
|
398
|
+
...assigneeArg,
|
|
399
|
+
"--state",
|
|
400
|
+
state,
|
|
401
|
+
"--limit",
|
|
402
|
+
String(listLimit),
|
|
403
|
+
"--json",
|
|
404
|
+
"number,title,body,labels,state,url,assignees"
|
|
405
|
+
];
|
|
406
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
407
|
+
const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
408
|
+
if (rawIssues.length >= listLimit) {
|
|
409
|
+
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
410
|
+
}
|
|
411
|
+
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
412
|
+
return issues.map((i) => issueToTask(i, repo));
|
|
413
|
+
},
|
|
414
|
+
async get(id) {
|
|
415
|
+
try {
|
|
416
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
417
|
+
const issue = runGh(bin, [
|
|
418
|
+
"issue",
|
|
419
|
+
"view",
|
|
420
|
+
String(id),
|
|
421
|
+
"--repo",
|
|
422
|
+
repo,
|
|
423
|
+
"--json",
|
|
424
|
+
"number,title,body,labels,state,url,assignees"
|
|
425
|
+
], spawnFn, env, timeoutMs);
|
|
426
|
+
return issueToTask(issue, repo);
|
|
427
|
+
} catch {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
async updateStatus(id, status) {
|
|
432
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
433
|
+
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
434
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
435
|
+
},
|
|
436
|
+
async updateTask(id, update) {
|
|
437
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
438
|
+
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
439
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
440
|
+
},
|
|
441
|
+
async addLabels(id, labels) {
|
|
442
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
443
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--add-label", env, timeoutMs);
|
|
444
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
445
|
+
},
|
|
446
|
+
async removeLabels(id, labels) {
|
|
447
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
448
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--remove-label", env, timeoutMs);
|
|
449
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
450
|
+
},
|
|
451
|
+
async createIssue(input) {
|
|
452
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
453
|
+
const args = [
|
|
454
|
+
"api",
|
|
455
|
+
"-X",
|
|
456
|
+
"POST",
|
|
457
|
+
`repos/${repo}/issues`,
|
|
458
|
+
"-f",
|
|
459
|
+
`title=${input.title}`,
|
|
460
|
+
"-f",
|
|
461
|
+
`body=${input.body ?? ""}`,
|
|
462
|
+
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
463
|
+
];
|
|
464
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
465
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
466
|
+
return issueToTask(issue, repo);
|
|
467
|
+
},
|
|
468
|
+
async getIssueBody(id) {
|
|
469
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
470
|
+
return fetchIssueBody(bin, repo, spawnFn, id, env, timeoutMs);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// packages/standard-plugin/src/files-source.ts
|
|
476
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
|
|
477
|
+
import { join, basename } from "path";
|
|
478
|
+
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
479
|
+
function readTaskFile(file, pattern) {
|
|
480
|
+
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
481
|
+
const inferredId = basename(file).replace(pattern, "");
|
|
482
|
+
const labels = Array.isArray(raw.labels) ? raw.labels.filter((label) => typeof label === "string") : [];
|
|
483
|
+
const scope = labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
484
|
+
const validators = labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
485
|
+
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
486
|
+
return {
|
|
487
|
+
id: raw["id"] ?? inferredId,
|
|
488
|
+
deps: raw["deps"] ?? raw["depends_on"] ?? [],
|
|
489
|
+
status: raw["status"] ?? "ready",
|
|
490
|
+
...scope.length > 0 ? { scope } : {},
|
|
491
|
+
...roleLabel ? { role: roleLabel.slice("role:".length) } : {},
|
|
492
|
+
...validators.length > 0 ? { validators, validation: validators } : {},
|
|
493
|
+
...raw
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function createFilesTaskSource(opts) {
|
|
497
|
+
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
498
|
+
const directory = opts.path ?? opts.dir;
|
|
499
|
+
if (!directory) {
|
|
500
|
+
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
501
|
+
}
|
|
502
|
+
const findTaskFile = (id) => {
|
|
503
|
+
if (!existsSync2(directory))
|
|
504
|
+
return;
|
|
505
|
+
for (const name of readdirSync(directory)) {
|
|
506
|
+
if (!pattern.test(name))
|
|
507
|
+
continue;
|
|
508
|
+
const p = join(directory, name);
|
|
509
|
+
try {
|
|
510
|
+
if (!statSync(p).isFile())
|
|
511
|
+
continue;
|
|
512
|
+
if (readTaskFile(p, pattern).id === id)
|
|
513
|
+
return p;
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
};
|
|
518
|
+
const applyUpdate = (id, update) => {
|
|
519
|
+
const file = findTaskFile(id);
|
|
520
|
+
if (!file) {
|
|
521
|
+
throw new Error(`files task not found: ${id}`);
|
|
522
|
+
}
|
|
523
|
+
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
524
|
+
if (update.status)
|
|
525
|
+
raw.status = update.status;
|
|
526
|
+
if (update.title !== undefined)
|
|
527
|
+
raw.title = update.title;
|
|
528
|
+
if (update.body !== undefined)
|
|
529
|
+
raw.body = update.body;
|
|
530
|
+
if (update.comment?.trim()) {
|
|
531
|
+
const existing = Array.isArray(raw.comments) ? raw.comments : [];
|
|
532
|
+
raw.comments = [
|
|
533
|
+
...existing,
|
|
534
|
+
{
|
|
535
|
+
body: update.comment,
|
|
536
|
+
createdAt: new Date().toISOString(),
|
|
537
|
+
source: "rig"
|
|
538
|
+
}
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
writeFileSync(file, `${JSON.stringify(raw, null, 2)}
|
|
542
|
+
`, "utf-8");
|
|
543
|
+
};
|
|
544
|
+
return {
|
|
545
|
+
id: "std:files",
|
|
546
|
+
kind: "files",
|
|
547
|
+
async list() {
|
|
548
|
+
if (!existsSync2(directory))
|
|
549
|
+
return [];
|
|
550
|
+
const out = [];
|
|
551
|
+
for (const name of readdirSync(directory)) {
|
|
552
|
+
if (!pattern.test(name))
|
|
553
|
+
continue;
|
|
554
|
+
const p = join(directory, name);
|
|
555
|
+
try {
|
|
556
|
+
if (!statSync(p).isFile())
|
|
557
|
+
continue;
|
|
558
|
+
out.push(readTaskFile(p, pattern));
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.warn(`[files-source] skipped ${name}: ${err.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return out;
|
|
564
|
+
},
|
|
565
|
+
async get(id) {
|
|
566
|
+
const all = await this.list();
|
|
567
|
+
return all.find((t) => t.id === id);
|
|
568
|
+
},
|
|
569
|
+
async updateStatus(id, status) {
|
|
570
|
+
applyUpdate(id, { status });
|
|
571
|
+
},
|
|
572
|
+
async updateTask(id, update) {
|
|
573
|
+
applyUpdate(id, update);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// packages/standard-plugin/src/index.ts
|
|
579
|
+
function requireStringField(config, field, kind) {
|
|
580
|
+
const value = config[field];
|
|
581
|
+
if (!value) {
|
|
582
|
+
throw new Error(`task source ${kind}: ${field} is required`);
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
function standardPlugin(opts = {}) {
|
|
587
|
+
return definePlugin({
|
|
588
|
+
name: "rig-standard",
|
|
589
|
+
version: "0.1.0",
|
|
590
|
+
contributes: {
|
|
591
|
+
taskSources: [
|
|
592
|
+
{
|
|
593
|
+
id: "std:github-issues",
|
|
594
|
+
kind: "github-issues",
|
|
595
|
+
description: "GitHub Issues via gh CLI"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
id: "std:files",
|
|
599
|
+
kind: "files",
|
|
600
|
+
description: "JSON files in a local directory"
|
|
601
|
+
}
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
}, {
|
|
605
|
+
taskSources: [
|
|
606
|
+
{
|
|
607
|
+
id: "std:github-issues",
|
|
608
|
+
kind: "github-issues",
|
|
609
|
+
description: "GitHub Issues via gh CLI",
|
|
610
|
+
factory(config) {
|
|
611
|
+
const options = {
|
|
612
|
+
owner: requireStringField(config, "owner", "github-issues"),
|
|
613
|
+
repo: requireStringField(config, "repo", "github-issues")
|
|
614
|
+
};
|
|
615
|
+
if (opts.githubCredentialProvider)
|
|
616
|
+
options.credentialProvider = opts.githubCredentialProvider;
|
|
617
|
+
if (opts.githubWorkspaceId)
|
|
618
|
+
options.workspaceId = opts.githubWorkspaceId;
|
|
619
|
+
if (opts.githubUserId)
|
|
620
|
+
options.userId = opts.githubUserId;
|
|
621
|
+
if (opts.githubSpawn)
|
|
622
|
+
options.spawn = opts.githubSpawn;
|
|
623
|
+
if (opts.onGitHubTaskChanged)
|
|
624
|
+
options.onTaskChanged = opts.onGitHubTaskChanged;
|
|
625
|
+
if (config.labels !== undefined)
|
|
626
|
+
options.labels = config.labels;
|
|
627
|
+
if (config.state !== undefined)
|
|
628
|
+
options.state = config.state;
|
|
629
|
+
const assignee = typeof config.options?.assignee === "string" ? config.options.assignee : process.env.RIG_GITHUB_ASSIGNEE;
|
|
630
|
+
if (assignee?.trim())
|
|
631
|
+
options.assignee = assignee.trim();
|
|
632
|
+
const timeoutMs = typeof config.options?.timeoutMs === "number" ? config.options.timeoutMs : undefined;
|
|
633
|
+
if (timeoutMs !== undefined)
|
|
634
|
+
options.timeoutMs = timeoutMs;
|
|
635
|
+
const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
|
|
636
|
+
if (listLimit !== undefined)
|
|
637
|
+
options.listLimit = listLimit;
|
|
638
|
+
return createGitHubIssuesTaskSource(options);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "std:files",
|
|
643
|
+
kind: "files",
|
|
644
|
+
description: "JSON files in a local directory",
|
|
645
|
+
factory(config) {
|
|
646
|
+
return createFilesTaskSource({
|
|
647
|
+
path: requireStringField(config, "path", "files")
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
export {
|
|
655
|
+
standardPlugin as default,
|
|
656
|
+
createStateGitHubCredentialProvider,
|
|
657
|
+
createGitHubIssuesTaskSource,
|
|
658
|
+
createFilesTaskSource,
|
|
659
|
+
createEnvGitHubCredentialProvider
|
|
660
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@h-rig/standard-plugin",
|
|
3
|
+
"version": "0.0.6-alpha.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rig package",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/src/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.3.11"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/src/index.js",
|
|
20
|
+
"module": "./dist/src/index.js",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.0",
|
|
23
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.0",
|
|
24
|
+
"effect": "4.0.0-beta.78"
|
|
25
|
+
}
|
|
26
|
+
}
|