@h-rig/server 0.0.6-alpha.10 → 0.0.6-alpha.12
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 +620 -242
- 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-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +538 -160
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-mutations.js +68 -26
- package/dist/src/server.js +620 -242
- 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 {
|
|
@@ -803,8 +803,8 @@ import {
|
|
|
803
803
|
|
|
804
804
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
805
805
|
import { randomBytes } from "crypto";
|
|
806
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
807
|
-
import { resolve as resolve7 } from "path";
|
|
806
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
807
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
808
808
|
function cleanString(value) {
|
|
809
809
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
810
810
|
}
|
|
@@ -834,6 +834,26 @@ function parseApiSessions(value) {
|
|
|
834
834
|
}];
|
|
835
835
|
});
|
|
836
836
|
}
|
|
837
|
+
function parsePendingDevice(value) {
|
|
838
|
+
if (!value || typeof value !== "object")
|
|
839
|
+
return null;
|
|
840
|
+
const record = value;
|
|
841
|
+
const pollId = cleanString(record.pollId);
|
|
842
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
843
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
844
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
845
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
846
|
+
return null;
|
|
847
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
848
|
+
}
|
|
849
|
+
function parsePendingDevices(value) {
|
|
850
|
+
if (!Array.isArray(value))
|
|
851
|
+
return [];
|
|
852
|
+
return value.flatMap((entry) => {
|
|
853
|
+
const pending = parsePendingDevice(entry);
|
|
854
|
+
return pending ? [pending] : [];
|
|
855
|
+
});
|
|
856
|
+
}
|
|
837
857
|
function readStoredAuth(stateFile) {
|
|
838
858
|
if (!existsSync4(stateFile))
|
|
839
859
|
return {};
|
|
@@ -847,6 +867,7 @@ function readStoredAuth(stateFile) {
|
|
|
847
867
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
848
868
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
849
869
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
870
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
850
871
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
851
872
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
852
873
|
};
|
|
@@ -854,34 +875,36 @@ function readStoredAuth(stateFile) {
|
|
|
854
875
|
return {};
|
|
855
876
|
}
|
|
856
877
|
}
|
|
857
|
-
function parsePendingDevice(value) {
|
|
858
|
-
if (!value || typeof value !== "object")
|
|
859
|
-
return null;
|
|
860
|
-
const record = value;
|
|
861
|
-
const pollId = cleanString(record.pollId);
|
|
862
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
863
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
864
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
865
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
866
|
-
return null;
|
|
867
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
868
|
-
}
|
|
869
878
|
function newApiSessionToken() {
|
|
870
879
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
871
880
|
}
|
|
872
881
|
function writeStoredAuth(stateFile, payload) {
|
|
873
|
-
mkdirSync3(
|
|
882
|
+
mkdirSync3(dirname3(stateFile), { recursive: true });
|
|
874
883
|
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
875
884
|
`, { encoding: "utf8", mode: 384 });
|
|
876
885
|
try {
|
|
877
886
|
chmodSync(stateFile, 384);
|
|
878
887
|
} catch {}
|
|
879
888
|
}
|
|
889
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
890
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
891
|
+
}
|
|
880
892
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
881
893
|
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
882
894
|
}
|
|
883
|
-
function
|
|
884
|
-
const
|
|
895
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
896
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
897
|
+
mkdirSync3(dirname3(targetFile), { recursive: true });
|
|
898
|
+
if (existsSync4(stateFile)) {
|
|
899
|
+
copyFileSync(stateFile, targetFile);
|
|
900
|
+
try {
|
|
901
|
+
chmodSync(targetFile, 384);
|
|
902
|
+
} catch {}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
writeStoredAuth(targetFile, {});
|
|
906
|
+
}
|
|
907
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
885
908
|
return {
|
|
886
909
|
stateFile,
|
|
887
910
|
status(options) {
|
|
@@ -911,6 +934,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
911
934
|
scopes: input.scopes ?? [],
|
|
912
935
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
913
936
|
pendingDevice: null,
|
|
937
|
+
pendingDevices: [],
|
|
914
938
|
apiSessions: previous.apiSessions ?? [],
|
|
915
939
|
updatedAt: new Date().toISOString()
|
|
916
940
|
});
|
|
@@ -939,15 +963,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
939
963
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
940
964
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
941
965
|
},
|
|
942
|
-
copyToProjectRoot(
|
|
943
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
966
|
+
copyToProjectRoot(projectRoot) {
|
|
967
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
944
968
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
945
969
|
},
|
|
970
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
971
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
972
|
+
},
|
|
946
973
|
savePendingDevice(input) {
|
|
947
974
|
const previous = readStoredAuth(stateFile);
|
|
975
|
+
const pendingDevices = [
|
|
976
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
977
|
+
...previous.pendingDevices ?? [],
|
|
978
|
+
input
|
|
979
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
948
980
|
writeStoredAuth(stateFile, {
|
|
949
981
|
...previous,
|
|
950
|
-
pendingDevice:
|
|
982
|
+
pendingDevice: null,
|
|
983
|
+
pendingDevices,
|
|
951
984
|
updatedAt: new Date().toISOString()
|
|
952
985
|
});
|
|
953
986
|
},
|
|
@@ -960,23 +993,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
960
993
|
});
|
|
961
994
|
},
|
|
962
995
|
readPendingDevice(pollId) {
|
|
963
|
-
const
|
|
964
|
-
|
|
996
|
+
const previous = readStoredAuth(stateFile);
|
|
997
|
+
const pending = [
|
|
998
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
999
|
+
...previous.pendingDevices ?? []
|
|
1000
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
1001
|
+
if (!pending)
|
|
965
1002
|
return null;
|
|
966
1003
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
967
1004
|
return null;
|
|
968
1005
|
return pending;
|
|
969
1006
|
},
|
|
970
|
-
clearPendingDevice() {
|
|
1007
|
+
clearPendingDevice(pollId) {
|
|
971
1008
|
const previous = readStoredAuth(stateFile);
|
|
1009
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
972
1010
|
writeStoredAuth(stateFile, {
|
|
973
1011
|
...previous,
|
|
974
1012
|
pendingDevice: null,
|
|
1013
|
+
pendingDevices: remaining,
|
|
975
1014
|
updatedAt: new Date().toISOString()
|
|
976
1015
|
});
|
|
977
1016
|
}
|
|
978
1017
|
};
|
|
979
1018
|
}
|
|
1019
|
+
function createGitHubAuthStore(projectRoot) {
|
|
1020
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
1021
|
+
}
|
|
980
1022
|
|
|
981
1023
|
// packages/server/src/server-helpers/github-projects.ts
|
|
982
1024
|
function asRecord(value) {
|
|
@@ -1365,7 +1407,7 @@ import {
|
|
|
1365
1407
|
} from "@rig/runtime/control-plane/remote";
|
|
1366
1408
|
|
|
1367
1409
|
// packages/server/src/server-helpers/run-steering.ts
|
|
1368
|
-
import { dirname as
|
|
1410
|
+
import { dirname as dirname4, resolve as resolve8 } from "path";
|
|
1369
1411
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
|
|
1370
1412
|
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
1371
1413
|
var steeringSequence = 0;
|
|
@@ -1452,7 +1494,7 @@ function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
|
1452
1494
|
delivered: false
|
|
1453
1495
|
};
|
|
1454
1496
|
const path = runSteeringPath(projectRoot, runId);
|
|
1455
|
-
mkdirSync4(
|
|
1497
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1456
1498
|
appendJsonlRecord2(path, entry);
|
|
1457
1499
|
appendRunTimelineEntry(projectRoot, runId, {
|
|
1458
1500
|
id: entry.id,
|
|
@@ -1489,24 +1531,205 @@ import {
|
|
|
1489
1531
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
1490
1532
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1491
1533
|
|
|
1534
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1535
|
+
import { chmodSync as chmodSync3, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1536
|
+
import { dirname as dirname6, resolve as resolve10 } from "path";
|
|
1537
|
+
|
|
1538
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
1539
|
+
import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1540
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve9 } from "path";
|
|
1541
|
+
function cleanString3(value) {
|
|
1542
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1543
|
+
}
|
|
1544
|
+
function sanitizePathSegment(value) {
|
|
1545
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
1546
|
+
}
|
|
1547
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
1548
|
+
const userId = cleanString3(identity.userId);
|
|
1549
|
+
if (userId) {
|
|
1550
|
+
const safeId = sanitizePathSegment(userId);
|
|
1551
|
+
if (safeId)
|
|
1552
|
+
return `ghu-${safeId}`;
|
|
1553
|
+
}
|
|
1554
|
+
const login = cleanString3(identity.login);
|
|
1555
|
+
if (login) {
|
|
1556
|
+
const safeLogin = sanitizePathSegment(login);
|
|
1557
|
+
if (safeLogin)
|
|
1558
|
+
return `ghu-login-${safeLogin}`;
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
1561
|
+
}
|
|
1562
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
1563
|
+
const explicitRoot = cleanString3(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
1564
|
+
if (explicitRoot)
|
|
1565
|
+
return resolve9(explicitRoot);
|
|
1566
|
+
const stateDir2 = cleanString3(process.env.RIG_STATE_DIR);
|
|
1567
|
+
if (stateDir2)
|
|
1568
|
+
return resolve9(dirname5(resolve9(stateDir2)), "users");
|
|
1569
|
+
return resolve9(projectRoot, ".rig", "users");
|
|
1570
|
+
}
|
|
1571
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
1572
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
1573
|
+
const root = resolve9(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
1574
|
+
const stateDir2 = resolve9(root, ".rig", "state");
|
|
1575
|
+
return {
|
|
1576
|
+
key,
|
|
1577
|
+
userId: cleanString3(identity.userId),
|
|
1578
|
+
login: cleanString3(identity.login),
|
|
1579
|
+
root,
|
|
1580
|
+
stateDir: stateDir2,
|
|
1581
|
+
authStateFile: resolve9(stateDir2, "github-auth.json"),
|
|
1582
|
+
metadataFile: resolve9(stateDir2, "user-namespace.json"),
|
|
1583
|
+
checkoutBaseDir: resolve9(root, "remote-checkouts"),
|
|
1584
|
+
snapshotBaseDir: resolve9(root, "remote-snapshots")
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
1588
|
+
return {
|
|
1589
|
+
key: namespace.key,
|
|
1590
|
+
userId: namespace.userId,
|
|
1591
|
+
login: namespace.login,
|
|
1592
|
+
root: namespace.root,
|
|
1593
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1594
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
1598
|
+
const root = resolve9(namespaceRoot);
|
|
1599
|
+
const candidate = resolve9(candidatePath);
|
|
1600
|
+
const rel = relative(root, candidate);
|
|
1601
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1602
|
+
}
|
|
1603
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
1604
|
+
mkdirSync5(namespace.stateDir, { recursive: true });
|
|
1605
|
+
const previous = (() => {
|
|
1606
|
+
if (!existsSync6(namespace.metadataFile))
|
|
1607
|
+
return null;
|
|
1608
|
+
try {
|
|
1609
|
+
const parsed = JSON.parse(readFileSync4(namespace.metadataFile, "utf8"));
|
|
1610
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1611
|
+
} catch {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
})();
|
|
1615
|
+
const now = new Date().toISOString();
|
|
1616
|
+
writeFileSync5(namespace.metadataFile, `${JSON.stringify({
|
|
1617
|
+
key: namespace.key,
|
|
1618
|
+
userId: namespace.userId,
|
|
1619
|
+
login: namespace.login,
|
|
1620
|
+
root: namespace.root,
|
|
1621
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
1622
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
1623
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
1624
|
+
updatedAt: now
|
|
1625
|
+
}, null, 2)}
|
|
1626
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1627
|
+
try {
|
|
1628
|
+
chmodSync2(namespace.metadataFile, 384);
|
|
1629
|
+
} catch {}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
1633
|
+
function cleanString4(value) {
|
|
1634
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1635
|
+
}
|
|
1636
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
1637
|
+
return resolve10(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
1638
|
+
}
|
|
1639
|
+
function parseEntry(value) {
|
|
1640
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1641
|
+
return null;
|
|
1642
|
+
const record = value;
|
|
1643
|
+
const token = cleanString4(record.token);
|
|
1644
|
+
const namespaceKey = cleanString4(record.namespaceKey);
|
|
1645
|
+
const namespaceRoot = cleanString4(record.namespaceRoot);
|
|
1646
|
+
const authStateFile = cleanString4(record.authStateFile);
|
|
1647
|
+
const checkoutBaseDir = cleanString4(record.checkoutBaseDir);
|
|
1648
|
+
const snapshotBaseDir = cleanString4(record.snapshotBaseDir);
|
|
1649
|
+
const createdAt = cleanString4(record.createdAt);
|
|
1650
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
1651
|
+
return null;
|
|
1652
|
+
return {
|
|
1653
|
+
token,
|
|
1654
|
+
namespaceKey,
|
|
1655
|
+
namespaceRoot,
|
|
1656
|
+
authStateFile,
|
|
1657
|
+
checkoutBaseDir,
|
|
1658
|
+
snapshotBaseDir,
|
|
1659
|
+
createdAt,
|
|
1660
|
+
login: cleanString4(record.login),
|
|
1661
|
+
userId: cleanString4(record.userId),
|
|
1662
|
+
selectedRepo: cleanString4(record.selectedRepo)
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function readIndex(indexFile) {
|
|
1666
|
+
if (!existsSync7(indexFile))
|
|
1667
|
+
return [];
|
|
1668
|
+
try {
|
|
1669
|
+
const parsed = JSON.parse(readFileSync5(indexFile, "utf8"));
|
|
1670
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
1671
|
+
const parsedEntry = parseEntry(entry);
|
|
1672
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
1673
|
+
}) : [];
|
|
1674
|
+
} catch {
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function writeIndex(indexFile, sessions) {
|
|
1679
|
+
mkdirSync6(dirname6(indexFile), { recursive: true });
|
|
1680
|
+
writeFileSync6(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
1681
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1682
|
+
try {
|
|
1683
|
+
chmodSync3(indexFile, 384);
|
|
1684
|
+
} catch {}
|
|
1685
|
+
}
|
|
1686
|
+
function registerGitHubApiSession(input) {
|
|
1687
|
+
const cleanToken = cleanString4(input.token);
|
|
1688
|
+
if (!cleanToken)
|
|
1689
|
+
throw new Error("GitHub API session token is required");
|
|
1690
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
1691
|
+
const createdAt = new Date().toISOString();
|
|
1692
|
+
const entry = {
|
|
1693
|
+
token: cleanToken,
|
|
1694
|
+
login: input.namespace.login,
|
|
1695
|
+
userId: input.namespace.userId,
|
|
1696
|
+
namespaceKey: input.namespace.key,
|
|
1697
|
+
namespaceRoot: input.namespace.root,
|
|
1698
|
+
authStateFile: input.namespace.authStateFile,
|
|
1699
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
1700
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
1701
|
+
selectedRepo: cleanString4(input.selectedRepo),
|
|
1702
|
+
createdAt
|
|
1703
|
+
};
|
|
1704
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
1705
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
1706
|
+
return entry;
|
|
1707
|
+
}
|
|
1708
|
+
function readGitHubApiSession(input) {
|
|
1709
|
+
const cleanToken = cleanString4(input.token);
|
|
1710
|
+
if (!cleanToken)
|
|
1711
|
+
return null;
|
|
1712
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1492
1715
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1493
1716
|
import { createHash as createHash2 } from "crypto";
|
|
1494
1717
|
import { spawnSync } from "child_process";
|
|
1495
|
-
import { existsSync as
|
|
1496
|
-
import { dirname as
|
|
1718
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync7 } from "fs";
|
|
1719
|
+
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
1497
1720
|
function normalizeRepoSlug(value) {
|
|
1498
1721
|
const trimmed = value.trim();
|
|
1499
1722
|
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
1500
1723
|
}
|
|
1501
1724
|
function registryPath(projectRoot) {
|
|
1502
|
-
return
|
|
1725
|
+
return resolve11(projectRoot, ".rig", "state", "projects.json");
|
|
1503
1726
|
}
|
|
1504
1727
|
function readRegistry(projectRoot) {
|
|
1505
1728
|
const path = registryPath(projectRoot);
|
|
1506
|
-
if (!
|
|
1729
|
+
if (!existsSync8(path))
|
|
1507
1730
|
return {};
|
|
1508
1731
|
try {
|
|
1509
|
-
const payload = JSON.parse(
|
|
1732
|
+
const payload = JSON.parse(readFileSync6(path, "utf8"));
|
|
1510
1733
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1511
1734
|
return {};
|
|
1512
1735
|
const projects = payload.projects;
|
|
@@ -1517,14 +1740,14 @@ function readRegistry(projectRoot) {
|
|
|
1517
1740
|
}
|
|
1518
1741
|
function writeRegistry(projectRoot, projects) {
|
|
1519
1742
|
const path = registryPath(projectRoot);
|
|
1520
|
-
|
|
1521
|
-
|
|
1743
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
1744
|
+
writeFileSync7(path, `${JSON.stringify({ projects }, null, 2)}
|
|
1522
1745
|
`, "utf8");
|
|
1523
1746
|
}
|
|
1524
1747
|
function resolveConfigPath(projectRoot) {
|
|
1525
1748
|
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
1526
|
-
const path =
|
|
1527
|
-
if (
|
|
1749
|
+
const path = resolve11(projectRoot, name);
|
|
1750
|
+
if (existsSync8(path))
|
|
1528
1751
|
return path;
|
|
1529
1752
|
}
|
|
1530
1753
|
return null;
|
|
@@ -1533,7 +1756,7 @@ function hashFile(path) {
|
|
|
1533
1756
|
if (!path)
|
|
1534
1757
|
return null;
|
|
1535
1758
|
try {
|
|
1536
|
-
return createHash2("sha256").update(
|
|
1759
|
+
return createHash2("sha256").update(readFileSync6(path)).digest("hex");
|
|
1537
1760
|
} catch {
|
|
1538
1761
|
return null;
|
|
1539
1762
|
}
|
|
@@ -1549,11 +1772,11 @@ function readDefaultBranch(projectRoot) {
|
|
|
1549
1772
|
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
1550
1773
|
}
|
|
1551
1774
|
function buildRunSummary(projectRoot) {
|
|
1552
|
-
const runsDir =
|
|
1775
|
+
const runsDir = resolve11(projectRoot, ".rig", "runs");
|
|
1553
1776
|
try {
|
|
1554
1777
|
const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
1555
1778
|
try {
|
|
1556
|
-
const run = JSON.parse(
|
|
1779
|
+
const run = JSON.parse(readFileSync6(resolve11(runsDir, entry.name, "run.json"), "utf8"));
|
|
1557
1780
|
return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
|
|
1558
1781
|
} catch {
|
|
1559
1782
|
return [];
|
|
@@ -1605,10 +1828,14 @@ function upsertProjectRecord(projectRoot, input) {
|
|
|
1605
1828
|
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
1606
1829
|
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
1607
1830
|
}
|
|
1831
|
+
function projectRegistryContainsCheckout(projectRoot, checkoutPath) {
|
|
1832
|
+
const target = resolve11(checkoutPath);
|
|
1833
|
+
return Object.values(readRegistry(projectRoot)).some((project) => project.checkouts.some((checkout) => checkout.path ? resolve11(checkout.path) === target : false));
|
|
1834
|
+
}
|
|
1608
1835
|
|
|
1609
1836
|
// packages/server/src/server-helpers/remote-checkout.ts
|
|
1610
|
-
import { existsSync as
|
|
1611
|
-
import { dirname as
|
|
1837
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1838
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve12 } from "path";
|
|
1612
1839
|
function safeSlugSegments(repoSlug) {
|
|
1613
1840
|
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
1614
1841
|
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
@@ -1625,7 +1852,7 @@ function safeCheckoutKey(value) {
|
|
|
1625
1852
|
}
|
|
1626
1853
|
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
1627
1854
|
const key = safeCheckoutKey(checkoutKey);
|
|
1628
|
-
return
|
|
1855
|
+
return resolve12(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
1629
1856
|
}
|
|
1630
1857
|
function sanitizeSnapshotId(value, fallback) {
|
|
1631
1858
|
const raw = (value ?? fallback).trim();
|
|
@@ -1633,7 +1860,7 @@ function sanitizeSnapshotId(value, fallback) {
|
|
|
1633
1860
|
return safe || fallback;
|
|
1634
1861
|
}
|
|
1635
1862
|
function assertWithinRoot(root, relativePath) {
|
|
1636
|
-
if (!relativePath ||
|
|
1863
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath.includes("\x00")) {
|
|
1637
1864
|
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
1638
1865
|
}
|
|
1639
1866
|
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
@@ -1641,9 +1868,9 @@ function assertWithinRoot(root, relativePath) {
|
|
|
1641
1868
|
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1642
1869
|
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
1643
1870
|
}
|
|
1644
|
-
const target =
|
|
1645
|
-
const rel =
|
|
1646
|
-
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." ||
|
|
1871
|
+
const target = resolve12(root, ...segments);
|
|
1872
|
+
const rel = relative2(root, target);
|
|
1873
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute2(rel)) {
|
|
1647
1874
|
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
1648
1875
|
}
|
|
1649
1876
|
return target;
|
|
@@ -1661,17 +1888,17 @@ function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
|
1661
1888
|
function extractUploadedSnapshotArchive(input) {
|
|
1662
1889
|
const archive = decodeSnapshotArchive(input.archive);
|
|
1663
1890
|
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
1664
|
-
const checkoutPath =
|
|
1665
|
-
|
|
1891
|
+
const checkoutPath = resolve12(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
1892
|
+
mkdirSync8(checkoutPath, { recursive: true });
|
|
1666
1893
|
for (const file of archive.files) {
|
|
1667
1894
|
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
1668
1895
|
throw new Error("Invalid snapshot archive file entry");
|
|
1669
1896
|
}
|
|
1670
1897
|
const target = assertWithinRoot(checkoutPath, file.path);
|
|
1671
|
-
|
|
1672
|
-
|
|
1898
|
+
mkdirSync8(dirname8(target), { recursive: true });
|
|
1899
|
+
writeFileSync8(target, Buffer.from(file.contentBase64, "base64"));
|
|
1673
1900
|
}
|
|
1674
|
-
|
|
1901
|
+
writeFileSync8(resolve12(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
1675
1902
|
repoSlug: input.repoSlug,
|
|
1676
1903
|
snapshotId,
|
|
1677
1904
|
fileCount: archive.files.length,
|
|
@@ -1706,7 +1933,7 @@ function gitCredentialConfig(token) {
|
|
|
1706
1933
|
};
|
|
1707
1934
|
}
|
|
1708
1935
|
async function prepareRemoteCheckout(input) {
|
|
1709
|
-
const exists = input.exists ??
|
|
1936
|
+
const exists = input.exists ?? existsSync9;
|
|
1710
1937
|
const strategy = input.strategy;
|
|
1711
1938
|
if (strategy.kind === "uploaded-snapshot") {
|
|
1712
1939
|
return extractUploadedSnapshotArchive({
|
|
@@ -1718,7 +1945,7 @@ async function prepareRemoteCheckout(input) {
|
|
|
1718
1945
|
});
|
|
1719
1946
|
}
|
|
1720
1947
|
if (strategy.kind === "existing-path") {
|
|
1721
|
-
const checkoutPath2 =
|
|
1948
|
+
const checkoutPath2 = resolve12(strategy.path);
|
|
1722
1949
|
if (!exists(checkoutPath2)) {
|
|
1723
1950
|
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
1724
1951
|
}
|
|
@@ -1754,9 +1981,9 @@ function buildServerControlStatus() {
|
|
|
1754
1981
|
};
|
|
1755
1982
|
}
|
|
1756
1983
|
function buildProjectConfigStatus(root) {
|
|
1757
|
-
const hasConfigTs =
|
|
1758
|
-
const hasConfigJson =
|
|
1759
|
-
const hasLegacyTaskConfig =
|
|
1984
|
+
const hasConfigTs = existsSync10(resolve13(root, "rig.config.ts"));
|
|
1985
|
+
const hasConfigJson = existsSync10(resolve13(root, "rig.config.json"));
|
|
1986
|
+
const hasLegacyTaskConfig = existsSync10(resolve13(root, ".rig", "task-config.json"));
|
|
1760
1987
|
let kind = "missing";
|
|
1761
1988
|
if (hasConfigTs)
|
|
1762
1989
|
kind = "rig-config-ts";
|
|
@@ -1792,24 +2019,24 @@ function repoParts(repoSlug) {
|
|
|
1792
2019
|
return { owner, repo, slug: `${owner}/${repo}` };
|
|
1793
2020
|
}
|
|
1794
2021
|
function repairDir(checkoutPath) {
|
|
1795
|
-
const dir =
|
|
1796
|
-
|
|
2022
|
+
const dir = resolve13(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
2023
|
+
mkdirSync9(dir, { recursive: true });
|
|
1797
2024
|
return dir;
|
|
1798
2025
|
}
|
|
1799
2026
|
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
1800
|
-
const source =
|
|
1801
|
-
const backupPath =
|
|
1802
|
-
|
|
1803
|
-
|
|
2027
|
+
const source = resolve13(checkoutPath, relativePath);
|
|
2028
|
+
const backupPath = resolve13(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
2029
|
+
mkdirSync9(dirname9(backupPath), { recursive: true });
|
|
2030
|
+
copyFileSync2(source, backupPath);
|
|
1804
2031
|
return backupPath;
|
|
1805
2032
|
}
|
|
1806
2033
|
function parsePackageJsonLosslessly(checkoutPath) {
|
|
1807
|
-
const packagePath =
|
|
1808
|
-
if (!
|
|
2034
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2035
|
+
if (!existsSync10(packagePath)) {
|
|
1809
2036
|
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
1810
2037
|
}
|
|
1811
2038
|
try {
|
|
1812
|
-
const parsed = JSON.parse(
|
|
2039
|
+
const parsed = JSON.parse(readFileSync7(packagePath, "utf8"));
|
|
1813
2040
|
return {
|
|
1814
2041
|
existed: true,
|
|
1815
2042
|
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
@@ -1823,9 +2050,9 @@ function parsePackageJsonLosslessly(checkoutPath) {
|
|
|
1823
2050
|
}
|
|
1824
2051
|
}
|
|
1825
2052
|
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
1826
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1827
|
-
const packagePath =
|
|
1828
|
-
if (!hasConfig && !
|
|
2053
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2054
|
+
const packagePath = resolve13(checkoutPath, "package.json");
|
|
2055
|
+
if (!hasConfig && !existsSync10(packagePath)) {
|
|
1829
2056
|
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
1830
2057
|
}
|
|
1831
2058
|
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
@@ -1844,7 +2071,7 @@ function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
|
1844
2071
|
}
|
|
1845
2072
|
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
1846
2073
|
if (changed) {
|
|
1847
|
-
|
|
2074
|
+
writeFileSync9(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
1848
2075
|
`, "utf8");
|
|
1849
2076
|
}
|
|
1850
2077
|
return {
|
|
@@ -1870,11 +2097,11 @@ function configLooksStructurallyUsable(source) {
|
|
|
1870
2097
|
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
1871
2098
|
}
|
|
1872
2099
|
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
1873
|
-
const configPath =
|
|
1874
|
-
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2100
|
+
const configPath = resolve13(checkoutPath, "rig.config.ts");
|
|
2101
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1875
2102
|
if (existingConfigName) {
|
|
1876
|
-
const existingPath =
|
|
1877
|
-
const source =
|
|
2103
|
+
const existingPath = resolve13(checkoutPath, existingConfigName);
|
|
2104
|
+
const source = readFileSync7(existingPath, "utf8");
|
|
1878
2105
|
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
1879
2106
|
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1880
2107
|
}
|
|
@@ -1888,7 +2115,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1888
2115
|
}
|
|
1889
2116
|
}
|
|
1890
2117
|
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
1891
|
-
|
|
2118
|
+
writeFileSync9(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
1892
2119
|
return {
|
|
1893
2120
|
path: configPath,
|
|
1894
2121
|
changed: true,
|
|
@@ -1897,7 +2124,7 @@ function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing
|
|
|
1897
2124
|
};
|
|
1898
2125
|
}
|
|
1899
2126
|
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
1900
|
-
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) =>
|
|
2127
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
1901
2128
|
if (!configFile)
|
|
1902
2129
|
return { ok: false, error: "missing rig config" };
|
|
1903
2130
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1919,7 +2146,7 @@ function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
|
1919
2146
|
return { ok: true, configFile };
|
|
1920
2147
|
}
|
|
1921
2148
|
function installRemoteCheckoutPackages(checkoutPath) {
|
|
1922
|
-
if (!
|
|
2149
|
+
if (!existsSync10(resolve13(checkoutPath, "package.json"))) {
|
|
1923
2150
|
return { skipped: true, reason: "package.json missing" };
|
|
1924
2151
|
}
|
|
1925
2152
|
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
@@ -1932,8 +2159,8 @@ function installRemoteCheckoutPackages(checkoutPath) {
|
|
|
1932
2159
|
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
1933
2160
|
}
|
|
1934
2161
|
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
1935
|
-
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
1936
|
-
const hasPackage =
|
|
2162
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync10(resolve13(checkoutPath, name)));
|
|
2163
|
+
const hasPackage = existsSync10(resolve13(checkoutPath, "package.json"));
|
|
1937
2164
|
if (!hasConfig && !hasPackage) {
|
|
1938
2165
|
return {
|
|
1939
2166
|
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
@@ -2031,26 +2258,26 @@ function buildRemoteRunLogEntry(body, identifiers) {
|
|
|
2031
2258
|
}
|
|
2032
2259
|
function readGitHeadCommit(projectRoot) {
|
|
2033
2260
|
try {
|
|
2034
|
-
let gitDir =
|
|
2261
|
+
let gitDir = resolve13(projectRoot, ".git");
|
|
2035
2262
|
try {
|
|
2036
|
-
const dotGit =
|
|
2263
|
+
const dotGit = readFileSync7(gitDir, "utf8").trim();
|
|
2037
2264
|
const gitDirPrefix = "gitdir:";
|
|
2038
2265
|
if (dotGit.startsWith(gitDirPrefix)) {
|
|
2039
|
-
gitDir =
|
|
2266
|
+
gitDir = resolve13(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
2040
2267
|
}
|
|
2041
2268
|
} catch {}
|
|
2042
|
-
const head =
|
|
2269
|
+
const head = readFileSync7(resolve13(gitDir, "HEAD"), "utf8").trim();
|
|
2043
2270
|
const refPrefix = "ref:";
|
|
2044
2271
|
if (!head.startsWith(refPrefix)) {
|
|
2045
2272
|
return normalizeCommit(head);
|
|
2046
2273
|
}
|
|
2047
2274
|
const ref = head.slice(refPrefix.length).trim();
|
|
2048
|
-
const refPath =
|
|
2049
|
-
if (
|
|
2050
|
-
return normalizeCommit(
|
|
2275
|
+
const refPath = resolve13(gitDir, ref);
|
|
2276
|
+
if (existsSync10(refPath)) {
|
|
2277
|
+
return normalizeCommit(readFileSync7(refPath, "utf8").trim());
|
|
2051
2278
|
}
|
|
2052
|
-
const commonDir = normalizeString(
|
|
2053
|
-
return commonDir ? normalizeCommit(
|
|
2279
|
+
const commonDir = normalizeString(readFileSync7(resolve13(gitDir, "commondir"), "utf8"));
|
|
2280
|
+
return commonDir ? normalizeCommit(readFileSync7(resolve13(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
2054
2281
|
} catch {
|
|
2055
2282
|
return null;
|
|
2056
2283
|
}
|
|
@@ -2088,9 +2315,9 @@ function configuredRepoFromTaskSource(taskSource) {
|
|
|
2088
2315
|
const repo = normalizeString(taskSource?.repo);
|
|
2089
2316
|
return owner && repo ? `${owner}/${repo}` : null;
|
|
2090
2317
|
}
|
|
2091
|
-
async function buildTaskSourceStatus(state, config) {
|
|
2318
|
+
async function buildTaskSourceStatus(state, config, requestAuth) {
|
|
2092
2319
|
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
2093
|
-
const selectedRepo =
|
|
2320
|
+
const selectedRepo = requestScopedAuthStore(state.projectRoot, requestAuth).status({
|
|
2094
2321
|
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
2095
2322
|
}).selectedRepo;
|
|
2096
2323
|
try {
|
|
@@ -2188,31 +2415,83 @@ function normalizePrMode(value) {
|
|
|
2188
2415
|
const mode = normalizeString(value);
|
|
2189
2416
|
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
2190
2417
|
}
|
|
2418
|
+
function requestAuthResult(input) {
|
|
2419
|
+
return {
|
|
2420
|
+
authorized: input.authorized,
|
|
2421
|
+
actor: input.actor ?? null,
|
|
2422
|
+
reason: input.reason,
|
|
2423
|
+
login: input.login ?? null,
|
|
2424
|
+
userId: input.userId ?? null,
|
|
2425
|
+
userNamespace: input.userNamespace ?? null,
|
|
2426
|
+
authStateFile: input.authStateFile ?? null
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
function namespaceFromSessionIndex(entry) {
|
|
2430
|
+
const stateDir2 = dirname9(entry.authStateFile);
|
|
2431
|
+
return {
|
|
2432
|
+
key: entry.namespaceKey,
|
|
2433
|
+
userId: entry.userId,
|
|
2434
|
+
login: entry.login,
|
|
2435
|
+
root: entry.namespaceRoot,
|
|
2436
|
+
stateDir: stateDir2,
|
|
2437
|
+
authStateFile: entry.authStateFile,
|
|
2438
|
+
metadataFile: resolve13(stateDir2, "user-namespace.json"),
|
|
2439
|
+
checkoutBaseDir: entry.checkoutBaseDir,
|
|
2440
|
+
snapshotBaseDir: entry.snapshotBaseDir
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2191
2443
|
function authorizeRigHttpRequest(input) {
|
|
2192
2444
|
if (input.legacyAuthorized) {
|
|
2193
|
-
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
2445
|
+
return requestAuthResult({ authorized: true, actor: "rig-local-server", reason: "server-token" });
|
|
2194
2446
|
}
|
|
2195
2447
|
const bearer = bearerTokenFromRequest(input.req);
|
|
2196
2448
|
const store = createGitHubAuthStore(input.projectRoot);
|
|
2197
2449
|
const storedToken = store.readToken();
|
|
2198
2450
|
const session = bearer ? store.readApiSession(bearer) : null;
|
|
2199
2451
|
if (session) {
|
|
2200
|
-
return {
|
|
2452
|
+
return requestAuthResult({
|
|
2453
|
+
authorized: true,
|
|
2454
|
+
actor: session.login ?? "github-operator",
|
|
2455
|
+
reason: "github-session",
|
|
2456
|
+
login: session.login,
|
|
2457
|
+
userId: session.userId,
|
|
2458
|
+
authStateFile: store.stateFile
|
|
2459
|
+
});
|
|
2201
2460
|
}
|
|
2202
2461
|
if (bearer && storedToken && bearer === storedToken) {
|
|
2203
2462
|
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2204
|
-
return {
|
|
2463
|
+
return requestAuthResult({
|
|
2464
|
+
authorized: true,
|
|
2465
|
+
actor: status.login ?? "github-operator",
|
|
2466
|
+
reason: "github-token",
|
|
2467
|
+
login: status.login,
|
|
2468
|
+
userId: status.userId,
|
|
2469
|
+
authStateFile: store.stateFile
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
const indexedSession = readGitHubApiSession({ projectRoot: input.projectRoot, token: bearer });
|
|
2473
|
+
if (indexedSession) {
|
|
2474
|
+
const userNamespace = namespaceFromSessionIndex(indexedSession);
|
|
2475
|
+
return requestAuthResult({
|
|
2476
|
+
authorized: true,
|
|
2477
|
+
actor: indexedSession.login ?? "github-operator",
|
|
2478
|
+
reason: "github-user-session",
|
|
2479
|
+
login: indexedSession.login,
|
|
2480
|
+
userId: indexedSession.userId,
|
|
2481
|
+
userNamespace,
|
|
2482
|
+
authStateFile: indexedSession.authStateFile
|
|
2483
|
+
});
|
|
2205
2484
|
}
|
|
2206
2485
|
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
2207
|
-
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
2486
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "public-bootstrap" });
|
|
2208
2487
|
}
|
|
2209
2488
|
if (!input.serverAuthToken && !storedToken) {
|
|
2210
2489
|
if (isLoopbackRequest(input.req)) {
|
|
2211
|
-
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
2490
|
+
return requestAuthResult({ authorized: true, actor: null, reason: "loopback-dev-no-auth" });
|
|
2212
2491
|
}
|
|
2213
|
-
return { authorized: false, actor: null, reason: "auth-required" };
|
|
2492
|
+
return requestAuthResult({ authorized: false, actor: null, reason: "auth-required" });
|
|
2214
2493
|
}
|
|
2215
|
-
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
2494
|
+
return requestAuthResult({ authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" });
|
|
2216
2495
|
}
|
|
2217
2496
|
async function fetchGitHubUserInfo(token) {
|
|
2218
2497
|
const response = await fetch("https://api.github.com/user", {
|
|
@@ -2232,6 +2511,67 @@ async function fetchGitHubUserInfo(token) {
|
|
|
2232
2511
|
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
2233
2512
|
};
|
|
2234
2513
|
}
|
|
2514
|
+
function shouldWriteRootAuthCompat(projectRoot) {
|
|
2515
|
+
if (process.env.RIG_REMOTE_USER_NAMESPACE_ROOT?.trim())
|
|
2516
|
+
return false;
|
|
2517
|
+
const stateDir2 = normalizeString(process.env.RIG_STATE_DIR);
|
|
2518
|
+
if (!stateDir2)
|
|
2519
|
+
return true;
|
|
2520
|
+
return resolve13(stateDir2) === resolve13(projectRoot, ".rig", "state");
|
|
2521
|
+
}
|
|
2522
|
+
function requestScopedRegistryRoot(stateProjectRoot, requestAuth) {
|
|
2523
|
+
return requestAuth.userNamespace?.root ?? stateProjectRoot;
|
|
2524
|
+
}
|
|
2525
|
+
function requestScopedAuthStore(stateProjectRoot, requestAuth) {
|
|
2526
|
+
return requestAuth.authStateFile ? createGitHubAuthStoreFromStateFile(requestAuth.authStateFile) : createGitHubAuthStore(stateProjectRoot);
|
|
2527
|
+
}
|
|
2528
|
+
function userNamespaceResponse(namespace) {
|
|
2529
|
+
return namespace ? serializeRemoteUserNamespace(namespace) : undefined;
|
|
2530
|
+
}
|
|
2531
|
+
function resolveNamespacedBaseDir(input) {
|
|
2532
|
+
if (input.explicitBaseDir)
|
|
2533
|
+
return input.explicitBaseDir;
|
|
2534
|
+
const envBase = normalizeString(process.env[input.envName]);
|
|
2535
|
+
if (input.userNamespace) {
|
|
2536
|
+
return envBase ? resolve13(envBase, input.userNamespace.key) : input.userNamespace[input.legacySubdir === "remote-checkouts" ? "checkoutBaseDir" : "snapshotBaseDir"];
|
|
2537
|
+
}
|
|
2538
|
+
return envBase ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve13(normalizeString(process.env.RIG_STATE_DIR), input.legacySubdir) : resolve13(input.legacyProjectRoot, ".rig", input.legacySubdir));
|
|
2539
|
+
}
|
|
2540
|
+
function explicitCheckoutKey(body, checkoutInput, requestAuth) {
|
|
2541
|
+
return normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? (requestAuth.userNamespace ? undefined : "default");
|
|
2542
|
+
}
|
|
2543
|
+
function saveGitHubTokenForRemoteUser(input) {
|
|
2544
|
+
const namespace = resolveRemoteUserNamespace(input.projectRoot, { userId: input.user.userId, login: input.user.login });
|
|
2545
|
+
writeRemoteUserNamespaceMetadata(namespace);
|
|
2546
|
+
const store = createGitHubAuthStoreFromStateFile(namespace.authStateFile);
|
|
2547
|
+
store.saveToken({
|
|
2548
|
+
token: input.token,
|
|
2549
|
+
tokenSource: input.tokenSource,
|
|
2550
|
+
login: input.user.login,
|
|
2551
|
+
userId: input.user.userId,
|
|
2552
|
+
scopes: input.user.scopes,
|
|
2553
|
+
selectedRepo: input.selectedRepo
|
|
2554
|
+
});
|
|
2555
|
+
const apiSession = store.createApiSession();
|
|
2556
|
+
registerGitHubApiSession({ projectRoot: input.projectRoot, token: apiSession.token, namespace, selectedRepo: input.selectedRepo });
|
|
2557
|
+
const requestedRoot = normalizeString(input.requestedProjectRoot);
|
|
2558
|
+
if (requestedRoot && isAbsolute3(requestedRoot) && existsSync10(resolve13(requestedRoot))) {
|
|
2559
|
+
copyGitHubAuthStateToLocalProjectRoot(namespace.authStateFile, resolve13(requestedRoot));
|
|
2560
|
+
}
|
|
2561
|
+
if (shouldWriteRootAuthCompat(input.projectRoot)) {
|
|
2562
|
+
const rootStore = createGitHubAuthStore(input.projectRoot);
|
|
2563
|
+
rootStore.saveToken({
|
|
2564
|
+
token: input.token,
|
|
2565
|
+
tokenSource: input.tokenSource,
|
|
2566
|
+
login: input.user.login,
|
|
2567
|
+
userId: input.user.userId,
|
|
2568
|
+
scopes: input.user.scopes,
|
|
2569
|
+
selectedRepo: input.selectedRepo
|
|
2570
|
+
});
|
|
2571
|
+
rootStore.createApiSession();
|
|
2572
|
+
}
|
|
2573
|
+
return { store, namespace, apiSessionToken: apiSession.token };
|
|
2574
|
+
}
|
|
2235
2575
|
async function postGitHubForm(endpoint, body) {
|
|
2236
2576
|
const response = await fetch(endpoint, {
|
|
2237
2577
|
method: "POST",
|
|
@@ -2249,11 +2589,11 @@ function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
|
2249
2589
|
const requestedRoot = normalizeString(rawRoot);
|
|
2250
2590
|
if (!requestedRoot)
|
|
2251
2591
|
return currentRoot;
|
|
2252
|
-
if (!
|
|
2592
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
2253
2593
|
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
2254
2594
|
}
|
|
2255
|
-
const normalizedRoot =
|
|
2256
|
-
if (!
|
|
2595
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
2596
|
+
if (!existsSync10(normalizedRoot)) {
|
|
2257
2597
|
throw new Error("projectRoot does not exist on the Rig server host");
|
|
2258
2598
|
}
|
|
2259
2599
|
return normalizedRoot;
|
|
@@ -2914,7 +3254,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2914
3254
|
}
|
|
2915
3255
|
if (url.pathname === "/api/server/status") {
|
|
2916
3256
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2917
|
-
const taskSource = await buildTaskSourceStatus(state, config);
|
|
3257
|
+
const taskSource = await buildTaskSourceStatus(state, config, requestAuth);
|
|
2918
3258
|
return deps.jsonResponse({
|
|
2919
3259
|
ok: true,
|
|
2920
3260
|
projectRoot: state.projectRoot,
|
|
@@ -2938,8 +3278,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2938
3278
|
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
2939
3279
|
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
2940
3280
|
} : undefined;
|
|
2941
|
-
const
|
|
2942
|
-
|
|
3281
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3282
|
+
const record = upsertProjectRecord(registryRoot, { repoSlug, checkout });
|
|
3283
|
+
return deps.jsonResponse({ ok: true, project: record, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2943
3284
|
}
|
|
2944
3285
|
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
2945
3286
|
if (snapshotUploadMatch && req.method === "POST") {
|
|
@@ -2952,8 +3293,15 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2952
3293
|
if (!archiveContentBase64) {
|
|
2953
3294
|
return deps.badRequest("archiveContentBase64 is required");
|
|
2954
3295
|
}
|
|
2955
|
-
const
|
|
2956
|
-
const
|
|
3296
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3297
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3298
|
+
explicitBaseDir: normalizeString(body.baseDir),
|
|
3299
|
+
envName: "RIG_REMOTE_SNAPSHOT_BASE_DIR",
|
|
3300
|
+
userNamespace: requestAuth.userNamespace,
|
|
3301
|
+
legacyProjectRoot: state.projectRoot,
|
|
3302
|
+
legacySubdir: "remote-snapshots"
|
|
3303
|
+
});
|
|
3304
|
+
const checkoutKey = explicitCheckoutKey(body, body, requestAuth);
|
|
2957
3305
|
try {
|
|
2958
3306
|
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
2959
3307
|
const checkout = extractUploadedSnapshotArchive({
|
|
@@ -2966,14 +3314,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2966
3314
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2967
3315
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2968
3316
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2969
|
-
const project = linkProjectCheckout(
|
|
3317
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
2970
3318
|
kind: "uploaded-snapshot",
|
|
2971
3319
|
path: checkout.path,
|
|
2972
3320
|
ref: checkout.snapshotId
|
|
2973
3321
|
});
|
|
2974
3322
|
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
2975
3323
|
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
2976
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3324
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
2977
3325
|
} catch (error) {
|
|
2978
3326
|
return deps.jsonResponse({
|
|
2979
3327
|
ok: false,
|
|
@@ -2993,10 +3341,17 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2993
3341
|
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
2994
3342
|
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
2995
3343
|
}
|
|
2996
|
-
const
|
|
2997
|
-
const
|
|
3344
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3345
|
+
const baseDir = resolveNamespacedBaseDir({
|
|
3346
|
+
explicitBaseDir: normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir),
|
|
3347
|
+
envName: "RIG_REMOTE_CHECKOUT_BASE_DIR",
|
|
3348
|
+
userNamespace: requestAuth.userNamespace,
|
|
3349
|
+
legacyProjectRoot: state.projectRoot,
|
|
3350
|
+
legacySubdir: "remote-checkouts"
|
|
3351
|
+
});
|
|
3352
|
+
const checkoutKey = explicitCheckoutKey(body, checkoutInput, requestAuth);
|
|
2998
3353
|
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
2999
|
-
const credentialToken =
|
|
3354
|
+
const credentialToken = requestScopedAuthStore(state.projectRoot, requestAuth).readToken();
|
|
3000
3355
|
try {
|
|
3001
3356
|
const checkout = await prepareRemoteCheckout({
|
|
3002
3357
|
command: runRemoteCheckoutCommand,
|
|
@@ -3005,14 +3360,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3005
3360
|
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
3006
3361
|
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
3007
3362
|
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
3008
|
-
const project = linkProjectCheckout(
|
|
3363
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
3009
3364
|
kind: checkout.kind,
|
|
3010
3365
|
path: checkout.path,
|
|
3011
3366
|
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
3012
3367
|
});
|
|
3013
3368
|
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
3014
3369
|
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
3015
|
-
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
3370
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
3016
3371
|
} catch (error) {
|
|
3017
3372
|
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3018
3373
|
}
|
|
@@ -3029,16 +3384,18 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3029
3384
|
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
3030
3385
|
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
3031
3386
|
}
|
|
3032
|
-
const
|
|
3387
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3388
|
+
const project = linkProjectCheckout(registryRoot, repoSlug, {
|
|
3033
3389
|
kind,
|
|
3034
3390
|
path: normalizeString(body.path) ?? state.projectRoot,
|
|
3035
3391
|
ref: normalizeString(body.ref) ?? undefined
|
|
3036
3392
|
});
|
|
3037
|
-
return deps.jsonResponse({ ok: true, project });
|
|
3393
|
+
return deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
3038
3394
|
}
|
|
3039
3395
|
if (req.method === "GET") {
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3396
|
+
const registryRoot = requestScopedRegistryRoot(state.projectRoot, requestAuth);
|
|
3397
|
+
const project = getProjectRecord(registryRoot, repoSlug);
|
|
3398
|
+
return project ? deps.jsonResponse({ ok: true, project, userNamespace: userNamespaceResponse(requestAuth.userNamespace) }) : deps.notFound();
|
|
3042
3399
|
}
|
|
3043
3400
|
}
|
|
3044
3401
|
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
@@ -3047,13 +3404,26 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3047
3404
|
if (!requestedRoot) {
|
|
3048
3405
|
return deps.badRequest("projectRoot is required");
|
|
3049
3406
|
}
|
|
3050
|
-
if (!
|
|
3407
|
+
if (!isAbsolute3(requestedRoot)) {
|
|
3051
3408
|
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
3052
3409
|
}
|
|
3053
|
-
const normalizedRoot =
|
|
3054
|
-
const exists =
|
|
3055
|
-
if (exists) {
|
|
3056
|
-
|
|
3410
|
+
const normalizedRoot = resolve13(requestedRoot);
|
|
3411
|
+
const exists = existsSync10(normalizedRoot);
|
|
3412
|
+
if (exists && requestAuth.userNamespace) {
|
|
3413
|
+
const allowedByNamespace = isPathInsideNamespace(requestAuth.userNamespace.root, normalizedRoot);
|
|
3414
|
+
const allowedByRegistry = projectRegistryContainsCheckout(requestAuth.userNamespace.root, normalizedRoot);
|
|
3415
|
+
if (!allowedByNamespace && !allowedByRegistry) {
|
|
3416
|
+
return deps.jsonResponse({
|
|
3417
|
+
ok: false,
|
|
3418
|
+
error: "Requested project root is outside the authenticated GitHub user namespace.",
|
|
3419
|
+
projectRoot: state.projectRoot,
|
|
3420
|
+
requestedProjectRoot: normalizedRoot,
|
|
3421
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace)
|
|
3422
|
+
}, 403);
|
|
3423
|
+
}
|
|
3424
|
+
copyGitHubAuthStateToLocalProjectRoot(requestAuth.userNamespace.authStateFile, normalizedRoot);
|
|
3425
|
+
} else if (exists) {
|
|
3426
|
+
createGitHubAuthStore(state.projectRoot).copyToLocalProjectRoot(normalizedRoot);
|
|
3057
3427
|
}
|
|
3058
3428
|
const control = buildServerControlStatus();
|
|
3059
3429
|
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
@@ -3068,7 +3438,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3068
3438
|
message: "Requested project root does not exist on the Rig server host."
|
|
3069
3439
|
}, 404);
|
|
3070
3440
|
}
|
|
3071
|
-
if (!
|
|
3441
|
+
if (!existsSync10(resolve13(normalizedRoot, "rig.config.ts")) && !existsSync10(resolve13(normalizedRoot, "rig.config.json"))) {
|
|
3072
3442
|
return deps.jsonResponse({
|
|
3073
3443
|
ok: false,
|
|
3074
3444
|
projectRoot: state.projectRoot,
|
|
@@ -3103,6 +3473,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3103
3473
|
exists,
|
|
3104
3474
|
control,
|
|
3105
3475
|
requiresRestart: false,
|
|
3476
|
+
userNamespace: userNamespaceResponse(requestAuth.userNamespace),
|
|
3106
3477
|
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
3107
3478
|
}, 202);
|
|
3108
3479
|
}
|
|
@@ -3117,11 +3488,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3117
3488
|
}, 409);
|
|
3118
3489
|
}
|
|
3119
3490
|
if (url.pathname === "/api/github/auth/status") {
|
|
3120
|
-
const store =
|
|
3121
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
3491
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3492
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), userNamespace: userNamespaceResponse(requestAuth.userNamespace) });
|
|
3122
3493
|
}
|
|
3123
3494
|
if (url.pathname === "/api/github/repo/permissions") {
|
|
3124
|
-
const store =
|
|
3495
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3125
3496
|
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3126
3497
|
if (!auth.signedIn) {
|
|
3127
3498
|
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
@@ -3149,24 +3520,20 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3149
3520
|
}
|
|
3150
3521
|
try {
|
|
3151
3522
|
const user = await fetchGitHubUserInfo(token);
|
|
3152
|
-
const
|
|
3153
|
-
state.projectRoot,
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
}
|
|
3167
|
-
const store = stores[stores.length - 1] ?? createGitHubAuthStore(state.projectRoot);
|
|
3168
|
-
const apiSession = store.createApiSession();
|
|
3169
|
-
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }), apiSessionToken: apiSession.token });
|
|
3523
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3524
|
+
projectRoot: state.projectRoot,
|
|
3525
|
+
token,
|
|
3526
|
+
tokenSource: "manual-token",
|
|
3527
|
+
user,
|
|
3528
|
+
selectedRepo,
|
|
3529
|
+
requestedProjectRoot
|
|
3530
|
+
});
|
|
3531
|
+
return deps.jsonResponse({
|
|
3532
|
+
ok: true,
|
|
3533
|
+
...saved.store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }),
|
|
3534
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3535
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3536
|
+
});
|
|
3170
3537
|
} catch (error) {
|
|
3171
3538
|
const message = error instanceof Error ? error.message : String(error);
|
|
3172
3539
|
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
@@ -3233,9 +3600,21 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3233
3600
|
}
|
|
3234
3601
|
const token = result.payload.access_token;
|
|
3235
3602
|
const user = await fetchGitHubUserInfo(token);
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3603
|
+
const saved = saveGitHubTokenForRemoteUser({
|
|
3604
|
+
projectRoot: state.projectRoot,
|
|
3605
|
+
token,
|
|
3606
|
+
tokenSource: "oauth-device",
|
|
3607
|
+
user,
|
|
3608
|
+
selectedRepo: null
|
|
3609
|
+
});
|
|
3610
|
+
store.clearPendingDevice(pollId);
|
|
3611
|
+
return deps.jsonResponse({
|
|
3612
|
+
ok: true,
|
|
3613
|
+
status: "signed-in",
|
|
3614
|
+
...saved.store.status({ oauthConfigured: true }),
|
|
3615
|
+
apiSessionToken: saved.apiSessionToken,
|
|
3616
|
+
userNamespace: userNamespaceResponse(saved.namespace)
|
|
3617
|
+
});
|
|
3239
3618
|
}
|
|
3240
3619
|
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
3241
3620
|
const body = await deps.readJsonBody(req);
|
|
@@ -3244,7 +3623,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3244
3623
|
if (!owner || !repo) {
|
|
3245
3624
|
return deps.badRequest("owner and repo are required");
|
|
3246
3625
|
}
|
|
3247
|
-
const store =
|
|
3626
|
+
const store = requestScopedAuthStore(state.projectRoot, requestAuth);
|
|
3248
3627
|
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3249
3628
|
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
3250
3629
|
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
@@ -3265,7 +3644,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3265
3644
|
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
3266
3645
|
}
|
|
3267
3646
|
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
3268
|
-
const configPath =
|
|
3647
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3269
3648
|
const source = buildGitHubProjectConfigSource({
|
|
3270
3649
|
projectName: rawProjectName,
|
|
3271
3650
|
owner,
|
|
@@ -3277,8 +3656,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3277
3656
|
ok: true,
|
|
3278
3657
|
projectRoot: targetRoot,
|
|
3279
3658
|
configPath,
|
|
3280
|
-
exists:
|
|
3281
|
-
requiresOverwrite:
|
|
3659
|
+
exists: existsSync10(configPath),
|
|
3660
|
+
requiresOverwrite: existsSync10(configPath),
|
|
3282
3661
|
source,
|
|
3283
3662
|
owner,
|
|
3284
3663
|
repo,
|
|
@@ -3314,8 +3693,8 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3314
3693
|
assignee,
|
|
3315
3694
|
githubUserId: authStatus.userId ?? authStatus.login
|
|
3316
3695
|
});
|
|
3317
|
-
const configPath =
|
|
3318
|
-
if (
|
|
3696
|
+
const configPath = resolve13(targetRoot, "rig.config.ts");
|
|
3697
|
+
if (existsSync10(configPath) && !overwrite) {
|
|
3319
3698
|
return deps.jsonResponse({
|
|
3320
3699
|
ok: false,
|
|
3321
3700
|
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
@@ -3331,11 +3710,11 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3331
3710
|
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
3332
3711
|
}
|
|
3333
3712
|
let backupPath = null;
|
|
3334
|
-
if (
|
|
3713
|
+
if (existsSync10(configPath)) {
|
|
3335
3714
|
backupPath = backupConfigPath(configPath);
|
|
3336
|
-
|
|
3715
|
+
copyFileSync2(configPath, backupPath);
|
|
3337
3716
|
}
|
|
3338
|
-
|
|
3717
|
+
writeFileSync9(configPath, source, "utf8");
|
|
3339
3718
|
const selectedRepo = `${owner}/${repo}`;
|
|
3340
3719
|
store.saveSelectedRepo(selectedRepo);
|
|
3341
3720
|
const targetStore = createGitHubAuthStore(targetRoot);
|
|
@@ -3621,7 +4000,7 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3621
4000
|
}
|
|
3622
4001
|
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
3623
4002
|
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
3624
|
-
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) =>
|
|
4003
|
+
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve13(root, "packages", "pi-rig")).find((candidate) => existsSync10(resolve13(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
|
|
3625
4004
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
3626
4005
|
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
3627
4006
|
}
|
|
@@ -3937,9 +4316,9 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3937
4316
|
} catch {
|
|
3938
4317
|
return deps.badRequest("Invalid artifact path");
|
|
3939
4318
|
}
|
|
3940
|
-
|
|
4319
|
+
mkdirSync9(dirname9(artifactPath), { recursive: true });
|
|
3941
4320
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
3942
|
-
|
|
4321
|
+
writeFileSync9(artifactPath, bytes);
|
|
3943
4322
|
writeJsonFile4(`${artifactPath}.json`, {
|
|
3944
4323
|
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3945
4324
|
runId,
|
|
@@ -3982,7 +4361,6 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3982
4361
|
hostId,
|
|
3983
4362
|
endpointId: leaseId
|
|
3984
4363
|
});
|
|
3985
|
-
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
3986
4364
|
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3987
4365
|
type: "run.completed",
|
|
3988
4366
|
runId,
|
|
@@ -4101,12 +4479,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4101
4479
|
try {
|
|
4102
4480
|
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
4103
4481
|
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
4104
|
-
const artifactsRoot =
|
|
4482
|
+
const artifactsRoot = resolve13(runRoot, "remote-artifacts");
|
|
4105
4483
|
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
4106
4484
|
} catch {
|
|
4107
4485
|
return deps.badRequest("Invalid artifact path");
|
|
4108
4486
|
}
|
|
4109
|
-
if (!
|
|
4487
|
+
if (!existsSync10(artifactPath)) {
|
|
4110
4488
|
return deps.notFound();
|
|
4111
4489
|
}
|
|
4112
4490
|
return new Response(Bun.file(artifactPath));
|