@h-rig/server 0.0.6-alpha.1 → 0.0.6-alpha.11
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 +920 -287
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +117 -21
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +998 -151
- 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-mutations.js +248 -103
- package/dist/src/server-helpers/snapshot-orchestrator.js +1 -1
- package/dist/src/server-helpers/snapshot-service.js +1 -1
- package/dist/src/server-helpers/ws-router.js +4 -4
- package/dist/src/server.js +921 -287
- 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 {
|
|
@@ -469,6 +469,273 @@ async function refreshTaskProjection(projectRoot, input) {
|
|
|
469
469
|
});
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
// packages/server/src/server-helpers/issue-analysis.ts
|
|
473
|
+
import { createHash } from "crypto";
|
|
474
|
+
function stableIssueHash(issue) {
|
|
475
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
476
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
477
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
478
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
479
|
+
}
|
|
480
|
+
function renderIssueAnalysisPrompt(input) {
|
|
481
|
+
const issue = input.issue;
|
|
482
|
+
const neighbors = input.neighbors ?? [];
|
|
483
|
+
return [
|
|
484
|
+
"You are Rig issue analysis running inside Pi.",
|
|
485
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
|
|
486
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
487
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
488
|
+
"",
|
|
489
|
+
"Issue:",
|
|
490
|
+
JSON.stringify({
|
|
491
|
+
id: issue.id,
|
|
492
|
+
title: issue.title,
|
|
493
|
+
body: issue.body,
|
|
494
|
+
labels: issue.labels,
|
|
495
|
+
deps: issue.deps,
|
|
496
|
+
status: issue.status
|
|
497
|
+
}, null, 2),
|
|
498
|
+
"",
|
|
499
|
+
"Neighbor tasks:",
|
|
500
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
501
|
+
].join(`
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
504
|
+
function isRecord(value) {
|
|
505
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
506
|
+
}
|
|
507
|
+
function stringArray(value) {
|
|
508
|
+
if (!Array.isArray(value))
|
|
509
|
+
return;
|
|
510
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
511
|
+
}
|
|
512
|
+
function generatedIssues(value) {
|
|
513
|
+
if (!Array.isArray(value))
|
|
514
|
+
return;
|
|
515
|
+
return value.flatMap((entry) => {
|
|
516
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
517
|
+
return [];
|
|
518
|
+
return [{
|
|
519
|
+
title: entry.title,
|
|
520
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
521
|
+
labels: stringArray(entry.labels) ?? [],
|
|
522
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
523
|
+
}];
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function findJsonLikeText(value) {
|
|
527
|
+
if (typeof value === "string") {
|
|
528
|
+
const trimmed = value.trim();
|
|
529
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
530
|
+
return trimmed;
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(value)) {
|
|
534
|
+
for (const entry of value) {
|
|
535
|
+
const found = findJsonLikeText(entry);
|
|
536
|
+
if (found)
|
|
537
|
+
return found;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
if (!isRecord(value))
|
|
542
|
+
return null;
|
|
543
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
544
|
+
const found = findJsonLikeText(value[key]);
|
|
545
|
+
if (found)
|
|
546
|
+
return found;
|
|
547
|
+
}
|
|
548
|
+
for (const entry of Object.values(value)) {
|
|
549
|
+
const found = findJsonLikeText(entry);
|
|
550
|
+
if (found)
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
function candidateAnalysisObject(value) {
|
|
556
|
+
if (!isRecord(value))
|
|
557
|
+
return null;
|
|
558
|
+
if (isRecord(value.result))
|
|
559
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
560
|
+
if (isRecord(value.analysis))
|
|
561
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
562
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
const nested = findJsonLikeText(value);
|
|
566
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
567
|
+
try {
|
|
568
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
569
|
+
return candidateAnalysisObject(parsedNested);
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function parseIssueAnalysisResult(raw) {
|
|
577
|
+
let parsed = raw;
|
|
578
|
+
if (typeof raw === "string") {
|
|
579
|
+
const trimmed = raw.trim();
|
|
580
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
583
|
+
} catch {
|
|
584
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
585
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
589
|
+
if (!candidate)
|
|
590
|
+
return {};
|
|
591
|
+
const result = {};
|
|
592
|
+
if (isRecord(candidate.metadataPatch))
|
|
593
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
594
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
595
|
+
if (add?.length)
|
|
596
|
+
result.labelsToAdd = add;
|
|
597
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
598
|
+
if (remove?.length)
|
|
599
|
+
result.labelsToRemove = remove;
|
|
600
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
601
|
+
if (generated?.length)
|
|
602
|
+
result.generatedIssues = generated;
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
606
|
+
return async (command, args, options) => {
|
|
607
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
608
|
+
const proc = Bun.spawn([command, ...args], {
|
|
609
|
+
stdout: "pipe",
|
|
610
|
+
stderr: "pipe",
|
|
611
|
+
env
|
|
612
|
+
});
|
|
613
|
+
let timedOut = false;
|
|
614
|
+
const timer = setTimeout(() => {
|
|
615
|
+
timedOut = true;
|
|
616
|
+
proc.kill();
|
|
617
|
+
}, options.timeoutMs);
|
|
618
|
+
try {
|
|
619
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
620
|
+
new Response(proc.stdout).text(),
|
|
621
|
+
new Response(proc.stderr).text(),
|
|
622
|
+
proc.exited
|
|
623
|
+
]);
|
|
624
|
+
return {
|
|
625
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
626
|
+
stdout,
|
|
627
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
628
|
+
};
|
|
629
|
+
} finally {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
635
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
636
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
637
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
638
|
+
return async ({ prompt }) => {
|
|
639
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
640
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
641
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
642
|
+
if (provider)
|
|
643
|
+
args.push("--provider", provider);
|
|
644
|
+
if (model)
|
|
645
|
+
args.push("--model", model);
|
|
646
|
+
args.push(prompt);
|
|
647
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
648
|
+
if (result.exitCode !== 0) {
|
|
649
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
650
|
+
}
|
|
651
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function defaultStatusComment(input) {
|
|
655
|
+
const changes = [
|
|
656
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
657
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
658
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
659
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
660
|
+
].filter((entry) => Boolean(entry));
|
|
661
|
+
if (changes.length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
return [
|
|
664
|
+
"<!-- rig:status-comment -->",
|
|
665
|
+
"### Rig issue analysis",
|
|
666
|
+
"",
|
|
667
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
668
|
+
"",
|
|
669
|
+
...changes.map((change) => `- ${change}`)
|
|
670
|
+
].join(`
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
function uniqueLabels(labels, required = []) {
|
|
674
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
675
|
+
}
|
|
676
|
+
function createIssueAnalysisWriteBack(input) {
|
|
677
|
+
return async ({ issue, result, reason }) => {
|
|
678
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
679
|
+
if (!input.target.updateTask)
|
|
680
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
681
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
682
|
+
}
|
|
683
|
+
if (result.labelsToAdd?.length) {
|
|
684
|
+
if (!input.target.addLabels)
|
|
685
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
686
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
687
|
+
}
|
|
688
|
+
if (result.labelsToRemove?.length) {
|
|
689
|
+
if (!input.target.removeLabels)
|
|
690
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
691
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
692
|
+
}
|
|
693
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
694
|
+
if (comment?.trim()) {
|
|
695
|
+
if (!input.target.updateTask)
|
|
696
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
697
|
+
await input.target.updateTask(issue.id, { comment });
|
|
698
|
+
}
|
|
699
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
700
|
+
if (!input.target.createIssue)
|
|
701
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
702
|
+
await input.target.createIssue({
|
|
703
|
+
title: generated.title,
|
|
704
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
705
|
+
|
|
706
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
707
|
+
` : generated.body,
|
|
708
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function createIssueAnalysisService(input) {
|
|
714
|
+
const analyzedHashes = new Map;
|
|
715
|
+
return {
|
|
716
|
+
async analyze(issues, options = {}) {
|
|
717
|
+
const results = [];
|
|
718
|
+
const neighbors = options.neighbors ?? issues;
|
|
719
|
+
for (const issue of issues) {
|
|
720
|
+
const hash = stableIssueHash(issue);
|
|
721
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
722
|
+
continue;
|
|
723
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
724
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
725
|
+
analyzedHashes.set(issue.id, hash);
|
|
726
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
727
|
+
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
728
|
+
}
|
|
729
|
+
results.push({ issue, result });
|
|
730
|
+
}
|
|
731
|
+
return results;
|
|
732
|
+
},
|
|
733
|
+
clearCache() {
|
|
734
|
+
analyzedHashes.clear();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
472
739
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
473
740
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
474
741
|
|
|
@@ -535,8 +802,9 @@ import {
|
|
|
535
802
|
} from "@rig/runtime/control-plane/authority-files";
|
|
536
803
|
|
|
537
804
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
538
|
-
import {
|
|
539
|
-
import {
|
|
805
|
+
import { randomBytes } from "crypto";
|
|
806
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
807
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
540
808
|
function cleanString(value) {
|
|
541
809
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
542
810
|
}
|
|
@@ -548,6 +816,44 @@ function cleanScopes(value) {
|
|
|
548
816
|
return clean ? [clean] : [];
|
|
549
817
|
});
|
|
550
818
|
}
|
|
819
|
+
function parseApiSessions(value) {
|
|
820
|
+
if (!Array.isArray(value))
|
|
821
|
+
return [];
|
|
822
|
+
return value.flatMap((entry) => {
|
|
823
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
824
|
+
return [];
|
|
825
|
+
const record = entry;
|
|
826
|
+
const token = cleanString(record.token);
|
|
827
|
+
if (!token)
|
|
828
|
+
return [];
|
|
829
|
+
return [{
|
|
830
|
+
token,
|
|
831
|
+
login: cleanString(record.login),
|
|
832
|
+
userId: cleanString(record.userId),
|
|
833
|
+
createdAt: cleanString(record.createdAt) ?? undefined
|
|
834
|
+
}];
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function parsePendingDevice(value) {
|
|
838
|
+
if (!value || typeof value !== "object")
|
|
839
|
+
return null;
|
|
840
|
+
const record = value;
|
|
841
|
+
const pollId = cleanString(record.pollId);
|
|
842
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
843
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
844
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
845
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
846
|
+
return null;
|
|
847
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
848
|
+
}
|
|
849
|
+
function parsePendingDevices(value) {
|
|
850
|
+
if (!Array.isArray(value))
|
|
851
|
+
return [];
|
|
852
|
+
return value.flatMap((entry) => {
|
|
853
|
+
const pending = parsePendingDevice(entry);
|
|
854
|
+
return pending ? [pending] : [];
|
|
855
|
+
});
|
|
856
|
+
}
|
|
551
857
|
function readStoredAuth(stateFile) {
|
|
552
858
|
if (!existsSync4(stateFile))
|
|
553
859
|
return {};
|
|
@@ -561,37 +867,44 @@ function readStoredAuth(stateFile) {
|
|
|
561
867
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
562
868
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
563
869
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
870
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
871
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
564
872
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
565
873
|
};
|
|
566
874
|
} catch {
|
|
567
875
|
return {};
|
|
568
876
|
}
|
|
569
877
|
}
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
return null;
|
|
573
|
-
const record = value;
|
|
574
|
-
const pollId = cleanString(record.pollId);
|
|
575
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
576
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
577
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
578
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
579
|
-
return null;
|
|
580
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
878
|
+
function newApiSessionToken() {
|
|
879
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
581
880
|
}
|
|
582
881
|
function writeStoredAuth(stateFile, payload) {
|
|
583
|
-
mkdirSync3(
|
|
882
|
+
mkdirSync3(dirname3(stateFile), { recursive: true });
|
|
584
883
|
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
585
884
|
`, { encoding: "utf8", mode: 384 });
|
|
586
885
|
try {
|
|
587
886
|
chmodSync(stateFile, 384);
|
|
588
887
|
} catch {}
|
|
589
888
|
}
|
|
889
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
890
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
891
|
+
}
|
|
590
892
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
591
893
|
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
592
894
|
}
|
|
593
|
-
function
|
|
594
|
-
const
|
|
895
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
896
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
897
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
898
|
+
if (existsSync4(stateFile)) {
|
|
899
|
+
copyFileSync(stateFile, targetFile);
|
|
900
|
+
try {
|
|
901
|
+
chmodSync(targetFile, 384);
|
|
902
|
+
} catch {}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
writeStoredAuth(targetFile, {});
|
|
906
|
+
}
|
|
907
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
595
908
|
return {
|
|
596
909
|
stateFile,
|
|
597
910
|
status(options) {
|
|
@@ -621,14 +934,53 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
621
934
|
scopes: input.scopes ?? [],
|
|
622
935
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
623
936
|
pendingDevice: null,
|
|
937
|
+
pendingDevices: [],
|
|
938
|
+
apiSessions: previous.apiSessions ?? [],
|
|
939
|
+
updatedAt: new Date().toISOString()
|
|
940
|
+
});
|
|
941
|
+
},
|
|
942
|
+
createApiSession() {
|
|
943
|
+
const previous = readStoredAuth(stateFile);
|
|
944
|
+
const token = newApiSessionToken();
|
|
945
|
+
const session = {
|
|
946
|
+
token,
|
|
947
|
+
login: cleanString(previous.login),
|
|
948
|
+
userId: cleanString(previous.userId),
|
|
949
|
+
createdAt: new Date().toISOString()
|
|
950
|
+
};
|
|
951
|
+
writeStoredAuth(stateFile, {
|
|
952
|
+
...previous,
|
|
953
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
624
954
|
updatedAt: new Date().toISOString()
|
|
625
955
|
});
|
|
956
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
957
|
+
},
|
|
958
|
+
readApiSession(token) {
|
|
959
|
+
const clean = cleanString(token);
|
|
960
|
+
if (!clean)
|
|
961
|
+
return null;
|
|
962
|
+
const previous = readStoredAuth(stateFile);
|
|
963
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
964
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
965
|
+
},
|
|
966
|
+
copyToProjectRoot(projectRoot) {
|
|
967
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
968
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
969
|
+
},
|
|
970
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
971
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
626
972
|
},
|
|
627
973
|
savePendingDevice(input) {
|
|
628
974
|
const previous = readStoredAuth(stateFile);
|
|
975
|
+
const pendingDevices = [
|
|
976
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
977
|
+
...previous.pendingDevices ?? [],
|
|
978
|
+
input
|
|
979
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
629
980
|
writeStoredAuth(stateFile, {
|
|
630
981
|
...previous,
|
|
631
|
-
pendingDevice:
|
|
982
|
+
pendingDevice: null,
|
|
983
|
+
pendingDevices,
|
|
632
984
|
updatedAt: new Date().toISOString()
|
|
633
985
|
});
|
|
634
986
|
},
|
|
@@ -641,23 +993,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
641
993
|
});
|
|
642
994
|
},
|
|
643
995
|
readPendingDevice(pollId) {
|
|
644
|
-
const
|
|
645
|
-
|
|
996
|
+
const previous = readStoredAuth(stateFile);
|
|
997
|
+
const pending = [
|
|
998
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
999
|
+
...previous.pendingDevices ?? []
|
|
1000
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
1001
|
+
if (!pending)
|
|
646
1002
|
return null;
|
|
647
1003
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
648
1004
|
return null;
|
|
649
1005
|
return pending;
|
|
650
1006
|
},
|
|
651
|
-
clearPendingDevice() {
|
|
1007
|
+
clearPendingDevice(pollId) {
|
|
652
1008
|
const previous = readStoredAuth(stateFile);
|
|
1009
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
653
1010
|
writeStoredAuth(stateFile, {
|
|
654
1011
|
...previous,
|
|
655
1012
|
pendingDevice: null,
|
|
1013
|
+
pendingDevices: remaining,
|
|
656
1014
|
updatedAt: new Date().toISOString()
|
|
657
1015
|
});
|
|
658
1016
|
}
|
|
659
1017
|
};
|
|
660
1018
|
}
|
|
1019
|
+
function createGitHubAuthStore(projectRoot) {
|
|
1020
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
1021
|
+
}
|
|
661
1022
|
|
|
662
1023
|
// packages/server/src/server-helpers/github-projects.ts
|
|
663
1024
|
function asRecord(value) {
|
|
@@ -864,7 +1225,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
864
1225
|
"needs-attention",
|
|
865
1226
|
"stopped"
|
|
866
1227
|
]);
|
|
867
|
-
var
|
|
1228
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
868
1229
|
|
|
869
1230
|
// packages/server/src/server-helpers/ws-router.ts
|
|
870
1231
|
import {
|
|
@@ -1046,7 +1407,7 @@ import {
|
|
|
1046
1407
|
} from "@rig/runtime/control-plane/remote";
|
|
1047
1408
|
|
|
1048
1409
|
// packages/server/src/server-helpers/run-steering.ts
|
|
1049
|
-
import { dirname as
|
|
1410
|
+
import { dirname as dirname4, resolve as resolve8 } from "path";
|
|
1050
1411
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
|
|
1051
1412
|
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
1052
1413
|
var steeringSequence = 0;
|
|
@@ -1133,7 +1494,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
1133
1494
|
delivered: false
|
|
1134
1495
|
};
|
|
1135
1496
|
const path = runSteeringPath(projectRoot, runId);
|
|
1136
|
-
mkdirSync4(
|
|
1497
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1137
1498
|
appendJsonlRecord2(path, entry);
|
|
1138
1499
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
1139
1500
|
id: entry.id,
|
|
@@ -1170,24 +1531,205 @@ import {
|
|
|
1170
1531
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
1171
1532
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1172
1533
|
|
|
1534
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1535
|
+
import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1536
|
+
import { dirname as dirname6, resolve as resolve10 } from "path";
|
|
1537
|
+
|
|
1538
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
1539
|
+
import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1540
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
|
|
1541
|
+
function cleanString3(value) {
|
|
1542
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1543
|
+
}
|
|
1544
|
+
function sanitizePathSegment(value) {
|
|
1545
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
1546
|
+
}
|
|
1547
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
1548
|
+
const userId = cleanString3(identity.userId);
|
|
1549
|
+
if (userId) {
|
|
1550
|
+
const safeId = sanitizePathSegment(userId);
|
|
1551
|
+
if (safeId)
|
|
1552
|
+
return `ghu-${safeId}`;
|
|
1553
|
+
}
|
|
1554
|
+
const login = cleanString3(identity.login);
|
|
1555
|
+
if (login) {
|
|
1556
|
+
const safeLogin = sanitizePathSegment(login);
|
|
1557
|
+
if (safeLogin)
|
|
1558
|
+
return `ghu-login-${safeLogin}`;
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
1561
|
+
}
|
|
1562
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
1563
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
1564
|
+
if (explicitRoot)
|
|
1565
|
+
return resolve9(explicitRoot);
|
|
1566
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
1567
|
+
if (stateDir2)
|
|
1568
|
+
return resolve9(dirname5(resolve9(stateDir2)), "users");
|
|
1569
|
+
return resolve9(projectRoot, ".rig", "users");
|
|
1570
|
+
}
|
|
1571
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
1572
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
1573
|
+
const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
1574
|
+
const stateDir2 = resolve9(root, ".rig", "state");
|
|
1575
|
+
return {
|
|
1576
|
+
key,
|
|
1577
|
+
userId: cleanString3(identity.userId),
|
|
1578
|
+
login: cleanString3(identity.login),
|
|
1579
|
+
root,
|
|
1580
|
+
stateDir: stateDir2,
|
|
1581
|
+
authStateFile: resolve9(stateDir2, "github-auth.json"),
|
|
1582
|
+
metadataFile: resolve9(stateDir2, "user-namespace.json"),
|
|
1583
|
+
checkoutBaseDir: resolve9(root, "remote-checkouts"),
|
|
1584
|
+
snapshotBaseDir: resolve9(root, "remote-snapshots")
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
1588
|
+
return {
|
|
1589
|
+
key: namespace.key,
|
|
1590
|
+
userId: namespace.userId,
|
|
1591
|
+
login: namespace.login,
|
|
1592
|
+
root: namespace.root,
|
|
1593
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1594
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
1598
|
+
const root = resolve9(namespaceRoot);
|
|
1599
|
+
const candidate = resolve9(candidatePath);
|
|
1600
|
+
const rel = relative(root, candidate);
|
|
1601
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1602
|
+
}
|
|
1603
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
1604
|
+
mkdirSync5(namespace.stateDir, { recursive: true });
|
|
1605
|
+
const previous = (() => {
|
|
1606
|
+
if (!existsSync6(namespace.metadataFile))
|
|
1607
|
+
return null;
|
|
1608
|
+
try {
|
|
1609
|
+
const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
|
|
1610
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1611
|
+
} catch {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
})();
|
|
1615
|
+
const now = new Date().toISOString();
|
|
1616
|
+
writeFileSync5(namespace.metadataFile, `${JSON.stringify({
|
|
1617
|
+
key: namespace.key,
|
|
1618
|
+
userId: namespace.userId,
|
|
1619
|
+
login: namespace.login,
|
|
1620
|
+
root: namespace.root,
|
|
1621
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1622
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
1623
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
1624
|
+
updatedAt: now
|
|
1625
|
+
}, null, 2)}
|
|
1626
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1627
|
+
try {
|
|
1628
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
1629
|
+
} catch {}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1633
|
+
function cleanString4(value) {
|
|
1634
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1635
|
+
}
|
|
1636
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
1637
|
+
return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
1638
|
+
}
|
|
1639
|
+
function parseEntry(value) {
|
|
1640
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1641
|
+
return null;
|
|
1642
|
+
const record = value;
|
|
1643
|
+
const token = cleanString4(record.token);
|
|
1644
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
1645
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
1646
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
1647
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
1648
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
1649
|
+
const createdAt = cleanString4(record.createdAt);
|
|
1650
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
1651
|
+
return null;
|
|
1652
|
+
return {
|
|
1653
|
+
token,
|
|
1654
|
+
namespaceKey,
|
|
1655
|
+
namespaceRoot,
|
|
1656
|
+
authStateFile,
|
|
1657
|
+
checkoutBaseDir,
|
|
1658
|
+
snapshotBaseDir,
|
|
1659
|
+
createdAt,
|
|
1660
|
+
login: cleanString4(record.login),
|
|
1661
|
+
userId: cleanString4(record.userId),
|
|
1662
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function readIndex(indexFile) {
|
|
1666
|
+
if (!existsSync7(indexFile))
|
|
1667
|
+
return [];
|
|
1668
|
+
try {
|
|
1669
|
+
const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
|
|
1670
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
1671
|
+
const parsedEntry = parseEntry(entry);
|
|
1672
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
1673
|
+
}) : [];
|
|
1674
|
+
} catch {
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function writeIndex(indexFile, sessions) {
|
|
1679
|
+
mkdirSync6(dirname6(indexFile), { recursive: true });
|
|
1680
|
+
writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
1681
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1682
|
+
try {
|
|
1683
|
+
chmodSync3(indexFile, 384);
|
|
1684
|
+
} catch {}
|
|
1685
|
+
}
|
|
1686
|
+
function registerGitHubApiSession(input) {
|
|
1687
|
+
const cleanToken = cleanString4(input.token);
|
|
1688
|
+
if (!cleanToken)
|
|
1689
|
+
throw new Error("GitHub API session token is required");
|
|
1690
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
1691
|
+
const createdAt = new Date().toISOString();
|
|
1692
|
+
const entry = {
|
|
1693
|
+
token: cleanToken,
|
|
1694
|
+
login: input.namespace.login,
|
|
1695
|
+
userId: input.namespace.userId,
|
|
1696
|
+
namespaceKey: input.namespace.key,
|
|
1697
|
+
namespaceRoot: input.namespace.root,
|
|
1698
|
+
authStateFile: input.namespace.authStateFile,
|
|
1699
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
1700
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
1701
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
1702
|
+
createdAt
|
|
1703
|
+
};
|
|
1704
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
1705
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
1706
|
+
return entry;
|
|
1707
|
+
}
|
|
1708
|
+
function readGitHubApiSession(input) {
|
|
1709
|
+
const cleanToken = cleanString4(input.token);
|
|
1710
|
+
if (!cleanToken)
|
|
1711
|
+
return null;
|
|
1712
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1173
1715
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1174
|
-
import { createHash } from "crypto";
|
|
1716
|
+
import { createHash as createHash2 } from "crypto";
|
|
1175
1717
|
import { spawnSync } from "child_process";
|
|
1176
|
-
import { existsSync as
|
|
1177
|
-
import { dirname as
|
|
1718
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
|
|
1719
|
+
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
1178
1720
|
function normalizeRepoSlug(value) {
|
|
1179
1721
|
const trimmed = value.trim();
|
|
1180
1722
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
1181
1723
|
}
|
|
1182
1724
|
function registryPath(projectRoot) {
|
|
1183
|
-
return
|
|
1725
|
+
return resolve11(projectRoot, ".rig", "state", "projects.json");
|
|
1184
1726
|
}
|
|
1185
1727
|
function readRegistry(projectRoot) {
|
|
1186
1728
|
const path = registryPath(projectRoot);
|
|
1187
|
-
if (!
|
|
1729
|
+
if (!existsSync8(path))
|
|
1188
1730
|
return {};
|
|
1189
1731
|
try {
|
|
1190
|
-
const payload = JSON.parse(
|
|
1732
|
+
const payload = JSON.parse(readFileSync6(path, "utf8"));
|
|
1191
1733
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1192
1734
|
return {};
|
|
1193
1735
|
const projects = payload.projects;
|
|
@@ -1198,14 +1740,14 @@ function readRegistry(projectRoot) {
|
|
|
1198
1740
|
}
|
|
1199
1741
|
function writeRegistry(projectRoot, projects) {
|
|
1200
1742
|
const path = registryPath(projectRoot);
|
|
1201
|
-
|
|
1202
|
-
|
|
1743
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
1744
|
+
writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
|
|
1203
1745
|
`, "utf8");
|
|
1204
1746
|
}
|
|
1205
1747
|
function resolveConfigPath(projectRoot) {
|
|
1206
1748
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
1207
|
-
const path =
|
|
1208
|
-
if (
|
|
1749
|
+
const path = resolve11(projectRoot, name);
|
|
1750
|
+
if (existsSync8(path))
|
|
1209
1751
|
return path;
|
|
1210
1752
|
}
|
|
1211
1753
|
return null;
|
|
@@ -1214,7 +1756,7 @@ function hashFile(path) {
|
|
|
1214
1756
|
if (!path)
|
|
1215
1757
|
return null;
|
|
1216
1758
|
try {
|
|
1217
|
-
return
|
|
1759
|
+
return createHash2("sha256").update(readFileSync6(path)).digest("hex");
|
|
1218
1760
|
} catch {
|
|
1219
1761
|
return null;
|
|
1220
1762
|
}
|
|
@@ -1230,11 +1772,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
1230
1772
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
1231
1773
|
}
|
|
1232
1774
|
function buildRunSummary(projectRoot) {
|
|
1233
|
-
const runsDir =
|
|
1775
|
+
const runsDir = resolve11(projectRoot, ".rig", "runs");
|
|
1234
1776
|
try {
|
|
1235
1777
|
const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
1236
1778
|
try {
|
|
1237
|
-
const run = JSON.parse(
|
|
1779
|
+
const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
|
|
1238
1780
|
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 : "" }];
|
|
1239
1781
|
} catch {
|
|
1240
1782
|
return [];
|
|
@@ -1286,10 +1828,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
1286
1828
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
1287
1829
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
1288
1830
|
}
|
|
1831
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
1832
|
+
const target = resolve11(checkoutPath);
|
|
1833
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
|
|
1834
|
+
}
|
|
1289
1835
|
|
|
1290
1836
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
1291
|
-
import { existsSync as
|
|
1292
|
-
import { dirname as
|
|
1837
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1838
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
|
|
1293
1839
|
function safeSlugSegments(repoSlug) {
|
|
1294
1840
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
1295
1841
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -1306,7 +1852,7 @@ function safeCheckoutKey(value) {
|
|
|
1306
1852
|
}
|
|
1307
1853
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
1308
1854
|
const key = safeCheckoutKey(checkoutKey);
|
|
1309
|
-
return
|
|
1855
|
+
return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
1310
1856
|
}
|
|
1311
1857
|
function sanitizeSnapshotId(value, fallback) {
|
|
1312
1858
|
const raw = (value ?? fallback).trim();
|
|
@@ -1314,7 +1860,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
1314
1860
|
return safe || fallback;
|
|
1315
1861
|
}
|
|
1316
1862
|
function assertWithinRoot(root, relativePath) {
|
|
1317
|
-
if (!relativePath ||
|
|
1863
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
|
|
1318
1864
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
1319
1865
|
}
|
|
1320
1866
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -1322,9 +1868,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
1322
1868
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1323
1869
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
1324
1870
|
}
|
|
1325
|
-
const target =
|
|
1326
|
-
const rel =
|
|
1327
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
1871
|
+
const target = resolve12(root, ...segments);
|
|
1872
|
+
const rel = relative2(root, target);
|
|
1873
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
|
|
1328
1874
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
1329
1875
|
}
|
|
1330
1876
|
return target;
|
|
@@ -1342,17 +1888,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
1342
1888
|
function extractUploadedSnapshotArchive(input) {
|
|
1343
1889
|
const archive = decodeSnapshotArchive(input.archive);
|
|
1344
1890
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
1345
|
-
const checkoutPath =
|
|
1346
|
-
|
|
1891
|
+
const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
1892
|
+
mkdirSync8(checkoutPath, { recursive: true });
|
|
1347
1893
|
for (const file of archive.files) {
|
|
1348
1894
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
1349
1895
|
throw new Error("Invalid snapshot archive file entry");
|
|
1350
1896
|
}
|
|
1351
1897
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
1352
|
-
|
|
1353
|
-
|
|
1898
|
+
mkdirSync8(dirname8(target), { recursive: true });
|
|
1899
|
+
writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
|
|
1354
1900
|
}
|
|
1355
|
-
|
|
1901
|
+
writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
1356
1902
|
repoSlug: input.repoSlug,
|
|
1357
1903
|
snapshotId,
|
|
1358
1904
|
fileCount: archive.files.length,
|
|
@@ -1387,7 +1933,7 @@ function gitCredentialConfig(token) {
|
|
|
1387
1933
|
};
|
|
1388
1934
|
}
|
|
1389
1935
|
async function prepareRemoteCheckout(input) {
|
|
1390
|
-
const exists = input.exists ??
|
|
1936
|
+
const exists = input.exists ?? existsSync9;
|
|
1391
1937
|
const strategy = input.strategy;
|
|
1392
1938
|
if (strategy.kind === "uploaded-snapshot") {
|
|
1393
1939
|
return extractUploadedSnapshotArchive({
|
|
@@ -1399,7 +1945,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
1399
1945
|
});
|
|
1400
1946
|
}
|
|
1401
1947
|
if (strategy.kind === "existing-path") {
|
|
1402
|
-
const checkoutPath2 =
|
|
1948
|
+
const checkoutPath2 = resolve12(strategy.path);
|
|
1403
1949
|
if (!exists(checkoutPath2)) {
|
|
1404
1950
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
1405
1951
|
}
|
|
@@ -1435,9 +1981,9 @@ function buildServerControlStatus() {
|
|
|
1435
1981
|
};
|
|
1436
1982
|
}
|
|
1437
1983
|
function buildProjectConfigStatus(root) {
|
|
1438
|
-
const hasConfigTs =
|
|
1439
|
-
const hasConfigJson =
|
|
1440
|
-
const hasLegacyTaskConfig =
|
|
1984
|
+
const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
|
|
1985
|
+
const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
|
|
1986
|
+
const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
|
|
1441
1987
|
let kind = "missing";
|
|
1442
1988
|
if (hasConfigTs)
|
|
1443
1989
|
kind = "rig-config-ts";
|
|
@@ -1461,10 +2007,10 @@ function normalizeCommit(value) {
|
|
|
1461
2007
|
function asPlainRecord(value) {
|
|
1462
2008
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1463
2009
|
}
|
|
1464
|
-
var
|
|
2010
|
+
var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
|
|
1465
2011
|
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
1466
|
-
"@rig/core": `npm:@h-rig/core@${
|
|
1467
|
-
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${
|
|
2012
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
|
|
2013
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
|
|
1468
2014
|
};
|
|
1469
2015
|
function repoParts(repoSlug) {
|
|
1470
2016
|
const [owner, repo] = repoSlug.split("/");
|
|
@@ -1473,24 +2019,24 @@ function repoParts(repoSlug) {
|
|
|
1473
2019
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
1474
2020
|
}
|
|
1475
2021
|
function repairDir(checkoutPath) {
|
|
1476
|
-
const dir =
|
|
1477
|
-
|
|
2022
|
+
const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
2023
|
+
mkdirSync9(dir, { recursive: true });
|
|
1478
2024
|
return dir;
|
|
1479
2025
|
}
|
|
1480
2026
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
1481
|
-
const source =
|
|
1482
|
-
const backupPath =
|
|
1483
|
-
|
|
1484
|
-
|
|
2027
|
+
const source = resolve13(checkoutPath, relativePath);
|
|
2028
|
+
const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
2029
|
+
mkdirSync9(dirname9(backupPath), { recursive: true });
|
|
2030
|
+
copyFileSync2(source, backupPath);
|
|
1485
2031
|
return backupPath;
|
|
1486
2032
|
}
|
|
1487
2033
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
1488
|
-
const packagePath =
|
|
1489
|
-
if (!
|
|
2034
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2035
|
+
if (!existsSync10(packagePath)) {
|
|
1490
2036
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
1491
2037
|
}
|
|
1492
2038
|
try {
|
|
1493
|
-
const parsed = JSON.parse(
|
|
2039
|
+
const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
|
|
1494
2040
|
return {
|
|
1495
2041
|
existed: true,
|
|
1496
2042
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -1504,9 +2050,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
1504
2050
|
}
|
|
1505
2051
|
}
|
|
1506
2052
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
1507
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1508
|
-
const packagePath =
|
|
1509
|
-
if (!hasConfig && !
|
|
2053
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2054
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2055
|
+
if (!hasConfig && !existsSync10(packagePath)) {
|
|
1510
2056
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
1511
2057
|
}
|
|
1512
2058
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -1525,7 +2071,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
1525
2071
|
}
|
|
1526
2072
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
1527
2073
|
if (changed) {
|
|
1528
|
-
|
|
2074
|
+
writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
1529
2075
|
`, "utf8");
|
|
1530
2076
|
}
|
|
1531
2077
|
return {
|
|
@@ -1551,11 +2097,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
1551
2097
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
1552
2098
|
}
|
|
1553
2099
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
1554
|
-
const configPath =
|
|
1555
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2100
|
+
const configPath = resolve13(checkoutPath, "rig.config.ts");
|
|
2101
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1556
2102
|
if (existingConfigName) {
|
|
1557
|
-
const existingPath =
|
|
1558
|
-
const source =
|
|
2103
|
+
const existingPath = resolve13(checkoutPath, existingConfigName);
|
|
2104
|
+
const source = readFileSync7(existingPath, "utf8");
|
|
1559
2105
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
1560
2106
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1561
2107
|
}
|
|
@@ -1569,7 +2115,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1569
2115
|
}
|
|
1570
2116
|
}
|
|
1571
2117
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
1572
|
-
|
|
2118
|
+
writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
1573
2119
|
return {
|
|
1574
2120
|
path: configPath,
|
|
1575
2121
|
changed: true,
|
|
@@ -1578,7 +2124,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1578
2124
|
};
|
|
1579
2125
|
}
|
|
1580
2126
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
1581
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2127
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1582
2128
|
if (!configFile)
|
|
1583
2129
|
return { ok: false, error: "missing rig config" };
|
|
1584
2130
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1600,7 +2146,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
1600
2146
|
return { ok: true, configFile };
|
|
1601
2147
|
}
|
|
1602
2148
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
1603
|
-
if (!
|
|
2149
|
+
if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
|
|
1604
2150
|
return { skipped: true, reason: "package.json missing" };
|
|
1605
2151
|
}
|
|
1606
2152
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1613,8 +2159,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
1613
2159
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
1614
2160
|
}
|
|
1615
2161
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
1616
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1617
|
-
const hasPackage =
|
|
2162
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2163
|
+
const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
|
|
1618
2164
|
if (!hasConfig && !hasPackage) {
|
|
1619
2165
|
return {
|
|
1620
2166
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -1712,26 +2258,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
1712
2258
|
}
|
|
1713
2259
|
function readGitHeadCommit(projectRoot) {
|
|
1714
2260
|
try {
|
|
1715
|
-
let gitDir =
|
|
2261
|
+
let gitDir = resolve13(projectRoot, ".git");
|
|
1716
2262
|
try {
|
|
1717
|
-
const dotGit =
|
|
2263
|
+
const dotGit = readFileSync7(gitDir, "utf8").trim();
|
|
1718
2264
|
const gitDirPrefix = "gitdir:";
|
|
1719
2265
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
1720
|
-
gitDir =
|
|
2266
|
+
gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
1721
2267
|
}
|
|
1722
2268
|
} catch {}
|
|
1723
|
-
const head =
|
|
2269
|
+
const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
|
|
1724
2270
|
const refPrefix = "ref:";
|
|
1725
2271
|
if (!head.startsWith(refPrefix)) {
|
|
1726
2272
|
return normalizeCommit(head);
|
|
1727
2273
|
}
|
|
1728
2274
|
const ref = head.slice(refPrefix.length).trim();
|
|
1729
|
-
const refPath =
|
|
1730
|
-
if (
|
|
1731
|
-
return normalizeCommit(
|
|
2275
|
+
const refPath = resolve13(gitDir, ref);
|
|
2276
|
+
if (existsSync10(refPath)) {
|
|
2277
|
+
return normalizeCommit(readFileSync7(refPath, "utf8").trim());
|
|
1732
2278
|
}
|
|
1733
|
-
const commonDir = normalizeString(
|
|
1734
|
-
return commonDir ? normalizeCommit(
|
|
2279
|
+
const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
|
|
2280
|
+
return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
1735
2281
|
} catch {
|
|
1736
2282
|
return null;
|
|
1737
2283
|
}
|
|
@@ -1769,9 +2315,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
1769
2315
|
const repo = normalizeString(taskSource?.repo);
|
|
1770
2316
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
1771
2317
|
}
|
|
1772
|
-
async function buildTaskSourceStatus(state, config) {
|
|
2318
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
1773
2319
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
1774
|
-
const selectedRepo =
|
|
2320
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
1775
2321
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
1776
2322
|
}).selectedRepo;
|
|
1777
2323
|
try {
|
|
@@ -1818,36 +2364,134 @@ function bearerTokenFromRequest(req) {
|
|
|
1818
2364
|
function isLoopbackRequest(req) {
|
|
1819
2365
|
try {
|
|
1820
2366
|
const hostname = new URL(req.url).hostname.toLowerCase();
|
|
1821
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
2367
|
+
return hostname === "localhost" || hostname === "rig.local" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1822
2368
|
} catch {
|
|
1823
2369
|
return false;
|
|
1824
2370
|
}
|
|
1825
2371
|
}
|
|
1826
2372
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
1827
|
-
return pathname === "/" || pathname === "/
|
|
2373
|
+
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";
|
|
2374
|
+
}
|
|
2375
|
+
function buildRigInstallScript() {
|
|
2376
|
+
return `#!/usr/bin/env bash
|
|
2377
|
+
set -euo pipefail
|
|
2378
|
+
|
|
2379
|
+
say() {
|
|
2380
|
+
printf 'rig-install: %s
|
|
2381
|
+
' "$*"
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2385
|
+
say "Bun not found; installing Bun first"
|
|
2386
|
+
curl -fsSL https://bun.sh/install | bash
|
|
2387
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2388
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2389
|
+
fi
|
|
2390
|
+
|
|
2391
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2392
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
2393
|
+
' >&2
|
|
2394
|
+
exit 1
|
|
2395
|
+
fi
|
|
2396
|
+
|
|
2397
|
+
say "Installing @h-rig/cli@latest"
|
|
2398
|
+
bun add -g @h-rig/cli@latest
|
|
2399
|
+
|
|
2400
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2401
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2402
|
+
|
|
2403
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
2404
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
2405
|
+
' "$BUN_INSTALL" >&2
|
|
2406
|
+
exit 1
|
|
2407
|
+
fi
|
|
2408
|
+
|
|
2409
|
+
say "Verifying rig"
|
|
2410
|
+
rig --help >/dev/null
|
|
2411
|
+
say "Done. Run: rig --help"
|
|
2412
|
+
`;
|
|
1828
2413
|
}
|
|
1829
2414
|
function normalizePrMode(value) {
|
|
1830
2415
|
const mode = normalizeString(value);
|
|
1831
2416
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
1832
2417
|
}
|
|
2418
|
+
function requestAuthResult(input) {
|
|
2419
|
+
return {
|
|
2420
|
+
authorized: input.authorized,
|
|
2421
|
+
actor: input.actor ?? null,
|
|
2422
|
+
reason: input.reason,
|
|
2423
|
+
login: input.login ?? null,
|
|
2424
|
+
userId: input.userId ?? null,
|
|
2425
|
+
userNamespace: input.userNamespace ?? null,
|
|
2426
|
+
authStateFile: input.authStateFile ?? null
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
function namespaceFromSessionIndex(entry) {
|
|
2430
|
+
const stateDir2 = dirname9(entry.authStateFile);
|
|
2431
|
+
return {
|
|
2432
|
+
key: entry.namespaceKey,
|
|
2433
|
+
userId: entry.userId,
|
|
2434
|
+
login: entry.login,
|
|
2435
|
+
root: entry.namespaceRoot,
|
|
2436
|
+
stateDir: stateDir2,
|
|
2437
|
+
authStateFile: entry.authStateFile,
|
|
2438
|
+
metadataFile: resolve13(stateDir2, "user-namespace.json"),
|
|
2439
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
2440
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
1833
2443
|
function authorizeRigHttpRequest(input) {
|
|
1834
2444
|
if (input.legacyAuthorized) {
|
|
1835
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
2445
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
1836
2446
|
}
|
|
1837
2447
|
const bearer = bearerTokenFromRequest(input.req);
|
|
1838
2448
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
1839
2449
|
const storedToken = store.readToken();
|
|
2450
|
+
const session = bearer ? store.readApiSession(bearer) : null;
|
|
2451
|
+
if (session) {
|
|
2452
|
+
return requestAuthResult({
|
|
2453
|
+
authorized: true,
|
|
2454
|
+
actor: session.login ?? "github-operator",
|
|
2455
|
+
reason: "github-session",
|
|
2456
|
+
login: session.login,
|
|
2457
|
+
userId: session.userId,
|
|
2458
|
+
authStateFile: store.stateFile
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
1840
2461
|
if (bearer && storedToken && bearer === storedToken) {
|
|
1841
2462
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
1842
|
-
return {
|
|
2463
|
+
return requestAuthResult({
|
|
2464
|
+
authorized: true,
|
|
2465
|
+
actor: status.login ?? "github-operator",
|
|
2466
|
+
reason: "github-token",
|
|
2467
|
+
login: status.login,
|
|
2468
|
+
userId: status.userId,
|
|
2469
|
+
authStateFile: store.stateFile
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
2473
|
+
if (indexedSession) {
|
|
2474
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
2475
|
+
return requestAuthResult({
|
|
2476
|
+
authorized: true,
|
|
2477
|
+
actor: indexedSession.login ?? "github-operator",
|
|
2478
|
+
reason: "github-user-session",
|
|
2479
|
+
login: indexedSession.login,
|
|
2480
|
+
userId: indexedSession.userId,
|
|
2481
|
+
userNamespace,
|
|
2482
|
+
authStateFile: indexedSession.authStateFile
|
|
2483
|
+
});
|
|
1843
2484
|
}
|
|
1844
2485
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
1845
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
2486
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
1846
2487
|
}
|
|
1847
|
-
if (!input.serverAuthToken && !storedToken
|
|
1848
|
-
|
|
2488
|
+
if (!input.serverAuthToken && !storedToken) {
|
|
2489
|
+
if (isLoopbackRequest(input.req)) {
|
|
2490
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
2491
|
+
}
|
|
2492
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
1849
2493
|
}
|
|
1850
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
2494
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
1851
2495
|
}
|
|
1852
2496
|
async function fetchGitHubUserInfo(token) {
|
|
1853
2497
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -1867,6 +2511,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
1867
2511
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
1868
2512
|
};
|
|
1869
2513
|
}
|
|
2514
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
2515
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
2516
|
+
return false;
|
|
2517
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
2518
|
+
if (!stateDir2)
|
|
2519
|
+
return true;
|
|
2520
|
+
return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
|
|
2521
|
+
}
|
|
2522
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
2523
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
2524
|
+
}
|
|
2525
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
2526
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
2527
|
+
}
|
|
2528
|
+
function userNamespaceResponse(namespace) {
|
|
2529
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
2530
|
+
}
|
|
2531
|
+
function resolveNamespacedBaseDir(input) {
|
|
2532
|
+
if (input.explicitBaseDir)
|
|
2533
|
+
return input.explicitBaseDir;
|
|
2534
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
2535
|
+
if (input.userNamespace) {
|
|
2536
|
+
return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
2537
|
+
}
|
|
2538
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
2539
|
+
}
|
|
2540
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
2541
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
2542
|
+
}
|
|
2543
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
2544
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
2545
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
2546
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
2547
|
+
store.saveToken({
|
|
2548
|
+
token: input.token,
|
|
2549
|
+
tokenSource: input.tokenSource,
|
|
2550
|
+
login: input.user.login,
|
|
2551
|
+
userId: input.user.userId,
|
|
2552
|
+
scopes: input.user.scopes,
|
|
2553
|
+
selectedRepo: input.selectedRepo
|
|
2554
|
+
});
|
|
2555
|
+
const apiSession = store.createApiSession();
|
|
2556
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
2557
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
2558
|
+
if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
|
|
2559
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
|
|
2560
|
+
}
|
|
2561
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
2562
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
2563
|
+
rootStore.saveToken({
|
|
2564
|
+
token: input.token,
|
|
2565
|
+
tokenSource: input.tokenSource,
|
|
2566
|
+
login: input.user.login,
|
|
2567
|
+
userId: input.user.userId,
|
|
2568
|
+
scopes: input.user.scopes,
|
|
2569
|
+
selectedRepo: input.selectedRepo
|
|
2570
|
+
});
|
|
2571
|
+
rootStore.createApiSession();
|
|
2572
|
+
}
|
|
2573
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
2574
|
+
}
|
|
1870
2575
|
async function postGitHubForm(endpoint, body) {
|
|
1871
2576
|
const response = await fetch(endpoint, {
|
|
1872
2577
|
method: "POST",
|
|
@@ -1884,11 +2589,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
1884
2589
|
const requestedRoot = normalizeString(rawRoot);
|
|
1885
2590
|
if (!requestedRoot)
|
|
1886
2591
|
return currentRoot;
|
|
1887
|
-
if (!
|
|
2592
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
1888
2593
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
1889
2594
|
}
|
|
1890
|
-
const normalizedRoot =
|
|
1891
|
-
if (!
|
|
2595
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
2596
|
+
if (!existsSync10(normalizedRoot)) {
|
|
1892
2597
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
1893
2598
|
}
|
|
1894
2599
|
return normalizedRoot;
|
|
@@ -2076,7 +2781,7 @@ function selectNextWorkspaceTask(projectRoot, tasks) {
|
|
|
2076
2781
|
if (runnable.length === 0)
|
|
2077
2782
|
return null;
|
|
2078
2783
|
const queue = readQueueState(projectRoot);
|
|
2079
|
-
const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
|
|
2784
|
+
const queueRank = new Map(queue.map((entry, index) => [String(entry.taskId), { score: entry.score, position: index }]));
|
|
2080
2785
|
return runnable.toSorted((left, right) => {
|
|
2081
2786
|
const leftId = taskIdOf(left) ?? "";
|
|
2082
2787
|
const rightId = taskIdOf(right) ?? "";
|
|
@@ -2116,6 +2821,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
2116
2821
|
}
|
|
2117
2822
|
return filtered;
|
|
2118
2823
|
}
|
|
2824
|
+
function issueAnalysisTargetFor(source) {
|
|
2825
|
+
if (!source)
|
|
2826
|
+
return null;
|
|
2827
|
+
const candidate = source;
|
|
2828
|
+
if (typeof candidate.updateTask !== "function")
|
|
2829
|
+
return null;
|
|
2830
|
+
return {
|
|
2831
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
2832
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
2833
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
2834
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
2835
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function uniqueStringList(value) {
|
|
2839
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
2840
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
2841
|
+
}
|
|
2842
|
+
function taskRecordId(task) {
|
|
2843
|
+
return String(task.id ?? "");
|
|
2844
|
+
}
|
|
2119
2845
|
function redactRemoteEndpoint(endpoint) {
|
|
2120
2846
|
const { token, ...rest } = endpoint;
|
|
2121
2847
|
return {
|
|
@@ -2200,9 +2926,16 @@ function createRigServerFetch(state, deps) {
|
|
|
2200
2926
|
notifications: state.targets.length
|
|
2201
2927
|
});
|
|
2202
2928
|
}
|
|
2929
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
2930
|
+
return new Response(buildRigInstallScript(), {
|
|
2931
|
+
headers: {
|
|
2932
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2203
2936
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
2204
2937
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
2205
|
-
const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
|
|
2938
|
+
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
2206
2939
|
const requestAuth = authorizeRigHttpRequest({
|
|
2207
2940
|
req,
|
|
2208
2941
|
pathname: url.pathname,
|
|
@@ -2458,9 +3191,70 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2458
3191
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
2459
3192
|
});
|
|
2460
3193
|
}
|
|
3194
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
3195
|
+
const body = await deps.readJsonBody(req);
|
|
3196
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
3197
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
3198
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
3199
|
+
return deps.badRequest("ids is required unless all=true");
|
|
3200
|
+
}
|
|
3201
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
3202
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
3203
|
+
const target = issueAnalysisTargetFor(source);
|
|
3204
|
+
if (!source || !target) {
|
|
3205
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
3206
|
+
}
|
|
3207
|
+
const allTasks = [...await source.list()];
|
|
3208
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
3209
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
3210
|
+
if (cached)
|
|
3211
|
+
return cached;
|
|
3212
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
3213
|
+
}))).filter((task) => Boolean(task));
|
|
3214
|
+
if (issues.length === 0) {
|
|
3215
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
3216
|
+
}
|
|
3217
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
3218
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
3219
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
3220
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
3221
|
+
const service = createIssueAnalysisService({
|
|
3222
|
+
analyzer: createPiIssueAnalyzer({
|
|
3223
|
+
...model ? { model } : {},
|
|
3224
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
3225
|
+
}),
|
|
3226
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
3227
|
+
});
|
|
3228
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
3229
|
+
let results;
|
|
3230
|
+
try {
|
|
3231
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
return deps.jsonResponse({
|
|
3234
|
+
ok: false,
|
|
3235
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3236
|
+
reason,
|
|
3237
|
+
ids: issues.map((issue) => issue.id)
|
|
3238
|
+
}, 502);
|
|
3239
|
+
}
|
|
3240
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
3241
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
3242
|
+
return;
|
|
3243
|
+
});
|
|
3244
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
3245
|
+
return deps.jsonResponse({
|
|
3246
|
+
ok: true,
|
|
3247
|
+
reason,
|
|
3248
|
+
analyzed: results.map((entry) => ({
|
|
3249
|
+
id: entry.issue.id,
|
|
3250
|
+
title: entry.issue.title ?? null,
|
|
3251
|
+
result: entry.result
|
|
3252
|
+
}))
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
2461
3255
|
if (url.pathname === "/api/server/status") {
|
|
2462
3256
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2463
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
3257
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
2464
3258
|
return deps.jsonResponse({
|
|
2465
3259
|
ok: true,
|
|
2466
3260
|
projectRoot: state.projectRoot,
|
|
@@ -2484,8 +3278,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2484
3278
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
2485
3279
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
2486
3280
|
} : undefined;
|
|
2487
|
-
const
|
|
2488
|
-
|
|
3281
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3282
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
3283
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2489
3284
|
}
|
|
2490
3285
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
2491
3286
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -2498,8 +3293,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2498
3293
|
if (!archiveContentBase64) {
|
|
2499
3294
|
return deps.badRequest("archiveContentBase64 is required");
|
|
2500
3295
|
}
|
|
2501
|
-
const
|
|
2502
|
-
const
|
|
3296
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3297
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3298
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
3299
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
3300
|
+
userNamespace: requestAuth.userNamespace,
|
|
3301
|
+
legacyProjectRoot: state.projectRoot,
|
|
3302
|
+
legacySubdir: "remote-snapshots"
|
|
3303
|
+
});
|
|
3304
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
2503
3305
|
try {
|
|
2504
3306
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
2505
3307
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -2512,14 +3314,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2512
3314
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2513
3315
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2514
3316
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2515
|
-
const project = linkProjectCheckout(
|
|
3317
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2516
3318
|
kind: "uploaded-snapshot",
|
|
2517
3319
|
path: checkout.path,
|
|
2518
3320
|
ref: checkout.snapshotId
|
|
2519
3321
|
});
|
|
2520
3322
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
2521
3323
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
2522
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3324
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2523
3325
|
} catch (error) {
|
|
2524
3326
|
return deps.jsonResponse({
|
|
2525
3327
|
ok: false,
|
|
@@ -2539,10 +3341,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2539
3341
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
2540
3342
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
2541
3343
|
}
|
|
2542
|
-
const
|
|
2543
|
-
const
|
|
3344
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3345
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3346
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
3347
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
3348
|
+
userNamespace: requestAuth.userNamespace,
|
|
3349
|
+
legacyProjectRoot: state.projectRoot,
|
|
3350
|
+
legacySubdir: "remote-checkouts"
|
|
3351
|
+
});
|
|
3352
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
2544
3353
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
2545
|
-
const credentialToken =
|
|
3354
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
2546
3355
|
try {
|
|
2547
3356
|
const checkout = await prepareRemoteCheckout({
|
|
2548
3357
|
command: runRemoteCheckoutCommand,
|
|
@@ -2551,14 +3360,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2551
3360
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2552
3361
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2553
3362
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2554
|
-
const project = linkProjectCheckout(
|
|
3363
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2555
3364
|
kind: checkout.kind,
|
|
2556
3365
|
path: checkout.path,
|
|
2557
3366
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
2558
3367
|
});
|
|
2559
3368
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
2560
3369
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
2561
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3370
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2562
3371
|
} catch (error) {
|
|
2563
3372
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2564
3373
|
}
|
|
@@ -2575,16 +3384,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2575
3384
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
2576
3385
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
2577
3386
|
}
|
|
2578
|
-
const
|
|
3387
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3388
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2579
3389
|
kind,
|
|
2580
3390
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
2581
3391
|
ref: normalizeString(body.ref) ?? undefined
|
|
2582
3392
|
});
|
|
2583
|
-
return deps.jsonResponse({ ok: true, project });
|
|
3393
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2584
3394
|
}
|
|
2585
3395
|
if (req.method === "GET") {
|
|
2586
|
-
const
|
|
2587
|
-
|
|
3396
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3397
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
3398
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
2588
3399
|
}
|
|
2589
3400
|
}
|
|
2590
3401
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -2593,11 +3404,27 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2593
3404
|
if (!requestedRoot) {
|
|
2594
3405
|
return deps.badRequest("projectRoot is required");
|
|
2595
3406
|
}
|
|
2596
|
-
if (!
|
|
3407
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
2597
3408
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
2598
3409
|
}
|
|
2599
|
-
const normalizedRoot =
|
|
2600
|
-
const exists =
|
|
3410
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
3411
|
+
const exists = existsSync10(normalizedRoot);
|
|
3412
|
+
if (exists && requestAuth.userNamespace) {
|
|
3413
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
3414
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
3415
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
3416
|
+
return deps.jsonResponse({
|
|
3417
|
+
ok: false,
|
|
3418
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
3419
|
+
projectRoot: state.projectRoot,
|
|
3420
|
+
requestedProjectRoot: normalizedRoot,
|
|
3421
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
3422
|
+
}, 403);
|
|
3423
|
+
}
|
|
3424
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
3425
|
+
} else if (exists) {
|
|
3426
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
3427
|
+
}
|
|
2601
3428
|
const control = buildServerControlStatus();
|
|
2602
3429
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
2603
3430
|
if (!exists) {
|
|
@@ -2611,7 +3438,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2611
3438
|
message: "Requested project root does not exist on the Rig server host."
|
|
2612
3439
|
}, 404);
|
|
2613
3440
|
}
|
|
2614
|
-
if (!
|
|
3441
|
+
if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
|
|
2615
3442
|
return deps.jsonResponse({
|
|
2616
3443
|
ok: false,
|
|
2617
3444
|
projectRoot: state.projectRoot,
|
|
@@ -2646,6 +3473,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2646
3473
|
exists,
|
|
2647
3474
|
control,
|
|
2648
3475
|
requiresRestart: false,
|
|
3476
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
2649
3477
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
2650
3478
|
}, 202);
|
|
2651
3479
|
}
|
|
@@ -2660,11 +3488,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2660
3488
|
}, 409);
|
|
2661
3489
|
}
|
|
2662
3490
|
if (url.pathname === "/api/github/auth/status") {
|
|
2663
|
-
const store =
|
|
2664
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
3491
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3492
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2665
3493
|
}
|
|
2666
3494
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
2667
|
-
const store =
|
|
3495
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
2668
3496
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2669
3497
|
if (!auth.signedIn) {
|
|
2670
3498
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -2686,21 +3514,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2686
3514
|
const body = await deps.readJsonBody(req);
|
|
2687
3515
|
const token = normalizeString(body.token);
|
|
2688
3516
|
const selectedRepo = normalizeString(body.selectedRepo);
|
|
3517
|
+
const requestedProjectRoot = normalizeString(body.projectRoot);
|
|
2689
3518
|
if (!token) {
|
|
2690
3519
|
return deps.badRequest("token is required");
|
|
2691
3520
|
}
|
|
2692
3521
|
try {
|
|
2693
3522
|
const user = await fetchGitHubUserInfo(token);
|
|
2694
|
-
const
|
|
2695
|
-
|
|
3523
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3524
|
+
projectRoot: state.projectRoot,
|
|
2696
3525
|
token,
|
|
2697
3526
|
tokenSource: "manual-token",
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3527
|
+
user,
|
|
3528
|
+
selectedRepo,
|
|
3529
|
+
requestedProjectRoot
|
|
3530
|
+
});
|
|
3531
|
+
return deps.jsonResponse({
|
|
3532
|
+
ok: true,
|
|
3533
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
3534
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3535
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
2702
3536
|
});
|
|
2703
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
2704
3537
|
} catch (error) {
|
|
2705
3538
|
const message = error instanceof Error ? error.message : String(error);
|
|
2706
3539
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -2767,8 +3600,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2767
3600
|
}
|
|
2768
3601
|
const token = result.payload.access_token;
|
|
2769
3602
|
const user = await fetchGitHubUserInfo(token);
|
|
2770
|
-
|
|
2771
|
-
|
|
3603
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3604
|
+
projectRoot: state.projectRoot,
|
|
3605
|
+
token,
|
|
3606
|
+
tokenSource: "oauth-device",
|
|
3607
|
+
user,
|
|
3608
|
+
selectedRepo: null
|
|
3609
|
+
});
|
|
3610
|
+
store.clearPendingDevice(pollId);
|
|
3611
|
+
return deps.jsonResponse({
|
|
3612
|
+
ok: true,
|
|
3613
|
+
status: "signed-in",
|
|
3614
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
3615
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3616
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3617
|
+
});
|
|
2772
3618
|
}
|
|
2773
3619
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
2774
3620
|
const body = await deps.readJsonBody(req);
|
|
@@ -2777,7 +3623,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2777
3623
|
if (!owner || !repo) {
|
|
2778
3624
|
return deps.badRequest("owner and repo are required");
|
|
2779
3625
|
}
|
|
2780
|
-
const store =
|
|
3626
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
2781
3627
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2782
3628
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
2783
3629
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -2798,7 +3644,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2798
3644
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
2799
3645
|
}
|
|
2800
3646
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2801
|
-
const configPath =
|
|
3647
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
2802
3648
|
const source = buildGitHubProjectConfigSource({
|
|
2803
3649
|
projectName: rawProjectName,
|
|
2804
3650
|
owner,
|
|
@@ -2810,8 +3656,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2810
3656
|
ok: true,
|
|
2811
3657
|
projectRoot: targetRoot,
|
|
2812
3658
|
configPath,
|
|
2813
|
-
exists:
|
|
2814
|
-
requiresOverwrite:
|
|
3659
|
+
exists: existsSync10(configPath),
|
|
3660
|
+
requiresOverwrite: existsSync10(configPath),
|
|
2815
3661
|
source,
|
|
2816
3662
|
owner,
|
|
2817
3663
|
repo,
|
|
@@ -2847,8 +3693,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2847
3693
|
assignee,
|
|
2848
3694
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
2849
3695
|
});
|
|
2850
|
-
const configPath =
|
|
2851
|
-
if (
|
|
3696
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3697
|
+
if (existsSync10(configPath) && !overwrite) {
|
|
2852
3698
|
return deps.jsonResponse({
|
|
2853
3699
|
ok: false,
|
|
2854
3700
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -2864,11 +3710,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2864
3710
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
2865
3711
|
}
|
|
2866
3712
|
let backupPath = null;
|
|
2867
|
-
if (
|
|
3713
|
+
if (existsSync10(configPath)) {
|
|
2868
3714
|
backupPath = backupConfigPath(configPath);
|
|
2869
|
-
|
|
3715
|
+
copyFileSync2(configPath, backupPath);
|
|
2870
3716
|
}
|
|
2871
|
-
|
|
3717
|
+
writeFileSync9(configPath, source, "utf8");
|
|
2872
3718
|
const selectedRepo = `${owner}/${repo}`;
|
|
2873
3719
|
store.saveSelectedRepo(selectedRepo);
|
|
2874
3720
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -3140,11 +3986,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3140
3986
|
const runId = normalizeString(body.runId);
|
|
3141
3987
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3142
3988
|
const promptOverride = normalizeString(body.promptOverride);
|
|
3989
|
+
const restart = body.restart === true;
|
|
3143
3990
|
if (!runId) {
|
|
3144
3991
|
return deps.badRequest("runId is required");
|
|
3145
3992
|
}
|
|
3146
3993
|
try {
|
|
3147
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
3994
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
3148
3995
|
deps.broadcastSnapshotInvalidation(state);
|
|
3149
3996
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3150
3997
|
} catch (error) {
|
|
@@ -3153,7 +4000,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3153
4000
|
}
|
|
3154
4001
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
3155
4002
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
3156
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
4003
|
+
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:@rig/pi-rig";
|
|
3157
4004
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
3158
4005
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
3159
4006
|
}
|
|
@@ -3469,9 +4316,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3469
4316
|
} catch {
|
|
3470
4317
|
return deps.badRequest("Invalid artifact path");
|
|
3471
4318
|
}
|
|
3472
|
-
|
|
4319
|
+
mkdirSync9(dirname9(artifactPath), { recursive: true });
|
|
3473
4320
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
3474
|
-
|
|
4321
|
+
writeFileSync9(artifactPath, bytes);
|
|
3475
4322
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
3476
4323
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3477
4324
|
runId,
|
|
@@ -3633,12 +4480,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3633
4480
|
try {
|
|
3634
4481
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
3635
4482
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
3636
|
-
const artifactsRoot =
|
|
4483
|
+
const artifactsRoot = resolve13(runRoot, "remote-artifacts");
|
|
3637
4484
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
3638
4485
|
} catch {
|
|
3639
4486
|
return deps.badRequest("Invalid artifact path");
|
|
3640
4487
|
}
|
|
3641
|
-
if (!
|
|
4488
|
+
if (!existsSync10(artifactPath)) {
|
|
3642
4489
|
return deps.notFound();
|
|
3643
4490
|
}
|
|
3644
4491
|
return new Response(Bun.file(artifactPath));
|