@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.20
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 +23 -0
- package/dist/src/index.js +1513 -331
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +68 -24
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +1207 -185
- package/dist/src/server-helpers/issue-analysis.js +30 -11
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +599 -114
- package/dist/src/server-helpers/run-writers.js +1 -2
- package/dist/src/server-helpers/ws-router.js +7 -1
- package/dist/src/server.js +1512 -331
- package/package.json +4 -4
|
@@ -4,8 +4,8 @@ var __require = import.meta.require;
|
|
|
4
4
|
// packages/server/src/server-helpers/http-router.ts
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
7
|
-
import { basename, dirname as
|
|
8
|
-
import { copyFileSync, existsSync as
|
|
7
|
+
import { basename, dirname as dirname9, isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
|
|
8
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
9
9
|
|
|
10
10
|
// packages/server/src/server.ts
|
|
11
11
|
import {
|
|
@@ -224,6 +224,18 @@ function readJsonlFileTail(path, options) {
|
|
|
224
224
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
225
225
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
226
226
|
}
|
|
227
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
228
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
229
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
230
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
231
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
232
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
233
|
+
return {
|
|
234
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
235
|
+
nextCursor: String(endExclusive),
|
|
236
|
+
hasMore: endExclusive < entries.length
|
|
237
|
+
};
|
|
238
|
+
}
|
|
227
239
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
228
240
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
229
241
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -469,6 +481,273 @@ async function refreshTaskProjection(projectRoot, input) {
|
|
|
469
481
|
});
|
|
470
482
|
}
|
|
471
483
|
|
|
484
|
+
// packages/server/src/server-helpers/issue-analysis.ts
|
|
485
|
+
import { createHash } from "crypto";
|
|
486
|
+
function stableIssueHash(issue) {
|
|
487
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
488
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
489
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
490
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
491
|
+
}
|
|
492
|
+
function renderIssueAnalysisPrompt(input) {
|
|
493
|
+
const issue = input.issue;
|
|
494
|
+
const neighbors = input.neighbors ?? [];
|
|
495
|
+
return [
|
|
496
|
+
"You are Rig issue analysis running inside Pi.",
|
|
497
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
|
|
498
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
499
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
500
|
+
"",
|
|
501
|
+
"Issue:",
|
|
502
|
+
JSON.stringify({
|
|
503
|
+
id: issue.id,
|
|
504
|
+
title: issue.title,
|
|
505
|
+
body: issue.body,
|
|
506
|
+
labels: issue.labels,
|
|
507
|
+
deps: issue.deps,
|
|
508
|
+
status: issue.status
|
|
509
|
+
}, null, 2),
|
|
510
|
+
"",
|
|
511
|
+
"Neighbor tasks:",
|
|
512
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
513
|
+
].join(`
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
function isRecord(value) {
|
|
517
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
518
|
+
}
|
|
519
|
+
function stringArray(value) {
|
|
520
|
+
if (!Array.isArray(value))
|
|
521
|
+
return;
|
|
522
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
523
|
+
}
|
|
524
|
+
function generatedIssues(value) {
|
|
525
|
+
if (!Array.isArray(value))
|
|
526
|
+
return;
|
|
527
|
+
return value.flatMap((entry) => {
|
|
528
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
529
|
+
return [];
|
|
530
|
+
return [{
|
|
531
|
+
title: entry.title,
|
|
532
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
533
|
+
labels: stringArray(entry.labels) ?? [],
|
|
534
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
535
|
+
}];
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function findJsonLikeText(value) {
|
|
539
|
+
if (typeof value === "string") {
|
|
540
|
+
const trimmed = value.trim();
|
|
541
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
542
|
+
return trimmed;
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
for (const entry of value) {
|
|
547
|
+
const found = findJsonLikeText(entry);
|
|
548
|
+
if (found)
|
|
549
|
+
return found;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
if (!isRecord(value))
|
|
554
|
+
return null;
|
|
555
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
556
|
+
const found = findJsonLikeText(value[key]);
|
|
557
|
+
if (found)
|
|
558
|
+
return found;
|
|
559
|
+
}
|
|
560
|
+
for (const entry of Object.values(value)) {
|
|
561
|
+
const found = findJsonLikeText(entry);
|
|
562
|
+
if (found)
|
|
563
|
+
return found;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function candidateAnalysisObject(value) {
|
|
568
|
+
if (!isRecord(value))
|
|
569
|
+
return null;
|
|
570
|
+
if (isRecord(value.result))
|
|
571
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
572
|
+
if (isRecord(value.analysis))
|
|
573
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
574
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
const nested = findJsonLikeText(value);
|
|
578
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
579
|
+
try {
|
|
580
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
581
|
+
return candidateAnalysisObject(parsedNested);
|
|
582
|
+
} catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function parseIssueAnalysisResult(raw) {
|
|
589
|
+
let parsed = raw;
|
|
590
|
+
if (typeof raw === "string") {
|
|
591
|
+
const trimmed = raw.trim();
|
|
592
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
593
|
+
try {
|
|
594
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
595
|
+
} catch {
|
|
596
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
597
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
601
|
+
if (!candidate)
|
|
602
|
+
return {};
|
|
603
|
+
const result = {};
|
|
604
|
+
if (isRecord(candidate.metadataPatch))
|
|
605
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
606
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
607
|
+
if (add?.length)
|
|
608
|
+
result.labelsToAdd = add;
|
|
609
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
610
|
+
if (remove?.length)
|
|
611
|
+
result.labelsToRemove = remove;
|
|
612
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
613
|
+
if (generated?.length)
|
|
614
|
+
result.generatedIssues = generated;
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
618
|
+
return async (command, args, options) => {
|
|
619
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
620
|
+
const proc = Bun.spawn([command, ...args], {
|
|
621
|
+
stdout: "pipe",
|
|
622
|
+
stderr: "pipe",
|
|
623
|
+
env
|
|
624
|
+
});
|
|
625
|
+
let timedOut = false;
|
|
626
|
+
const timer = setTimeout(() => {
|
|
627
|
+
timedOut = true;
|
|
628
|
+
proc.kill();
|
|
629
|
+
}, options.timeoutMs);
|
|
630
|
+
try {
|
|
631
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
632
|
+
new Response(proc.stdout).text(),
|
|
633
|
+
new Response(proc.stderr).text(),
|
|
634
|
+
proc.exited
|
|
635
|
+
]);
|
|
636
|
+
return {
|
|
637
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
638
|
+
stdout,
|
|
639
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
640
|
+
};
|
|
641
|
+
} finally {
|
|
642
|
+
clearTimeout(timer);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
647
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
648
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
649
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
650
|
+
return async ({ prompt }) => {
|
|
651
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
652
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
653
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
654
|
+
if (provider)
|
|
655
|
+
args.push("--provider", provider);
|
|
656
|
+
if (model)
|
|
657
|
+
args.push("--model", model);
|
|
658
|
+
args.push(prompt);
|
|
659
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
660
|
+
if (result.exitCode !== 0) {
|
|
661
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
662
|
+
}
|
|
663
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function defaultStatusComment(input) {
|
|
667
|
+
const changes = [
|
|
668
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
669
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
670
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
671
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
672
|
+
].filter((entry) => Boolean(entry));
|
|
673
|
+
if (changes.length === 0)
|
|
674
|
+
return null;
|
|
675
|
+
return [
|
|
676
|
+
"<!-- rig:status-comment -->",
|
|
677
|
+
"### Rig issue analysis",
|
|
678
|
+
"",
|
|
679
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
680
|
+
"",
|
|
681
|
+
...changes.map((change) => `- ${change}`)
|
|
682
|
+
].join(`
|
|
683
|
+
`);
|
|
684
|
+
}
|
|
685
|
+
function uniqueLabels(labels, required = []) {
|
|
686
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
687
|
+
}
|
|
688
|
+
function createIssueAnalysisWriteBack(input) {
|
|
689
|
+
return async ({ issue, result, reason }) => {
|
|
690
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
691
|
+
if (!input.target.updateTask)
|
|
692
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
693
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
694
|
+
}
|
|
695
|
+
if (result.labelsToAdd?.length) {
|
|
696
|
+
if (!input.target.addLabels)
|
|
697
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
698
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
699
|
+
}
|
|
700
|
+
if (result.labelsToRemove?.length) {
|
|
701
|
+
if (!input.target.removeLabels)
|
|
702
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
703
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
704
|
+
}
|
|
705
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
706
|
+
if (comment?.trim()) {
|
|
707
|
+
if (!input.target.updateTask)
|
|
708
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
709
|
+
await input.target.updateTask(issue.id, { comment });
|
|
710
|
+
}
|
|
711
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
712
|
+
if (!input.target.createIssue)
|
|
713
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
714
|
+
await input.target.createIssue({
|
|
715
|
+
title: generated.title,
|
|
716
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
717
|
+
|
|
718
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
719
|
+
` : generated.body,
|
|
720
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function createIssueAnalysisService(input) {
|
|
726
|
+
const analyzedHashes = new Map;
|
|
727
|
+
return {
|
|
728
|
+
async analyze(issues, options = {}) {
|
|
729
|
+
const results = [];
|
|
730
|
+
const neighbors = options.neighbors ?? issues;
|
|
731
|
+
for (const issue of issues) {
|
|
732
|
+
const hash = stableIssueHash(issue);
|
|
733
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
734
|
+
continue;
|
|
735
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
736
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
737
|
+
analyzedHashes.set(issue.id, hash);
|
|
738
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
739
|
+
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
740
|
+
}
|
|
741
|
+
results.push({ issue, result });
|
|
742
|
+
}
|
|
743
|
+
return results;
|
|
744
|
+
},
|
|
745
|
+
clearCache() {
|
|
746
|
+
analyzedHashes.clear();
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
472
751
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
473
752
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
474
753
|
|
|
@@ -521,6 +800,11 @@ import {
|
|
|
521
800
|
buildTaskRunLifecycleComment,
|
|
522
801
|
updateConfiguredTaskSourceTask
|
|
523
802
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
803
|
+
import {
|
|
804
|
+
closeIssueAfterMergedPr,
|
|
805
|
+
commitRunChanges,
|
|
806
|
+
runPrAutomation
|
|
807
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
524
808
|
|
|
525
809
|
// packages/server/src/scheduler.ts
|
|
526
810
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -536,8 +820,8 @@ import {
|
|
|
536
820
|
|
|
537
821
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
538
822
|
import { randomBytes } from "crypto";
|
|
539
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
540
|
-
import { resolve as resolve7 } from "path";
|
|
823
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
824
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
541
825
|
function cleanString(value) {
|
|
542
826
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
543
827
|
}
|
|
@@ -567,6 +851,26 @@ function parseApiSessions(value) {
|
|
|
567
851
|
}];
|
|
568
852
|
});
|
|
569
853
|
}
|
|
854
|
+
function parsePendingDevice(value) {
|
|
855
|
+
if (!value || typeof value !== "object")
|
|
856
|
+
return null;
|
|
857
|
+
const record = value;
|
|
858
|
+
const pollId = cleanString(record.pollId);
|
|
859
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
860
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
861
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
862
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
863
|
+
return null;
|
|
864
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
865
|
+
}
|
|
866
|
+
function parsePendingDevices(value) {
|
|
867
|
+
if (!Array.isArray(value))
|
|
868
|
+
return [];
|
|
869
|
+
return value.flatMap((entry) => {
|
|
870
|
+
const pending = parsePendingDevice(entry);
|
|
871
|
+
return pending ? [pending] : [];
|
|
872
|
+
});
|
|
873
|
+
}
|
|
570
874
|
function readStoredAuth(stateFile) {
|
|
571
875
|
if (!existsSync4(stateFile))
|
|
572
876
|
return {};
|
|
@@ -580,6 +884,7 @@ function readStoredAuth(stateFile) {
|
|
|
580
884
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
581
885
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
582
886
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
887
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
583
888
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
584
889
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
585
890
|
};
|
|
@@ -587,34 +892,36 @@ function readStoredAuth(stateFile) {
|
|
|
587
892
|
return {};
|
|
588
893
|
}
|
|
589
894
|
}
|
|
590
|
-
function parsePendingDevice(value) {
|
|
591
|
-
if (!value || typeof value !== "object")
|
|
592
|
-
return null;
|
|
593
|
-
const record = value;
|
|
594
|
-
const pollId = cleanString(record.pollId);
|
|
595
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
596
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
597
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
598
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
599
|
-
return null;
|
|
600
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
601
|
-
}
|
|
602
895
|
function newApiSessionToken() {
|
|
603
896
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
604
897
|
}
|
|
605
898
|
function writeStoredAuth(stateFile, payload) {
|
|
606
|
-
mkdirSync3(
|
|
899
|
+
mkdirSync3(dirname3(stateFile), { recursive: true });
|
|
607
900
|
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
608
901
|
`, { encoding: "utf8", mode: 384 });
|
|
609
902
|
try {
|
|
610
903
|
chmodSync(stateFile, 384);
|
|
611
904
|
} catch {}
|
|
612
905
|
}
|
|
906
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
907
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
908
|
+
}
|
|
613
909
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
614
910
|
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
615
911
|
}
|
|
616
|
-
function
|
|
617
|
-
const
|
|
912
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
913
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
914
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
915
|
+
if (existsSync4(stateFile)) {
|
|
916
|
+
copyFileSync(stateFile, targetFile);
|
|
917
|
+
try {
|
|
918
|
+
chmodSync(targetFile, 384);
|
|
919
|
+
} catch {}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
writeStoredAuth(targetFile, {});
|
|
923
|
+
}
|
|
924
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
618
925
|
return {
|
|
619
926
|
stateFile,
|
|
620
927
|
status(options) {
|
|
@@ -644,6 +951,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
644
951
|
scopes: input.scopes ?? [],
|
|
645
952
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
646
953
|
pendingDevice: null,
|
|
954
|
+
pendingDevices: [],
|
|
647
955
|
apiSessions: previous.apiSessions ?? [],
|
|
648
956
|
updatedAt: new Date().toISOString()
|
|
649
957
|
});
|
|
@@ -672,15 +980,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
672
980
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
673
981
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
674
982
|
},
|
|
675
|
-
copyToProjectRoot(
|
|
676
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
983
|
+
copyToProjectRoot(projectRoot) {
|
|
984
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
677
985
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
678
986
|
},
|
|
987
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
988
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
989
|
+
},
|
|
679
990
|
savePendingDevice(input) {
|
|
680
991
|
const previous = readStoredAuth(stateFile);
|
|
992
|
+
const pendingDevices = [
|
|
993
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
994
|
+
...previous.pendingDevices ?? [],
|
|
995
|
+
input
|
|
996
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
681
997
|
writeStoredAuth(stateFile, {
|
|
682
998
|
...previous,
|
|
683
|
-
pendingDevice:
|
|
999
|
+
pendingDevice: null,
|
|
1000
|
+
pendingDevices,
|
|
684
1001
|
updatedAt: new Date().toISOString()
|
|
685
1002
|
});
|
|
686
1003
|
},
|
|
@@ -693,23 +1010,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
693
1010
|
});
|
|
694
1011
|
},
|
|
695
1012
|
readPendingDevice(pollId) {
|
|
696
|
-
const
|
|
697
|
-
|
|
1013
|
+
const previous = readStoredAuth(stateFile);
|
|
1014
|
+
const pending = [
|
|
1015
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
1016
|
+
...previous.pendingDevices ?? []
|
|
1017
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
1018
|
+
if (!pending)
|
|
698
1019
|
return null;
|
|
699
1020
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
700
1021
|
return null;
|
|
701
1022
|
return pending;
|
|
702
1023
|
},
|
|
703
|
-
clearPendingDevice() {
|
|
1024
|
+
clearPendingDevice(pollId) {
|
|
704
1025
|
const previous = readStoredAuth(stateFile);
|
|
1026
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
705
1027
|
writeStoredAuth(stateFile, {
|
|
706
1028
|
...previous,
|
|
707
1029
|
pendingDevice: null,
|
|
1030
|
+
pendingDevices: remaining,
|
|
708
1031
|
updatedAt: new Date().toISOString()
|
|
709
1032
|
});
|
|
710
1033
|
}
|
|
711
1034
|
};
|
|
712
1035
|
}
|
|
1036
|
+
function createGitHubAuthStore(projectRoot) {
|
|
1037
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
1038
|
+
}
|
|
713
1039
|
|
|
714
1040
|
// packages/server/src/server-helpers/github-projects.ts
|
|
715
1041
|
function asRecord(value) {
|
|
@@ -718,6 +1044,9 @@ function asRecord(value) {
|
|
|
718
1044
|
function asString(value) {
|
|
719
1045
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
720
1046
|
}
|
|
1047
|
+
function asNumber(value) {
|
|
1048
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1049
|
+
}
|
|
721
1050
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
722
1051
|
const response = await fetch("https://api.github.com/graphql", {
|
|
723
1052
|
method: "POST",
|
|
@@ -734,6 +1063,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
734
1063
|
}
|
|
735
1064
|
return json.data;
|
|
736
1065
|
}
|
|
1066
|
+
function projectNodesFrom(data) {
|
|
1067
|
+
const root = asRecord(data);
|
|
1068
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
1069
|
+
const projects = asRecord(owner?.projectsV2);
|
|
1070
|
+
const nodes = projects?.nodes;
|
|
1071
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
1072
|
+
}
|
|
1073
|
+
async function listGitHubProjects(input) {
|
|
1074
|
+
const query = `
|
|
1075
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
1076
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1077
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1078
|
+
}
|
|
1079
|
+
`;
|
|
1080
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
1081
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
1082
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
1083
|
+
const record = asRecord(node);
|
|
1084
|
+
const id = asString(record?.id);
|
|
1085
|
+
const number = asNumber(record?.number);
|
|
1086
|
+
const title = asString(record?.title);
|
|
1087
|
+
if (!id || number === undefined || !title)
|
|
1088
|
+
return [];
|
|
1089
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
737
1092
|
async function resolveProjectStatusField(input) {
|
|
738
1093
|
const query = `
|
|
739
1094
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -828,6 +1183,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
828
1183
|
running: "In Progress",
|
|
829
1184
|
prOpen: "In Review",
|
|
830
1185
|
ciFixing: "In Review",
|
|
1186
|
+
merging: "Merging",
|
|
831
1187
|
done: "Done",
|
|
832
1188
|
needsAttention: "Needs Attention"
|
|
833
1189
|
};
|
|
@@ -841,6 +1197,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
841
1197
|
return "prOpen";
|
|
842
1198
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
843
1199
|
return "ciFixing";
|
|
1200
|
+
if (normalized === "merging" || normalized === "merge")
|
|
1201
|
+
return "merging";
|
|
844
1202
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
845
1203
|
return "needsAttention";
|
|
846
1204
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -916,7 +1274,8 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
916
1274
|
"needs-attention",
|
|
917
1275
|
"stopped"
|
|
918
1276
|
]);
|
|
919
|
-
var
|
|
1277
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
1278
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
920
1279
|
|
|
921
1280
|
// packages/server/src/server-helpers/ws-router.ts
|
|
922
1281
|
import {
|
|
@@ -1098,7 +1457,7 @@ import {
|
|
|
1098
1457
|
} from "@rig/runtime/control-plane/remote";
|
|
1099
1458
|
|
|
1100
1459
|
// packages/server/src/server-helpers/run-steering.ts
|
|
1101
|
-
import { dirname as
|
|
1460
|
+
import { dirname as dirname4, resolve as resolve8 } from "path";
|
|
1102
1461
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
|
|
1103
1462
|
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
1104
1463
|
var steeringSequence = 0;
|
|
@@ -1185,7 +1544,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
1185
1544
|
delivered: false
|
|
1186
1545
|
};
|
|
1187
1546
|
const path = runSteeringPath(projectRoot, runId);
|
|
1188
|
-
mkdirSync4(
|
|
1547
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1189
1548
|
appendJsonlRecord2(path, entry);
|
|
1190
1549
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
1191
1550
|
id: entry.id,
|
|
@@ -1222,24 +1581,205 @@ import {
|
|
|
1222
1581
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
1223
1582
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1224
1583
|
|
|
1584
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1585
|
+
import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1586
|
+
import { dirname as dirname6, resolve as resolve10 } from "path";
|
|
1587
|
+
|
|
1588
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
1589
|
+
import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1590
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
|
|
1591
|
+
function cleanString3(value) {
|
|
1592
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1593
|
+
}
|
|
1594
|
+
function sanitizePathSegment(value) {
|
|
1595
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
1596
|
+
}
|
|
1597
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
1598
|
+
const userId = cleanString3(identity.userId);
|
|
1599
|
+
if (userId) {
|
|
1600
|
+
const safeId = sanitizePathSegment(userId);
|
|
1601
|
+
if (safeId)
|
|
1602
|
+
return `ghu-${safeId}`;
|
|
1603
|
+
}
|
|
1604
|
+
const login = cleanString3(identity.login);
|
|
1605
|
+
if (login) {
|
|
1606
|
+
const safeLogin = sanitizePathSegment(login);
|
|
1607
|
+
if (safeLogin)
|
|
1608
|
+
return `ghu-login-${safeLogin}`;
|
|
1609
|
+
}
|
|
1610
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
1611
|
+
}
|
|
1612
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
1613
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
1614
|
+
if (explicitRoot)
|
|
1615
|
+
return resolve9(explicitRoot);
|
|
1616
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
1617
|
+
if (stateDir2)
|
|
1618
|
+
return resolve9(dirname5(resolve9(stateDir2)), "users");
|
|
1619
|
+
return resolve9(projectRoot, ".rig", "users");
|
|
1620
|
+
}
|
|
1621
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
1622
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
1623
|
+
const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
1624
|
+
const stateDir2 = resolve9(root, ".rig", "state");
|
|
1625
|
+
return {
|
|
1626
|
+
key,
|
|
1627
|
+
userId: cleanString3(identity.userId),
|
|
1628
|
+
login: cleanString3(identity.login),
|
|
1629
|
+
root,
|
|
1630
|
+
stateDir: stateDir2,
|
|
1631
|
+
authStateFile: resolve9(stateDir2, "github-auth.json"),
|
|
1632
|
+
metadataFile: resolve9(stateDir2, "user-namespace.json"),
|
|
1633
|
+
checkoutBaseDir: resolve9(root, "remote-checkouts"),
|
|
1634
|
+
snapshotBaseDir: resolve9(root, "remote-snapshots")
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
1638
|
+
return {
|
|
1639
|
+
key: namespace.key,
|
|
1640
|
+
userId: namespace.userId,
|
|
1641
|
+
login: namespace.login,
|
|
1642
|
+
root: namespace.root,
|
|
1643
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1644
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
1648
|
+
const root = resolve9(namespaceRoot);
|
|
1649
|
+
const candidate = resolve9(candidatePath);
|
|
1650
|
+
const rel = relative(root, candidate);
|
|
1651
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1652
|
+
}
|
|
1653
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
1654
|
+
mkdirSync5(namespace.stateDir, { recursive: true });
|
|
1655
|
+
const previous = (() => {
|
|
1656
|
+
if (!existsSync6(namespace.metadataFile))
|
|
1657
|
+
return null;
|
|
1658
|
+
try {
|
|
1659
|
+
const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
|
|
1660
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1661
|
+
} catch {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
})();
|
|
1665
|
+
const now = new Date().toISOString();
|
|
1666
|
+
writeFileSync5(namespace.metadataFile, `${JSON.stringify({
|
|
1667
|
+
key: namespace.key,
|
|
1668
|
+
userId: namespace.userId,
|
|
1669
|
+
login: namespace.login,
|
|
1670
|
+
root: namespace.root,
|
|
1671
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1672
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
1673
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
1674
|
+
updatedAt: now
|
|
1675
|
+
}, null, 2)}
|
|
1676
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1677
|
+
try {
|
|
1678
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
1679
|
+
} catch {}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1683
|
+
function cleanString4(value) {
|
|
1684
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1685
|
+
}
|
|
1686
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
1687
|
+
return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
1688
|
+
}
|
|
1689
|
+
function parseEntry(value) {
|
|
1690
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1691
|
+
return null;
|
|
1692
|
+
const record = value;
|
|
1693
|
+
const token = cleanString4(record.token);
|
|
1694
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
1695
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
1696
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
1697
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
1698
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
1699
|
+
const createdAt = cleanString4(record.createdAt);
|
|
1700
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
1701
|
+
return null;
|
|
1702
|
+
return {
|
|
1703
|
+
token,
|
|
1704
|
+
namespaceKey,
|
|
1705
|
+
namespaceRoot,
|
|
1706
|
+
authStateFile,
|
|
1707
|
+
checkoutBaseDir,
|
|
1708
|
+
snapshotBaseDir,
|
|
1709
|
+
createdAt,
|
|
1710
|
+
login: cleanString4(record.login),
|
|
1711
|
+
userId: cleanString4(record.userId),
|
|
1712
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function readIndex(indexFile) {
|
|
1716
|
+
if (!existsSync7(indexFile))
|
|
1717
|
+
return [];
|
|
1718
|
+
try {
|
|
1719
|
+
const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
|
|
1720
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
1721
|
+
const parsedEntry = parseEntry(entry);
|
|
1722
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
1723
|
+
}) : [];
|
|
1724
|
+
} catch {
|
|
1725
|
+
return [];
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
function writeIndex(indexFile, sessions) {
|
|
1729
|
+
mkdirSync6(dirname6(indexFile), { recursive: true });
|
|
1730
|
+
writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
1731
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1732
|
+
try {
|
|
1733
|
+
chmodSync3(indexFile, 384);
|
|
1734
|
+
} catch {}
|
|
1735
|
+
}
|
|
1736
|
+
function registerGitHubApiSession(input) {
|
|
1737
|
+
const cleanToken = cleanString4(input.token);
|
|
1738
|
+
if (!cleanToken)
|
|
1739
|
+
throw new Error("GitHub API session token is required");
|
|
1740
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
1741
|
+
const createdAt = new Date().toISOString();
|
|
1742
|
+
const entry = {
|
|
1743
|
+
token: cleanToken,
|
|
1744
|
+
login: input.namespace.login,
|
|
1745
|
+
userId: input.namespace.userId,
|
|
1746
|
+
namespaceKey: input.namespace.key,
|
|
1747
|
+
namespaceRoot: input.namespace.root,
|
|
1748
|
+
authStateFile: input.namespace.authStateFile,
|
|
1749
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
1750
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
1751
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
1752
|
+
createdAt
|
|
1753
|
+
};
|
|
1754
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
1755
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
1756
|
+
return entry;
|
|
1757
|
+
}
|
|
1758
|
+
function readGitHubApiSession(input) {
|
|
1759
|
+
const cleanToken = cleanString4(input.token);
|
|
1760
|
+
if (!cleanToken)
|
|
1761
|
+
return null;
|
|
1762
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1225
1765
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1226
|
-
import { createHash } from "crypto";
|
|
1766
|
+
import { createHash as createHash2 } from "crypto";
|
|
1227
1767
|
import { spawnSync } from "child_process";
|
|
1228
|
-
import { existsSync as
|
|
1229
|
-
import { dirname as
|
|
1768
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
|
|
1769
|
+
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
1230
1770
|
function normalizeRepoSlug(value) {
|
|
1231
1771
|
const trimmed = value.trim();
|
|
1232
1772
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
1233
1773
|
}
|
|
1234
1774
|
function registryPath(projectRoot) {
|
|
1235
|
-
return
|
|
1775
|
+
return resolve11(projectRoot, ".rig", "state", "projects.json");
|
|
1236
1776
|
}
|
|
1237
1777
|
function readRegistry(projectRoot) {
|
|
1238
1778
|
const path = registryPath(projectRoot);
|
|
1239
|
-
if (!
|
|
1779
|
+
if (!existsSync8(path))
|
|
1240
1780
|
return {};
|
|
1241
1781
|
try {
|
|
1242
|
-
const payload = JSON.parse(
|
|
1782
|
+
const payload = JSON.parse(readFileSync6(path, "utf8"));
|
|
1243
1783
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1244
1784
|
return {};
|
|
1245
1785
|
const projects = payload.projects;
|
|
@@ -1250,14 +1790,14 @@ function readRegistry(projectRoot) {
|
|
|
1250
1790
|
}
|
|
1251
1791
|
function writeRegistry(projectRoot, projects) {
|
|
1252
1792
|
const path = registryPath(projectRoot);
|
|
1253
|
-
|
|
1254
|
-
|
|
1793
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
1794
|
+
writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
|
|
1255
1795
|
`, "utf8");
|
|
1256
1796
|
}
|
|
1257
1797
|
function resolveConfigPath(projectRoot) {
|
|
1258
1798
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
1259
|
-
const path =
|
|
1260
|
-
if (
|
|
1799
|
+
const path = resolve11(projectRoot, name);
|
|
1800
|
+
if (existsSync8(path))
|
|
1261
1801
|
return path;
|
|
1262
1802
|
}
|
|
1263
1803
|
return null;
|
|
@@ -1266,7 +1806,7 @@ function hashFile(path) {
|
|
|
1266
1806
|
if (!path)
|
|
1267
1807
|
return null;
|
|
1268
1808
|
try {
|
|
1269
|
-
return
|
|
1809
|
+
return createHash2("sha256").update(readFileSync6(path)).digest("hex");
|
|
1270
1810
|
} catch {
|
|
1271
1811
|
return null;
|
|
1272
1812
|
}
|
|
@@ -1282,11 +1822,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
1282
1822
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
1283
1823
|
}
|
|
1284
1824
|
function buildRunSummary(projectRoot) {
|
|
1285
|
-
const runsDir =
|
|
1825
|
+
const runsDir = resolve11(projectRoot, ".rig", "runs");
|
|
1286
1826
|
try {
|
|
1287
1827
|
const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
1288
1828
|
try {
|
|
1289
|
-
const run = JSON.parse(
|
|
1829
|
+
const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
|
|
1290
1830
|
return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
|
|
1291
1831
|
} catch {
|
|
1292
1832
|
return [];
|
|
@@ -1338,10 +1878,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
1338
1878
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
1339
1879
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
1340
1880
|
}
|
|
1881
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
1882
|
+
const target = resolve11(checkoutPath);
|
|
1883
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
|
|
1884
|
+
}
|
|
1341
1885
|
|
|
1342
1886
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
1343
|
-
import { existsSync as
|
|
1344
|
-
import { dirname as
|
|
1887
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1888
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
|
|
1345
1889
|
function safeSlugSegments(repoSlug) {
|
|
1346
1890
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
1347
1891
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -1358,7 +1902,7 @@ function safeCheckoutKey(value) {
|
|
|
1358
1902
|
}
|
|
1359
1903
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
1360
1904
|
const key = safeCheckoutKey(checkoutKey);
|
|
1361
|
-
return
|
|
1905
|
+
return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
1362
1906
|
}
|
|
1363
1907
|
function sanitizeSnapshotId(value, fallback) {
|
|
1364
1908
|
const raw = (value ?? fallback).trim();
|
|
@@ -1366,7 +1910,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
1366
1910
|
return safe || fallback;
|
|
1367
1911
|
}
|
|
1368
1912
|
function assertWithinRoot(root, relativePath) {
|
|
1369
|
-
if (!relativePath ||
|
|
1913
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
|
|
1370
1914
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
1371
1915
|
}
|
|
1372
1916
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -1374,9 +1918,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
1374
1918
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1375
1919
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
1376
1920
|
}
|
|
1377
|
-
const target =
|
|
1378
|
-
const rel =
|
|
1379
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
1921
|
+
const target = resolve12(root, ...segments);
|
|
1922
|
+
const rel = relative2(root, target);
|
|
1923
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
|
|
1380
1924
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
1381
1925
|
}
|
|
1382
1926
|
return target;
|
|
@@ -1394,17 +1938,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
1394
1938
|
function extractUploadedSnapshotArchive(input) {
|
|
1395
1939
|
const archive = decodeSnapshotArchive(input.archive);
|
|
1396
1940
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
1397
|
-
const checkoutPath =
|
|
1398
|
-
|
|
1941
|
+
const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
1942
|
+
mkdirSync8(checkoutPath, { recursive: true });
|
|
1399
1943
|
for (const file of archive.files) {
|
|
1400
1944
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
1401
1945
|
throw new Error("Invalid snapshot archive file entry");
|
|
1402
1946
|
}
|
|
1403
1947
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
1404
|
-
|
|
1405
|
-
|
|
1948
|
+
mkdirSync8(dirname8(target), { recursive: true });
|
|
1949
|
+
writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
|
|
1406
1950
|
}
|
|
1407
|
-
|
|
1951
|
+
writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
1408
1952
|
repoSlug: input.repoSlug,
|
|
1409
1953
|
snapshotId,
|
|
1410
1954
|
fileCount: archive.files.length,
|
|
@@ -1439,7 +1983,7 @@ function gitCredentialConfig(token) {
|
|
|
1439
1983
|
};
|
|
1440
1984
|
}
|
|
1441
1985
|
async function prepareRemoteCheckout(input) {
|
|
1442
|
-
const exists = input.exists ??
|
|
1986
|
+
const exists = input.exists ?? existsSync9;
|
|
1443
1987
|
const strategy = input.strategy;
|
|
1444
1988
|
if (strategy.kind === "uploaded-snapshot") {
|
|
1445
1989
|
return extractUploadedSnapshotArchive({
|
|
@@ -1451,7 +1995,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
1451
1995
|
});
|
|
1452
1996
|
}
|
|
1453
1997
|
if (strategy.kind === "existing-path") {
|
|
1454
|
-
const checkoutPath2 =
|
|
1998
|
+
const checkoutPath2 = resolve12(strategy.path);
|
|
1455
1999
|
if (!exists(checkoutPath2)) {
|
|
1456
2000
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
1457
2001
|
}
|
|
@@ -1487,9 +2031,9 @@ function buildServerControlStatus() {
|
|
|
1487
2031
|
};
|
|
1488
2032
|
}
|
|
1489
2033
|
function buildProjectConfigStatus(root) {
|
|
1490
|
-
const hasConfigTs =
|
|
1491
|
-
const hasConfigJson =
|
|
1492
|
-
const hasLegacyTaskConfig =
|
|
2034
|
+
const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
|
|
2035
|
+
const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
|
|
2036
|
+
const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
|
|
1493
2037
|
let kind = "missing";
|
|
1494
2038
|
if (hasConfigTs)
|
|
1495
2039
|
kind = "rig-config-ts";
|
|
@@ -1506,6 +2050,75 @@ function buildProjectConfigStatus(root) {
|
|
|
1506
2050
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
1507
2051
|
};
|
|
1508
2052
|
}
|
|
2053
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
2054
|
+
"ready",
|
|
2055
|
+
"blocked",
|
|
2056
|
+
"in-progress",
|
|
2057
|
+
"under-review",
|
|
2058
|
+
"failed",
|
|
2059
|
+
"cancelled",
|
|
2060
|
+
"rig:running",
|
|
2061
|
+
"rig:pr-open",
|
|
2062
|
+
"rig:ci-fixing",
|
|
2063
|
+
"rig:merging",
|
|
2064
|
+
"rig:done",
|
|
2065
|
+
"rig:needs-attention"
|
|
2066
|
+
];
|
|
2067
|
+
function githubProjectsEnabled(config) {
|
|
2068
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2069
|
+
return false;
|
|
2070
|
+
const root = config;
|
|
2071
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
2072
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
2073
|
+
return projects?.enabled === true;
|
|
2074
|
+
}
|
|
2075
|
+
function githubIssueSourceRepo(config) {
|
|
2076
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2077
|
+
return null;
|
|
2078
|
+
const root = config;
|
|
2079
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
2080
|
+
const owner = normalizeString(taskSource?.owner);
|
|
2081
|
+
const repo = normalizeString(taskSource?.repo);
|
|
2082
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
2083
|
+
return { owner, repo };
|
|
2084
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
2085
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
2086
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
2087
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
2088
|
+
}
|
|
2089
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
2090
|
+
const repo = githubIssueSourceRepo(config);
|
|
2091
|
+
if (!repo)
|
|
2092
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2093
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
2094
|
+
if (!token)
|
|
2095
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2096
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
2097
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
2098
|
+
});
|
|
2099
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
2100
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
2101
|
+
const created = [];
|
|
2102
|
+
const alreadyPresent = [];
|
|
2103
|
+
const failed = [];
|
|
2104
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
2105
|
+
if (existing.has(label)) {
|
|
2106
|
+
alreadyPresent.push(label);
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
2110
|
+
method: "POST",
|
|
2111
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
2112
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
2113
|
+
});
|
|
2114
|
+
if (response.ok || response.status === 422) {
|
|
2115
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
2116
|
+
} else {
|
|
2117
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
2121
|
+
}
|
|
1509
2122
|
function normalizeCommit(value) {
|
|
1510
2123
|
const raw = normalizeString(value);
|
|
1511
2124
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -1525,24 +2138,24 @@ function repoParts(repoSlug) {
|
|
|
1525
2138
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
1526
2139
|
}
|
|
1527
2140
|
function repairDir(checkoutPath) {
|
|
1528
|
-
const dir =
|
|
1529
|
-
|
|
2141
|
+
const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
2142
|
+
mkdirSync9(dir, { recursive: true });
|
|
1530
2143
|
return dir;
|
|
1531
2144
|
}
|
|
1532
2145
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
1533
|
-
const source =
|
|
1534
|
-
const backupPath =
|
|
1535
|
-
|
|
1536
|
-
|
|
2146
|
+
const source = resolve13(checkoutPath, relativePath);
|
|
2147
|
+
const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
2148
|
+
mkdirSync9(dirname9(backupPath), { recursive: true });
|
|
2149
|
+
copyFileSync2(source, backupPath);
|
|
1537
2150
|
return backupPath;
|
|
1538
2151
|
}
|
|
1539
2152
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
1540
|
-
const packagePath =
|
|
1541
|
-
if (!
|
|
2153
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2154
|
+
if (!existsSync10(packagePath)) {
|
|
1542
2155
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
1543
2156
|
}
|
|
1544
2157
|
try {
|
|
1545
|
-
const parsed = JSON.parse(
|
|
2158
|
+
const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
|
|
1546
2159
|
return {
|
|
1547
2160
|
existed: true,
|
|
1548
2161
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -1556,9 +2169,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
1556
2169
|
}
|
|
1557
2170
|
}
|
|
1558
2171
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
1559
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1560
|
-
const packagePath =
|
|
1561
|
-
if (!hasConfig && !
|
|
2172
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2173
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2174
|
+
if (!hasConfig && !existsSync10(packagePath)) {
|
|
1562
2175
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
1563
2176
|
}
|
|
1564
2177
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -1577,7 +2190,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
1577
2190
|
}
|
|
1578
2191
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
1579
2192
|
if (changed) {
|
|
1580
|
-
|
|
2193
|
+
writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
1581
2194
|
`, "utf8");
|
|
1582
2195
|
}
|
|
1583
2196
|
return {
|
|
@@ -1603,11 +2216,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
1603
2216
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
1604
2217
|
}
|
|
1605
2218
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
1606
|
-
const configPath =
|
|
1607
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2219
|
+
const configPath = resolve13(checkoutPath, "rig.config.ts");
|
|
2220
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1608
2221
|
if (existingConfigName) {
|
|
1609
|
-
const existingPath =
|
|
1610
|
-
const source =
|
|
2222
|
+
const existingPath = resolve13(checkoutPath, existingConfigName);
|
|
2223
|
+
const source = readFileSync7(existingPath, "utf8");
|
|
1611
2224
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
1612
2225
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1613
2226
|
}
|
|
@@ -1621,7 +2234,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1621
2234
|
}
|
|
1622
2235
|
}
|
|
1623
2236
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
1624
|
-
|
|
2237
|
+
writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
1625
2238
|
return {
|
|
1626
2239
|
path: configPath,
|
|
1627
2240
|
changed: true,
|
|
@@ -1630,7 +2243,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1630
2243
|
};
|
|
1631
2244
|
}
|
|
1632
2245
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
1633
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2246
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1634
2247
|
if (!configFile)
|
|
1635
2248
|
return { ok: false, error: "missing rig config" };
|
|
1636
2249
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1652,7 +2265,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
1652
2265
|
return { ok: true, configFile };
|
|
1653
2266
|
}
|
|
1654
2267
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
1655
|
-
if (!
|
|
2268
|
+
if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
|
|
1656
2269
|
return { skipped: true, reason: "package.json missing" };
|
|
1657
2270
|
}
|
|
1658
2271
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1665,8 +2278,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
1665
2278
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
1666
2279
|
}
|
|
1667
2280
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
1668
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1669
|
-
const hasPackage =
|
|
2281
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2282
|
+
const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
|
|
1670
2283
|
if (!hasConfig && !hasPackage) {
|
|
1671
2284
|
return {
|
|
1672
2285
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -1764,26 +2377,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
1764
2377
|
}
|
|
1765
2378
|
function readGitHeadCommit(projectRoot) {
|
|
1766
2379
|
try {
|
|
1767
|
-
let gitDir =
|
|
2380
|
+
let gitDir = resolve13(projectRoot, ".git");
|
|
1768
2381
|
try {
|
|
1769
|
-
const dotGit =
|
|
2382
|
+
const dotGit = readFileSync7(gitDir, "utf8").trim();
|
|
1770
2383
|
const gitDirPrefix = "gitdir:";
|
|
1771
2384
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
1772
|
-
gitDir =
|
|
2385
|
+
gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
1773
2386
|
}
|
|
1774
2387
|
} catch {}
|
|
1775
|
-
const head =
|
|
2388
|
+
const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
|
|
1776
2389
|
const refPrefix = "ref:";
|
|
1777
2390
|
if (!head.startsWith(refPrefix)) {
|
|
1778
2391
|
return normalizeCommit(head);
|
|
1779
2392
|
}
|
|
1780
2393
|
const ref = head.slice(refPrefix.length).trim();
|
|
1781
|
-
const refPath =
|
|
1782
|
-
if (
|
|
1783
|
-
return normalizeCommit(
|
|
2394
|
+
const refPath = resolve13(gitDir, ref);
|
|
2395
|
+
if (existsSync10(refPath)) {
|
|
2396
|
+
return normalizeCommit(readFileSync7(refPath, "utf8").trim());
|
|
1784
2397
|
}
|
|
1785
|
-
const commonDir = normalizeString(
|
|
1786
|
-
return commonDir ? normalizeCommit(
|
|
2398
|
+
const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
|
|
2399
|
+
return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
1787
2400
|
} catch {
|
|
1788
2401
|
return null;
|
|
1789
2402
|
}
|
|
@@ -1821,9 +2434,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
1821
2434
|
const repo = normalizeString(taskSource?.repo);
|
|
1822
2435
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
1823
2436
|
}
|
|
1824
|
-
async function buildTaskSourceStatus(state, config) {
|
|
2437
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
1825
2438
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
1826
|
-
const selectedRepo =
|
|
2439
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
1827
2440
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
1828
2441
|
}).selectedRepo;
|
|
1829
2442
|
try {
|
|
@@ -1876,37 +2489,146 @@ function isLoopbackRequest(req) {
|
|
|
1876
2489
|
}
|
|
1877
2490
|
}
|
|
1878
2491
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
1879
|
-
return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
2492
|
+
return pathname === "/" || pathname === "/install" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
2493
|
+
}
|
|
2494
|
+
function buildRigInstallScript() {
|
|
2495
|
+
return `#!/usr/bin/env bash
|
|
2496
|
+
set -euo pipefail
|
|
2497
|
+
|
|
2498
|
+
say() {
|
|
2499
|
+
printf 'rig-install: %s
|
|
2500
|
+
' "$*"
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2504
|
+
say "Bun not found; installing Bun first"
|
|
2505
|
+
curl -fsSL https://bun.sh/install | bash
|
|
2506
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2507
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2508
|
+
fi
|
|
2509
|
+
|
|
2510
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2511
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
2512
|
+
' >&2
|
|
2513
|
+
exit 1
|
|
2514
|
+
fi
|
|
2515
|
+
|
|
2516
|
+
say "Installing @h-rig/cli@latest"
|
|
2517
|
+
bun add -g @h-rig/cli@latest
|
|
2518
|
+
|
|
2519
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2520
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
2521
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
2522
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
2523
|
+
' "$BUN_RIG" >&2
|
|
2524
|
+
exit 1
|
|
2525
|
+
fi
|
|
2526
|
+
|
|
2527
|
+
USER_BIN="$HOME/.local/bin"
|
|
2528
|
+
mkdir -p "$USER_BIN"
|
|
2529
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
2530
|
+
#!/usr/bin/env bash
|
|
2531
|
+
set -euo pipefail
|
|
2532
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
2533
|
+
RIG_SHIM
|
|
2534
|
+
chmod +x "$USER_BIN/rig"
|
|
2535
|
+
|
|
2536
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
2537
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
2538
|
+
|
|
2539
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
2540
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
2541
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
2542
|
+
exit 1
|
|
2543
|
+
fi
|
|
2544
|
+
|
|
2545
|
+
say "Verifying rig"
|
|
2546
|
+
"$BUN_RIG" --help >/dev/null
|
|
2547
|
+
rig --help >/dev/null
|
|
2548
|
+
say "Done. Run: rig --help"
|
|
2549
|
+
`;
|
|
1880
2550
|
}
|
|
1881
2551
|
function normalizePrMode(value) {
|
|
1882
2552
|
const mode = normalizeString(value);
|
|
1883
2553
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
1884
2554
|
}
|
|
2555
|
+
function requestAuthResult(input) {
|
|
2556
|
+
return {
|
|
2557
|
+
authorized: input.authorized,
|
|
2558
|
+
actor: input.actor ?? null,
|
|
2559
|
+
reason: input.reason,
|
|
2560
|
+
login: input.login ?? null,
|
|
2561
|
+
userId: input.userId ?? null,
|
|
2562
|
+
userNamespace: input.userNamespace ?? null,
|
|
2563
|
+
authStateFile: input.authStateFile ?? null
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
function namespaceFromSessionIndex(entry) {
|
|
2567
|
+
const stateDir2 = dirname9(entry.authStateFile);
|
|
2568
|
+
return {
|
|
2569
|
+
key: entry.namespaceKey,
|
|
2570
|
+
userId: entry.userId,
|
|
2571
|
+
login: entry.login,
|
|
2572
|
+
root: entry.namespaceRoot,
|
|
2573
|
+
stateDir: stateDir2,
|
|
2574
|
+
authStateFile: entry.authStateFile,
|
|
2575
|
+
metadataFile: resolve13(stateDir2, "user-namespace.json"),
|
|
2576
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
2577
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
1885
2580
|
function authorizeRigHttpRequest(input) {
|
|
1886
2581
|
if (input.legacyAuthorized) {
|
|
1887
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
2582
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
1888
2583
|
}
|
|
1889
2584
|
const bearer = bearerTokenFromRequest(input.req);
|
|
1890
2585
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
1891
2586
|
const storedToken = store.readToken();
|
|
1892
2587
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
1893
2588
|
if (session) {
|
|
1894
|
-
return {
|
|
2589
|
+
return requestAuthResult({
|
|
2590
|
+
authorized: true,
|
|
2591
|
+
actor: session.login ?? "github-operator",
|
|
2592
|
+
reason: "github-session",
|
|
2593
|
+
login: session.login,
|
|
2594
|
+
userId: session.userId,
|
|
2595
|
+
authStateFile: store.stateFile
|
|
2596
|
+
});
|
|
1895
2597
|
}
|
|
1896
2598
|
if (bearer && storedToken && bearer === storedToken) {
|
|
1897
2599
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
1898
|
-
return {
|
|
2600
|
+
return requestAuthResult({
|
|
2601
|
+
authorized: true,
|
|
2602
|
+
actor: status.login ?? "github-operator",
|
|
2603
|
+
reason: "github-token",
|
|
2604
|
+
login: status.login,
|
|
2605
|
+
userId: status.userId,
|
|
2606
|
+
authStateFile: store.stateFile
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
2610
|
+
if (indexedSession) {
|
|
2611
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
2612
|
+
return requestAuthResult({
|
|
2613
|
+
authorized: true,
|
|
2614
|
+
actor: indexedSession.login ?? "github-operator",
|
|
2615
|
+
reason: "github-user-session",
|
|
2616
|
+
login: indexedSession.login,
|
|
2617
|
+
userId: indexedSession.userId,
|
|
2618
|
+
userNamespace,
|
|
2619
|
+
authStateFile: indexedSession.authStateFile
|
|
2620
|
+
});
|
|
1899
2621
|
}
|
|
1900
2622
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
1901
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
2623
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
1902
2624
|
}
|
|
1903
2625
|
if (!input.serverAuthToken && !storedToken) {
|
|
1904
2626
|
if (isLoopbackRequest(input.req)) {
|
|
1905
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
2627
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
1906
2628
|
}
|
|
1907
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
2629
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
1908
2630
|
}
|
|
1909
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
2631
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
1910
2632
|
}
|
|
1911
2633
|
async function fetchGitHubUserInfo(token) {
|
|
1912
2634
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -1926,6 +2648,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
1926
2648
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
1927
2649
|
};
|
|
1928
2650
|
}
|
|
2651
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
2652
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
2653
|
+
return false;
|
|
2654
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
2655
|
+
if (!stateDir2)
|
|
2656
|
+
return true;
|
|
2657
|
+
return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
|
|
2658
|
+
}
|
|
2659
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
2660
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
2661
|
+
}
|
|
2662
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
2663
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
2664
|
+
}
|
|
2665
|
+
function userNamespaceResponse(namespace) {
|
|
2666
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
2667
|
+
}
|
|
2668
|
+
function resolveNamespacedBaseDir(input) {
|
|
2669
|
+
if (input.explicitBaseDir)
|
|
2670
|
+
return input.explicitBaseDir;
|
|
2671
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
2672
|
+
if (input.userNamespace) {
|
|
2673
|
+
return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
2674
|
+
}
|
|
2675
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
2676
|
+
}
|
|
2677
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
2678
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
2679
|
+
}
|
|
2680
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
2681
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
2682
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
2683
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
2684
|
+
store.saveToken({
|
|
2685
|
+
token: input.token,
|
|
2686
|
+
tokenSource: input.tokenSource,
|
|
2687
|
+
login: input.user.login,
|
|
2688
|
+
userId: input.user.userId,
|
|
2689
|
+
scopes: input.user.scopes,
|
|
2690
|
+
selectedRepo: input.selectedRepo
|
|
2691
|
+
});
|
|
2692
|
+
const apiSession = store.createApiSession();
|
|
2693
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
2694
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
2695
|
+
if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
|
|
2696
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
|
|
2697
|
+
}
|
|
2698
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
2699
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
2700
|
+
rootStore.saveToken({
|
|
2701
|
+
token: input.token,
|
|
2702
|
+
tokenSource: input.tokenSource,
|
|
2703
|
+
login: input.user.login,
|
|
2704
|
+
userId: input.user.userId,
|
|
2705
|
+
scopes: input.user.scopes,
|
|
2706
|
+
selectedRepo: input.selectedRepo
|
|
2707
|
+
});
|
|
2708
|
+
rootStore.createApiSession();
|
|
2709
|
+
}
|
|
2710
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
2711
|
+
}
|
|
1929
2712
|
async function postGitHubForm(endpoint, body) {
|
|
1930
2713
|
const response = await fetch(endpoint, {
|
|
1931
2714
|
method: "POST",
|
|
@@ -1943,11 +2726,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
1943
2726
|
const requestedRoot = normalizeString(rawRoot);
|
|
1944
2727
|
if (!requestedRoot)
|
|
1945
2728
|
return currentRoot;
|
|
1946
|
-
if (!
|
|
2729
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
1947
2730
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
1948
2731
|
}
|
|
1949
|
-
const normalizedRoot =
|
|
1950
|
-
if (!
|
|
2732
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
2733
|
+
if (!existsSync10(normalizedRoot)) {
|
|
1951
2734
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
1952
2735
|
}
|
|
1953
2736
|
return normalizedRoot;
|
|
@@ -2175,6 +2958,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
2175
2958
|
}
|
|
2176
2959
|
return filtered;
|
|
2177
2960
|
}
|
|
2961
|
+
function issueAnalysisTargetFor(source) {
|
|
2962
|
+
if (!source)
|
|
2963
|
+
return null;
|
|
2964
|
+
const candidate = source;
|
|
2965
|
+
if (typeof candidate.updateTask !== "function")
|
|
2966
|
+
return null;
|
|
2967
|
+
return {
|
|
2968
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
2969
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
2970
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
2971
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
2972
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function uniqueStringList(value) {
|
|
2976
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
2977
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
2978
|
+
}
|
|
2979
|
+
function taskRecordId(task) {
|
|
2980
|
+
return String(task.id ?? "");
|
|
2981
|
+
}
|
|
2178
2982
|
function redactRemoteEndpoint(endpoint) {
|
|
2179
2983
|
const { token, ...rest } = endpoint;
|
|
2180
2984
|
return {
|
|
@@ -2259,6 +3063,13 @@ function createRigServerFetch(state, deps) {
|
|
|
2259
3063
|
notifications: state.targets.length
|
|
2260
3064
|
});
|
|
2261
3065
|
}
|
|
3066
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
3067
|
+
return new Response(buildRigInstallScript(), {
|
|
3068
|
+
headers: {
|
|
3069
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
2262
3073
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
2263
3074
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
2264
3075
|
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
@@ -2471,16 +3282,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2471
3282
|
if (!source) {
|
|
2472
3283
|
return deps.badRequest("No task source is configured");
|
|
2473
3284
|
}
|
|
3285
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
3286
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
3287
|
+
}
|
|
2474
3288
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
2475
3289
|
return;
|
|
2476
3290
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
2477
|
-
if (source.updateTask) {
|
|
2478
|
-
await source.updateTask(id, update);
|
|
2479
|
-
} else if (update.status && source.updateStatus) {
|
|
2480
|
-
await source.updateStatus(id, update.status);
|
|
2481
|
-
} else {
|
|
2482
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
2483
|
-
}
|
|
2484
3291
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
2485
3292
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
2486
3293
|
taskId: id,
|
|
@@ -2489,6 +3296,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2489
3296
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
2490
3297
|
config: ctx?.config
|
|
2491
3298
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
3299
|
+
if (update.status && githubProjectsEnabled(ctx?.config) && projectSync.synced === false) {
|
|
3300
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
3301
|
+
}
|
|
3302
|
+
try {
|
|
3303
|
+
if (source.updateTask) {
|
|
3304
|
+
await source.updateTask(id, update);
|
|
3305
|
+
} else if (update.status && source.updateStatus) {
|
|
3306
|
+
await source.updateStatus(id, update.status);
|
|
3307
|
+
}
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
let rollback = null;
|
|
3310
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
3311
|
+
if (update.status && previousStatus && githubProjectsEnabled(ctx?.config) && projectSync.synced !== false) {
|
|
3312
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
3313
|
+
taskId: id,
|
|
3314
|
+
status: previousStatus,
|
|
3315
|
+
issueNodeId,
|
|
3316
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3317
|
+
config: ctx?.config
|
|
3318
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
3319
|
+
}
|
|
3320
|
+
return deps.jsonResponse({
|
|
3321
|
+
ok: false,
|
|
3322
|
+
id,
|
|
3323
|
+
projectSync,
|
|
3324
|
+
rollback,
|
|
3325
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3326
|
+
}, 502);
|
|
3327
|
+
}
|
|
2492
3328
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
2493
3329
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
2494
3330
|
return;
|
|
@@ -2497,29 +3333,105 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2497
3333
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
2498
3334
|
}
|
|
2499
3335
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
3336
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
3337
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
3338
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
3339
|
+
}
|
|
2500
3340
|
return deps.jsonResponse({
|
|
2501
3341
|
ok: true,
|
|
2502
3342
|
ready: true,
|
|
2503
3343
|
labelsReady: true,
|
|
2504
|
-
labels: [
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
3344
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
3345
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
3349
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
3350
|
+
if (!owner)
|
|
3351
|
+
return deps.badRequest("owner is required");
|
|
3352
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3353
|
+
if (!token)
|
|
3354
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
3355
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
3356
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3357
|
+
});
|
|
3358
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
3359
|
+
}
|
|
3360
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
3361
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
3362
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
3363
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3364
|
+
if (!token)
|
|
3365
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
3366
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
3367
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3368
|
+
});
|
|
3369
|
+
return deps.jsonResponse({ ok: true, field });
|
|
3370
|
+
}
|
|
3371
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
3372
|
+
const body = await deps.readJsonBody(req);
|
|
3373
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
3374
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
3375
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
3376
|
+
return deps.badRequest("ids is required unless all=true");
|
|
3377
|
+
}
|
|
3378
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
3379
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
3380
|
+
const target = issueAnalysisTargetFor(source);
|
|
3381
|
+
if (!source || !target) {
|
|
3382
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
3383
|
+
}
|
|
3384
|
+
const allTasks = [...await source.list()];
|
|
3385
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
3386
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
3387
|
+
if (cached)
|
|
3388
|
+
return cached;
|
|
3389
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
3390
|
+
}))).filter((task) => Boolean(task));
|
|
3391
|
+
if (issues.length === 0) {
|
|
3392
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
3393
|
+
}
|
|
3394
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
3395
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
3396
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
3397
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
3398
|
+
const service = createIssueAnalysisService({
|
|
3399
|
+
analyzer: createPiIssueAnalyzer({
|
|
3400
|
+
...model ? { model } : {},
|
|
3401
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
3402
|
+
}),
|
|
3403
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
3404
|
+
});
|
|
3405
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
3406
|
+
let results;
|
|
3407
|
+
try {
|
|
3408
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
return deps.jsonResponse({
|
|
3411
|
+
ok: false,
|
|
3412
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3413
|
+
reason,
|
|
3414
|
+
ids: issues.map((issue) => issue.id)
|
|
3415
|
+
}, 502);
|
|
3416
|
+
}
|
|
3417
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
3418
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
3419
|
+
return;
|
|
3420
|
+
});
|
|
3421
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
3422
|
+
return deps.jsonResponse({
|
|
3423
|
+
ok: true,
|
|
3424
|
+
reason,
|
|
3425
|
+
analyzed: results.map((entry) => ({
|
|
3426
|
+
id: entry.issue.id,
|
|
3427
|
+
title: entry.issue.title ?? null,
|
|
3428
|
+
result: entry.result
|
|
3429
|
+
}))
|
|
2518
3430
|
});
|
|
2519
3431
|
}
|
|
2520
3432
|
if (url.pathname === "/api/server/status") {
|
|
2521
3433
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2522
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
3434
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
2523
3435
|
return deps.jsonResponse({
|
|
2524
3436
|
ok: true,
|
|
2525
3437
|
projectRoot: state.projectRoot,
|
|
@@ -2543,8 +3455,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2543
3455
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
2544
3456
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
2545
3457
|
} : undefined;
|
|
2546
|
-
const
|
|
2547
|
-
|
|
3458
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3459
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
3460
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2548
3461
|
}
|
|
2549
3462
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
2550
3463
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -2557,8 +3470,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2557
3470
|
if (!archiveContentBase64) {
|
|
2558
3471
|
return deps.badRequest("archiveContentBase64 is required");
|
|
2559
3472
|
}
|
|
2560
|
-
const
|
|
2561
|
-
const
|
|
3473
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3474
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3475
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
3476
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
3477
|
+
userNamespace: requestAuth.userNamespace,
|
|
3478
|
+
legacyProjectRoot: state.projectRoot,
|
|
3479
|
+
legacySubdir: "remote-snapshots"
|
|
3480
|
+
});
|
|
3481
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
2562
3482
|
try {
|
|
2563
3483
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
2564
3484
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -2571,14 +3491,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2571
3491
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2572
3492
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2573
3493
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2574
|
-
const project = linkProjectCheckout(
|
|
3494
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2575
3495
|
kind: "uploaded-snapshot",
|
|
2576
3496
|
path: checkout.path,
|
|
2577
3497
|
ref: checkout.snapshotId
|
|
2578
3498
|
});
|
|
2579
3499
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
2580
3500
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
2581
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3501
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2582
3502
|
} catch (error) {
|
|
2583
3503
|
return deps.jsonResponse({
|
|
2584
3504
|
ok: false,
|
|
@@ -2598,10 +3518,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2598
3518
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
2599
3519
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
2600
3520
|
}
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
3521
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3522
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3523
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
3524
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
3525
|
+
userNamespace: requestAuth.userNamespace,
|
|
3526
|
+
legacyProjectRoot: state.projectRoot,
|
|
3527
|
+
legacySubdir: "remote-checkouts"
|
|
3528
|
+
});
|
|
3529
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
2603
3530
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
2604
|
-
const credentialToken =
|
|
3531
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
2605
3532
|
try {
|
|
2606
3533
|
const checkout = await prepareRemoteCheckout({
|
|
2607
3534
|
command: runRemoteCheckoutCommand,
|
|
@@ -2610,14 +3537,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2610
3537
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2611
3538
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2612
3539
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2613
|
-
const project = linkProjectCheckout(
|
|
3540
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2614
3541
|
kind: checkout.kind,
|
|
2615
3542
|
path: checkout.path,
|
|
2616
3543
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
2617
3544
|
});
|
|
2618
3545
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
2619
3546
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
2620
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3547
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2621
3548
|
} catch (error) {
|
|
2622
3549
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2623
3550
|
}
|
|
@@ -2634,16 +3561,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2634
3561
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
2635
3562
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
2636
3563
|
}
|
|
2637
|
-
const
|
|
3564
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3565
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2638
3566
|
kind,
|
|
2639
3567
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
2640
3568
|
ref: normalizeString(body.ref) ?? undefined
|
|
2641
3569
|
});
|
|
2642
|
-
return deps.jsonResponse({ ok: true, project });
|
|
3570
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2643
3571
|
}
|
|
2644
3572
|
if (req.method === "GET") {
|
|
2645
|
-
const
|
|
2646
|
-
|
|
3573
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3574
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
3575
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
2647
3576
|
}
|
|
2648
3577
|
}
|
|
2649
3578
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -2652,13 +3581,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2652
3581
|
if (!requestedRoot) {
|
|
2653
3582
|
return deps.badRequest("projectRoot is required");
|
|
2654
3583
|
}
|
|
2655
|
-
if (!
|
|
3584
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
2656
3585
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
2657
3586
|
}
|
|
2658
|
-
const normalizedRoot =
|
|
2659
|
-
const exists =
|
|
2660
|
-
if (exists) {
|
|
2661
|
-
|
|
3587
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
3588
|
+
const exists = existsSync10(normalizedRoot);
|
|
3589
|
+
if (exists && requestAuth.userNamespace) {
|
|
3590
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
3591
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
3592
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
3593
|
+
return deps.jsonResponse({
|
|
3594
|
+
ok: false,
|
|
3595
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
3596
|
+
projectRoot: state.projectRoot,
|
|
3597
|
+
requestedProjectRoot: normalizedRoot,
|
|
3598
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
3599
|
+
}, 403);
|
|
3600
|
+
}
|
|
3601
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
3602
|
+
} else if (exists) {
|
|
3603
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
2662
3604
|
}
|
|
2663
3605
|
const control = buildServerControlStatus();
|
|
2664
3606
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -2673,7 +3615,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2673
3615
|
message: "Requested project root does not exist on the Rig server host."
|
|
2674
3616
|
}, 404);
|
|
2675
3617
|
}
|
|
2676
|
-
if (!
|
|
3618
|
+
if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
|
|
2677
3619
|
return deps.jsonResponse({
|
|
2678
3620
|
ok: false,
|
|
2679
3621
|
projectRoot: state.projectRoot,
|
|
@@ -2708,6 +3650,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2708
3650
|
exists,
|
|
2709
3651
|
control,
|
|
2710
3652
|
requiresRestart: false,
|
|
3653
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
2711
3654
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
2712
3655
|
}, 202);
|
|
2713
3656
|
}
|
|
@@ -2722,11 +3665,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2722
3665
|
}, 409);
|
|
2723
3666
|
}
|
|
2724
3667
|
if (url.pathname === "/api/github/auth/status") {
|
|
2725
|
-
const store =
|
|
2726
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
3668
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3669
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2727
3670
|
}
|
|
2728
3671
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
2729
|
-
const store =
|
|
3672
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
2730
3673
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2731
3674
|
if (!auth.signedIn) {
|
|
2732
3675
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -2754,24 +3697,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2754
3697
|
}
|
|
2755
3698
|
try {
|
|
2756
3699
|
const user = await fetchGitHubUserInfo(token);
|
|
2757
|
-
const
|
|
2758
|
-
state.projectRoot,
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
}
|
|
2772
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
2773
|
-
const apiSession = store.createApiSession();
|
|
2774
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
3700
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3701
|
+
projectRoot: state.projectRoot,
|
|
3702
|
+
token,
|
|
3703
|
+
tokenSource: "manual-token",
|
|
3704
|
+
user,
|
|
3705
|
+
selectedRepo,
|
|
3706
|
+
requestedProjectRoot
|
|
3707
|
+
});
|
|
3708
|
+
return deps.jsonResponse({
|
|
3709
|
+
ok: true,
|
|
3710
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
3711
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3712
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3713
|
+
});
|
|
2775
3714
|
} catch (error) {
|
|
2776
3715
|
const message = error instanceof Error ? error.message : String(error);
|
|
2777
3716
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -2838,9 +3777,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2838
3777
|
}
|
|
2839
3778
|
const token = result.payload.access_token;
|
|
2840
3779
|
const user = await fetchGitHubUserInfo(token);
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3780
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3781
|
+
projectRoot: state.projectRoot,
|
|
3782
|
+
token,
|
|
3783
|
+
tokenSource: "oauth-device",
|
|
3784
|
+
user,
|
|
3785
|
+
selectedRepo: null
|
|
3786
|
+
});
|
|
3787
|
+
store.clearPendingDevice(pollId);
|
|
3788
|
+
return deps.jsonResponse({
|
|
3789
|
+
ok: true,
|
|
3790
|
+
status: "signed-in",
|
|
3791
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
3792
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3793
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3794
|
+
});
|
|
2844
3795
|
}
|
|
2845
3796
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
2846
3797
|
const body = await deps.readJsonBody(req);
|
|
@@ -2849,7 +3800,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2849
3800
|
if (!owner || !repo) {
|
|
2850
3801
|
return deps.badRequest("owner and repo are required");
|
|
2851
3802
|
}
|
|
2852
|
-
const store =
|
|
3803
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
2853
3804
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2854
3805
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
2855
3806
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -2870,7 +3821,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2870
3821
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
2871
3822
|
}
|
|
2872
3823
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2873
|
-
const configPath =
|
|
3824
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
2874
3825
|
const source = buildGitHubProjectConfigSource({
|
|
2875
3826
|
projectName: rawProjectName,
|
|
2876
3827
|
owner,
|
|
@@ -2882,8 +3833,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2882
3833
|
ok: true,
|
|
2883
3834
|
projectRoot: targetRoot,
|
|
2884
3835
|
configPath,
|
|
2885
|
-
exists:
|
|
2886
|
-
requiresOverwrite:
|
|
3836
|
+
exists: existsSync10(configPath),
|
|
3837
|
+
requiresOverwrite: existsSync10(configPath),
|
|
2887
3838
|
source,
|
|
2888
3839
|
owner,
|
|
2889
3840
|
repo,
|
|
@@ -2919,8 +3870,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2919
3870
|
assignee,
|
|
2920
3871
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
2921
3872
|
});
|
|
2922
|
-
const configPath =
|
|
2923
|
-
if (
|
|
3873
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3874
|
+
if (existsSync10(configPath) && !overwrite) {
|
|
2924
3875
|
return deps.jsonResponse({
|
|
2925
3876
|
ok: false,
|
|
2926
3877
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -2936,11 +3887,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2936
3887
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
2937
3888
|
}
|
|
2938
3889
|
let backupPath = null;
|
|
2939
|
-
if (
|
|
3890
|
+
if (existsSync10(configPath)) {
|
|
2940
3891
|
backupPath = backupConfigPath(configPath);
|
|
2941
|
-
|
|
3892
|
+
copyFileSync2(configPath, backupPath);
|
|
2942
3893
|
}
|
|
2943
|
-
|
|
3894
|
+
writeFileSync9(configPath, source, "utf8");
|
|
2944
3895
|
const selectedRepo = `${owner}/${repo}`;
|
|
2945
3896
|
store.saveSelectedRepo(selectedRepo);
|
|
2946
3897
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -3212,11 +4163,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3212
4163
|
const runId = normalizeString(body.runId);
|
|
3213
4164
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3214
4165
|
const promptOverride = normalizeString(body.promptOverride);
|
|
4166
|
+
const restart = body.restart === true;
|
|
3215
4167
|
if (!runId) {
|
|
3216
4168
|
return deps.badRequest("runId is required");
|
|
3217
4169
|
}
|
|
3218
4170
|
try {
|
|
3219
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
4171
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
3220
4172
|
deps.broadcastSnapshotInvalidation(state);
|
|
3221
4173
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3222
4174
|
} catch (error) {
|
|
@@ -3225,7 +4177,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3225
4177
|
}
|
|
3226
4178
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
3227
4179
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
3228
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
4180
|
+
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve13(root, "packages", "pi-rig")).find((candidate) => existsSync10(resolve13(candidate, "package.json"))) ?? "npm:@h-rig/pi-rig";
|
|
3229
4181
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
3230
4182
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
3231
4183
|
}
|
|
@@ -3541,9 +4493,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3541
4493
|
} catch {
|
|
3542
4494
|
return deps.badRequest("Invalid artifact path");
|
|
3543
4495
|
}
|
|
3544
|
-
|
|
4496
|
+
mkdirSync9(dirname9(artifactPath), { recursive: true });
|
|
3545
4497
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
3546
|
-
|
|
4498
|
+
writeFileSync9(artifactPath, bytes);
|
|
3547
4499
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
3548
4500
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3549
4501
|
runId,
|
|
@@ -3580,13 +4532,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3580
4532
|
}
|
|
3581
4533
|
const run = leaseValidation.run;
|
|
3582
4534
|
const completedAt = new Date().toISOString();
|
|
4535
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
4536
|
+
if (run.taskId && workspaceDir) {
|
|
4537
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4538
|
+
status: "reviewing",
|
|
4539
|
+
completedAt: null,
|
|
4540
|
+
hostId,
|
|
4541
|
+
endpointId: leaseId,
|
|
4542
|
+
worktreePath: workspaceDir,
|
|
4543
|
+
serverCloseout: {
|
|
4544
|
+
status: "pending",
|
|
4545
|
+
phase: "queued",
|
|
4546
|
+
requestedAt: completedAt,
|
|
4547
|
+
updatedAt: completedAt,
|
|
4548
|
+
runtimeWorkspace: workspaceDir,
|
|
4549
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
4550
|
+
taskId: run.taskId,
|
|
4551
|
+
source: "remote-complete"
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4555
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
4556
|
+
title: "Server-owned closeout requested",
|
|
4557
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
4558
|
+
tone: "info",
|
|
4559
|
+
status: "reviewing",
|
|
4560
|
+
createdAt: completedAt,
|
|
4561
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
4562
|
+
}, "remote-server-closeout-requested");
|
|
4563
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
4564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4565
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4566
|
+
status: "failed",
|
|
4567
|
+
completedAt: new Date().toISOString(),
|
|
4568
|
+
errorText: detail,
|
|
4569
|
+
serverCloseout: {
|
|
4570
|
+
status: "failed",
|
|
4571
|
+
phase: "failed",
|
|
4572
|
+
updatedAt: new Date().toISOString(),
|
|
4573
|
+
error: detail
|
|
4574
|
+
}
|
|
4575
|
+
});
|
|
4576
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4577
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
4578
|
+
title: "Server-owned closeout failed",
|
|
4579
|
+
detail,
|
|
4580
|
+
tone: "error",
|
|
4581
|
+
status: "failed",
|
|
4582
|
+
createdAt: new Date().toISOString()
|
|
4583
|
+
}, "remote-server-closeout-failed");
|
|
4584
|
+
}).finally(() => {
|
|
4585
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
4586
|
+
});
|
|
4587
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
4588
|
+
return deps.jsonResponse({
|
|
4589
|
+
ok: true,
|
|
4590
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
4591
|
+
hostId,
|
|
4592
|
+
runId,
|
|
4593
|
+
leaseId,
|
|
4594
|
+
closeout: "server-owned",
|
|
4595
|
+
acceptedAt: new Date().toISOString()
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
3583
4598
|
patchRunRecord(state.projectRoot, runId, {
|
|
3584
4599
|
status: "completed",
|
|
3585
4600
|
completedAt,
|
|
3586
4601
|
hostId,
|
|
3587
4602
|
endpointId: leaseId
|
|
3588
4603
|
});
|
|
3589
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
3590
4604
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3591
4605
|
type: "run.completed",
|
|
3592
4606
|
runId,
|
|
@@ -3705,12 +4719,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3705
4719
|
try {
|
|
3706
4720
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
3707
4721
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
3708
|
-
const artifactsRoot =
|
|
4722
|
+
const artifactsRoot = resolve13(runRoot, "remote-artifacts");
|
|
3709
4723
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
3710
4724
|
} catch {
|
|
3711
4725
|
return deps.badRequest("Invalid artifact path");
|
|
3712
4726
|
}
|
|
3713
|
-
if (!
|
|
4727
|
+
if (!existsSync10(artifactPath)) {
|
|
3714
4728
|
return deps.notFound();
|
|
3715
4729
|
}
|
|
3716
4730
|
return new Response(Bun.file(artifactPath));
|
|
@@ -3723,6 +4737,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3723
4737
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
3724
4738
|
return deps.jsonResponse(page);
|
|
3725
4739
|
}
|
|
4740
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
4741
|
+
if (runTimelineMatch) {
|
|
4742
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
4743
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
4744
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
4745
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
4746
|
+
return deps.jsonResponse(page);
|
|
4747
|
+
}
|
|
3726
4748
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
3727
4749
|
if (runSteerMatch && req.method === "POST") {
|
|
3728
4750
|
const runId = decodeURIComponent(runSteerMatch[1]);
|