@ricsam/r5d-worker 0.0.75 → 0.0.76

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/dist/cjs/main.cjs CHANGED
@@ -2290,6 +2290,15 @@ async function startWorker(options) {
2290
2290
  `);
2291
2291
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2292
2292
  `);
2293
+ const snapshotCleanup = (0, import_project_worktrees.cleanupStaleProjectWorktreeSnapshots)();
2294
+ if (snapshotCleanup.removed.length > 0) {
2295
+ process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
2296
+ `);
2297
+ }
2298
+ for (const failure of snapshotCleanup.failed) {
2299
+ process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
2300
+ `);
2301
+ }
2293
2302
  runGit(["--version"]);
2294
2303
  await verifyR5dctlAuth(baseUrl, token);
2295
2304
  import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "type": "commonjs"
5
5
  }
@@ -28,6 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var project_worktrees_exports = {};
30
30
  __export(project_worktrees_exports, {
31
+ PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
32
+ cleanupStaleProjectWorktreeSnapshots: () => cleanupStaleProjectWorktreeSnapshots,
31
33
  createLinkedProjectBranch: () => createLinkedProjectBranch,
32
34
  deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
33
35
  deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
@@ -47,6 +49,10 @@ var import_node_os = __toESM(require("node:os"), 1);
47
49
  var import_node_path = __toESM(require("node:path"), 1);
48
50
  var import_managed_paths = require("./managed-paths.cjs");
49
51
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
52
+ const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
53
+ const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
54
+ `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
55
+ );
50
56
  function projectWorktreeConfigurationFingerprint(input) {
51
57
  return (0, import_node_crypto.createHash)("sha256").update(
52
58
  JSON.stringify({
@@ -167,17 +173,63 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
167
173
  return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
168
174
  });
169
175
  }
176
+ function createProjectWorktreeSnapshotRoot() {
177
+ return import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
178
+ }
170
179
  function snapshotBranchTrees(projectRoot, branches) {
171
- const root = import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), "r5d-project-worktrees-"));
180
+ const root = createProjectWorktreeSnapshotRoot();
172
181
  const paths = /* @__PURE__ */ new Map();
173
- for (const { branchName } of branches) {
174
- const checkoutPath = branchPath(projectRoot, branchName);
175
- if (!import_node_fs.default.existsSync(checkoutPath)) continue;
176
- const snapshotPath = import_node_path.default.join(root, ...branchName.split("/"));
177
- (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
178
- paths.set(branchName, snapshotPath);
182
+ try {
183
+ for (const { branchName } of branches) {
184
+ const checkoutPath = branchPath(projectRoot, branchName);
185
+ if (!import_node_fs.default.existsSync(checkoutPath)) continue;
186
+ const snapshotPath = import_node_path.default.join(root, ...branchName.split("/"));
187
+ (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
188
+ paths.set(branchName, snapshotPath);
189
+ }
190
+ return { root, paths };
191
+ } catch (error) {
192
+ import_node_fs.default.rmSync(root, { recursive: true, force: true });
193
+ throw error;
194
+ }
195
+ }
196
+ function isProcessAlive(processId) {
197
+ try {
198
+ process.kill(processId, 0);
199
+ return true;
200
+ } catch (error) {
201
+ return error.code !== "ESRCH";
202
+ }
203
+ }
204
+ function cleanupStaleProjectWorktreeSnapshots(input = {}) {
205
+ const temporaryRoot = import_node_path.default.resolve(input.temporaryRoot ?? import_node_os.default.tmpdir());
206
+ const processAlive = input.processAlive ?? isProcessAlive;
207
+ const removed = [];
208
+ const failed = [];
209
+ let entries;
210
+ try {
211
+ entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
212
+ } catch (error) {
213
+ if (error.code === "ENOENT") return { removed, failed };
214
+ return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
215
+ }
216
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
217
+ const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
218
+ if (!match) continue;
219
+ const ownerProcessId = Number(match[1]);
220
+ if (!Number.isSafeInteger(ownerProcessId) || processAlive(ownerProcessId)) continue;
221
+ const candidate = import_node_path.default.join(temporaryRoot, entry.name);
222
+ try {
223
+ const stat = import_node_fs.default.lstatSync(candidate);
224
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
225
+ import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
226
+ removed.push(candidate);
227
+ } catch (error) {
228
+ if (error.code === "ENOENT") continue;
229
+ failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
230
+ }
179
231
  }
180
- return { root, paths };
232
+ return { removed, failed };
181
233
  }
182
234
  function configureRepository(input) {
183
235
  if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
@@ -373,7 +425,7 @@ function createLinkedProjectBranch(input) {
373
425
  });
374
426
  if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
375
427
  const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
376
- const snapshotRoot = import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), "r5d-project-branch-"));
428
+ const snapshotRoot = createProjectWorktreeSnapshotRoot();
377
429
  try {
378
430
  (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
379
431
  git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
@@ -491,6 +543,8 @@ const projectWorktreesTestHarness = {
491
543
  };
492
544
  // Annotate the CommonJS export names for ESM import in node:
493
545
  0 && (module.exports = {
546
+ PROJECT_WORKTREE_SNAPSHOT_PREFIX,
547
+ cleanupStaleProjectWorktreeSnapshots,
494
548
  createLinkedProjectBranch,
495
549
  deleteLinkedProjectBranch,
496
550
  deleteProjectMirrorBranch,
package/dist/mjs/main.mjs CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  } from "./supervisor.mjs";
27
27
  import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
28
28
  import {
29
+ cleanupStaleProjectWorktreeSnapshots,
29
30
  createLinkedProjectBranch,
30
31
  deleteLinkedProjectBranch,
31
32
  deleteProjectMirrorBranch,
@@ -2266,6 +2267,15 @@ async function startWorker(options) {
2266
2267
  `);
2267
2268
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2268
2269
  `);
2270
+ const snapshotCleanup = cleanupStaleProjectWorktreeSnapshots();
2271
+ if (snapshotCleanup.removed.length > 0) {
2272
+ process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
2273
+ `);
2274
+ }
2275
+ for (const failure of snapshotCleanup.failed) {
2276
+ process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
2277
+ `);
2278
+ }
2269
2279
  runGit(["--version"]);
2270
2280
  await verifyR5dctlAuth(baseUrl, token);
2271
2281
  fs.mkdirSync(projectsRoot, { recursive: true });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "type": "module"
5
5
  }
@@ -4,6 +4,10 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { validateManagedBranchName } from "./managed-paths.mjs";
6
6
  import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
7
+ const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
8
+ const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
9
+ `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
10
+ );
7
11
  function projectWorktreeConfigurationFingerprint(input) {
8
12
  return createHash("sha256").update(
9
13
  JSON.stringify({
@@ -124,17 +128,63 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
124
128
  return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
125
129
  });
126
130
  }
131
+ function createProjectWorktreeSnapshotRoot() {
132
+ return fs.mkdtempSync(path.join(os.tmpdir(), `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
133
+ }
127
134
  function snapshotBranchTrees(projectRoot, branches) {
128
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "r5d-project-worktrees-"));
135
+ const root = createProjectWorktreeSnapshotRoot();
129
136
  const paths = /* @__PURE__ */ new Map();
130
- for (const { branchName } of branches) {
131
- const checkoutPath = branchPath(projectRoot, branchName);
132
- if (!fs.existsSync(checkoutPath)) continue;
133
- const snapshotPath = path.join(root, ...branchName.split("/"));
134
- mirrorWorkingTree({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
135
- paths.set(branchName, snapshotPath);
137
+ try {
138
+ for (const { branchName } of branches) {
139
+ const checkoutPath = branchPath(projectRoot, branchName);
140
+ if (!fs.existsSync(checkoutPath)) continue;
141
+ const snapshotPath = path.join(root, ...branchName.split("/"));
142
+ mirrorWorkingTree({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
143
+ paths.set(branchName, snapshotPath);
144
+ }
145
+ return { root, paths };
146
+ } catch (error) {
147
+ fs.rmSync(root, { recursive: true, force: true });
148
+ throw error;
149
+ }
150
+ }
151
+ function isProcessAlive(processId) {
152
+ try {
153
+ process.kill(processId, 0);
154
+ return true;
155
+ } catch (error) {
156
+ return error.code !== "ESRCH";
157
+ }
158
+ }
159
+ function cleanupStaleProjectWorktreeSnapshots(input = {}) {
160
+ const temporaryRoot = path.resolve(input.temporaryRoot ?? os.tmpdir());
161
+ const processAlive = input.processAlive ?? isProcessAlive;
162
+ const removed = [];
163
+ const failed = [];
164
+ let entries;
165
+ try {
166
+ entries = fs.readdirSync(temporaryRoot, { withFileTypes: true });
167
+ } catch (error) {
168
+ if (error.code === "ENOENT") return { removed, failed };
169
+ return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
170
+ }
171
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
172
+ const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
173
+ if (!match) continue;
174
+ const ownerProcessId = Number(match[1]);
175
+ if (!Number.isSafeInteger(ownerProcessId) || processAlive(ownerProcessId)) continue;
176
+ const candidate = path.join(temporaryRoot, entry.name);
177
+ try {
178
+ const stat = fs.lstatSync(candidate);
179
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
180
+ fs.rmSync(candidate, { recursive: true, force: true });
181
+ removed.push(candidate);
182
+ } catch (error) {
183
+ if (error.code === "ENOENT") continue;
184
+ failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
185
+ }
136
186
  }
137
- return { root, paths };
187
+ return { removed, failed };
138
188
  }
139
189
  function configureRepository(input) {
140
190
  if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
@@ -330,7 +380,7 @@ function createLinkedProjectBranch(input) {
330
380
  });
331
381
  if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
332
382
  const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
333
- const snapshotRoot = fs.mkdtempSync(path.join(os.tmpdir(), "r5d-project-branch-"));
383
+ const snapshotRoot = createProjectWorktreeSnapshotRoot();
334
384
  try {
335
385
  mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
336
386
  git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
@@ -447,6 +497,8 @@ const projectWorktreesTestHarness = {
447
497
  commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG, ...authArgs(auth), ...args]
448
498
  };
449
499
  export {
500
+ PROJECT_WORKTREE_SNAPSHOT_PREFIX,
501
+ cleanupStaleProjectWorktreeSnapshots,
450
502
  createLinkedProjectBranch,
451
503
  deleteLinkedProjectBranch,
452
504
  deleteProjectMirrorBranch,
@@ -1,3 +1,4 @@
1
+ export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
1
2
  export type GitHttpAuth = {
2
3
  extraHeaderUrl: string;
3
4
  header: string;
@@ -32,6 +33,16 @@ export declare function hasLinkedProjectWorktreeLayout(input: {
32
33
  primaryBranchName: string;
33
34
  branches: readonly Pick<ProjectWorktreeBranch, "branchName">[];
34
35
  }): boolean;
36
+ export declare function cleanupStaleProjectWorktreeSnapshots(input?: {
37
+ temporaryRoot?: string;
38
+ processAlive?: (processId: number) => boolean;
39
+ }): {
40
+ removed: string[];
41
+ failed: Array<{
42
+ path: string;
43
+ error: string;
44
+ }>;
45
+ };
35
46
  export declare function ensureProjectWorktrees(input: {
36
47
  projectRoot: string;
37
48
  primaryBranchName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",