@h-rig/standard-plugin 0.0.6-alpha.12 → 0.0.6-alpha.120
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/dist/src/files-source.d.ts +18 -0
- package/dist/src/files-source.js +4 -3
- package/dist/src/github-issues-source.d.ts +78 -0
- package/dist/src/github-issues-source.js +287 -39
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +351 -47
- package/package.json +6 -4
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RegisteredTaskSource } from "@rig/contracts";
|
|
2
|
+
export interface FilesTaskSourceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Directory containing one JSON file per task. Use either `path` (matches
|
|
5
|
+
* the `taskSource.path` field in rig.config.ts) or `dir` (back-compat).
|
|
6
|
+
*/
|
|
7
|
+
path?: string;
|
|
8
|
+
dir?: string;
|
|
9
|
+
pattern?: RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* Root a relative `path`/`dir` resolves against. The serving process's cwd
|
|
12
|
+
* is NOT the project (workspace-spawned servers run from the engine
|
|
13
|
+
* checkout), so without this a relative path silently reads the WRONG
|
|
14
|
+
* repo's tasks. Defaults to cwd for direct programmatic use.
|
|
15
|
+
*/
|
|
16
|
+
projectRoot?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function createFilesTaskSource(opts: FilesTaskSourceOptions): RegisteredTaskSource;
|
package/dist/src/files-source.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/standard-plugin/src/files-source.ts
|
|
3
3
|
import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { join, basename } from "path";
|
|
4
|
+
import { join, basename, isAbsolute, resolve } from "path";
|
|
5
5
|
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
6
6
|
function readTaskFile(file, pattern) {
|
|
7
7
|
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
@@ -22,10 +22,11 @@ function readTaskFile(file, pattern) {
|
|
|
22
22
|
}
|
|
23
23
|
function createFilesTaskSource(opts) {
|
|
24
24
|
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
25
|
+
const configured = opts.path ?? opts.dir;
|
|
26
|
+
if (!configured) {
|
|
27
27
|
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
28
28
|
}
|
|
29
|
+
const directory = isAbsolute(configured) ? configured : resolve(opts.projectRoot ?? process.cwd(), configured);
|
|
29
30
|
const findTaskFile = (id) => {
|
|
30
31
|
if (!existsSync(directory))
|
|
31
32
|
return;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { RegisteredTaskSource, TaskRecord } from "@rig/contracts";
|
|
3
|
+
export type GitHubCredentialPurpose = "selected-repo" | "admin-fallback";
|
|
4
|
+
export type GitHubIssueUpdatesMode = "lifecycle" | "minimal" | "off";
|
|
5
|
+
export interface GitHubCredentialProvider {
|
|
6
|
+
resolveGitHubToken(input: {
|
|
7
|
+
owner: string;
|
|
8
|
+
repo: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
userId?: string;
|
|
11
|
+
purpose: GitHubCredentialPurpose;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
token: string;
|
|
14
|
+
source: "signed-in-user" | "host-admin-fallback";
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export type GitHubProjectLifecycleStatus = "todo" | "running" | "prOpen" | "ciFixing" | "merging" | "done" | "needsAttention";
|
|
18
|
+
export interface GitHubProjectsOptions {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
statusFieldId?: string;
|
|
22
|
+
statuses?: Partial<Record<GitHubProjectLifecycleStatus, string>>;
|
|
23
|
+
}
|
|
24
|
+
export interface GitHubIssuesOptions {
|
|
25
|
+
owner: string;
|
|
26
|
+
repo: string;
|
|
27
|
+
labels?: readonly string[];
|
|
28
|
+
state?: "open" | "closed" | "all";
|
|
29
|
+
assignee?: string;
|
|
30
|
+
ghBinary?: string;
|
|
31
|
+
workspaceId?: string;
|
|
32
|
+
userId?: string;
|
|
33
|
+
credentialProvider?: GitHubCredentialProvider;
|
|
34
|
+
issueUpdates?: GitHubIssueUpdatesMode;
|
|
35
|
+
/** Timeout for every gh CLI call. Defaults to 15 seconds. */
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
/** Maximum issue-list rows before Rig fails loudly instead of silently truncating. Defaults to 1,000. */
|
|
38
|
+
listLimit?: number;
|
|
39
|
+
/** @internal — for testing. Override the spawnSync used by the adapter. */
|
|
40
|
+
spawn?: typeof spawnSync;
|
|
41
|
+
/** Notify the host that issue-backed task state changed and snapshots should refresh. */
|
|
42
|
+
onTaskChanged?: (event: {
|
|
43
|
+
repo: string;
|
|
44
|
+
id: string;
|
|
45
|
+
status?: string;
|
|
46
|
+
reason: "github-issue-updated";
|
|
47
|
+
}) => void;
|
|
48
|
+
/** Optional GitHub Projects (v2) status-field sync mapped from Rig task status. */
|
|
49
|
+
projects?: GitHubProjectsOptions;
|
|
50
|
+
}
|
|
51
|
+
export interface GitHubIssueCreateInput {
|
|
52
|
+
title: string;
|
|
53
|
+
body?: string;
|
|
54
|
+
labels?: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
export interface GitHubIssuesTaskSource extends RegisteredTaskSource {
|
|
57
|
+
addLabels(id: string, labels: readonly string[]): Promise<void>;
|
|
58
|
+
removeLabels(id: string, labels: readonly string[]): Promise<void>;
|
|
59
|
+
createIssue(input: GitHubIssueCreateInput): Promise<TaskRecord>;
|
|
60
|
+
getIssueBody(id: string): Promise<string | undefined>;
|
|
61
|
+
}
|
|
62
|
+
export declare function createEnvGitHubCredentialProvider(): GitHubCredentialProvider;
|
|
63
|
+
export declare function createStateGitHubCredentialProvider(options?: {
|
|
64
|
+
stateFile?: string;
|
|
65
|
+
stateDir?: string;
|
|
66
|
+
}): GitHubCredentialProvider;
|
|
67
|
+
export declare const RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
|
|
68
|
+
export declare const RIG_METADATA_START = "<!-- rig:metadata:start -->";
|
|
69
|
+
export declare const RIG_METADATA_END = "<!-- rig:metadata:end -->";
|
|
70
|
+
export declare function updateRigOwnedMetadataBlock(body: string, metadata: Record<string, unknown>): string;
|
|
71
|
+
export declare function buildRigStickyStatusComment(input: {
|
|
72
|
+
status: "running" | "prOpen" | "ciFixing" | "done" | "needsAttention" | string;
|
|
73
|
+
summary: string;
|
|
74
|
+
runId?: string;
|
|
75
|
+
prUrl?: string;
|
|
76
|
+
details?: readonly string[];
|
|
77
|
+
}): string;
|
|
78
|
+
export declare function createGitHubIssuesTaskSource(opts: GitHubIssuesOptions): GitHubIssuesTaskSource;
|
|
@@ -11,9 +11,9 @@ function createEnvGitHubCredentialProvider() {
|
|
|
11
11
|
return {
|
|
12
12
|
async resolveGitHubToken(input) {
|
|
13
13
|
if (input.purpose === "selected-repo") {
|
|
14
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
14
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
15
15
|
}
|
|
16
|
-
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
16
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
17
17
|
if (!token) {
|
|
18
18
|
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
19
19
|
}
|
|
@@ -22,34 +22,40 @@ function createEnvGitHubCredentialProvider() {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function createStateGitHubCredentialProvider(options = {}) {
|
|
25
|
-
const
|
|
25
|
+
const stateFileCandidates = () => {
|
|
26
|
+
const candidates = [];
|
|
26
27
|
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
27
28
|
if (explicitFile?.trim())
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
candidates.push(resolve(explicitFile.trim()));
|
|
30
|
+
for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
|
|
31
|
+
if (dir?.trim())
|
|
32
|
+
candidates.push(resolve(dir.trim(), "github-auth.json"));
|
|
33
|
+
}
|
|
34
|
+
return candidates;
|
|
31
35
|
};
|
|
32
36
|
const readToken = () => {
|
|
33
|
-
const stateFile
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
for (const stateFile of stateFileCandidates()) {
|
|
38
|
+
if (!existsSync(stateFile))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
42
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
43
|
+
if (token)
|
|
44
|
+
return token;
|
|
45
|
+
} catch {}
|
|
41
46
|
}
|
|
47
|
+
return null;
|
|
42
48
|
};
|
|
43
49
|
return {
|
|
44
50
|
async resolveGitHubToken(input) {
|
|
45
51
|
const token = readToken();
|
|
46
52
|
if (input.purpose === "selected-repo") {
|
|
47
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
53
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
48
54
|
}
|
|
49
55
|
if (token) {
|
|
50
56
|
return { token, source: "signed-in-user" };
|
|
51
57
|
}
|
|
52
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
58
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
53
59
|
if (!fallback) {
|
|
54
60
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
55
61
|
}
|
|
@@ -183,13 +189,21 @@ function isRigStickyStatusComment(body) {
|
|
|
183
189
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
184
190
|
}
|
|
185
191
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
return {
|
|
193
|
+
encoding: "utf-8",
|
|
194
|
+
timeout: timeoutMs,
|
|
195
|
+
env: {
|
|
196
|
+
...process.env,
|
|
197
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
198
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
199
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
200
|
+
...extraEnv ?? {}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
189
203
|
}
|
|
190
204
|
function credentialEnv(token) {
|
|
191
205
|
const clean = token?.trim() ?? "";
|
|
192
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
206
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
193
207
|
}
|
|
194
208
|
async function resolveCredentialEnv(opts, purpose) {
|
|
195
209
|
if (!opts.credentialProvider)
|
|
@@ -204,28 +218,239 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
204
218
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
205
219
|
return credentialEnv(resolved.token);
|
|
206
220
|
}
|
|
221
|
+
function tokenDiagnostic(value) {
|
|
222
|
+
const clean = value?.trim() ?? "";
|
|
223
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
224
|
+
}
|
|
207
225
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
208
|
-
const
|
|
209
|
-
|
|
226
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
227
|
+
const res = spawn(bin, [...args], options);
|
|
228
|
+
assertGhSuccess(args, res, options.env);
|
|
210
229
|
if (!res.stdout || res.stdout.trim() === "")
|
|
211
230
|
return [];
|
|
212
231
|
return JSON.parse(res.stdout);
|
|
213
232
|
}
|
|
214
233
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
215
|
-
const
|
|
216
|
-
|
|
234
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
235
|
+
const res = spawn(bin, [...args], options);
|
|
236
|
+
assertGhSuccess(args, res, options.env);
|
|
217
237
|
}
|
|
218
|
-
function assertGhSuccess(args, res) {
|
|
238
|
+
function assertGhSuccess(args, res, env) {
|
|
219
239
|
if (res.error) {
|
|
220
240
|
const msg = res.error.message ?? String(res.error);
|
|
221
241
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
222
242
|
}
|
|
223
243
|
if (res.status !== 0) {
|
|
224
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
244
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
245
|
+
[rig gh env:standard-plugin] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
249
|
+
todo: "Todo",
|
|
250
|
+
running: "In Progress",
|
|
251
|
+
prOpen: "In Review",
|
|
252
|
+
ciFixing: "In Review",
|
|
253
|
+
merging: "In Review",
|
|
254
|
+
done: "Done",
|
|
255
|
+
needsAttention: "Needs Attention"
|
|
256
|
+
};
|
|
257
|
+
function asProjectRecord(value) {
|
|
258
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
259
|
+
}
|
|
260
|
+
function projectString(value) {
|
|
261
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
262
|
+
}
|
|
263
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
264
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
265
|
+
switch (normalized) {
|
|
266
|
+
case "draft":
|
|
267
|
+
case "open":
|
|
268
|
+
case "queued":
|
|
269
|
+
case "ready":
|
|
270
|
+
return "todo";
|
|
271
|
+
case "running":
|
|
272
|
+
case "in_progress":
|
|
273
|
+
return "running";
|
|
274
|
+
case "under_review":
|
|
275
|
+
case "review":
|
|
276
|
+
case "pr_open":
|
|
277
|
+
return "prOpen";
|
|
278
|
+
case "ci_fixing":
|
|
279
|
+
case "fixing":
|
|
280
|
+
return "ciFixing";
|
|
281
|
+
case "merging":
|
|
282
|
+
case "merge":
|
|
283
|
+
return "merging";
|
|
284
|
+
case "closed":
|
|
285
|
+
case "completed":
|
|
286
|
+
case "done":
|
|
287
|
+
return "done";
|
|
288
|
+
case "blocked":
|
|
289
|
+
case "cancelled":
|
|
290
|
+
case "failed":
|
|
291
|
+
case "needs_attention":
|
|
292
|
+
return "needsAttention";
|
|
293
|
+
default:
|
|
294
|
+
return null;
|
|
225
295
|
}
|
|
226
296
|
}
|
|
297
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
298
|
+
return async (query, variables) => {
|
|
299
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
300
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
301
|
+
if (value === undefined || value === null)
|
|
302
|
+
continue;
|
|
303
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
304
|
+
}
|
|
305
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
306
|
+
return asProjectRecord(response)?.data ?? response;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
310
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
311
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
312
|
+
const record = asProjectRecord(node);
|
|
313
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
314
|
+
continue;
|
|
315
|
+
const id = projectString(record?.id);
|
|
316
|
+
if (!id)
|
|
317
|
+
continue;
|
|
318
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
319
|
+
const optionRecord = asProjectRecord(option);
|
|
320
|
+
const optionId = projectString(optionRecord?.id);
|
|
321
|
+
const name = projectString(optionRecord?.name);
|
|
322
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
323
|
+
}) : [];
|
|
324
|
+
return { id, name: "Status", options };
|
|
325
|
+
}
|
|
326
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
327
|
+
}
|
|
328
|
+
async function resolveProjectStatusField(input) {
|
|
329
|
+
const query = `
|
|
330
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
331
|
+
node(id: $projectId) {
|
|
332
|
+
... on ProjectV2 {
|
|
333
|
+
fields(first: 50) {
|
|
334
|
+
nodes {
|
|
335
|
+
... on ProjectV2FieldCommon { id name }
|
|
336
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
344
|
+
}
|
|
345
|
+
async function ensureIssueProjectItem(input) {
|
|
346
|
+
const query = `
|
|
347
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
348
|
+
node(id: $projectId) {
|
|
349
|
+
... on ProjectV2 {
|
|
350
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
`;
|
|
355
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
356
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
357
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
358
|
+
const record = asProjectRecord(node);
|
|
359
|
+
const content = asProjectRecord(record?.content);
|
|
360
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
361
|
+
const id2 = projectString(record?.id);
|
|
362
|
+
if (id2)
|
|
363
|
+
return { id: id2, created: false };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const mutation = `
|
|
367
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
368
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
369
|
+
}
|
|
370
|
+
`;
|
|
371
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
372
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
373
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
374
|
+
if (!id)
|
|
375
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
376
|
+
return { id, created: true };
|
|
377
|
+
}
|
|
378
|
+
async function updateIssueProjectStatus(input) {
|
|
379
|
+
const mutation = `
|
|
380
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
381
|
+
updateProjectV2ItemFieldValue(input: {
|
|
382
|
+
projectId: $projectId,
|
|
383
|
+
itemId: $itemId,
|
|
384
|
+
fieldId: $fieldId,
|
|
385
|
+
value: { singleSelectOptionId: $optionId }
|
|
386
|
+
}) { projectV2Item { id } }
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
await input.fetchGraphQL(mutation, {
|
|
390
|
+
projectId: input.projectId,
|
|
391
|
+
itemId: input.itemId,
|
|
392
|
+
fieldId: input.fieldId,
|
|
393
|
+
optionId: input.optionId
|
|
394
|
+
}, input.token);
|
|
395
|
+
}
|
|
396
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
397
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
398
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
399
|
+
}
|
|
400
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
401
|
+
if (!projects?.enabled)
|
|
402
|
+
return;
|
|
403
|
+
const projectId = projectString(projects.projectId);
|
|
404
|
+
if (!projectId)
|
|
405
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
406
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
407
|
+
if (!lifecycleStatus)
|
|
408
|
+
return;
|
|
409
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
410
|
+
if (!issueNodeId)
|
|
411
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
412
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
413
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
414
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
415
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
416
|
+
if (!option)
|
|
417
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
418
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
419
|
+
await updateIssueProjectStatus({
|
|
420
|
+
projectId,
|
|
421
|
+
itemId: item.id,
|
|
422
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
423
|
+
optionId: option.id,
|
|
424
|
+
token: "gh-cli",
|
|
425
|
+
fetchGraphQL
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
429
|
+
function normalizeTaskStatusToken(status) {
|
|
430
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
431
|
+
}
|
|
432
|
+
function issueUpdatesMode(value) {
|
|
433
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
434
|
+
}
|
|
435
|
+
function isTerminalTaskStatus(status) {
|
|
436
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
437
|
+
}
|
|
438
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
439
|
+
if (mode === "off")
|
|
440
|
+
return false;
|
|
441
|
+
if (mode === "lifecycle")
|
|
442
|
+
return true;
|
|
443
|
+
return isTerminalTaskStatus(status);
|
|
444
|
+
}
|
|
445
|
+
function isRunningStatus(status) {
|
|
446
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
447
|
+
}
|
|
448
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
449
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
450
|
+
}
|
|
227
451
|
function statusLabelFor(status) {
|
|
228
452
|
switch (status) {
|
|
453
|
+
case "running":
|
|
229
454
|
case "in_progress":
|
|
230
455
|
return "in-progress";
|
|
231
456
|
case "blocked":
|
|
@@ -244,6 +469,8 @@ function statusLabelFor(status) {
|
|
|
244
469
|
return "under-review";
|
|
245
470
|
case "needs_attention":
|
|
246
471
|
return "blocked";
|
|
472
|
+
case "closed":
|
|
473
|
+
case "completed":
|
|
247
474
|
case "open":
|
|
248
475
|
return null;
|
|
249
476
|
default:
|
|
@@ -252,11 +479,13 @@ function statusLabelFor(status) {
|
|
|
252
479
|
}
|
|
253
480
|
function rigStatusLabelFor(status) {
|
|
254
481
|
switch (status) {
|
|
482
|
+
case "running":
|
|
255
483
|
case "in_progress":
|
|
256
484
|
return "rig:running";
|
|
257
485
|
case "under_review":
|
|
258
486
|
return "rig:pr-open";
|
|
259
487
|
case "closed":
|
|
488
|
+
case "completed":
|
|
260
489
|
return "rig:done";
|
|
261
490
|
case "ci_fixing":
|
|
262
491
|
return "rig:ci-fixing";
|
|
@@ -274,9 +503,10 @@ function rigStatusLabelFor(status) {
|
|
|
274
503
|
return null;
|
|
275
504
|
}
|
|
276
505
|
}
|
|
277
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
278
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
506
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
507
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
279
508
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
509
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
280
510
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
281
511
|
if (targetLabel !== null && l === targetLabel)
|
|
282
512
|
continue;
|
|
@@ -298,7 +528,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
298
528
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
299
529
|
}
|
|
300
530
|
}
|
|
301
|
-
if (status
|
|
531
|
+
if (isRunningStatus(status)) {
|
|
532
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
533
|
+
if (shouldSyncLifecycle) {
|
|
534
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
535
|
+
status: "running",
|
|
536
|
+
summary: "Rig run started."
|
|
537
|
+
}), extraEnv, timeoutMs);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (shouldSyncLifecycle) {
|
|
541
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
542
|
+
}
|
|
543
|
+
if (status === "closed" || status === "completed") {
|
|
302
544
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
303
545
|
}
|
|
304
546
|
}
|
|
@@ -368,11 +610,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
368
610
|
} catch {}
|
|
369
611
|
}
|
|
370
612
|
}
|
|
371
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
613
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
372
614
|
if (update.status) {
|
|
373
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
615
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
374
616
|
}
|
|
375
|
-
if (update.comment?.trim()) {
|
|
617
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
376
618
|
if (isRigStickyStatusComment(update.comment)) {
|
|
377
619
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
378
620
|
} else {
|
|
@@ -398,6 +640,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
398
640
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
399
641
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
400
642
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
643
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
401
644
|
return {
|
|
402
645
|
id: "std:github-issues",
|
|
403
646
|
kind: "github-issues",
|
|
@@ -427,9 +670,10 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
427
670
|
return issues.map((i) => issueToTask(i, repo));
|
|
428
671
|
},
|
|
429
672
|
async get(id) {
|
|
673
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
674
|
+
let issue;
|
|
430
675
|
try {
|
|
431
|
-
|
|
432
|
-
const issue = runGh(bin, [
|
|
676
|
+
issue = runGh(bin, [
|
|
433
677
|
"issue",
|
|
434
678
|
"view",
|
|
435
679
|
String(id),
|
|
@@ -438,19 +682,23 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
438
682
|
"--json",
|
|
439
683
|
"number,title,body,labels,state,url,assignees,id"
|
|
440
684
|
], spawnFn, env, timeoutMs);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
687
|
+
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found/i.test(detail)) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
444
691
|
}
|
|
692
|
+
return issueToTask(issue, repo);
|
|
445
693
|
},
|
|
446
694
|
async updateStatus(id, status) {
|
|
447
695
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
448
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
696
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
449
697
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
450
698
|
},
|
|
451
699
|
async updateTask(id, update) {
|
|
452
700
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
453
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
701
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
454
702
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
455
703
|
},
|
|
456
704
|
async addLabels(id, labels) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RigPluginWithRuntime } from "@rig/core";
|
|
2
|
+
import { createEnvGitHubCredentialProvider, createStateGitHubCredentialProvider, createGitHubIssuesTaskSource, type GitHubCredentialProvider, type GitHubIssuesOptions } from "./github-issues-source";
|
|
3
|
+
import { createFilesTaskSource } from "./files-source";
|
|
4
|
+
export { createGitHubIssuesTaskSource, createEnvGitHubCredentialProvider, createStateGitHubCredentialProvider, createFilesTaskSource };
|
|
5
|
+
export type { GitHubIssuesOptions } from "./github-issues-source";
|
|
6
|
+
export type { FilesTaskSourceOptions } from "./files-source";
|
|
7
|
+
export interface StandardPluginOptions {
|
|
8
|
+
githubCredentialProvider?: GitHubCredentialProvider;
|
|
9
|
+
githubWorkspaceId?: string;
|
|
10
|
+
githubUserId?: string;
|
|
11
|
+
githubSpawn?: GitHubIssuesOptions["spawn"];
|
|
12
|
+
onGitHubTaskChanged?: GitHubIssuesOptions["onTaskChanged"];
|
|
13
|
+
}
|
|
14
|
+
export default function standardPlugin(opts?: StandardPluginOptions): RigPluginWithRuntime;
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/standard-plugin/src/index.ts
|
|
3
|
+
import { resolve as resolve3 } from "path";
|
|
3
4
|
import { definePlugin } from "@rig/core";
|
|
4
5
|
|
|
5
6
|
// packages/standard-plugin/src/github-issues-source.ts
|
|
@@ -14,9 +15,9 @@ function createEnvGitHubCredentialProvider() {
|
|
|
14
15
|
return {
|
|
15
16
|
async resolveGitHubToken(input) {
|
|
16
17
|
if (input.purpose === "selected-repo") {
|
|
17
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
19
|
}
|
|
19
|
-
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
21
|
if (!token) {
|
|
21
22
|
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
22
23
|
}
|
|
@@ -25,34 +26,40 @@ function createEnvGitHubCredentialProvider() {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
function createStateGitHubCredentialProvider(options = {}) {
|
|
28
|
-
const
|
|
29
|
+
const stateFileCandidates = () => {
|
|
30
|
+
const candidates = [];
|
|
29
31
|
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
30
32
|
if (explicitFile?.trim())
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
candidates.push(resolve(explicitFile.trim()));
|
|
34
|
+
for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
|
|
35
|
+
if (dir?.trim())
|
|
36
|
+
candidates.push(resolve(dir.trim(), "github-auth.json"));
|
|
37
|
+
}
|
|
38
|
+
return candidates;
|
|
34
39
|
};
|
|
35
40
|
const readToken = () => {
|
|
36
|
-
const stateFile
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
for (const stateFile of stateFileCandidates()) {
|
|
42
|
+
if (!existsSync(stateFile))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
46
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
47
|
+
if (token)
|
|
48
|
+
return token;
|
|
49
|
+
} catch {}
|
|
44
50
|
}
|
|
51
|
+
return null;
|
|
45
52
|
};
|
|
46
53
|
return {
|
|
47
54
|
async resolveGitHubToken(input) {
|
|
48
55
|
const token = readToken();
|
|
49
56
|
if (input.purpose === "selected-repo") {
|
|
50
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
57
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
51
58
|
}
|
|
52
59
|
if (token) {
|
|
53
60
|
return { token, source: "signed-in-user" };
|
|
54
61
|
}
|
|
55
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
62
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
56
63
|
if (!fallback) {
|
|
57
64
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
58
65
|
}
|
|
@@ -166,17 +173,41 @@ ${rendered}
|
|
|
166
173
|
` : `${rendered}
|
|
167
174
|
`;
|
|
168
175
|
}
|
|
176
|
+
function buildRigStickyStatusComment(input) {
|
|
177
|
+
const lines = [
|
|
178
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
179
|
+
`### Rig status: ${input.status}`,
|
|
180
|
+
"",
|
|
181
|
+
input.summary
|
|
182
|
+
];
|
|
183
|
+
if (input.runId)
|
|
184
|
+
lines.push("", `- Run: ${input.runId}`);
|
|
185
|
+
if (input.prUrl)
|
|
186
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
187
|
+
for (const detail of input.details ?? [])
|
|
188
|
+
lines.push(`- ${detail}`);
|
|
189
|
+
return lines.join(`
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
169
192
|
function isRigStickyStatusComment(body) {
|
|
170
193
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
171
194
|
}
|
|
172
195
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
196
|
+
return {
|
|
197
|
+
encoding: "utf-8",
|
|
198
|
+
timeout: timeoutMs,
|
|
199
|
+
env: {
|
|
200
|
+
...process.env,
|
|
201
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
202
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
203
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
204
|
+
...extraEnv ?? {}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
176
207
|
}
|
|
177
208
|
function credentialEnv(token) {
|
|
178
209
|
const clean = token?.trim() ?? "";
|
|
179
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
210
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
180
211
|
}
|
|
181
212
|
async function resolveCredentialEnv(opts, purpose) {
|
|
182
213
|
if (!opts.credentialProvider)
|
|
@@ -191,28 +222,239 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
191
222
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
192
223
|
return credentialEnv(resolved.token);
|
|
193
224
|
}
|
|
225
|
+
function tokenDiagnostic(value) {
|
|
226
|
+
const clean = value?.trim() ?? "";
|
|
227
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
228
|
+
}
|
|
194
229
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
195
|
-
const
|
|
196
|
-
|
|
230
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
231
|
+
const res = spawn(bin, [...args], options);
|
|
232
|
+
assertGhSuccess(args, res, options.env);
|
|
197
233
|
if (!res.stdout || res.stdout.trim() === "")
|
|
198
234
|
return [];
|
|
199
235
|
return JSON.parse(res.stdout);
|
|
200
236
|
}
|
|
201
237
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
202
|
-
const
|
|
203
|
-
|
|
238
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
239
|
+
const res = spawn(bin, [...args], options);
|
|
240
|
+
assertGhSuccess(args, res, options.env);
|
|
204
241
|
}
|
|
205
|
-
function assertGhSuccess(args, res) {
|
|
242
|
+
function assertGhSuccess(args, res, env) {
|
|
206
243
|
if (res.error) {
|
|
207
244
|
const msg = res.error.message ?? String(res.error);
|
|
208
245
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
209
246
|
}
|
|
210
247
|
if (res.status !== 0) {
|
|
211
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
248
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
249
|
+
[rig gh env:standard-plugin] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
253
|
+
todo: "Todo",
|
|
254
|
+
running: "In Progress",
|
|
255
|
+
prOpen: "In Review",
|
|
256
|
+
ciFixing: "In Review",
|
|
257
|
+
merging: "In Review",
|
|
258
|
+
done: "Done",
|
|
259
|
+
needsAttention: "Needs Attention"
|
|
260
|
+
};
|
|
261
|
+
function asProjectRecord(value) {
|
|
262
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
263
|
+
}
|
|
264
|
+
function projectString(value) {
|
|
265
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
266
|
+
}
|
|
267
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
268
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
269
|
+
switch (normalized) {
|
|
270
|
+
case "draft":
|
|
271
|
+
case "open":
|
|
272
|
+
case "queued":
|
|
273
|
+
case "ready":
|
|
274
|
+
return "todo";
|
|
275
|
+
case "running":
|
|
276
|
+
case "in_progress":
|
|
277
|
+
return "running";
|
|
278
|
+
case "under_review":
|
|
279
|
+
case "review":
|
|
280
|
+
case "pr_open":
|
|
281
|
+
return "prOpen";
|
|
282
|
+
case "ci_fixing":
|
|
283
|
+
case "fixing":
|
|
284
|
+
return "ciFixing";
|
|
285
|
+
case "merging":
|
|
286
|
+
case "merge":
|
|
287
|
+
return "merging";
|
|
288
|
+
case "closed":
|
|
289
|
+
case "completed":
|
|
290
|
+
case "done":
|
|
291
|
+
return "done";
|
|
292
|
+
case "blocked":
|
|
293
|
+
case "cancelled":
|
|
294
|
+
case "failed":
|
|
295
|
+
case "needs_attention":
|
|
296
|
+
return "needsAttention";
|
|
297
|
+
default:
|
|
298
|
+
return null;
|
|
212
299
|
}
|
|
213
300
|
}
|
|
301
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
302
|
+
return async (query, variables) => {
|
|
303
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
304
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
305
|
+
if (value === undefined || value === null)
|
|
306
|
+
continue;
|
|
307
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
308
|
+
}
|
|
309
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
310
|
+
return asProjectRecord(response)?.data ?? response;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
314
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
315
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
316
|
+
const record = asProjectRecord(node);
|
|
317
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
318
|
+
continue;
|
|
319
|
+
const id = projectString(record?.id);
|
|
320
|
+
if (!id)
|
|
321
|
+
continue;
|
|
322
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
323
|
+
const optionRecord = asProjectRecord(option);
|
|
324
|
+
const optionId = projectString(optionRecord?.id);
|
|
325
|
+
const name = projectString(optionRecord?.name);
|
|
326
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
327
|
+
}) : [];
|
|
328
|
+
return { id, name: "Status", options };
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
331
|
+
}
|
|
332
|
+
async function resolveProjectStatusField(input) {
|
|
333
|
+
const query = `
|
|
334
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
335
|
+
node(id: $projectId) {
|
|
336
|
+
... on ProjectV2 {
|
|
337
|
+
fields(first: 50) {
|
|
338
|
+
nodes {
|
|
339
|
+
... on ProjectV2FieldCommon { id name }
|
|
340
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
348
|
+
}
|
|
349
|
+
async function ensureIssueProjectItem(input) {
|
|
350
|
+
const query = `
|
|
351
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
352
|
+
node(id: $projectId) {
|
|
353
|
+
... on ProjectV2 {
|
|
354
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
359
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
360
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
361
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
362
|
+
const record = asProjectRecord(node);
|
|
363
|
+
const content = asProjectRecord(record?.content);
|
|
364
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
365
|
+
const id2 = projectString(record?.id);
|
|
366
|
+
if (id2)
|
|
367
|
+
return { id: id2, created: false };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const mutation = `
|
|
371
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
372
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
373
|
+
}
|
|
374
|
+
`;
|
|
375
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
376
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
377
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
378
|
+
if (!id)
|
|
379
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
380
|
+
return { id, created: true };
|
|
381
|
+
}
|
|
382
|
+
async function updateIssueProjectStatus(input) {
|
|
383
|
+
const mutation = `
|
|
384
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
385
|
+
updateProjectV2ItemFieldValue(input: {
|
|
386
|
+
projectId: $projectId,
|
|
387
|
+
itemId: $itemId,
|
|
388
|
+
fieldId: $fieldId,
|
|
389
|
+
value: { singleSelectOptionId: $optionId }
|
|
390
|
+
}) { projectV2Item { id } }
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
await input.fetchGraphQL(mutation, {
|
|
394
|
+
projectId: input.projectId,
|
|
395
|
+
itemId: input.itemId,
|
|
396
|
+
fieldId: input.fieldId,
|
|
397
|
+
optionId: input.optionId
|
|
398
|
+
}, input.token);
|
|
399
|
+
}
|
|
400
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
401
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
402
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
403
|
+
}
|
|
404
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
405
|
+
if (!projects?.enabled)
|
|
406
|
+
return;
|
|
407
|
+
const projectId = projectString(projects.projectId);
|
|
408
|
+
if (!projectId)
|
|
409
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
410
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
411
|
+
if (!lifecycleStatus)
|
|
412
|
+
return;
|
|
413
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
414
|
+
if (!issueNodeId)
|
|
415
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
416
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
417
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
418
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
419
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
420
|
+
if (!option)
|
|
421
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
422
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
423
|
+
await updateIssueProjectStatus({
|
|
424
|
+
projectId,
|
|
425
|
+
itemId: item.id,
|
|
426
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
427
|
+
optionId: option.id,
|
|
428
|
+
token: "gh-cli",
|
|
429
|
+
fetchGraphQL
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
433
|
+
function normalizeTaskStatusToken(status) {
|
|
434
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
435
|
+
}
|
|
436
|
+
function issueUpdatesMode(value) {
|
|
437
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
438
|
+
}
|
|
439
|
+
function isTerminalTaskStatus(status) {
|
|
440
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
441
|
+
}
|
|
442
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
443
|
+
if (mode === "off")
|
|
444
|
+
return false;
|
|
445
|
+
if (mode === "lifecycle")
|
|
446
|
+
return true;
|
|
447
|
+
return isTerminalTaskStatus(status);
|
|
448
|
+
}
|
|
449
|
+
function isRunningStatus(status) {
|
|
450
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
451
|
+
}
|
|
452
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
453
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
454
|
+
}
|
|
214
455
|
function statusLabelFor(status) {
|
|
215
456
|
switch (status) {
|
|
457
|
+
case "running":
|
|
216
458
|
case "in_progress":
|
|
217
459
|
return "in-progress";
|
|
218
460
|
case "blocked":
|
|
@@ -231,6 +473,8 @@ function statusLabelFor(status) {
|
|
|
231
473
|
return "under-review";
|
|
232
474
|
case "needs_attention":
|
|
233
475
|
return "blocked";
|
|
476
|
+
case "closed":
|
|
477
|
+
case "completed":
|
|
234
478
|
case "open":
|
|
235
479
|
return null;
|
|
236
480
|
default:
|
|
@@ -239,11 +483,13 @@ function statusLabelFor(status) {
|
|
|
239
483
|
}
|
|
240
484
|
function rigStatusLabelFor(status) {
|
|
241
485
|
switch (status) {
|
|
486
|
+
case "running":
|
|
242
487
|
case "in_progress":
|
|
243
488
|
return "rig:running";
|
|
244
489
|
case "under_review":
|
|
245
490
|
return "rig:pr-open";
|
|
246
491
|
case "closed":
|
|
492
|
+
case "completed":
|
|
247
493
|
return "rig:done";
|
|
248
494
|
case "ci_fixing":
|
|
249
495
|
return "rig:ci-fixing";
|
|
@@ -261,9 +507,10 @@ function rigStatusLabelFor(status) {
|
|
|
261
507
|
return null;
|
|
262
508
|
}
|
|
263
509
|
}
|
|
264
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
265
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
510
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
511
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
266
512
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
513
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
267
514
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
268
515
|
if (targetLabel !== null && l === targetLabel)
|
|
269
516
|
continue;
|
|
@@ -285,7 +532,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
285
532
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
286
533
|
}
|
|
287
534
|
}
|
|
288
|
-
if (status
|
|
535
|
+
if (isRunningStatus(status)) {
|
|
536
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
537
|
+
if (shouldSyncLifecycle) {
|
|
538
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
539
|
+
status: "running",
|
|
540
|
+
summary: "Rig run started."
|
|
541
|
+
}), extraEnv, timeoutMs);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (shouldSyncLifecycle) {
|
|
545
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
546
|
+
}
|
|
547
|
+
if (status === "closed" || status === "completed") {
|
|
289
548
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
290
549
|
}
|
|
291
550
|
}
|
|
@@ -355,11 +614,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
355
614
|
} catch {}
|
|
356
615
|
}
|
|
357
616
|
}
|
|
358
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
617
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
359
618
|
if (update.status) {
|
|
360
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
619
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
361
620
|
}
|
|
362
|
-
if (update.comment?.trim()) {
|
|
621
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
363
622
|
if (isRigStickyStatusComment(update.comment)) {
|
|
364
623
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
365
624
|
} else {
|
|
@@ -385,6 +644,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
385
644
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
386
645
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
387
646
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
647
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
388
648
|
return {
|
|
389
649
|
id: "std:github-issues",
|
|
390
650
|
kind: "github-issues",
|
|
@@ -414,9 +674,10 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
414
674
|
return issues.map((i) => issueToTask(i, repo));
|
|
415
675
|
},
|
|
416
676
|
async get(id) {
|
|
677
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
678
|
+
let issue;
|
|
417
679
|
try {
|
|
418
|
-
|
|
419
|
-
const issue = runGh(bin, [
|
|
680
|
+
issue = runGh(bin, [
|
|
420
681
|
"issue",
|
|
421
682
|
"view",
|
|
422
683
|
String(id),
|
|
@@ -425,19 +686,23 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
425
686
|
"--json",
|
|
426
687
|
"number,title,body,labels,state,url,assignees,id"
|
|
427
688
|
], spawnFn, env, timeoutMs);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
689
|
+
} catch (error) {
|
|
690
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
691
|
+
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found/i.test(detail)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
431
695
|
}
|
|
696
|
+
return issueToTask(issue, repo);
|
|
432
697
|
},
|
|
433
698
|
async updateStatus(id, status) {
|
|
434
699
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
435
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
700
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
436
701
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
437
702
|
},
|
|
438
703
|
async updateTask(id, update) {
|
|
439
704
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
440
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
705
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
441
706
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
442
707
|
},
|
|
443
708
|
async addLabels(id, labels) {
|
|
@@ -476,7 +741,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
476
741
|
|
|
477
742
|
// packages/standard-plugin/src/files-source.ts
|
|
478
743
|
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
|
|
479
|
-
import { join, basename } from "path";
|
|
744
|
+
import { join, basename, isAbsolute, resolve as resolve2 } from "path";
|
|
480
745
|
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
481
746
|
function readTaskFile(file, pattern) {
|
|
482
747
|
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
@@ -497,10 +762,11 @@ function readTaskFile(file, pattern) {
|
|
|
497
762
|
}
|
|
498
763
|
function createFilesTaskSource(opts) {
|
|
499
764
|
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
500
|
-
const
|
|
501
|
-
if (!
|
|
765
|
+
const configured = opts.path ?? opts.dir;
|
|
766
|
+
if (!configured) {
|
|
502
767
|
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
503
768
|
}
|
|
769
|
+
const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
|
|
504
770
|
const findTaskFile = (id) => {
|
|
505
771
|
if (!existsSync2(directory))
|
|
506
772
|
return;
|
|
@@ -585,6 +851,40 @@ function requireStringField(config, field, kind) {
|
|
|
585
851
|
}
|
|
586
852
|
return value;
|
|
587
853
|
}
|
|
854
|
+
function isRecord(value) {
|
|
855
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
856
|
+
}
|
|
857
|
+
function optionalString(value) {
|
|
858
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
859
|
+
}
|
|
860
|
+
function parseGitHubProjectsOptions(value) {
|
|
861
|
+
if (!isRecord(value))
|
|
862
|
+
return;
|
|
863
|
+
const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
|
|
864
|
+
const statuses = {};
|
|
865
|
+
for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
|
|
866
|
+
const status = optionalString(statusesSource?.[key]);
|
|
867
|
+
if (status)
|
|
868
|
+
statuses[key] = status;
|
|
869
|
+
}
|
|
870
|
+
const parsed = {};
|
|
871
|
+
if (typeof value.enabled === "boolean")
|
|
872
|
+
parsed.enabled = value.enabled;
|
|
873
|
+
const projectId = optionalString(value.projectId);
|
|
874
|
+
if (projectId)
|
|
875
|
+
parsed.projectId = projectId;
|
|
876
|
+
const statusFieldId = optionalString(value.statusFieldId);
|
|
877
|
+
if (statusFieldId)
|
|
878
|
+
parsed.statusFieldId = statusFieldId;
|
|
879
|
+
if (Object.keys(statuses).length > 0)
|
|
880
|
+
parsed.statuses = statuses;
|
|
881
|
+
return parsed;
|
|
882
|
+
}
|
|
883
|
+
function githubProjectsOptionsFromConfig(config, context) {
|
|
884
|
+
const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
|
|
885
|
+
const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
|
|
886
|
+
return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
|
|
887
|
+
}
|
|
588
888
|
function standardPlugin(opts = {}) {
|
|
589
889
|
return definePlugin({
|
|
590
890
|
name: "rig-standard",
|
|
@@ -609,13 +909,13 @@ function standardPlugin(opts = {}) {
|
|
|
609
909
|
id: "std:github-issues",
|
|
610
910
|
kind: "github-issues",
|
|
611
911
|
description: "GitHub Issues via gh CLI",
|
|
612
|
-
factory(config) {
|
|
912
|
+
factory(config, context) {
|
|
613
913
|
const options = {
|
|
614
914
|
owner: requireStringField(config, "owner", "github-issues"),
|
|
615
915
|
repo: requireStringField(config, "repo", "github-issues")
|
|
616
916
|
};
|
|
617
|
-
|
|
618
|
-
|
|
917
|
+
const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
|
|
918
|
+
options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
|
|
619
919
|
if (opts.githubWorkspaceId)
|
|
620
920
|
options.workspaceId = opts.githubWorkspaceId;
|
|
621
921
|
if (opts.githubUserId)
|
|
@@ -637,6 +937,9 @@ function standardPlugin(opts = {}) {
|
|
|
637
937
|
const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
|
|
638
938
|
if (listLimit !== undefined)
|
|
639
939
|
options.listLimit = listLimit;
|
|
940
|
+
const projects = githubProjectsOptionsFromConfig(config, context);
|
|
941
|
+
if (projects)
|
|
942
|
+
options.projects = projects;
|
|
640
943
|
return createGitHubIssuesTaskSource(options);
|
|
641
944
|
}
|
|
642
945
|
},
|
|
@@ -644,9 +947,10 @@ function standardPlugin(opts = {}) {
|
|
|
644
947
|
id: "std:files",
|
|
645
948
|
kind: "files",
|
|
646
949
|
description: "JSON files in a local directory",
|
|
647
|
-
factory(config) {
|
|
950
|
+
factory(config, context) {
|
|
648
951
|
return createFilesTaskSource({
|
|
649
|
-
path: requireStringField(config, "path", "files")
|
|
952
|
+
path: requireStringField(config, "path", "files"),
|
|
953
|
+
...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
|
|
650
954
|
});
|
|
651
955
|
}
|
|
652
956
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-rig/standard-plugin",
|
|
3
|
-
"version": "0.0.6-alpha.
|
|
3
|
+
"version": "0.0.6-alpha.120",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Rig
|
|
5
|
+
"description": "First-party contribution bundle for Rig's OMP extension plugin graph; not a standalone plugin runtime.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
13
14
|
"import": "./dist/src/index.js"
|
|
14
15
|
}
|
|
15
16
|
},
|
|
@@ -18,9 +19,10 @@
|
|
|
18
19
|
},
|
|
19
20
|
"main": "./dist/src/index.js",
|
|
20
21
|
"module": "./dist/src/index.js",
|
|
22
|
+
"types": "./dist/src/index.d.ts",
|
|
21
23
|
"dependencies": {
|
|
22
|
-
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.
|
|
23
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
24
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.120",
|
|
25
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.120",
|
|
24
26
|
"effect": "4.0.0-beta.78"
|
|
25
27
|
}
|
|
26
28
|
}
|