@ricsam/r5d-worker 0.0.78 → 0.0.79

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.
Files changed (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1550 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +641 -56
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +191 -34
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1028 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1584 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +632 -55
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +190 -34
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1023 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +7 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
@@ -0,0 +1,269 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const APP_MANAGED_GITHUB_REGISTRY = "ghcr.io";
4
+ class RegistryAuthConfigurationError extends Error {
5
+ constructor(cause) {
6
+ super("Could not authoritatively configure GitHub registry authentication", { cause });
7
+ this.name = "RegistryAuthConfigurationError";
8
+ }
9
+ }
10
+ function isJsonObject(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function lstatIfExists(filePath) {
14
+ try {
15
+ return fs.lstatSync(filePath);
16
+ } catch (error) {
17
+ if (error.code === "ENOENT") return null;
18
+ throw error;
19
+ }
20
+ }
21
+ function readRegistryAuthConfig(filePath) {
22
+ const stat = lstatIfExists(filePath);
23
+ if (!stat) return null;
24
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`Registry auth config must be a regular file: ${filePath}`);
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
28
+ } catch (error) {
29
+ throw new Error(`Registry auth config is not valid JSON: ${filePath}`, { cause: error });
30
+ }
31
+ if (!isJsonObject(parsed)) throw new Error(`Registry auth config must contain a JSON object: ${filePath}`);
32
+ return parsed;
33
+ }
34
+ function registryAuthHasAppManagedGitHubCredential(filePath) {
35
+ const config = readRegistryAuthConfig(filePath);
36
+ if (!config) return false;
37
+ if (config.auths === void 0) return false;
38
+ if (!isJsonObject(config.auths)) throw new Error(`Registry auth config has an invalid auths object: ${filePath}`);
39
+ return Object.prototype.hasOwnProperty.call(config.auths, APP_MANAGED_GITHUB_REGISTRY);
40
+ }
41
+ function fsyncDirectory(directory, dependencies = {
42
+ platform: process.platform,
43
+ open: (target) => fs.openSync(target, "r"),
44
+ fsync: fs.fsyncSync,
45
+ close: fs.closeSync
46
+ }) {
47
+ if (dependencies.platform === "win32") return;
48
+ const descriptor = dependencies.open(directory);
49
+ try {
50
+ dependencies.fsync(descriptor);
51
+ } finally {
52
+ dependencies.close(descriptor);
53
+ }
54
+ }
55
+ function ensurePrivateParentDirectory(filePath) {
56
+ const directory = path.dirname(filePath);
57
+ let existingAncestor = directory;
58
+ while (!lstatIfExists(existingAncestor)) {
59
+ const parent = path.dirname(existingAncestor);
60
+ if (parent === existingAncestor) throw new Error(`Registry auth parent has no existing ancestor: ${directory}`);
61
+ existingAncestor = parent;
62
+ }
63
+ const ancestorStat = lstatIfExists(existingAncestor);
64
+ if (!ancestorStat || ancestorStat.isSymbolicLink() || !ancestorStat.isDirectory()) {
65
+ throw new Error(`Registry auth parent must descend from a real directory: ${directory}`);
66
+ }
67
+ fs.mkdirSync(directory, { recursive: true, mode: 448 });
68
+ let current = directory;
69
+ while (true) {
70
+ const stat = lstatIfExists(current);
71
+ if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
72
+ throw new Error(`Registry auth parent must be a real directory: ${current}`);
73
+ }
74
+ fsyncDirectory(current);
75
+ if (current === existingAncestor) break;
76
+ current = path.dirname(current);
77
+ }
78
+ }
79
+ function stagePrivateFile(filePath, content) {
80
+ ensurePrivateParentDirectory(filePath);
81
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.r5d-auth.${process.pid}.${crypto.randomUUID()}.tmp`);
82
+ let descriptor;
83
+ try {
84
+ descriptor = fs.openSync(temporaryPath, "wx", 384);
85
+ fs.writeFileSync(descriptor, content, "utf8");
86
+ fs.fsyncSync(descriptor);
87
+ fs.closeSync(descriptor);
88
+ descriptor = void 0;
89
+ fs.chmodSync(temporaryPath, 384);
90
+ return { filePath, temporaryPath };
91
+ } catch (error) {
92
+ if (descriptor !== void 0) fs.closeSync(descriptor);
93
+ fs.rmSync(temporaryPath, { force: true });
94
+ throw error;
95
+ }
96
+ }
97
+ function commitStagedPrivateFile(staged, beforeRename) {
98
+ beforeRename?.();
99
+ fs.renameSync(staged.temporaryPath, staged.filePath);
100
+ fs.chmodSync(staged.filePath, 384);
101
+ fsyncDirectory(path.dirname(staged.filePath));
102
+ }
103
+ function discardStagedPrivateFile(staged) {
104
+ fs.rmSync(staged.temporaryPath, { force: true });
105
+ }
106
+ function cleanupAbandonedStagedFiles(filePath) {
107
+ const directory = path.dirname(filePath);
108
+ const prefix = `.${path.basename(filePath)}.r5d-auth.`;
109
+ let entries;
110
+ try {
111
+ entries = fs.readdirSync(directory);
112
+ } catch (error) {
113
+ if (error.code === "ENOENT") return;
114
+ throw error;
115
+ }
116
+ let removed = false;
117
+ for (const name of entries) {
118
+ if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
119
+ const candidate = path.join(directory, name);
120
+ const stat = fs.lstatSync(candidate);
121
+ if (stat.isSymbolicLink() || !stat.isFile()) {
122
+ throw new Error(`Registry auth staging artifact must be a regular file: ${candidate}`);
123
+ }
124
+ fs.rmSync(candidate);
125
+ removed = true;
126
+ }
127
+ if (removed) fsyncDirectory(directory);
128
+ }
129
+ function atomicReplacePrivateFile(filePath, content, beforeRename) {
130
+ const staged = stagePrivateFile(filePath, content);
131
+ try {
132
+ commitStagedPrivateFile(staged, beforeRename);
133
+ } finally {
134
+ discardStagedPrivateFile(staged);
135
+ }
136
+ }
137
+ function preparePrivateAuthFileGeneration(updates) {
138
+ try {
139
+ const desiredByPath = /* @__PURE__ */ new Map();
140
+ for (const update of updates) {
141
+ const filePath = path.resolve(update.filePath);
142
+ const existing = desiredByPath.get(filePath);
143
+ const desired = { content: update.content, allowSymlinkRemoval: update.allowSymlinkRemoval === true };
144
+ if (existing && (existing.content !== desired.content || existing.allowSymlinkRemoval !== desired.allowSymlinkRemoval)) {
145
+ throw new Error(`Conflicting desired auth content for ${filePath}`);
146
+ }
147
+ desiredByPath.set(filePath, desired);
148
+ }
149
+ const changes = [];
150
+ const stagedFiles = [];
151
+ try {
152
+ for (const [filePath, desired] of desiredByPath) {
153
+ const { content, allowSymlinkRemoval } = desired;
154
+ cleanupAbandonedStagedFiles(filePath);
155
+ const status = lstatIfExists(filePath);
156
+ if (status && (status.isSymbolicLink() ? !(content === null && allowSymlinkRemoval) : !status.isFile())) {
157
+ throw new Error(`Private auth target must be a regular file: ${filePath}`);
158
+ }
159
+ const unchanged = content === null ? !status : Boolean(status && fs.readFileSync(filePath, "utf8") === content && (status.mode & 511) === 384);
160
+ if (unchanged) continue;
161
+ if (content === null) {
162
+ changes.push({ filePath, content });
163
+ continue;
164
+ }
165
+ const staged = stagePrivateFile(filePath, content);
166
+ stagedFiles.push(staged);
167
+ changes.push({ filePath, content, staged });
168
+ }
169
+ } catch (error) {
170
+ for (const staged of stagedFiles) discardStagedPrivateFile(staged);
171
+ throw error;
172
+ }
173
+ let finished = false;
174
+ const discard = () => {
175
+ for (const staged of stagedFiles) discardStagedPrivateFile(staged);
176
+ };
177
+ return {
178
+ changed: changes.length > 0,
179
+ commit(beforeMutation) {
180
+ if (finished) throw new Error("Private auth generation has already been finalized");
181
+ finished = true;
182
+ try {
183
+ changes.forEach((change, index) => {
184
+ beforeMutation?.(change.filePath, index);
185
+ if (change.content === null) {
186
+ fs.unlinkSync(change.filePath);
187
+ fsyncDirectory(path.dirname(change.filePath));
188
+ return;
189
+ }
190
+ if (!change.staged) throw new Error(`Private auth replacement was not staged: ${change.filePath}`);
191
+ commitStagedPrivateFile(change.staged);
192
+ });
193
+ } catch (error) {
194
+ throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
195
+ } finally {
196
+ discard();
197
+ }
198
+ },
199
+ discard() {
200
+ if (finished) return;
201
+ finished = true;
202
+ discard();
203
+ }
204
+ };
205
+ } catch (error) {
206
+ throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
207
+ }
208
+ }
209
+ function registryAuthFileContent(filePath, credential) {
210
+ if (credential && credential.registry !== APP_MANAGED_GITHUB_REGISTRY) {
211
+ throw new Error(`Unsupported app-managed registry: ${credential.registry}`);
212
+ }
213
+ const existing = readRegistryAuthConfig(filePath);
214
+ if (!existing && !credential) return null;
215
+ const config = { ...existing ?? {} };
216
+ const existingAuths = config.auths;
217
+ if (existingAuths !== void 0 && !isJsonObject(existingAuths)) {
218
+ throw new Error(`Registry auth config has an invalid auths object: ${filePath}`);
219
+ }
220
+ const auths = { ...existingAuths ?? {} };
221
+ delete auths[APP_MANAGED_GITHUB_REGISTRY];
222
+ if (credential) {
223
+ auths[APP_MANAGED_GITHUB_REGISTRY] = {
224
+ username: credential.username,
225
+ password: credential.token,
226
+ auth: Buffer.from(`${credential.username}:${credential.token}`).toString("base64")
227
+ };
228
+ }
229
+ config.auths = auths;
230
+ return `${JSON.stringify(config, null, 2)}
231
+ `;
232
+ }
233
+ function configureGitHubRegistryAuthFilesWithHook(filePaths, credential, beforeRename) {
234
+ try {
235
+ const prepared = preparePrivateAuthFileGeneration(planGitHubRegistryAuthFiles(filePaths, credential));
236
+ prepared.commit(beforeRename);
237
+ } catch (error) {
238
+ throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
239
+ }
240
+ }
241
+ function planGitHubRegistryAuthFiles(filePaths, credential) {
242
+ const uniquePaths = [...new Set(filePaths.map((filePath) => path.resolve(filePath)))];
243
+ return uniquePaths.map((filePath) => ({ filePath, content: registryAuthFileContent(filePath, credential) }));
244
+ }
245
+ function updateGitHubRegistryAuthFile(filePath, credential) {
246
+ configureGitHubRegistryAuthFilesWithHook([filePath], credential);
247
+ }
248
+ function configureGitHubRegistryAuthFiles(filePaths, credential) {
249
+ configureGitHubRegistryAuthFilesWithHook(filePaths, credential);
250
+ }
251
+ const registryAuthTestHarness = {
252
+ fsyncDirectory,
253
+ atomicReplacePrivateFile(filePath, content, beforeRename) {
254
+ atomicReplacePrivateFile(filePath, content, beforeRename);
255
+ },
256
+ configureFiles(filePaths, credential, beforeRename) {
257
+ configureGitHubRegistryAuthFilesWithHook(filePaths, credential, beforeRename);
258
+ }
259
+ };
260
+ export {
261
+ APP_MANAGED_GITHUB_REGISTRY,
262
+ RegistryAuthConfigurationError,
263
+ configureGitHubRegistryAuthFiles,
264
+ planGitHubRegistryAuthFiles,
265
+ preparePrivateAuthFileGeneration,
266
+ registryAuthHasAppManagedGitHubCredential,
267
+ registryAuthTestHarness,
268
+ updateGitHubRegistryAuthFile
269
+ };
@@ -0,0 +1,22 @@
1
+ function assertRepositoryTransitionState(project) {
2
+ if (project.repositoryTransitionId !== null && (typeof project.repositoryTransitionId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(project.repositoryTransitionId)) || typeof project.executionDisabled !== "boolean" || typeof project.mirrorWritesDisabled !== "boolean") {
3
+ throw new Error(`Project ${project.projectId} has invalid repository transition state`);
4
+ }
5
+ }
6
+ function assertRepositoryExecutionEnabled(project) {
7
+ if (project.executionDisabled) {
8
+ throw new Error(`Project ${project.projectId} execution is disabled while its repository connection is transitioning`);
9
+ }
10
+ }
11
+ function repositoryMirrorWritesAllowed(project) {
12
+ return project?.executionDisabled !== true && project?.mirrorWritesDisabled !== true;
13
+ }
14
+ function enabledRepositoryTransitionRequiresReseed(input) {
15
+ return !input.project.executionDisabled && (!input.lastEnabled.known || input.lastEnabled.transitionId !== input.project.repositoryTransitionId);
16
+ }
17
+ export {
18
+ assertRepositoryExecutionEnabled,
19
+ assertRepositoryTransitionState,
20
+ enabledRepositoryTransitionRequiresReseed,
21
+ repositoryMirrorWritesAllowed
22
+ };
@@ -1,17 +1,30 @@
1
+ import { constants as osConstants } from "node:os";
2
+ import { terminateProcessTree } from "./process-tree.mjs";
1
3
  const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
2
4
  const WORKER_RELOAD_EXIT_CODE = 75;
3
5
  const WORKER_RECONNECT_EXIT_CODE = 76;
6
+ const WORKER_CREDENTIAL_RESTART_EXIT_CODE = 77;
4
7
  const WORKER_RECONNECT_DELAY_MS = 2e3;
8
+ const SUPERVISOR_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
5
9
  function defaultRuntimeSpawner(argv, options) {
6
10
  return Bun.spawn(argv, options);
7
11
  }
8
12
  function defaultRuntimeDelay(delayMs) {
9
13
  return new Promise((resolve) => setTimeout(resolve, delayMs));
10
14
  }
15
+ async function defaultRuntimeReaper(runtime) {
16
+ await terminateProcessTree(runtime);
17
+ }
11
18
  function isRetryableWorkerServerStatus(status) {
12
19
  return status === 408 || status === 429 || status >= 500;
13
20
  }
14
- async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay) {
21
+ function signalExitCode(signal) {
22
+ return 128 + (osSignalNumber(signal) ?? 1);
23
+ }
24
+ function osSignalNumber(signal) {
25
+ return osConstants.signals[signal];
26
+ }
27
+ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper) {
15
28
  if (!process.argv[1]) {
16
29
  throw new Error("Cannot locate the r5d-worker entrypoint");
17
30
  }
@@ -20,26 +33,63 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
20
33
  stdin: "inherit",
21
34
  stdout: "inherit",
22
35
  stderr: "inherit",
36
+ detached: true,
23
37
  env: {
24
38
  ...env,
25
39
  [WORKER_RUNTIME_ENV]: "1"
26
40
  }
27
41
  });
28
- const exitCode = await runtime.exited;
29
- if (exitCode === WORKER_RELOAD_EXIT_CODE) {
30
- process.stdout.write("[r5d-worker] activating updated worker runtime\n");
31
- continue;
32
- }
33
- if (exitCode === WORKER_RECONNECT_EXIT_CODE) {
34
- process.stderr.write(`[r5d-worker] reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
42
+ let receivedSignal;
43
+ let resolveSignal;
44
+ const signalReceived = new Promise((resolve) => {
45
+ resolveSignal = resolve;
46
+ });
47
+ const handlers = SUPERVISOR_SIGNALS.map((signal) => {
48
+ const handler = () => {
49
+ if (receivedSignal) return;
50
+ receivedSignal = signal;
51
+ resolveSignal?.(signal);
52
+ };
53
+ process.on(signal, handler);
54
+ return { signal, handler };
55
+ });
56
+ try {
57
+ const outcome = await Promise.race([
58
+ runtime.exited.then((exitCode2) => ({ type: "exit", exitCode: exitCode2 })),
59
+ signalReceived.then((signal) => ({ type: "signal", signal }))
60
+ ]);
61
+ try {
62
+ await reapCredentialRuntime(runtime);
63
+ } catch (error) {
64
+ process.stderr.write(
65
+ `[r5d-worker] runtime restart/exit aborted because process-group reaping failed: ${error instanceof Error ? error.message : String(error)}
66
+ `
67
+ );
68
+ return 1;
69
+ }
70
+ if (outcome.type === "signal" || receivedSignal) {
71
+ return signalExitCode(outcome.type === "signal" ? outcome.signal : receivedSignal);
72
+ }
73
+ const { exitCode } = outcome;
74
+ if (exitCode === WORKER_RELOAD_EXIT_CODE) {
75
+ process.stdout.write("[r5d-worker] activating updated worker runtime\n");
76
+ continue;
77
+ }
78
+ if (exitCode === WORKER_RECONNECT_EXIT_CODE) {
79
+ process.stderr.write(`[r5d-worker] reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
35
80
  `);
36
- await waitBeforeReconnect(WORKER_RECONNECT_DELAY_MS);
37
- continue;
81
+ await waitBeforeReconnect(WORKER_RECONNECT_DELAY_MS);
82
+ if (receivedSignal) return signalExitCode(receivedSignal);
83
+ continue;
84
+ }
85
+ return exitCode;
86
+ } finally {
87
+ for (const { signal, handler } of handlers) process.off(signal, handler);
38
88
  }
39
- return exitCode;
40
89
  }
41
90
  }
42
91
  export {
92
+ WORKER_CREDENTIAL_RESTART_EXIT_CODE,
43
93
  WORKER_RECONNECT_DELAY_MS,
44
94
  WORKER_RECONNECT_EXIT_CODE,
45
95
  WORKER_RELOAD_EXIT_CODE,