@h-rig/server 0.0.6-alpha.1 → 0.0.6-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/src/index.js +920 -287
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +117 -21
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +998 -151
- package/dist/src/server-helpers/issue-analysis.js +30 -11
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-mutations.js +248 -103
- package/dist/src/server-helpers/snapshot-orchestrator.js +1 -1
- package/dist/src/server-helpers/snapshot-service.js +1 -1
- package/dist/src/server-helpers/ws-router.js +4 -4
- package/dist/src/server.js +921 -287
- package/package.json +4 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
7
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
8
|
+
function cleanString(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
12
|
+
const explicitRoot = cleanString(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
13
|
+
if (explicitRoot)
|
|
14
|
+
return resolve(explicitRoot);
|
|
15
|
+
const stateDir = cleanString(process.env.RIG_STATE_DIR);
|
|
16
|
+
if (stateDir)
|
|
17
|
+
return resolve(dirname(resolve(stateDir)), "users");
|
|
18
|
+
return resolve(projectRoot, ".rig", "users");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
22
|
+
function cleanString2(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
26
|
+
return resolve2(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
27
|
+
}
|
|
28
|
+
function parseEntry(value) {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return null;
|
|
31
|
+
const record = value;
|
|
32
|
+
const token = cleanString2(record.token);
|
|
33
|
+
const namespaceKey = cleanString2(record.namespaceKey);
|
|
34
|
+
const namespaceRoot = cleanString2(record.namespaceRoot);
|
|
35
|
+
const authStateFile = cleanString2(record.authStateFile);
|
|
36
|
+
const checkoutBaseDir = cleanString2(record.checkoutBaseDir);
|
|
37
|
+
const snapshotBaseDir = cleanString2(record.snapshotBaseDir);
|
|
38
|
+
const createdAt = cleanString2(record.createdAt);
|
|
39
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
token,
|
|
43
|
+
namespaceKey,
|
|
44
|
+
namespaceRoot,
|
|
45
|
+
authStateFile,
|
|
46
|
+
checkoutBaseDir,
|
|
47
|
+
snapshotBaseDir,
|
|
48
|
+
createdAt,
|
|
49
|
+
login: cleanString2(record.login),
|
|
50
|
+
userId: cleanString2(record.userId),
|
|
51
|
+
selectedRepo: cleanString2(record.selectedRepo)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readIndex(indexFile) {
|
|
55
|
+
if (!existsSync(indexFile))
|
|
56
|
+
return [];
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(readFileSync(indexFile, "utf8"));
|
|
59
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
60
|
+
const parsedEntry = parseEntry(entry);
|
|
61
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
62
|
+
}) : [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeIndex(indexFile, sessions) {
|
|
68
|
+
mkdirSync(dirname2(indexFile), { recursive: true });
|
|
69
|
+
writeFileSync(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
70
|
+
`, { encoding: "utf8", mode: 384 });
|
|
71
|
+
try {
|
|
72
|
+
chmodSync(indexFile, 384);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
function registerGitHubApiSession(input) {
|
|
76
|
+
const cleanToken = cleanString2(input.token);
|
|
77
|
+
if (!cleanToken)
|
|
78
|
+
throw new Error("GitHub API session token is required");
|
|
79
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
80
|
+
const createdAt = new Date().toISOString();
|
|
81
|
+
const entry = {
|
|
82
|
+
token: cleanToken,
|
|
83
|
+
login: input.namespace.login,
|
|
84
|
+
userId: input.namespace.userId,
|
|
85
|
+
namespaceKey: input.namespace.key,
|
|
86
|
+
namespaceRoot: input.namespace.root,
|
|
87
|
+
authStateFile: input.namespace.authStateFile,
|
|
88
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
89
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
90
|
+
selectedRepo: cleanString2(input.selectedRepo),
|
|
91
|
+
createdAt
|
|
92
|
+
};
|
|
93
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
94
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
function readGitHubApiSession(input) {
|
|
98
|
+
const cleanToken = cleanString2(input.token);
|
|
99
|
+
if (!cleanToken)
|
|
100
|
+
return null;
|
|
101
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
resolveGitHubApiSessionIndexFile,
|
|
105
|
+
registerGitHubApiSession,
|
|
106
|
+
readGitHubApiSession
|
|
107
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
5
6
|
|
|
6
7
|
// packages/server/src/server-helpers/server-paths.ts
|
|
7
8
|
import { dirname, resolve } from "path";
|
|
@@ -39,6 +40,44 @@ function cleanScopes(value) {
|
|
|
39
40
|
return clean ? [clean] : [];
|
|
40
41
|
});
|
|
41
42
|
}
|
|
43
|
+
function parseApiSessions(value) {
|
|
44
|
+
if (!Array.isArray(value))
|
|
45
|
+
return [];
|
|
46
|
+
return value.flatMap((entry) => {
|
|
47
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
48
|
+
return [];
|
|
49
|
+
const record = entry;
|
|
50
|
+
const token = cleanString(record.token);
|
|
51
|
+
if (!token)
|
|
52
|
+
return [];
|
|
53
|
+
return [{
|
|
54
|
+
token,
|
|
55
|
+
login: cleanString(record.login),
|
|
56
|
+
userId: cleanString(record.userId),
|
|
57
|
+
createdAt: cleanString(record.createdAt) ?? undefined
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function parsePendingDevice(value) {
|
|
62
|
+
if (!value || typeof value !== "object")
|
|
63
|
+
return null;
|
|
64
|
+
const record = value;
|
|
65
|
+
const pollId = cleanString(record.pollId);
|
|
66
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
67
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
68
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
69
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
70
|
+
return null;
|
|
71
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
72
|
+
}
|
|
73
|
+
function parsePendingDevices(value) {
|
|
74
|
+
if (!Array.isArray(value))
|
|
75
|
+
return [];
|
|
76
|
+
return value.flatMap((entry) => {
|
|
77
|
+
const pending = parsePendingDevice(entry);
|
|
78
|
+
return pending ? [pending] : [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
42
81
|
function readStoredAuth(stateFile) {
|
|
43
82
|
if (!existsSync(stateFile))
|
|
44
83
|
return {};
|
|
@@ -52,37 +91,44 @@ function readStoredAuth(stateFile) {
|
|
|
52
91
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
53
92
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
54
93
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
94
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
95
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
55
96
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
56
97
|
};
|
|
57
98
|
} catch {
|
|
58
99
|
return {};
|
|
59
100
|
}
|
|
60
101
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
return null;
|
|
64
|
-
const record = value;
|
|
65
|
-
const pollId = cleanString(record.pollId);
|
|
66
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
67
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
68
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
69
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
70
|
-
return null;
|
|
71
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
102
|
+
function newApiSessionToken() {
|
|
103
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
72
104
|
}
|
|
73
105
|
function writeStoredAuth(stateFile, payload) {
|
|
74
|
-
mkdirSync(
|
|
106
|
+
mkdirSync(dirname2(stateFile), { recursive: true });
|
|
75
107
|
writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
76
108
|
`, { encoding: "utf8", mode: 384 });
|
|
77
109
|
try {
|
|
78
110
|
chmodSync(stateFile, 384);
|
|
79
111
|
} catch {}
|
|
80
112
|
}
|
|
113
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
114
|
+
return resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
115
|
+
}
|
|
81
116
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
82
117
|
return resolve2(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
83
118
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
119
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
120
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
121
|
+
mkdirSync(dirname2(targetFile), { recursive: true });
|
|
122
|
+
if (existsSync(stateFile)) {
|
|
123
|
+
copyFileSync(stateFile, targetFile);
|
|
124
|
+
try {
|
|
125
|
+
chmodSync(targetFile, 384);
|
|
126
|
+
} catch {}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
writeStoredAuth(targetFile, {});
|
|
130
|
+
}
|
|
131
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
86
132
|
return {
|
|
87
133
|
stateFile,
|
|
88
134
|
status(options) {
|
|
@@ -112,14 +158,53 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
112
158
|
scopes: input.scopes ?? [],
|
|
113
159
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
114
160
|
pendingDevice: null,
|
|
161
|
+
pendingDevices: [],
|
|
162
|
+
apiSessions: previous.apiSessions ?? [],
|
|
163
|
+
updatedAt: new Date().toISOString()
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
createApiSession() {
|
|
167
|
+
const previous = readStoredAuth(stateFile);
|
|
168
|
+
const token = newApiSessionToken();
|
|
169
|
+
const session = {
|
|
170
|
+
token,
|
|
171
|
+
login: cleanString(previous.login),
|
|
172
|
+
userId: cleanString(previous.userId),
|
|
173
|
+
createdAt: new Date().toISOString()
|
|
174
|
+
};
|
|
175
|
+
writeStoredAuth(stateFile, {
|
|
176
|
+
...previous,
|
|
177
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
115
178
|
updatedAt: new Date().toISOString()
|
|
116
179
|
});
|
|
180
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
181
|
+
},
|
|
182
|
+
readApiSession(token) {
|
|
183
|
+
const clean = cleanString(token);
|
|
184
|
+
if (!clean)
|
|
185
|
+
return null;
|
|
186
|
+
const previous = readStoredAuth(stateFile);
|
|
187
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
188
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
189
|
+
},
|
|
190
|
+
copyToProjectRoot(projectRoot) {
|
|
191
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
192
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
193
|
+
},
|
|
194
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
195
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
117
196
|
},
|
|
118
197
|
savePendingDevice(input) {
|
|
119
198
|
const previous = readStoredAuth(stateFile);
|
|
199
|
+
const pendingDevices = [
|
|
200
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
201
|
+
...previous.pendingDevices ?? [],
|
|
202
|
+
input
|
|
203
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
120
204
|
writeStoredAuth(stateFile, {
|
|
121
205
|
...previous,
|
|
122
|
-
pendingDevice:
|
|
206
|
+
pendingDevice: null,
|
|
207
|
+
pendingDevices,
|
|
123
208
|
updatedAt: new Date().toISOString()
|
|
124
209
|
});
|
|
125
210
|
},
|
|
@@ -132,24 +217,35 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
132
217
|
});
|
|
133
218
|
},
|
|
134
219
|
readPendingDevice(pollId) {
|
|
135
|
-
const
|
|
136
|
-
|
|
220
|
+
const previous = readStoredAuth(stateFile);
|
|
221
|
+
const pending = [
|
|
222
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
223
|
+
...previous.pendingDevices ?? []
|
|
224
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
225
|
+
if (!pending)
|
|
137
226
|
return null;
|
|
138
227
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
139
228
|
return null;
|
|
140
229
|
return pending;
|
|
141
230
|
},
|
|
142
|
-
clearPendingDevice() {
|
|
231
|
+
clearPendingDevice(pollId) {
|
|
143
232
|
const previous = readStoredAuth(stateFile);
|
|
233
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
144
234
|
writeStoredAuth(stateFile, {
|
|
145
235
|
...previous,
|
|
146
236
|
pendingDevice: null,
|
|
237
|
+
pendingDevices: remaining,
|
|
147
238
|
updatedAt: new Date().toISOString()
|
|
148
239
|
});
|
|
149
240
|
}
|
|
150
241
|
};
|
|
151
242
|
}
|
|
243
|
+
function createGitHubAuthStore(projectRoot) {
|
|
244
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
245
|
+
}
|
|
152
246
|
export {
|
|
153
247
|
resolveGitHubAuthStateFile,
|
|
154
|
-
|
|
248
|
+
createGitHubAuthStoreFromStateFile,
|
|
249
|
+
createGitHubAuthStore,
|
|
250
|
+
copyGitHubAuthStateToLocalProjectRoot
|
|
155
251
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
5
|
+
function cleanString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
7
|
+
}
|
|
8
|
+
function sanitizePathSegment(value) {
|
|
9
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
10
|
+
}
|
|
11
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
12
|
+
const userId = cleanString(identity.userId);
|
|
13
|
+
if (userId) {
|
|
14
|
+
const safeId = sanitizePathSegment(userId);
|
|
15
|
+
if (safeId)
|
|
16
|
+
return `ghu-${safeId}`;
|
|
17
|
+
}
|
|
18
|
+
const login = cleanString(identity.login);
|
|
19
|
+
if (login) {
|
|
20
|
+
const safeLogin = sanitizePathSegment(login);
|
|
21
|
+
if (safeLogin)
|
|
22
|
+
return `ghu-login-${safeLogin}`;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
25
|
+
}
|
|
26
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
27
|
+
const explicitRoot = cleanString(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
28
|
+
if (explicitRoot)
|
|
29
|
+
return resolve(explicitRoot);
|
|
30
|
+
const stateDir = cleanString(process.env.RIG_STATE_DIR);
|
|
31
|
+
if (stateDir)
|
|
32
|
+
return resolve(dirname(resolve(stateDir)), "users");
|
|
33
|
+
return resolve(projectRoot, ".rig", "users");
|
|
34
|
+
}
|
|
35
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
36
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
37
|
+
const root = resolve(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
38
|
+
const stateDir = resolve(root, ".rig", "state");
|
|
39
|
+
return {
|
|
40
|
+
key,
|
|
41
|
+
userId: cleanString(identity.userId),
|
|
42
|
+
login: cleanString(identity.login),
|
|
43
|
+
root,
|
|
44
|
+
stateDir,
|
|
45
|
+
authStateFile: resolve(stateDir, "github-auth.json"),
|
|
46
|
+
metadataFile: resolve(stateDir, "user-namespace.json"),
|
|
47
|
+
checkoutBaseDir: resolve(root, "remote-checkouts"),
|
|
48
|
+
snapshotBaseDir: resolve(root, "remote-snapshots")
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
52
|
+
return {
|
|
53
|
+
key: namespace.key,
|
|
54
|
+
userId: namespace.userId,
|
|
55
|
+
login: namespace.login,
|
|
56
|
+
root: namespace.root,
|
|
57
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
58
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
62
|
+
const root = resolve(namespaceRoot);
|
|
63
|
+
const candidate = resolve(candidatePath);
|
|
64
|
+
const rel = relative(root, candidate);
|
|
65
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
66
|
+
}
|
|
67
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
68
|
+
mkdirSync(namespace.stateDir, { recursive: true });
|
|
69
|
+
const previous = (() => {
|
|
70
|
+
if (!existsSync(namespace.metadataFile))
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(readFileSync(namespace.metadataFile, "utf8"));
|
|
74
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
writeFileSync(namespace.metadataFile, `${JSON.stringify({
|
|
81
|
+
key: namespace.key,
|
|
82
|
+
userId: namespace.userId,
|
|
83
|
+
login: namespace.login,
|
|
84
|
+
root: namespace.root,
|
|
85
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
86
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
87
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
88
|
+
updatedAt: now
|
|
89
|
+
}, null, 2)}
|
|
90
|
+
`, { encoding: "utf8", mode: 384 });
|
|
91
|
+
try {
|
|
92
|
+
chmodSync(namespace.metadataFile, 384);
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
writeRemoteUserNamespaceMetadata,
|
|
97
|
+
serializeRemoteUserNamespace,
|
|
98
|
+
resolveRemoteUserNamespacesRoot,
|
|
99
|
+
resolveRemoteUserNamespace,
|
|
100
|
+
isPathInsideNamespace,
|
|
101
|
+
deriveGitHubUserNamespaceKey
|
|
102
|
+
};
|