@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.30
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 +1688 -351
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +68 -24
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +1040 -200
- package/dist/src/server-helpers/inspector-jobs.js +1 -12
- package/dist/src/server-helpers/issue-analysis.js +26 -10
- package/dist/src/server-helpers/pi-session-proxy.js +84 -0
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +30 -1
- package/dist/src/server-helpers/run-mutations.js +679 -123
- package/dist/src/server-helpers/run-writers.js +8 -2
- package/dist/src/server-helpers/ws-router.js +16 -6
- package/dist/src/server.js +1687 -351
- 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 {
|
|
@@ -174,14 +174,29 @@ function summarizeUsefulRunError(projectRoot, runId, fallback) {
|
|
|
174
174
|
const nonGeneric = errorLines.at(-1);
|
|
175
175
|
return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
|
|
176
176
|
}
|
|
177
|
+
function readRunPiSessionMetadata(projectRoot, runId) {
|
|
178
|
+
const run = readAuthorityRun(projectRoot, runId);
|
|
179
|
+
const metadata = run?.piSessionPrivate;
|
|
180
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
181
|
+
return null;
|
|
182
|
+
const record = metadata;
|
|
183
|
+
const publicMetadata = record.public;
|
|
184
|
+
const daemonConnection = record.daemonConnection;
|
|
185
|
+
if (!publicMetadata || typeof publicMetadata !== "object" || Array.isArray(publicMetadata))
|
|
186
|
+
return null;
|
|
187
|
+
if (!daemonConnection || typeof daemonConnection !== "object" || Array.isArray(daemonConnection))
|
|
188
|
+
return null;
|
|
189
|
+
return metadata;
|
|
190
|
+
}
|
|
177
191
|
function readRunDetails(projectRoot, runId) {
|
|
178
192
|
const run = readAuthorityRun(projectRoot, runId);
|
|
179
193
|
if (!run) {
|
|
180
194
|
return null;
|
|
181
195
|
}
|
|
182
196
|
const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
|
|
197
|
+
const { piSessionPrivate: _piSessionPrivate, ...publicRun } = run;
|
|
183
198
|
return {
|
|
184
|
-
run: usefulErrorText ? { ...
|
|
199
|
+
run: usefulErrorText ? { ...publicRun, errorText: usefulErrorText } : publicRun,
|
|
185
200
|
timeline: readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
|
|
186
201
|
approvals: readApprovals(projectRoot, { runId }),
|
|
187
202
|
userInputs: readUserInputs(projectRoot, { runId })
|
|
@@ -224,6 +239,18 @@ function readJsonlFileTail(path, options) {
|
|
|
224
239
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
225
240
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
226
241
|
}
|
|
242
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
243
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
244
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
245
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
246
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
247
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
248
|
+
return {
|
|
249
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
250
|
+
nextCursor: String(endExclusive),
|
|
251
|
+
hasMore: endExclusive < entries.length
|
|
252
|
+
};
|
|
253
|
+
}
|
|
227
254
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
228
255
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
229
256
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -470,7 +497,6 @@ async function refreshTaskProjection(projectRoot, input) {
|
|
|
470
497
|
}
|
|
471
498
|
|
|
472
499
|
// packages/server/src/server-helpers/issue-analysis.ts
|
|
473
|
-
import { execFile } from "child_process";
|
|
474
500
|
import { createHash } from "crypto";
|
|
475
501
|
function stableIssueHash(issue) {
|
|
476
502
|
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
@@ -604,16 +630,33 @@ function parseIssueAnalysisResult(raw) {
|
|
|
604
630
|
return result;
|
|
605
631
|
}
|
|
606
632
|
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
607
|
-
return (command, args, options) =>
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
|
614
|
-
resolve6({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
633
|
+
return async (command, args, options) => {
|
|
634
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
635
|
+
const proc = Bun.spawn([command, ...args], {
|
|
636
|
+
stdout: "pipe",
|
|
637
|
+
stderr: "pipe",
|
|
638
|
+
env
|
|
615
639
|
});
|
|
616
|
-
|
|
640
|
+
let timedOut = false;
|
|
641
|
+
const timer = setTimeout(() => {
|
|
642
|
+
timedOut = true;
|
|
643
|
+
proc.kill();
|
|
644
|
+
}, options.timeoutMs);
|
|
645
|
+
try {
|
|
646
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
647
|
+
new Response(proc.stdout).text(),
|
|
648
|
+
new Response(proc.stderr).text(),
|
|
649
|
+
proc.exited
|
|
650
|
+
]);
|
|
651
|
+
return {
|
|
652
|
+
exitCode: timedOut && exitCode === 0 ? 1 : exitCode,
|
|
653
|
+
stdout,
|
|
654
|
+
stderr: timedOut && stderr.trim().length === 0 ? `Pi issue analysis timed out after ${options.timeoutMs}ms` : stderr
|
|
655
|
+
};
|
|
656
|
+
} finally {
|
|
657
|
+
clearTimeout(timer);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
617
660
|
}
|
|
618
661
|
function createPiIssueAnalyzer(input = {}) {
|
|
619
662
|
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
@@ -772,6 +815,11 @@ import {
|
|
|
772
815
|
buildTaskRunLifecycleComment,
|
|
773
816
|
updateConfiguredTaskSourceTask
|
|
774
817
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
818
|
+
import {
|
|
819
|
+
closeIssueAfterMergedPr,
|
|
820
|
+
commitRunChanges,
|
|
821
|
+
runPrAutomation
|
|
822
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
775
823
|
|
|
776
824
|
// packages/server/src/scheduler.ts
|
|
777
825
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -787,8 +835,8 @@ import {
|
|
|
787
835
|
|
|
788
836
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
789
837
|
import { randomBytes } from "crypto";
|
|
790
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
791
|
-
import { resolve as resolve7 } from "path";
|
|
838
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
839
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
792
840
|
function cleanString(value) {
|
|
793
841
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
794
842
|
}
|
|
@@ -818,6 +866,26 @@ function parseApiSessions(value) {
|
|
|
818
866
|
}];
|
|
819
867
|
});
|
|
820
868
|
}
|
|
869
|
+
function parsePendingDevice(value) {
|
|
870
|
+
if (!value || typeof value !== "object")
|
|
871
|
+
return null;
|
|
872
|
+
const record = value;
|
|
873
|
+
const pollId = cleanString(record.pollId);
|
|
874
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
875
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
876
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
877
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
878
|
+
return null;
|
|
879
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
880
|
+
}
|
|
881
|
+
function parsePendingDevices(value) {
|
|
882
|
+
if (!Array.isArray(value))
|
|
883
|
+
return [];
|
|
884
|
+
return value.flatMap((entry) => {
|
|
885
|
+
const pending = parsePendingDevice(entry);
|
|
886
|
+
return pending ? [pending] : [];
|
|
887
|
+
});
|
|
888
|
+
}
|
|
821
889
|
function readStoredAuth(stateFile) {
|
|
822
890
|
if (!existsSync4(stateFile))
|
|
823
891
|
return {};
|
|
@@ -831,6 +899,7 @@ function readStoredAuth(stateFile) {
|
|
|
831
899
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
832
900
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
833
901
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
902
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
834
903
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
835
904
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
836
905
|
};
|
|
@@ -838,34 +907,36 @@ function readStoredAuth(stateFile) {
|
|
|
838
907
|
return {};
|
|
839
908
|
}
|
|
840
909
|
}
|
|
841
|
-
function parsePendingDevice(value) {
|
|
842
|
-
if (!value || typeof value !== "object")
|
|
843
|
-
return null;
|
|
844
|
-
const record = value;
|
|
845
|
-
const pollId = cleanString(record.pollId);
|
|
846
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
847
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
848
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
849
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
850
|
-
return null;
|
|
851
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
852
|
-
}
|
|
853
910
|
function newApiSessionToken() {
|
|
854
911
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
855
912
|
}
|
|
856
913
|
function writeStoredAuth(stateFile, payload) {
|
|
857
|
-
mkdirSync3(
|
|
914
|
+
mkdirSync3(dirname3(stateFile), { recursive: true });
|
|
858
915
|
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
859
916
|
`, { encoding: "utf8", mode: 384 });
|
|
860
917
|
try {
|
|
861
918
|
chmodSync(stateFile, 384);
|
|
862
919
|
} catch {}
|
|
863
920
|
}
|
|
921
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
922
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
923
|
+
}
|
|
864
924
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
865
925
|
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
866
926
|
}
|
|
867
|
-
function
|
|
868
|
-
const
|
|
927
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
928
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
929
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
930
|
+
if (existsSync4(stateFile)) {
|
|
931
|
+
copyFileSync(stateFile, targetFile);
|
|
932
|
+
try {
|
|
933
|
+
chmodSync(targetFile, 384);
|
|
934
|
+
} catch {}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
writeStoredAuth(targetFile, {});
|
|
938
|
+
}
|
|
939
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
869
940
|
return {
|
|
870
941
|
stateFile,
|
|
871
942
|
status(options) {
|
|
@@ -895,6 +966,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
895
966
|
scopes: input.scopes ?? [],
|
|
896
967
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
897
968
|
pendingDevice: null,
|
|
969
|
+
pendingDevices: [],
|
|
898
970
|
apiSessions: previous.apiSessions ?? [],
|
|
899
971
|
updatedAt: new Date().toISOString()
|
|
900
972
|
});
|
|
@@ -923,15 +995,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
923
995
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
924
996
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
925
997
|
},
|
|
926
|
-
copyToProjectRoot(
|
|
927
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
998
|
+
copyToProjectRoot(projectRoot) {
|
|
999
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
928
1000
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
929
1001
|
},
|
|
1002
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
1003
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
1004
|
+
},
|
|
930
1005
|
savePendingDevice(input) {
|
|
931
1006
|
const previous = readStoredAuth(stateFile);
|
|
1007
|
+
const pendingDevices = [
|
|
1008
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
1009
|
+
...previous.pendingDevices ?? [],
|
|
1010
|
+
input
|
|
1011
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
932
1012
|
writeStoredAuth(stateFile, {
|
|
933
1013
|
...previous,
|
|
934
|
-
pendingDevice:
|
|
1014
|
+
pendingDevice: null,
|
|
1015
|
+
pendingDevices,
|
|
935
1016
|
updatedAt: new Date().toISOString()
|
|
936
1017
|
});
|
|
937
1018
|
},
|
|
@@ -944,23 +1025,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
944
1025
|
});
|
|
945
1026
|
},
|
|
946
1027
|
readPendingDevice(pollId) {
|
|
947
|
-
const
|
|
948
|
-
|
|
1028
|
+
const previous = readStoredAuth(stateFile);
|
|
1029
|
+
const pending = [
|
|
1030
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
1031
|
+
...previous.pendingDevices ?? []
|
|
1032
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
1033
|
+
if (!pending)
|
|
949
1034
|
return null;
|
|
950
1035
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
951
1036
|
return null;
|
|
952
1037
|
return pending;
|
|
953
1038
|
},
|
|
954
|
-
clearPendingDevice() {
|
|
1039
|
+
clearPendingDevice(pollId) {
|
|
955
1040
|
const previous = readStoredAuth(stateFile);
|
|
1041
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
956
1042
|
writeStoredAuth(stateFile, {
|
|
957
1043
|
...previous,
|
|
958
1044
|
pendingDevice: null,
|
|
1045
|
+
pendingDevices: remaining,
|
|
959
1046
|
updatedAt: new Date().toISOString()
|
|
960
1047
|
});
|
|
961
1048
|
}
|
|
962
1049
|
};
|
|
963
1050
|
}
|
|
1051
|
+
function createGitHubAuthStore(projectRoot) {
|
|
1052
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
1053
|
+
}
|
|
964
1054
|
|
|
965
1055
|
// packages/server/src/server-helpers/github-projects.ts
|
|
966
1056
|
function asRecord(value) {
|
|
@@ -969,6 +1059,9 @@ function asRecord(value) {
|
|
|
969
1059
|
function asString(value) {
|
|
970
1060
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
971
1061
|
}
|
|
1062
|
+
function asNumber(value) {
|
|
1063
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1064
|
+
}
|
|
972
1065
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
973
1066
|
const response = await fetch("https://api.github.com/graphql", {
|
|
974
1067
|
method: "POST",
|
|
@@ -985,6 +1078,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
985
1078
|
}
|
|
986
1079
|
return json.data;
|
|
987
1080
|
}
|
|
1081
|
+
function projectNodesFrom(data) {
|
|
1082
|
+
const root = asRecord(data);
|
|
1083
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
1084
|
+
const projects = asRecord(owner?.projectsV2);
|
|
1085
|
+
const nodes = projects?.nodes;
|
|
1086
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
1087
|
+
}
|
|
1088
|
+
async function listGitHubProjects(input) {
|
|
1089
|
+
const query = `
|
|
1090
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
1091
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1092
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1093
|
+
}
|
|
1094
|
+
`;
|
|
1095
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
1096
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
1097
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
1098
|
+
const record = asRecord(node);
|
|
1099
|
+
const id = asString(record?.id);
|
|
1100
|
+
const number = asNumber(record?.number);
|
|
1101
|
+
const title = asString(record?.title);
|
|
1102
|
+
if (!id || number === undefined || !title)
|
|
1103
|
+
return [];
|
|
1104
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
988
1107
|
async function resolveProjectStatusField(input) {
|
|
989
1108
|
const query = `
|
|
990
1109
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -1079,6 +1198,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
1079
1198
|
running: "In Progress",
|
|
1080
1199
|
prOpen: "In Review",
|
|
1081
1200
|
ciFixing: "In Review",
|
|
1201
|
+
merging: "Merging",
|
|
1082
1202
|
done: "Done",
|
|
1083
1203
|
needsAttention: "Needs Attention"
|
|
1084
1204
|
};
|
|
@@ -1092,6 +1212,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
1092
1212
|
return "prOpen";
|
|
1093
1213
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
1094
1214
|
return "ciFixing";
|
|
1215
|
+
if (normalized === "merging" || normalized === "merge")
|
|
1216
|
+
return "merging";
|
|
1095
1217
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
1096
1218
|
return "needsAttention";
|
|
1097
1219
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -1167,7 +1289,9 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
1167
1289
|
"needs-attention",
|
|
1168
1290
|
"stopped"
|
|
1169
1291
|
]);
|
|
1170
|
-
var
|
|
1292
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
1293
|
+
var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
|
|
1294
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
1171
1295
|
|
|
1172
1296
|
// packages/server/src/server-helpers/ws-router.ts
|
|
1173
1297
|
import {
|
|
@@ -1328,7 +1452,7 @@ if (false) {}
|
|
|
1328
1452
|
// packages/server/src/server-helpers/http-router.ts
|
|
1329
1453
|
import {
|
|
1330
1454
|
listAuthorityRuns as listAuthorityRuns7,
|
|
1331
|
-
readAuthorityRun as
|
|
1455
|
+
readAuthorityRun as readAuthorityRun9,
|
|
1332
1456
|
resolveAuthorityPaths,
|
|
1333
1457
|
writeJsonFile as writeJsonFile4
|
|
1334
1458
|
} from "@rig/runtime/control-plane/authority-files";
|
|
@@ -1348,10 +1472,64 @@ import {
|
|
|
1348
1472
|
RemoteWsClient as RemoteWsClient2
|
|
1349
1473
|
} from "@rig/runtime/control-plane/remote";
|
|
1350
1474
|
|
|
1475
|
+
// packages/server/src/server-helpers/pi-session-proxy.ts
|
|
1476
|
+
import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
|
|
1477
|
+
function resolveRunPiSessionProxy(projectRoot, runId) {
|
|
1478
|
+
const run = readAuthorityRun7(projectRoot, runId);
|
|
1479
|
+
if (!run)
|
|
1480
|
+
return null;
|
|
1481
|
+
const privateMetadata = readRunPiSessionMetadata(projectRoot, runId);
|
|
1482
|
+
if (!privateMetadata)
|
|
1483
|
+
return { pending: true, runId, status: typeof run.status === "string" ? run.status : undefined };
|
|
1484
|
+
const connection = privateMetadata.daemonConnection;
|
|
1485
|
+
if (connection.mode !== "http")
|
|
1486
|
+
throw new Error("Only loopback HTTP Rig Pi session daemon connections are supported");
|
|
1487
|
+
const token = tokenFromRef(connection.tokenRef);
|
|
1488
|
+
if (!token)
|
|
1489
|
+
throw new Error("Rig Pi session daemon token is unavailable");
|
|
1490
|
+
return {
|
|
1491
|
+
runId,
|
|
1492
|
+
sessionId: privateMetadata.public.sessionId,
|
|
1493
|
+
metadata: privateMetadata.public,
|
|
1494
|
+
privateMetadata,
|
|
1495
|
+
baseUrl: connection.baseUrl.replace(/\/+$/, ""),
|
|
1496
|
+
token
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
async function proxyRunPiHttp(projectRoot, runId, input) {
|
|
1500
|
+
const resolved = resolveRunPiSessionProxy(projectRoot, runId);
|
|
1501
|
+
if (resolved === null)
|
|
1502
|
+
return { status: 404, payload: { ok: false, error: "Run not found" } };
|
|
1503
|
+
if ("pending" in resolved)
|
|
1504
|
+
return { status: 409, payload: { ready: false, runId, status: resolved.status, retryAfterMs: 500 } };
|
|
1505
|
+
const response = await fetch(`${resolved.baseUrl}${input.daemonPath}`, {
|
|
1506
|
+
method: input.method,
|
|
1507
|
+
headers: {
|
|
1508
|
+
authorization: `Bearer ${resolved.token}`,
|
|
1509
|
+
...input.body === undefined ? {} : { "content-type": "application/json" }
|
|
1510
|
+
},
|
|
1511
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body)
|
|
1512
|
+
});
|
|
1513
|
+
const text = await response.text();
|
|
1514
|
+
const payload = text.trim() ? JSON.parse(text) : null;
|
|
1515
|
+
return { status: response.status, payload };
|
|
1516
|
+
}
|
|
1517
|
+
function buildRunPiDaemonWebSocketUrl(resolved) {
|
|
1518
|
+
const url = new URL(`${resolved.baseUrl}/sessions/${encodeURIComponent(resolved.sessionId)}/events`);
|
|
1519
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1520
|
+
url.searchParams.set("token", resolved.token);
|
|
1521
|
+
return url.toString();
|
|
1522
|
+
}
|
|
1523
|
+
function tokenFromRef(ref) {
|
|
1524
|
+
if (ref.startsWith("inline:"))
|
|
1525
|
+
return ref.slice("inline:".length) || null;
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1351
1529
|
// packages/server/src/server-helpers/run-steering.ts
|
|
1352
|
-
import { dirname as
|
|
1530
|
+
import { dirname as dirname4, resolve as resolve8 } from "path";
|
|
1353
1531
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
|
|
1354
|
-
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as
|
|
1532
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun8, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
1355
1533
|
var steeringSequence = 0;
|
|
1356
1534
|
function runSteeringPath(projectRoot, runId) {
|
|
1357
1535
|
return resolve8(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
|
|
@@ -1420,7 +1598,7 @@ function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
|
|
|
1420
1598
|
return delivered;
|
|
1421
1599
|
}
|
|
1422
1600
|
function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
1423
|
-
const run =
|
|
1601
|
+
const run = readAuthorityRun8(projectRoot, runId);
|
|
1424
1602
|
if (!run)
|
|
1425
1603
|
throw new Error(`Run not found: ${runId}`);
|
|
1426
1604
|
const text = input.message.trim();
|
|
@@ -1436,7 +1614,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
1436
1614
|
delivered: false
|
|
1437
1615
|
};
|
|
1438
1616
|
const path = runSteeringPath(projectRoot, runId);
|
|
1439
|
-
mkdirSync4(
|
|
1617
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1440
1618
|
appendJsonlRecord2(path, entry);
|
|
1441
1619
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
1442
1620
|
id: entry.id,
|
|
@@ -1473,24 +1651,205 @@ import {
|
|
|
1473
1651
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
1474
1652
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1475
1653
|
|
|
1654
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1655
|
+
import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1656
|
+
import { dirname as dirname6, resolve as resolve10 } from "path";
|
|
1657
|
+
|
|
1658
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
1659
|
+
import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1660
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
|
|
1661
|
+
function cleanString3(value) {
|
|
1662
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1663
|
+
}
|
|
1664
|
+
function sanitizePathSegment(value) {
|
|
1665
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
1666
|
+
}
|
|
1667
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
1668
|
+
const userId = cleanString3(identity.userId);
|
|
1669
|
+
if (userId) {
|
|
1670
|
+
const safeId = sanitizePathSegment(userId);
|
|
1671
|
+
if (safeId)
|
|
1672
|
+
return `ghu-${safeId}`;
|
|
1673
|
+
}
|
|
1674
|
+
const login = cleanString3(identity.login);
|
|
1675
|
+
if (login) {
|
|
1676
|
+
const safeLogin = sanitizePathSegment(login);
|
|
1677
|
+
if (safeLogin)
|
|
1678
|
+
return `ghu-login-${safeLogin}`;
|
|
1679
|
+
}
|
|
1680
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
1681
|
+
}
|
|
1682
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
1683
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
1684
|
+
if (explicitRoot)
|
|
1685
|
+
return resolve9(explicitRoot);
|
|
1686
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
1687
|
+
if (stateDir2)
|
|
1688
|
+
return resolve9(dirname5(resolve9(stateDir2)), "users");
|
|
1689
|
+
return resolve9(projectRoot, ".rig", "users");
|
|
1690
|
+
}
|
|
1691
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
1692
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
1693
|
+
const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
1694
|
+
const stateDir2 = resolve9(root, ".rig", "state");
|
|
1695
|
+
return {
|
|
1696
|
+
key,
|
|
1697
|
+
userId: cleanString3(identity.userId),
|
|
1698
|
+
login: cleanString3(identity.login),
|
|
1699
|
+
root,
|
|
1700
|
+
stateDir: stateDir2,
|
|
1701
|
+
authStateFile: resolve9(stateDir2, "github-auth.json"),
|
|
1702
|
+
metadataFile: resolve9(stateDir2, "user-namespace.json"),
|
|
1703
|
+
checkoutBaseDir: resolve9(root, "remote-checkouts"),
|
|
1704
|
+
snapshotBaseDir: resolve9(root, "remote-snapshots")
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
1708
|
+
return {
|
|
1709
|
+
key: namespace.key,
|
|
1710
|
+
userId: namespace.userId,
|
|
1711
|
+
login: namespace.login,
|
|
1712
|
+
root: namespace.root,
|
|
1713
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1714
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
1718
|
+
const root = resolve9(namespaceRoot);
|
|
1719
|
+
const candidate = resolve9(candidatePath);
|
|
1720
|
+
const rel = relative(root, candidate);
|
|
1721
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1722
|
+
}
|
|
1723
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
1724
|
+
mkdirSync5(namespace.stateDir, { recursive: true });
|
|
1725
|
+
const previous = (() => {
|
|
1726
|
+
if (!existsSync6(namespace.metadataFile))
|
|
1727
|
+
return null;
|
|
1728
|
+
try {
|
|
1729
|
+
const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
|
|
1730
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
})();
|
|
1735
|
+
const now = new Date().toISOString();
|
|
1736
|
+
writeFileSync5(namespace.metadataFile, `${JSON.stringify({
|
|
1737
|
+
key: namespace.key,
|
|
1738
|
+
userId: namespace.userId,
|
|
1739
|
+
login: namespace.login,
|
|
1740
|
+
root: namespace.root,
|
|
1741
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1742
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
1743
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
1744
|
+
updatedAt: now
|
|
1745
|
+
}, null, 2)}
|
|
1746
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1747
|
+
try {
|
|
1748
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
1749
|
+
} catch {}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1753
|
+
function cleanString4(value) {
|
|
1754
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1755
|
+
}
|
|
1756
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
1757
|
+
return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
1758
|
+
}
|
|
1759
|
+
function parseEntry(value) {
|
|
1760
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1761
|
+
return null;
|
|
1762
|
+
const record = value;
|
|
1763
|
+
const token = cleanString4(record.token);
|
|
1764
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
1765
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
1766
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
1767
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
1768
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
1769
|
+
const createdAt = cleanString4(record.createdAt);
|
|
1770
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
1771
|
+
return null;
|
|
1772
|
+
return {
|
|
1773
|
+
token,
|
|
1774
|
+
namespaceKey,
|
|
1775
|
+
namespaceRoot,
|
|
1776
|
+
authStateFile,
|
|
1777
|
+
checkoutBaseDir,
|
|
1778
|
+
snapshotBaseDir,
|
|
1779
|
+
createdAt,
|
|
1780
|
+
login: cleanString4(record.login),
|
|
1781
|
+
userId: cleanString4(record.userId),
|
|
1782
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function readIndex(indexFile) {
|
|
1786
|
+
if (!existsSync7(indexFile))
|
|
1787
|
+
return [];
|
|
1788
|
+
try {
|
|
1789
|
+
const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
|
|
1790
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
1791
|
+
const parsedEntry = parseEntry(entry);
|
|
1792
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
1793
|
+
}) : [];
|
|
1794
|
+
} catch {
|
|
1795
|
+
return [];
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function writeIndex(indexFile, sessions) {
|
|
1799
|
+
mkdirSync6(dirname6(indexFile), { recursive: true });
|
|
1800
|
+
writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
1801
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1802
|
+
try {
|
|
1803
|
+
chmodSync3(indexFile, 384);
|
|
1804
|
+
} catch {}
|
|
1805
|
+
}
|
|
1806
|
+
function registerGitHubApiSession(input) {
|
|
1807
|
+
const cleanToken = cleanString4(input.token);
|
|
1808
|
+
if (!cleanToken)
|
|
1809
|
+
throw new Error("GitHub API session token is required");
|
|
1810
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
1811
|
+
const createdAt = new Date().toISOString();
|
|
1812
|
+
const entry = {
|
|
1813
|
+
token: cleanToken,
|
|
1814
|
+
login: input.namespace.login,
|
|
1815
|
+
userId: input.namespace.userId,
|
|
1816
|
+
namespaceKey: input.namespace.key,
|
|
1817
|
+
namespaceRoot: input.namespace.root,
|
|
1818
|
+
authStateFile: input.namespace.authStateFile,
|
|
1819
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
1820
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
1821
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
1822
|
+
createdAt
|
|
1823
|
+
};
|
|
1824
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
1825
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
1826
|
+
return entry;
|
|
1827
|
+
}
|
|
1828
|
+
function readGitHubApiSession(input) {
|
|
1829
|
+
const cleanToken = cleanString4(input.token);
|
|
1830
|
+
if (!cleanToken)
|
|
1831
|
+
return null;
|
|
1832
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1476
1835
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1477
1836
|
import { createHash as createHash2 } from "crypto";
|
|
1478
1837
|
import { spawnSync } from "child_process";
|
|
1479
|
-
import { existsSync as
|
|
1480
|
-
import { dirname as
|
|
1838
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
|
|
1839
|
+
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
1481
1840
|
function normalizeRepoSlug(value) {
|
|
1482
1841
|
const trimmed = value.trim();
|
|
1483
1842
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
1484
1843
|
}
|
|
1485
1844
|
function registryPath(projectRoot) {
|
|
1486
|
-
return
|
|
1845
|
+
return resolve11(projectRoot, ".rig", "state", "projects.json");
|
|
1487
1846
|
}
|
|
1488
1847
|
function readRegistry(projectRoot) {
|
|
1489
1848
|
const path = registryPath(projectRoot);
|
|
1490
|
-
if (!
|
|
1849
|
+
if (!existsSync8(path))
|
|
1491
1850
|
return {};
|
|
1492
1851
|
try {
|
|
1493
|
-
const payload = JSON.parse(
|
|
1852
|
+
const payload = JSON.parse(readFileSync6(path, "utf8"));
|
|
1494
1853
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1495
1854
|
return {};
|
|
1496
1855
|
const projects = payload.projects;
|
|
@@ -1501,14 +1860,14 @@ function readRegistry(projectRoot) {
|
|
|
1501
1860
|
}
|
|
1502
1861
|
function writeRegistry(projectRoot, projects) {
|
|
1503
1862
|
const path = registryPath(projectRoot);
|
|
1504
|
-
|
|
1505
|
-
|
|
1863
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
1864
|
+
writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
|
|
1506
1865
|
`, "utf8");
|
|
1507
1866
|
}
|
|
1508
1867
|
function resolveConfigPath(projectRoot) {
|
|
1509
1868
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
1510
|
-
const path =
|
|
1511
|
-
if (
|
|
1869
|
+
const path = resolve11(projectRoot, name);
|
|
1870
|
+
if (existsSync8(path))
|
|
1512
1871
|
return path;
|
|
1513
1872
|
}
|
|
1514
1873
|
return null;
|
|
@@ -1517,7 +1876,7 @@ function hashFile(path) {
|
|
|
1517
1876
|
if (!path)
|
|
1518
1877
|
return null;
|
|
1519
1878
|
try {
|
|
1520
|
-
return createHash2("sha256").update(
|
|
1879
|
+
return createHash2("sha256").update(readFileSync6(path)).digest("hex");
|
|
1521
1880
|
} catch {
|
|
1522
1881
|
return null;
|
|
1523
1882
|
}
|
|
@@ -1533,11 +1892,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
1533
1892
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
1534
1893
|
}
|
|
1535
1894
|
function buildRunSummary(projectRoot) {
|
|
1536
|
-
const runsDir =
|
|
1895
|
+
const runsDir = resolve11(projectRoot, ".rig", "runs");
|
|
1537
1896
|
try {
|
|
1538
1897
|
const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
1539
1898
|
try {
|
|
1540
|
-
const run = JSON.parse(
|
|
1899
|
+
const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
|
|
1541
1900
|
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 : "" }];
|
|
1542
1901
|
} catch {
|
|
1543
1902
|
return [];
|
|
@@ -1589,10 +1948,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
1589
1948
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
1590
1949
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
1591
1950
|
}
|
|
1951
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
1952
|
+
const target = resolve11(checkoutPath);
|
|
1953
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
|
|
1954
|
+
}
|
|
1592
1955
|
|
|
1593
1956
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
1594
|
-
import { existsSync as
|
|
1595
|
-
import { dirname as
|
|
1957
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1958
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
|
|
1596
1959
|
function safeSlugSegments(repoSlug) {
|
|
1597
1960
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
1598
1961
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -1609,7 +1972,7 @@ function safeCheckoutKey(value) {
|
|
|
1609
1972
|
}
|
|
1610
1973
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
1611
1974
|
const key = safeCheckoutKey(checkoutKey);
|
|
1612
|
-
return
|
|
1975
|
+
return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
1613
1976
|
}
|
|
1614
1977
|
function sanitizeSnapshotId(value, fallback) {
|
|
1615
1978
|
const raw = (value ?? fallback).trim();
|
|
@@ -1617,7 +1980,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
1617
1980
|
return safe || fallback;
|
|
1618
1981
|
}
|
|
1619
1982
|
function assertWithinRoot(root, relativePath) {
|
|
1620
|
-
if (!relativePath ||
|
|
1983
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
|
|
1621
1984
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
1622
1985
|
}
|
|
1623
1986
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -1625,9 +1988,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
1625
1988
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1626
1989
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
1627
1990
|
}
|
|
1628
|
-
const target =
|
|
1629
|
-
const rel =
|
|
1630
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
1991
|
+
const target = resolve12(root, ...segments);
|
|
1992
|
+
const rel = relative2(root, target);
|
|
1993
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
|
|
1631
1994
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
1632
1995
|
}
|
|
1633
1996
|
return target;
|
|
@@ -1645,17 +2008,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
1645
2008
|
function extractUploadedSnapshotArchive(input) {
|
|
1646
2009
|
const archive = decodeSnapshotArchive(input.archive);
|
|
1647
2010
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
1648
|
-
const checkoutPath =
|
|
1649
|
-
|
|
2011
|
+
const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
2012
|
+
mkdirSync8(checkoutPath, { recursive: true });
|
|
1650
2013
|
for (const file of archive.files) {
|
|
1651
2014
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
1652
2015
|
throw new Error("Invalid snapshot archive file entry");
|
|
1653
2016
|
}
|
|
1654
2017
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
1655
|
-
|
|
1656
|
-
|
|
2018
|
+
mkdirSync8(dirname8(target), { recursive: true });
|
|
2019
|
+
writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
|
|
1657
2020
|
}
|
|
1658
|
-
|
|
2021
|
+
writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
1659
2022
|
repoSlug: input.repoSlug,
|
|
1660
2023
|
snapshotId,
|
|
1661
2024
|
fileCount: archive.files.length,
|
|
@@ -1690,7 +2053,7 @@ function gitCredentialConfig(token) {
|
|
|
1690
2053
|
};
|
|
1691
2054
|
}
|
|
1692
2055
|
async function prepareRemoteCheckout(input) {
|
|
1693
|
-
const exists = input.exists ??
|
|
2056
|
+
const exists = input.exists ?? existsSync9;
|
|
1694
2057
|
const strategy = input.strategy;
|
|
1695
2058
|
if (strategy.kind === "uploaded-snapshot") {
|
|
1696
2059
|
return extractUploadedSnapshotArchive({
|
|
@@ -1702,7 +2065,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
1702
2065
|
});
|
|
1703
2066
|
}
|
|
1704
2067
|
if (strategy.kind === "existing-path") {
|
|
1705
|
-
const checkoutPath2 =
|
|
2068
|
+
const checkoutPath2 = resolve12(strategy.path);
|
|
1706
2069
|
if (!exists(checkoutPath2)) {
|
|
1707
2070
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
1708
2071
|
}
|
|
@@ -1738,9 +2101,9 @@ function buildServerControlStatus() {
|
|
|
1738
2101
|
};
|
|
1739
2102
|
}
|
|
1740
2103
|
function buildProjectConfigStatus(root) {
|
|
1741
|
-
const hasConfigTs =
|
|
1742
|
-
const hasConfigJson =
|
|
1743
|
-
const hasLegacyTaskConfig =
|
|
2104
|
+
const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
|
|
2105
|
+
const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
|
|
2106
|
+
const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
|
|
1744
2107
|
let kind = "missing";
|
|
1745
2108
|
if (hasConfigTs)
|
|
1746
2109
|
kind = "rig-config-ts";
|
|
@@ -1757,6 +2120,75 @@ function buildProjectConfigStatus(root) {
|
|
|
1757
2120
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
1758
2121
|
};
|
|
1759
2122
|
}
|
|
2123
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
2124
|
+
"ready",
|
|
2125
|
+
"blocked",
|
|
2126
|
+
"in-progress",
|
|
2127
|
+
"under-review",
|
|
2128
|
+
"failed",
|
|
2129
|
+
"cancelled",
|
|
2130
|
+
"rig:running",
|
|
2131
|
+
"rig:pr-open",
|
|
2132
|
+
"rig:ci-fixing",
|
|
2133
|
+
"rig:merging",
|
|
2134
|
+
"rig:done",
|
|
2135
|
+
"rig:needs-attention"
|
|
2136
|
+
];
|
|
2137
|
+
function githubProjectsEnabled(config) {
|
|
2138
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2139
|
+
return false;
|
|
2140
|
+
const root = config;
|
|
2141
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
2142
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
2143
|
+
return projects?.enabled === true;
|
|
2144
|
+
}
|
|
2145
|
+
function githubIssueSourceRepo(config) {
|
|
2146
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2147
|
+
return null;
|
|
2148
|
+
const root = config;
|
|
2149
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
2150
|
+
const owner = normalizeString(taskSource?.owner);
|
|
2151
|
+
const repo = normalizeString(taskSource?.repo);
|
|
2152
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
2153
|
+
return { owner, repo };
|
|
2154
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
2155
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
2156
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
2157
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
2158
|
+
}
|
|
2159
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
2160
|
+
const repo = githubIssueSourceRepo(config);
|
|
2161
|
+
if (!repo)
|
|
2162
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2163
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
2164
|
+
if (!token)
|
|
2165
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2166
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
2167
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
2168
|
+
});
|
|
2169
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
2170
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
2171
|
+
const created = [];
|
|
2172
|
+
const alreadyPresent = [];
|
|
2173
|
+
const failed = [];
|
|
2174
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
2175
|
+
if (existing.has(label)) {
|
|
2176
|
+
alreadyPresent.push(label);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
2180
|
+
method: "POST",
|
|
2181
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
2182
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
2183
|
+
});
|
|
2184
|
+
if (response.ok || response.status === 422) {
|
|
2185
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
2186
|
+
} else {
|
|
2187
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
2191
|
+
}
|
|
1760
2192
|
function normalizeCommit(value) {
|
|
1761
2193
|
const raw = normalizeString(value);
|
|
1762
2194
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -1776,24 +2208,24 @@ function repoParts(repoSlug) {
|
|
|
1776
2208
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
1777
2209
|
}
|
|
1778
2210
|
function repairDir(checkoutPath) {
|
|
1779
|
-
const dir =
|
|
1780
|
-
|
|
2211
|
+
const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
2212
|
+
mkdirSync9(dir, { recursive: true });
|
|
1781
2213
|
return dir;
|
|
1782
2214
|
}
|
|
1783
2215
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
1784
|
-
const source =
|
|
1785
|
-
const backupPath =
|
|
1786
|
-
|
|
1787
|
-
|
|
2216
|
+
const source = resolve13(checkoutPath, relativePath);
|
|
2217
|
+
const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
2218
|
+
mkdirSync9(dirname9(backupPath), { recursive: true });
|
|
2219
|
+
copyFileSync2(source, backupPath);
|
|
1788
2220
|
return backupPath;
|
|
1789
2221
|
}
|
|
1790
2222
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
1791
|
-
const packagePath =
|
|
1792
|
-
if (!
|
|
2223
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2224
|
+
if (!existsSync10(packagePath)) {
|
|
1793
2225
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
1794
2226
|
}
|
|
1795
2227
|
try {
|
|
1796
|
-
const parsed = JSON.parse(
|
|
2228
|
+
const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
|
|
1797
2229
|
return {
|
|
1798
2230
|
existed: true,
|
|
1799
2231
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -1807,9 +2239,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
1807
2239
|
}
|
|
1808
2240
|
}
|
|
1809
2241
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
1810
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1811
|
-
const packagePath =
|
|
1812
|
-
if (!hasConfig && !
|
|
2242
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2243
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2244
|
+
if (!hasConfig && !existsSync10(packagePath)) {
|
|
1813
2245
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
1814
2246
|
}
|
|
1815
2247
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -1828,7 +2260,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
1828
2260
|
}
|
|
1829
2261
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
1830
2262
|
if (changed) {
|
|
1831
|
-
|
|
2263
|
+
writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
1832
2264
|
`, "utf8");
|
|
1833
2265
|
}
|
|
1834
2266
|
return {
|
|
@@ -1854,11 +2286,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
1854
2286
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
1855
2287
|
}
|
|
1856
2288
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
1857
|
-
const configPath =
|
|
1858
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2289
|
+
const configPath = resolve13(checkoutPath, "rig.config.ts");
|
|
2290
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1859
2291
|
if (existingConfigName) {
|
|
1860
|
-
const existingPath =
|
|
1861
|
-
const source =
|
|
2292
|
+
const existingPath = resolve13(checkoutPath, existingConfigName);
|
|
2293
|
+
const source = readFileSync7(existingPath, "utf8");
|
|
1862
2294
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
1863
2295
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1864
2296
|
}
|
|
@@ -1872,7 +2304,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1872
2304
|
}
|
|
1873
2305
|
}
|
|
1874
2306
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
1875
|
-
|
|
2307
|
+
writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
1876
2308
|
return {
|
|
1877
2309
|
path: configPath,
|
|
1878
2310
|
changed: true,
|
|
@@ -1881,7 +2313,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1881
2313
|
};
|
|
1882
2314
|
}
|
|
1883
2315
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
1884
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2316
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1885
2317
|
if (!configFile)
|
|
1886
2318
|
return { ok: false, error: "missing rig config" };
|
|
1887
2319
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1903,7 +2335,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
1903
2335
|
return { ok: true, configFile };
|
|
1904
2336
|
}
|
|
1905
2337
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
1906
|
-
if (!
|
|
2338
|
+
if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
|
|
1907
2339
|
return { skipped: true, reason: "package.json missing" };
|
|
1908
2340
|
}
|
|
1909
2341
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1916,8 +2348,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
1916
2348
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
1917
2349
|
}
|
|
1918
2350
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
1919
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1920
|
-
const hasPackage =
|
|
2351
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2352
|
+
const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
|
|
1921
2353
|
if (!hasConfig && !hasPackage) {
|
|
1922
2354
|
return {
|
|
1923
2355
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -2015,26 +2447,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
2015
2447
|
}
|
|
2016
2448
|
function readGitHeadCommit(projectRoot) {
|
|
2017
2449
|
try {
|
|
2018
|
-
let gitDir =
|
|
2450
|
+
let gitDir = resolve13(projectRoot, ".git");
|
|
2019
2451
|
try {
|
|
2020
|
-
const dotGit =
|
|
2452
|
+
const dotGit = readFileSync7(gitDir, "utf8").trim();
|
|
2021
2453
|
const gitDirPrefix = "gitdir:";
|
|
2022
2454
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
2023
|
-
gitDir =
|
|
2455
|
+
gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
2024
2456
|
}
|
|
2025
2457
|
} catch {}
|
|
2026
|
-
const head =
|
|
2458
|
+
const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
|
|
2027
2459
|
const refPrefix = "ref:";
|
|
2028
2460
|
if (!head.startsWith(refPrefix)) {
|
|
2029
2461
|
return normalizeCommit(head);
|
|
2030
2462
|
}
|
|
2031
2463
|
const ref = head.slice(refPrefix.length).trim();
|
|
2032
|
-
const refPath =
|
|
2033
|
-
if (
|
|
2034
|
-
return normalizeCommit(
|
|
2464
|
+
const refPath = resolve13(gitDir, ref);
|
|
2465
|
+
if (existsSync10(refPath)) {
|
|
2466
|
+
return normalizeCommit(readFileSync7(refPath, "utf8").trim());
|
|
2035
2467
|
}
|
|
2036
|
-
const commonDir = normalizeString(
|
|
2037
|
-
return commonDir ? normalizeCommit(
|
|
2468
|
+
const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
|
|
2469
|
+
return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
2038
2470
|
} catch {
|
|
2039
2471
|
return null;
|
|
2040
2472
|
}
|
|
@@ -2054,7 +2486,8 @@ function isAuthorizedInspectorStreamRequest(req, authToken) {
|
|
|
2054
2486
|
}
|
|
2055
2487
|
function buildDeploymentStatus(projectRoot) {
|
|
2056
2488
|
const envCommit = normalizeCommit(process.env.RIG_COMMIT_SHA ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.COMMIT_SHA);
|
|
2057
|
-
const
|
|
2489
|
+
const deploymentRoot = normalizeString(process.env.RIG_HOST_PROJECT_ROOT) ?? projectRoot;
|
|
2490
|
+
const gitCommit = envCommit ?? readGitHeadCommit(deploymentRoot) ?? readGitHeadCommit(projectRoot);
|
|
2058
2491
|
return {
|
|
2059
2492
|
currentCommit: gitCommit,
|
|
2060
2493
|
commitSource: envCommit ? "env" : gitCommit ? "git" : null,
|
|
@@ -2072,9 +2505,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
2072
2505
|
const repo = normalizeString(taskSource?.repo);
|
|
2073
2506
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
2074
2507
|
}
|
|
2075
|
-
async function buildTaskSourceStatus(state, config) {
|
|
2508
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
2076
2509
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
2077
|
-
const selectedRepo =
|
|
2510
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
2078
2511
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
2079
2512
|
}).selectedRepo;
|
|
2080
2513
|
try {
|
|
@@ -2127,37 +2560,146 @@ function isLoopbackRequest(req) {
|
|
|
2127
2560
|
}
|
|
2128
2561
|
}
|
|
2129
2562
|
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
2130
|
-
return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
2563
|
+
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";
|
|
2564
|
+
}
|
|
2565
|
+
function buildRigInstallScript() {
|
|
2566
|
+
return `#!/usr/bin/env bash
|
|
2567
|
+
set -euo pipefail
|
|
2568
|
+
|
|
2569
|
+
say() {
|
|
2570
|
+
printf 'rig-install: %s
|
|
2571
|
+
' "$*"
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2575
|
+
say "Bun not found; installing Bun first"
|
|
2576
|
+
curl -fsSL https://bun.sh/install | bash
|
|
2577
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2578
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
2579
|
+
fi
|
|
2580
|
+
|
|
2581
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
2582
|
+
printf 'rig-install: bun install completed, but bun is still not on PATH. Add ~/.bun/bin to PATH and retry.
|
|
2583
|
+
' >&2
|
|
2584
|
+
exit 1
|
|
2585
|
+
fi
|
|
2586
|
+
|
|
2587
|
+
say "Installing @h-rig/cli@latest"
|
|
2588
|
+
bun add -g --force @h-rig/cli@latest
|
|
2589
|
+
|
|
2590
|
+
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2591
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
2592
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
2593
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
2594
|
+
' "$BUN_RIG" >&2
|
|
2595
|
+
exit 1
|
|
2596
|
+
fi
|
|
2597
|
+
|
|
2598
|
+
USER_BIN="$HOME/.local/bin"
|
|
2599
|
+
mkdir -p "$USER_BIN"
|
|
2600
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
2601
|
+
#!/usr/bin/env bash
|
|
2602
|
+
set -euo pipefail
|
|
2603
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
2604
|
+
RIG_SHIM
|
|
2605
|
+
chmod +x "$USER_BIN/rig"
|
|
2606
|
+
|
|
2607
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
2608
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
2609
|
+
|
|
2610
|
+
if ! command -v rig >/dev/null 2>&1; then
|
|
2611
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
2612
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
2613
|
+
exit 1
|
|
2614
|
+
fi
|
|
2615
|
+
|
|
2616
|
+
say "Verifying rig"
|
|
2617
|
+
"$BUN_RIG" --help >/dev/null
|
|
2618
|
+
rig --help >/dev/null
|
|
2619
|
+
say "Done. Run: rig --help"
|
|
2620
|
+
`;
|
|
2131
2621
|
}
|
|
2132
2622
|
function normalizePrMode(value) {
|
|
2133
2623
|
const mode = normalizeString(value);
|
|
2134
2624
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
2135
2625
|
}
|
|
2626
|
+
function requestAuthResult(input) {
|
|
2627
|
+
return {
|
|
2628
|
+
authorized: input.authorized,
|
|
2629
|
+
actor: input.actor ?? null,
|
|
2630
|
+
reason: input.reason,
|
|
2631
|
+
login: input.login ?? null,
|
|
2632
|
+
userId: input.userId ?? null,
|
|
2633
|
+
userNamespace: input.userNamespace ?? null,
|
|
2634
|
+
authStateFile: input.authStateFile ?? null
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
function namespaceFromSessionIndex(entry) {
|
|
2638
|
+
const stateDir2 = dirname9(entry.authStateFile);
|
|
2639
|
+
return {
|
|
2640
|
+
key: entry.namespaceKey,
|
|
2641
|
+
userId: entry.userId,
|
|
2642
|
+
login: entry.login,
|
|
2643
|
+
root: entry.namespaceRoot,
|
|
2644
|
+
stateDir: stateDir2,
|
|
2645
|
+
authStateFile: entry.authStateFile,
|
|
2646
|
+
metadataFile: resolve13(stateDir2, "user-namespace.json"),
|
|
2647
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
2648
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2136
2651
|
function authorizeRigHttpRequest(input) {
|
|
2137
2652
|
if (input.legacyAuthorized) {
|
|
2138
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
2653
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
2139
2654
|
}
|
|
2140
2655
|
const bearer = bearerTokenFromRequest(input.req);
|
|
2141
2656
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
2142
2657
|
const storedToken = store.readToken();
|
|
2143
2658
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
2144
2659
|
if (session) {
|
|
2145
|
-
return {
|
|
2660
|
+
return requestAuthResult({
|
|
2661
|
+
authorized: true,
|
|
2662
|
+
actor: session.login ?? "github-operator",
|
|
2663
|
+
reason: "github-session",
|
|
2664
|
+
login: session.login,
|
|
2665
|
+
userId: session.userId,
|
|
2666
|
+
authStateFile: store.stateFile
|
|
2667
|
+
});
|
|
2146
2668
|
}
|
|
2147
2669
|
if (bearer && storedToken && bearer === storedToken) {
|
|
2148
2670
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2149
|
-
return {
|
|
2671
|
+
return requestAuthResult({
|
|
2672
|
+
authorized: true,
|
|
2673
|
+
actor: status.login ?? "github-operator",
|
|
2674
|
+
reason: "github-token",
|
|
2675
|
+
login: status.login,
|
|
2676
|
+
userId: status.userId,
|
|
2677
|
+
authStateFile: store.stateFile
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
2681
|
+
if (indexedSession) {
|
|
2682
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
2683
|
+
return requestAuthResult({
|
|
2684
|
+
authorized: true,
|
|
2685
|
+
actor: indexedSession.login ?? "github-operator",
|
|
2686
|
+
reason: "github-user-session",
|
|
2687
|
+
login: indexedSession.login,
|
|
2688
|
+
userId: indexedSession.userId,
|
|
2689
|
+
userNamespace,
|
|
2690
|
+
authStateFile: indexedSession.authStateFile
|
|
2691
|
+
});
|
|
2150
2692
|
}
|
|
2151
2693
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
2152
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
2694
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
2153
2695
|
}
|
|
2154
2696
|
if (!input.serverAuthToken && !storedToken) {
|
|
2155
2697
|
if (isLoopbackRequest(input.req)) {
|
|
2156
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
2698
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
2157
2699
|
}
|
|
2158
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
2700
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
2159
2701
|
}
|
|
2160
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
2702
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
2161
2703
|
}
|
|
2162
2704
|
async function fetchGitHubUserInfo(token) {
|
|
2163
2705
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -2177,6 +2719,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
2177
2719
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
2178
2720
|
};
|
|
2179
2721
|
}
|
|
2722
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
2723
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
2724
|
+
return false;
|
|
2725
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
2726
|
+
if (!stateDir2)
|
|
2727
|
+
return true;
|
|
2728
|
+
return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
|
|
2729
|
+
}
|
|
2730
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
2731
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
2732
|
+
}
|
|
2733
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
2734
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
2735
|
+
}
|
|
2736
|
+
function userNamespaceResponse(namespace) {
|
|
2737
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
2738
|
+
}
|
|
2739
|
+
function resolveNamespacedBaseDir(input) {
|
|
2740
|
+
if (input.explicitBaseDir)
|
|
2741
|
+
return input.explicitBaseDir;
|
|
2742
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
2743
|
+
if (input.userNamespace) {
|
|
2744
|
+
return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
2745
|
+
}
|
|
2746
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
2747
|
+
}
|
|
2748
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
2749
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
2750
|
+
}
|
|
2751
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
2752
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
2753
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
2754
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
2755
|
+
store.saveToken({
|
|
2756
|
+
token: input.token,
|
|
2757
|
+
tokenSource: input.tokenSource,
|
|
2758
|
+
login: input.user.login,
|
|
2759
|
+
userId: input.user.userId,
|
|
2760
|
+
scopes: input.user.scopes,
|
|
2761
|
+
selectedRepo: input.selectedRepo
|
|
2762
|
+
});
|
|
2763
|
+
const apiSession = store.createApiSession();
|
|
2764
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
2765
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
2766
|
+
if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
|
|
2767
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
|
|
2768
|
+
}
|
|
2769
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
2770
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
2771
|
+
rootStore.saveToken({
|
|
2772
|
+
token: input.token,
|
|
2773
|
+
tokenSource: input.tokenSource,
|
|
2774
|
+
login: input.user.login,
|
|
2775
|
+
userId: input.user.userId,
|
|
2776
|
+
scopes: input.user.scopes,
|
|
2777
|
+
selectedRepo: input.selectedRepo
|
|
2778
|
+
});
|
|
2779
|
+
rootStore.createApiSession();
|
|
2780
|
+
}
|
|
2781
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
2782
|
+
}
|
|
2180
2783
|
async function postGitHubForm(endpoint, body) {
|
|
2181
2784
|
const response = await fetch(endpoint, {
|
|
2182
2785
|
method: "POST",
|
|
@@ -2194,11 +2797,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
2194
2797
|
const requestedRoot = normalizeString(rawRoot);
|
|
2195
2798
|
if (!requestedRoot)
|
|
2196
2799
|
return currentRoot;
|
|
2197
|
-
if (!
|
|
2800
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
2198
2801
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
2199
2802
|
}
|
|
2200
|
-
const normalizedRoot =
|
|
2201
|
-
if (!
|
|
2803
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
2804
|
+
if (!existsSync10(normalizedRoot)) {
|
|
2202
2805
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
2203
2806
|
}
|
|
2204
2807
|
return normalizedRoot;
|
|
@@ -2474,7 +3077,7 @@ function redactSecretFields(value) {
|
|
|
2474
3077
|
return redacted;
|
|
2475
3078
|
}
|
|
2476
3079
|
function validateRemoteLease(deps, state, input) {
|
|
2477
|
-
const run =
|
|
3080
|
+
const run = readAuthorityRun9(state.projectRoot, input.runId);
|
|
2478
3081
|
if (!run) {
|
|
2479
3082
|
return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
|
|
2480
3083
|
}
|
|
@@ -2494,6 +3097,43 @@ function createRigServerFetch(state, deps) {
|
|
|
2494
3097
|
return deps.withServerPathEnv(state.projectRoot, async () => {
|
|
2495
3098
|
const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
|
|
2496
3099
|
const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
|
|
3100
|
+
const earlyUrl = new URL(req.url);
|
|
3101
|
+
const piEventsWsMatch = earlyUrl.pathname.match(/^\/api\/runs\/([^/]+)\/pi\/events$/);
|
|
3102
|
+
if (piEventsWsMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
3103
|
+
const queryToken = earlyUrl.searchParams.get("token");
|
|
3104
|
+
const authHeaders = new Headers(req.headers);
|
|
3105
|
+
if (queryToken && !authHeaders.has("authorization")) {
|
|
3106
|
+
authHeaders.set("authorization", `Bearer ${queryToken}`);
|
|
3107
|
+
}
|
|
3108
|
+
const legacyAuthorized = Boolean(state.authToken && queryToken === state.authToken);
|
|
3109
|
+
const requestAuth = authorizeRigHttpRequest({
|
|
3110
|
+
req: new Request(req.url, { method: req.method, headers: authHeaders }),
|
|
3111
|
+
pathname: earlyUrl.pathname,
|
|
3112
|
+
projectRoot: state.projectRoot,
|
|
3113
|
+
serverAuthToken: state.authToken,
|
|
3114
|
+
legacyAuthorized
|
|
3115
|
+
});
|
|
3116
|
+
if (!requestAuth.authorized) {
|
|
3117
|
+
return deps.jsonResponse({ ok: false, error: "Unauthorized WebSocket connection", reason: requestAuth.reason }, 401);
|
|
3118
|
+
}
|
|
3119
|
+
const runId = decodeURIComponent(piEventsWsMatch[1]);
|
|
3120
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
3121
|
+
if (resolved === null)
|
|
3122
|
+
return deps.jsonResponse({ ok: false, error: "Run not found" }, 404);
|
|
3123
|
+
if ("pending" in resolved)
|
|
3124
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
3125
|
+
if (!server)
|
|
3126
|
+
return deps.jsonResponse({ ok: false, error: "WebSocket upgrade unavailable" }, 400);
|
|
3127
|
+
const upgraded = server.upgrade(req, {
|
|
3128
|
+
data: {
|
|
3129
|
+
kind: "pi-session-proxy",
|
|
3130
|
+
connectedAt: new Date().toISOString(),
|
|
3131
|
+
upstreamUrl: buildRunPiDaemonWebSocketUrl(resolved),
|
|
3132
|
+
runId
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
return upgraded ? new Response(null) : deps.jsonResponse({ ok: false, error: "WebSocket upgrade failed" }, 400);
|
|
3136
|
+
}
|
|
2497
3137
|
const upgradeResponse = handleWebSocketUpgrade({
|
|
2498
3138
|
req,
|
|
2499
3139
|
server,
|
|
@@ -2531,6 +3171,13 @@ function createRigServerFetch(state, deps) {
|
|
|
2531
3171
|
notifications: state.targets.length
|
|
2532
3172
|
});
|
|
2533
3173
|
}
|
|
3174
|
+
if (url.pathname === "/install" && req.method === "GET") {
|
|
3175
|
+
return new Response(buildRigInstallScript(), {
|
|
3176
|
+
headers: {
|
|
3177
|
+
"Content-Type": "text/x-shellscript; charset=utf-8"
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
2534
3181
|
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
2535
3182
|
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
2536
3183
|
const legacyAuthorizedHttpRequest = Boolean(state.authToken) && (isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken));
|
|
@@ -2743,16 +3390,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2743
3390
|
if (!source) {
|
|
2744
3391
|
return deps.badRequest("No task source is configured");
|
|
2745
3392
|
}
|
|
3393
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
3394
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
3395
|
+
}
|
|
2746
3396
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
2747
3397
|
return;
|
|
2748
3398
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
2749
|
-
if (source.updateTask) {
|
|
2750
|
-
await source.updateTask(id, update);
|
|
2751
|
-
} else if (update.status && source.updateStatus) {
|
|
2752
|
-
await source.updateStatus(id, update.status);
|
|
2753
|
-
} else {
|
|
2754
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
2755
|
-
}
|
|
2756
3399
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
2757
3400
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
2758
3401
|
taskId: id,
|
|
@@ -2761,6 +3404,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2761
3404
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
2762
3405
|
config: ctx?.config
|
|
2763
3406
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
3407
|
+
if (update.status && githubProjectsEnabled(ctx?.config) && projectSync.synced === false) {
|
|
3408
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
3409
|
+
}
|
|
3410
|
+
try {
|
|
3411
|
+
if (source.updateTask) {
|
|
3412
|
+
await source.updateTask(id, update);
|
|
3413
|
+
} else if (update.status && source.updateStatus) {
|
|
3414
|
+
await source.updateStatus(id, update.status);
|
|
3415
|
+
}
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
let rollback = null;
|
|
3418
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
3419
|
+
if (update.status && previousStatus && githubProjectsEnabled(ctx?.config) && projectSync.synced !== false) {
|
|
3420
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
3421
|
+
taskId: id,
|
|
3422
|
+
status: previousStatus,
|
|
3423
|
+
issueNodeId,
|
|
3424
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3425
|
+
config: ctx?.config
|
|
3426
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
3427
|
+
}
|
|
3428
|
+
return deps.jsonResponse({
|
|
3429
|
+
ok: false,
|
|
3430
|
+
id,
|
|
3431
|
+
projectSync,
|
|
3432
|
+
rollback,
|
|
3433
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3434
|
+
}, 502);
|
|
3435
|
+
}
|
|
2764
3436
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
2765
3437
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
2766
3438
|
return;
|
|
@@ -2769,25 +3441,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2769
3441
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
2770
3442
|
}
|
|
2771
3443
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
3444
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
3445
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
3446
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
3447
|
+
}
|
|
2772
3448
|
return deps.jsonResponse({
|
|
2773
3449
|
ok: true,
|
|
2774
3450
|
ready: true,
|
|
2775
3451
|
labelsReady: true,
|
|
2776
|
-
labels: [
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
3452
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
3453
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
3457
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
3458
|
+
if (!owner)
|
|
3459
|
+
return deps.badRequest("owner is required");
|
|
3460
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3461
|
+
if (!token)
|
|
3462
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
3463
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
3464
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
2790
3465
|
});
|
|
3466
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
3467
|
+
}
|
|
3468
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
3469
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
3470
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
3471
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3472
|
+
if (!token)
|
|
3473
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
3474
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
3475
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3476
|
+
});
|
|
3477
|
+
return deps.jsonResponse({ ok: true, field });
|
|
2791
3478
|
}
|
|
2792
3479
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
2793
3480
|
const body = await deps.readJsonBody(req);
|
|
@@ -2852,7 +3539,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2852
3539
|
}
|
|
2853
3540
|
if (url.pathname === "/api/server/status") {
|
|
2854
3541
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2855
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
3542
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
2856
3543
|
return deps.jsonResponse({
|
|
2857
3544
|
ok: true,
|
|
2858
3545
|
projectRoot: state.projectRoot,
|
|
@@ -2876,8 +3563,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2876
3563
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
2877
3564
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
2878
3565
|
} : undefined;
|
|
2879
|
-
const
|
|
2880
|
-
|
|
3566
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3567
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
3568
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2881
3569
|
}
|
|
2882
3570
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
2883
3571
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -2890,8 +3578,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2890
3578
|
if (!archiveContentBase64) {
|
|
2891
3579
|
return deps.badRequest("archiveContentBase64 is required");
|
|
2892
3580
|
}
|
|
2893
|
-
const
|
|
2894
|
-
const
|
|
3581
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3582
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3583
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
3584
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
3585
|
+
userNamespace: requestAuth.userNamespace,
|
|
3586
|
+
legacyProjectRoot: state.projectRoot,
|
|
3587
|
+
legacySubdir: "remote-snapshots"
|
|
3588
|
+
});
|
|
3589
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
2895
3590
|
try {
|
|
2896
3591
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
2897
3592
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -2904,14 +3599,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2904
3599
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2905
3600
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2906
3601
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2907
|
-
const project = linkProjectCheckout(
|
|
3602
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2908
3603
|
kind: "uploaded-snapshot",
|
|
2909
3604
|
path: checkout.path,
|
|
2910
3605
|
ref: checkout.snapshotId
|
|
2911
3606
|
});
|
|
2912
3607
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
2913
3608
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
2914
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3609
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2915
3610
|
} catch (error) {
|
|
2916
3611
|
return deps.jsonResponse({
|
|
2917
3612
|
ok: false,
|
|
@@ -2931,10 +3626,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2931
3626
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
2932
3627
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
2933
3628
|
}
|
|
2934
|
-
const
|
|
2935
|
-
const
|
|
3629
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3630
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3631
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
3632
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
3633
|
+
userNamespace: requestAuth.userNamespace,
|
|
3634
|
+
legacyProjectRoot: state.projectRoot,
|
|
3635
|
+
legacySubdir: "remote-checkouts"
|
|
3636
|
+
});
|
|
3637
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
2936
3638
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
2937
|
-
const credentialToken =
|
|
3639
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
2938
3640
|
try {
|
|
2939
3641
|
const checkout = await prepareRemoteCheckout({
|
|
2940
3642
|
command: runRemoteCheckoutCommand,
|
|
@@ -2943,14 +3645,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2943
3645
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2944
3646
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2945
3647
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2946
|
-
const project = linkProjectCheckout(
|
|
3648
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2947
3649
|
kind: checkout.kind,
|
|
2948
3650
|
path: checkout.path,
|
|
2949
3651
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
2950
3652
|
});
|
|
2951
3653
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
2952
3654
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
2953
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3655
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2954
3656
|
} catch (error) {
|
|
2955
3657
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2956
3658
|
}
|
|
@@ -2967,16 +3669,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2967
3669
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
2968
3670
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
2969
3671
|
}
|
|
2970
|
-
const
|
|
3672
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3673
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2971
3674
|
kind,
|
|
2972
3675
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
2973
3676
|
ref: normalizeString(body.ref) ?? undefined
|
|
2974
3677
|
});
|
|
2975
|
-
return deps.jsonResponse({ ok: true, project });
|
|
3678
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2976
3679
|
}
|
|
2977
3680
|
if (req.method === "GET") {
|
|
2978
|
-
const
|
|
2979
|
-
|
|
3681
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3682
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
3683
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
2980
3684
|
}
|
|
2981
3685
|
}
|
|
2982
3686
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -2985,13 +3689,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2985
3689
|
if (!requestedRoot) {
|
|
2986
3690
|
return deps.badRequest("projectRoot is required");
|
|
2987
3691
|
}
|
|
2988
|
-
if (!
|
|
3692
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
2989
3693
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
2990
3694
|
}
|
|
2991
|
-
const normalizedRoot =
|
|
2992
|
-
const exists =
|
|
2993
|
-
if (exists) {
|
|
2994
|
-
|
|
3695
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
3696
|
+
const exists = existsSync10(normalizedRoot);
|
|
3697
|
+
if (exists && requestAuth.userNamespace) {
|
|
3698
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
3699
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
3700
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
3701
|
+
return deps.jsonResponse({
|
|
3702
|
+
ok: false,
|
|
3703
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
3704
|
+
projectRoot: state.projectRoot,
|
|
3705
|
+
requestedProjectRoot: normalizedRoot,
|
|
3706
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
3707
|
+
}, 403);
|
|
3708
|
+
}
|
|
3709
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
3710
|
+
} else if (exists) {
|
|
3711
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
2995
3712
|
}
|
|
2996
3713
|
const control = buildServerControlStatus();
|
|
2997
3714
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -3006,7 +3723,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3006
3723
|
message: "Requested project root does not exist on the Rig server host."
|
|
3007
3724
|
}, 404);
|
|
3008
3725
|
}
|
|
3009
|
-
if (!
|
|
3726
|
+
if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
|
|
3010
3727
|
return deps.jsonResponse({
|
|
3011
3728
|
ok: false,
|
|
3012
3729
|
projectRoot: state.projectRoot,
|
|
@@ -3041,6 +3758,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3041
3758
|
exists,
|
|
3042
3759
|
control,
|
|
3043
3760
|
requiresRestart: false,
|
|
3761
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
3044
3762
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
3045
3763
|
}, 202);
|
|
3046
3764
|
}
|
|
@@ -3055,11 +3773,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3055
3773
|
}, 409);
|
|
3056
3774
|
}
|
|
3057
3775
|
if (url.pathname === "/api/github/auth/status") {
|
|
3058
|
-
const store =
|
|
3059
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
3776
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3777
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
3060
3778
|
}
|
|
3061
3779
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
3062
|
-
const store =
|
|
3780
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3063
3781
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3064
3782
|
if (!auth.signedIn) {
|
|
3065
3783
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -3087,24 +3805,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3087
3805
|
}
|
|
3088
3806
|
try {
|
|
3089
3807
|
const user = await fetchGitHubUserInfo(token);
|
|
3090
|
-
const
|
|
3091
|
-
state.projectRoot,
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
}
|
|
3105
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
3106
|
-
const apiSession = store.createApiSession();
|
|
3107
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
3808
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3809
|
+
projectRoot: state.projectRoot,
|
|
3810
|
+
token,
|
|
3811
|
+
tokenSource: "manual-token",
|
|
3812
|
+
user,
|
|
3813
|
+
selectedRepo,
|
|
3814
|
+
requestedProjectRoot
|
|
3815
|
+
});
|
|
3816
|
+
return deps.jsonResponse({
|
|
3817
|
+
ok: true,
|
|
3818
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
3819
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3820
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3821
|
+
});
|
|
3108
3822
|
} catch (error) {
|
|
3109
3823
|
const message = error instanceof Error ? error.message : String(error);
|
|
3110
3824
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -3171,9 +3885,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3171
3885
|
}
|
|
3172
3886
|
const token = result.payload.access_token;
|
|
3173
3887
|
const user = await fetchGitHubUserInfo(token);
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3888
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3889
|
+
projectRoot: state.projectRoot,
|
|
3890
|
+
token,
|
|
3891
|
+
tokenSource: "oauth-device",
|
|
3892
|
+
user,
|
|
3893
|
+
selectedRepo: null
|
|
3894
|
+
});
|
|
3895
|
+
store.clearPendingDevice(pollId);
|
|
3896
|
+
return deps.jsonResponse({
|
|
3897
|
+
ok: true,
|
|
3898
|
+
status: "signed-in",
|
|
3899
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
3900
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3901
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3902
|
+
});
|
|
3177
3903
|
}
|
|
3178
3904
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
3179
3905
|
const body = await deps.readJsonBody(req);
|
|
@@ -3182,7 +3908,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3182
3908
|
if (!owner || !repo) {
|
|
3183
3909
|
return deps.badRequest("owner and repo are required");
|
|
3184
3910
|
}
|
|
3185
|
-
const store =
|
|
3911
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3186
3912
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3187
3913
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
3188
3914
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -3203,7 +3929,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3203
3929
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
3204
3930
|
}
|
|
3205
3931
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3206
|
-
const configPath =
|
|
3932
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3207
3933
|
const source = buildGitHubProjectConfigSource({
|
|
3208
3934
|
projectName: rawProjectName,
|
|
3209
3935
|
owner,
|
|
@@ -3215,8 +3941,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3215
3941
|
ok: true,
|
|
3216
3942
|
projectRoot: targetRoot,
|
|
3217
3943
|
configPath,
|
|
3218
|
-
exists:
|
|
3219
|
-
requiresOverwrite:
|
|
3944
|
+
exists: existsSync10(configPath),
|
|
3945
|
+
requiresOverwrite: existsSync10(configPath),
|
|
3220
3946
|
source,
|
|
3221
3947
|
owner,
|
|
3222
3948
|
repo,
|
|
@@ -3252,8 +3978,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3252
3978
|
assignee,
|
|
3253
3979
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
3254
3980
|
});
|
|
3255
|
-
const configPath =
|
|
3256
|
-
if (
|
|
3981
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3982
|
+
if (existsSync10(configPath) && !overwrite) {
|
|
3257
3983
|
return deps.jsonResponse({
|
|
3258
3984
|
ok: false,
|
|
3259
3985
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -3269,11 +3995,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3269
3995
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
3270
3996
|
}
|
|
3271
3997
|
let backupPath = null;
|
|
3272
|
-
if (
|
|
3998
|
+
if (existsSync10(configPath)) {
|
|
3273
3999
|
backupPath = backupConfigPath(configPath);
|
|
3274
|
-
|
|
4000
|
+
copyFileSync2(configPath, backupPath);
|
|
3275
4001
|
}
|
|
3276
|
-
|
|
4002
|
+
writeFileSync9(configPath, source, "utf8");
|
|
3277
4003
|
const selectedRepo = `${owner}/${repo}`;
|
|
3278
4004
|
store.saveSelectedRepo(selectedRepo);
|
|
3279
4005
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -3545,11 +4271,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3545
4271
|
const runId = normalizeString(body.runId);
|
|
3546
4272
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3547
4273
|
const promptOverride = normalizeString(body.promptOverride);
|
|
4274
|
+
const restart = body.restart === true;
|
|
3548
4275
|
if (!runId) {
|
|
3549
4276
|
return deps.badRequest("runId is required");
|
|
3550
4277
|
}
|
|
3551
4278
|
try {
|
|
3552
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
4279
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
3553
4280
|
deps.broadcastSnapshotInvalidation(state);
|
|
3554
4281
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3555
4282
|
} catch (error) {
|
|
@@ -3558,7 +4285,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3558
4285
|
}
|
|
3559
4286
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
3560
4287
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
3561
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
4288
|
+
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve13(root, "packages", "pi-rig")).find((candidate) => existsSync10(resolve13(candidate, "package.json"))) ?? "npm:@h-rig/pi-rig";
|
|
3562
4289
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
3563
4290
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
3564
4291
|
}
|
|
@@ -3874,9 +4601,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3874
4601
|
} catch {
|
|
3875
4602
|
return deps.badRequest("Invalid artifact path");
|
|
3876
4603
|
}
|
|
3877
|
-
|
|
4604
|
+
mkdirSync9(dirname9(artifactPath), { recursive: true });
|
|
3878
4605
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
3879
|
-
|
|
4606
|
+
writeFileSync9(artifactPath, bytes);
|
|
3880
4607
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
3881
4608
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3882
4609
|
runId,
|
|
@@ -3913,13 +4640,75 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3913
4640
|
}
|
|
3914
4641
|
const run = leaseValidation.run;
|
|
3915
4642
|
const completedAt = new Date().toISOString();
|
|
4643
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
4644
|
+
if (run.taskId && workspaceDir) {
|
|
4645
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4646
|
+
status: "reviewing",
|
|
4647
|
+
completedAt: null,
|
|
4648
|
+
hostId,
|
|
4649
|
+
endpointId: leaseId,
|
|
4650
|
+
worktreePath: workspaceDir,
|
|
4651
|
+
serverCloseout: {
|
|
4652
|
+
status: "pending",
|
|
4653
|
+
phase: "queued",
|
|
4654
|
+
requestedAt: completedAt,
|
|
4655
|
+
updatedAt: completedAt,
|
|
4656
|
+
runtimeWorkspace: workspaceDir,
|
|
4657
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
4658
|
+
taskId: run.taskId,
|
|
4659
|
+
source: "remote-complete"
|
|
4660
|
+
}
|
|
4661
|
+
});
|
|
4662
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4663
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
4664
|
+
title: "Server-owned closeout requested",
|
|
4665
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
4666
|
+
tone: "info",
|
|
4667
|
+
status: "reviewing",
|
|
4668
|
+
createdAt: completedAt,
|
|
4669
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
4670
|
+
}, "remote-server-closeout-requested");
|
|
4671
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
4672
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4673
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4674
|
+
status: "failed",
|
|
4675
|
+
completedAt: new Date().toISOString(),
|
|
4676
|
+
errorText: detail,
|
|
4677
|
+
serverCloseout: {
|
|
4678
|
+
status: "failed",
|
|
4679
|
+
phase: "failed",
|
|
4680
|
+
updatedAt: new Date().toISOString(),
|
|
4681
|
+
error: detail
|
|
4682
|
+
}
|
|
4683
|
+
});
|
|
4684
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4685
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
4686
|
+
title: "Server-owned closeout failed",
|
|
4687
|
+
detail,
|
|
4688
|
+
tone: "error",
|
|
4689
|
+
status: "failed",
|
|
4690
|
+
createdAt: new Date().toISOString()
|
|
4691
|
+
}, "remote-server-closeout-failed");
|
|
4692
|
+
}).finally(() => {
|
|
4693
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
4694
|
+
});
|
|
4695
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
4696
|
+
return deps.jsonResponse({
|
|
4697
|
+
ok: true,
|
|
4698
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
4699
|
+
hostId,
|
|
4700
|
+
runId,
|
|
4701
|
+
leaseId,
|
|
4702
|
+
closeout: "server-owned",
|
|
4703
|
+
acceptedAt: new Date().toISOString()
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
3916
4706
|
patchRunRecord(state.projectRoot, runId, {
|
|
3917
4707
|
status: "completed",
|
|
3918
4708
|
completedAt,
|
|
3919
4709
|
hostId,
|
|
3920
4710
|
endpointId: leaseId
|
|
3921
4711
|
});
|
|
3922
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
3923
4712
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3924
4713
|
type: "run.completed",
|
|
3925
4714
|
runId,
|
|
@@ -4038,12 +4827,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4038
4827
|
try {
|
|
4039
4828
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
4040
4829
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
4041
|
-
const artifactsRoot =
|
|
4830
|
+
const artifactsRoot = resolve13(runRoot, "remote-artifacts");
|
|
4042
4831
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
4043
4832
|
} catch {
|
|
4044
4833
|
return deps.badRequest("Invalid artifact path");
|
|
4045
4834
|
}
|
|
4046
|
-
if (!
|
|
4835
|
+
if (!existsSync10(artifactPath)) {
|
|
4047
4836
|
return deps.notFound();
|
|
4048
4837
|
}
|
|
4049
4838
|
return new Response(Bun.file(artifactPath));
|
|
@@ -4056,6 +4845,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4056
4845
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
4057
4846
|
return deps.jsonResponse(page);
|
|
4058
4847
|
}
|
|
4848
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
4849
|
+
if (runTimelineMatch) {
|
|
4850
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
4851
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
4852
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
4853
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
4854
|
+
return deps.jsonResponse(page);
|
|
4855
|
+
}
|
|
4059
4856
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
4060
4857
|
if (runSteerMatch && req.method === "POST") {
|
|
4061
4858
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -4075,6 +4872,49 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4075
4872
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
4076
4873
|
}
|
|
4077
4874
|
}
|
|
4875
|
+
const runPiMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/pi(?:\/(.*))?$/);
|
|
4876
|
+
if (runPiMatch) {
|
|
4877
|
+
const runId = decodeURIComponent(runPiMatch[1]);
|
|
4878
|
+
const action = runPiMatch[2] || "";
|
|
4879
|
+
const resolved = resolveRunPiSessionProxy(state.projectRoot, runId);
|
|
4880
|
+
if (resolved === null)
|
|
4881
|
+
return deps.notFound();
|
|
4882
|
+
if ("pending" in resolved) {
|
|
4883
|
+
if (action === "" && req.method === "GET") {
|
|
4884
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500 }, 202);
|
|
4885
|
+
}
|
|
4886
|
+
return deps.jsonResponse({ ready: false, runId, status: resolved.status, retryAfterMs: 500, error: "Pi session is not ready" }, 409);
|
|
4887
|
+
}
|
|
4888
|
+
if (action === "" && req.method === "GET")
|
|
4889
|
+
return deps.jsonResponse({ ready: true, metadata: resolved.metadata });
|
|
4890
|
+
const body = req.method === "GET" ? undefined : await deps.readJsonBody(req);
|
|
4891
|
+
const sessionPath = `/sessions/${encodeURIComponent(resolved.sessionId)}`;
|
|
4892
|
+
const daemonPath = (() => {
|
|
4893
|
+
if (action === "messages" && req.method === "GET")
|
|
4894
|
+
return `${sessionPath}/messages`;
|
|
4895
|
+
if (action === "status" && req.method === "GET")
|
|
4896
|
+
return `${sessionPath}/status`;
|
|
4897
|
+
if (action === "commands" && req.method === "GET")
|
|
4898
|
+
return `${sessionPath}/commands`;
|
|
4899
|
+
if (action === "prompt" && req.method === "POST")
|
|
4900
|
+
return `${sessionPath}/prompt`;
|
|
4901
|
+
if (action === "shell" && req.method === "POST")
|
|
4902
|
+
return `${sessionPath}/shell`;
|
|
4903
|
+
if (action === "commands/run" && req.method === "POST")
|
|
4904
|
+
return `${sessionPath}/commands/run`;
|
|
4905
|
+
if (action === "commands/respond" && req.method === "POST")
|
|
4906
|
+
return `${sessionPath}/commands/respond`;
|
|
4907
|
+
if (action === "extension-ui/respond" && req.method === "POST")
|
|
4908
|
+
return `${sessionPath}/extension-ui/respond`;
|
|
4909
|
+
if (action === "abort" && req.method === "POST")
|
|
4910
|
+
return `${sessionPath}/abort`;
|
|
4911
|
+
return null;
|
|
4912
|
+
})();
|
|
4913
|
+
if (!daemonPath)
|
|
4914
|
+
return deps.notFound();
|
|
4915
|
+
const proxied = await proxyRunPiHttp(state.projectRoot, runId, { method: req.method, daemonPath, body });
|
|
4916
|
+
return deps.jsonResponse(proxied.payload, proxied.status);
|
|
4917
|
+
}
|
|
4078
4918
|
const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
|
|
4079
4919
|
if (runSteeringAckMatch && req.method === "POST") {
|
|
4080
4920
|
const runId = decodeURIComponent(runSteeringAckMatch[1]);
|