@ricsam/r5d-worker 0.0.146 → 0.0.147

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.146",
3
+ "version": "0.0.147",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -7,7 +7,7 @@ import { startManagerRpc } from "./runtime/releases/rpc-main.mjs";
7
7
  import { ManagerRpcConfig } from "./runtime/releases/rpc-protocol.mjs";
8
8
  const args = process.argv.slice(2);
9
9
  if (args.includes("--version")) {
10
- console.log(`r5d-worker ${true ? "0.0.146" : "development"}`);
10
+ console.log(`r5d-worker ${true ? "0.0.147" : "development"}`);
11
11
  } else if (!args.length || args.includes("--help")) {
12
12
  console.log(
13
13
  "Usage: r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <worker-token>]\n r5d-worker executor /absolute/private/config.json\n r5d-worker manager /absolute/private/config.json\n\nRun independently supervised durable runtime services. Provision configuration with r5dinfra."
@@ -15,7 +15,7 @@ if (args.includes("--version")) {
15
15
  } else if (args[0] === "start") {
16
16
  const runtime = await startPersonalWorker(
17
17
  parsePersonalWorkerOptions(args.slice(1)),
18
- true ? "0.0.146" : "development"
18
+ true ? "0.0.147" : "development"
19
19
  );
20
20
  console.log(`Worker connected: ${runtime.resourceId}`);
21
21
  let closing = false;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.146",
3
+ "version": "0.0.147",
4
4
  "type": "module"
5
5
  }
@@ -19,6 +19,7 @@ import {
19
19
  durableJson,
20
20
  ensureAuthorityGitRepositoryLayout,
21
21
  git,
22
+ materializeTree,
22
23
  noSymlinkAncestors,
23
24
  privateRoot,
24
25
  readHostRegular,
@@ -542,15 +543,7 @@ class WorkspaceAuthority {
542
543
  const entries = await selectedTree(b.repo, head);
543
544
  const staged = path.join(b.directory, `hydrate-${randomUUID()}`);
544
545
  await fs.mkdir(staged, { mode: 448 });
545
- let total = 0;
546
- for (const entry of entries) {
547
- const bytes = await git(b.repo, ["cat-file", "blob", entry.oid]);
548
- sourceBytes(bytes, entry.file);
549
- if ((total += bytes.length) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Hydrated tree exceeds limit");
550
- const target = path.join(staged, entry.file);
551
- await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
552
- await fs.writeFile(target, bytes, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
553
- }
546
+ await materializeTree(b.repo, staged, entries, 128 * 1024 * 1024, "Hydrated tree exceeds limit");
554
547
  await verifyIgnore(b.repo, staged);
555
548
  const latest = await storage.read({
556
549
  method: "repository.get",
@@ -573,14 +566,16 @@ class WorkspaceAuthority {
573
566
  } else {
574
567
  await this.cleanRefreshBase(b, expectedBase);
575
568
  const original = await selectedTree(b.repo, expectedBase);
576
- const originalPaths = new Set(original.map((entry) => entry.file));
569
+ const originalPaths = new Map(original.map((entry) => [entry.file, entry]));
577
570
  for (const entry of entries) {
578
571
  const target = path.join(b.config.cwd, entry.file);
579
572
  const existing = await fs.lstat(target).catch((error) => {
580
573
  if (error.code === "ENOENT") return null;
581
574
  throw error;
582
575
  });
583
- if (existing && (!originalPaths.has(entry.file) || !existing.isFile() || existing.isSymbolicLink()))
576
+ const originalEntry = originalPaths.get(entry.file);
577
+ const expectedType = entry.mode === "160000" ? existing?.isDirectory() : existing?.isFile();
578
+ if (existing && (!originalEntry || originalEntry.mode === "160000" !== (entry.mode === "160000") || !expectedType || existing.isSymbolicLink()))
584
579
  throw new WorkspaceError("refresh_collision", "Canonical source would replace retained local content");
585
580
  let parent = path.dirname(target);
586
581
  while (parent !== b.config.cwd) {
@@ -625,6 +620,7 @@ class WorkspaceAuthority {
625
620
  changed(path.dirname(destination), retained);
626
621
  }
627
622
  for (const entry of original) {
623
+ if (entry.mode === "160000") continue;
628
624
  const destination = path.join(retained, "source", entry.file);
629
625
  await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
630
626
  await persistFile(path.join(b.config.cwd, entry.file));
@@ -634,6 +630,11 @@ class WorkspaceAuthority {
634
630
  }
635
631
  for (const entry of entries) {
636
632
  const destination = path.join(b.config.cwd, entry.file);
633
+ if (entry.mode === "160000") {
634
+ await fs.mkdir(destination, { recursive: true, mode: 448 });
635
+ changed(destination, b.config.cwd);
636
+ continue;
637
+ }
637
638
  await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
638
639
  await persistFile(path.join(staged, entry.file));
639
640
  await fs.rename(path.join(staged, entry.file), destination);
@@ -1001,15 +1002,7 @@ class WorkspaceAuthority {
1001
1002
  const entries = await selectedTree(b.repo, head);
1002
1003
  const staged = path.join(b.directory, `import-${randomUUID()}`);
1003
1004
  await fs.mkdir(staged, { mode: 448 });
1004
- let total = 0;
1005
- for (const entry of entries) {
1006
- const bytes2 = await git(b.repo, ["cat-file", "blob", entry.oid]);
1007
- sourceBytes(bytes2, entry.file);
1008
- if ((total += bytes2.length) > STORAGE_LIMITS.blobBytes) throw new WorkspaceError("too_large", "Imported tree exceeds limit");
1009
- const destination = path.join(staged, entry.file);
1010
- await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
1011
- await fs.writeFile(destination, bytes2, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
1012
- }
1005
+ await materializeTree(b.repo, staged, entries, STORAGE_LIMITS.blobBytes, "Imported tree exceeds limit");
1013
1006
  const ignore = path.join(staged, ".gitignore");
1014
1007
  try {
1015
1008
  await verifyIgnore(b.repo, staged);
@@ -225,17 +225,34 @@ function sourceBytes(bytes, file) {
225
225
  async function selectedTree(repo, commit) {
226
226
  if (!/^[0-9a-f]{40}$/.test(commit) || (await git(repo, ["cat-file", "-t", commit])).toString().trim() !== "commit")
227
227
  throw new WorkspaceError("invalid_tip", "Selected tip must be a SHA-1 commit");
228
- const raw = await git(repo, ["ls-tree", "-rz", "--full-tree", commit]);
228
+ const raw = await git(repo, ["ls-tree", "-r", "-z", "--full-tree", commit]);
229
229
  if (!Buffer.from(raw.toString("utf8")).equals(raw)) throw new WorkspaceError("unsafe_path", "Non-UTF8 tree names unsupported");
230
- const entries = raw.toString("utf8").split("\0").filter(Boolean).map((record) => {
231
- const match = /^(100644|100755) blob ([0-9a-f]{40})\t(.+)$/.exec(record);
232
- if (!match) throw new WorkspaceError("unsafe_tree", "Symlinks, gitlinks and special modes unsupported");
233
- sourcePath(match[3]);
234
- return { file: match[3], mode: match[1], oid: match[2] };
235
- });
236
- if (entries.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source entries");
230
+ const records = raw.toString("utf8").split("\0").filter(Boolean);
231
+ if (records.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source entries");
232
+ const entries = [];
233
+ for (const record of records) {
234
+ const match = /^(?:(100644|100755) blob|(160000) commit) ([0-9a-f]{40})\t(.+)$/.exec(record);
235
+ if (!match) throw new WorkspaceError("unsafe_tree", "Symlinks and special tree modes unsupported");
236
+ sourcePath(match[4]);
237
+ entries.push({ file: match[4], mode: match[1] ?? match[2], oid: match[3] });
238
+ }
237
239
  return entries;
238
240
  }
241
+ async function materializeTree(repo, destination, entries, limit, message) {
242
+ let total = 0;
243
+ for (const entry of entries) {
244
+ const target = path.join(destination, entry.file);
245
+ if (entry.mode === "160000") {
246
+ await fs.mkdir(target, { recursive: true, mode: 448 });
247
+ continue;
248
+ }
249
+ const bytes = await git(repo, ["cat-file", "blob", entry.oid]);
250
+ sourceBytes(bytes, entry.file);
251
+ if ((total += bytes.length) > limit) throw new WorkspaceError("too_large", message);
252
+ await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
253
+ await fs.writeFile(target, bytes, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
254
+ }
255
+ }
239
256
  async function verifyIgnore(repo, cwd) {
240
257
  await readRegular(path.join(cwd, ".gitignore"), 64 * 1024);
241
258
  const probes = [
@@ -261,6 +278,7 @@ async function verifyIgnore(repo, cwd) {
261
278
  }
262
279
  async function snapshotTree(repo, cwd, canonicalHead) {
263
280
  const metadata = path.join(cwd, ".git");
281
+ let workbenchIndex = null;
264
282
  if (await fs.lstat(metadata).then(
265
283
  () => true,
266
284
  (e) => {
@@ -305,24 +323,84 @@ async function snapshotTree(repo, cwd, canonicalHead) {
305
323
  await fs.writeFile(copy, await readRegular(index, 8 * 1024 * 1024), { mode: 384 });
306
324
  if ((await git(repo, ["ls-files", "--unmerged", "-z"], void 0, copy)).length)
307
325
  throw new WorkspaceError("workbench_conflict", "Unmerged index entries retained; resolve explicitly before publication");
326
+ workbenchIndex = copy;
327
+ }
328
+ }
329
+ const canonical = canonicalHead ? await selectedTree(repo, canonicalHead) : [];
330
+ const indexedGitlinks = /* @__PURE__ */ new Map();
331
+ if (workbenchIndex) {
332
+ const indexed = await git(repo, ["ls-files", "--stage", "-z"], void 0, workbenchIndex);
333
+ if (!Buffer.from(indexed.toString()).equals(indexed)) throw new WorkspaceError("unsafe_path", "Non-UTF8 index names unsupported");
334
+ for (const record of indexed.toString().split("\0").filter(Boolean)) {
335
+ const match = /^160000 ([0-9a-f]{40}) 0\t(.+)$/.exec(record);
336
+ if (record.startsWith("160000 ") && !match) throw new WorkspaceError("unsafe_tree", "Malformed gitlink index entry");
337
+ if (!match) continue;
338
+ sourcePath(match[2]);
339
+ indexedGitlinks.set(match[2], match[1]);
308
340
  }
309
341
  }
342
+ const canonicalGitlinks = new Map(canonical.filter((entry) => entry.mode === "160000").map((entry) => [entry.file, entry.oid]));
343
+ const gitlinks = new Map([...canonicalGitlinks, ...indexedGitlinks]);
344
+ for (const file of [...gitlinks.keys()]) {
345
+ const entry = await fs.lstat(path.join(cwd, file)).catch((error) => {
346
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
347
+ throw error;
348
+ });
349
+ if (!entry) gitlinks.delete(file);
350
+ else if (!entry.isDirectory() || entry.isSymbolicLink()) gitlinks.delete(file);
351
+ }
352
+ const gitlinkRoots = [];
353
+ for (const file of [...gitlinks.keys()].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))) {
354
+ let parent = path.posix.dirname(file);
355
+ let nested = false;
356
+ while (parent !== ".") {
357
+ if (gitlinks.has(parent)) {
358
+ nested = true;
359
+ break;
360
+ }
361
+ parent = path.posix.dirname(parent);
362
+ }
363
+ if (nested) gitlinks.delete(file);
364
+ else gitlinkRoots.push(file);
365
+ }
310
366
  await verifyIgnore(repo, cwd);
311
367
  await git(repo, ["read-tree", "--empty"]);
312
- const listing = await git(repo, [`--work-tree=${cwd}`, "ls-files", "--others", "--exclude-standard", "-z"]);
368
+ const listing = await git(repo, [
369
+ `--work-tree=${cwd}`,
370
+ "ls-files",
371
+ "--others",
372
+ "--exclude-standard",
373
+ "-z",
374
+ "--",
375
+ ".",
376
+ ...gitlinkRoots.map((file) => `:(exclude,literal)${file}`)
377
+ ]);
313
378
  if (!Buffer.from(listing.toString()).equals(listing)) throw new WorkspaceError("unsafe_path", "Non-UTF8 names unsupported");
314
- const tracked = [];
315
- if (canonicalHead) for (const entry of await selectedTree(repo, canonicalHead)) {
316
- const exists = await fs.lstat(path.join(cwd, entry.file)).then(() => true, (error) => {
317
- if (error.code === "ENOENT") return false;
379
+ const beneathGitlink = (file) => {
380
+ let current = file;
381
+ while (true) {
382
+ if (gitlinks.has(current)) return true;
383
+ const parent = path.posix.dirname(current);
384
+ if (parent === "." || parent === current) return false;
385
+ current = parent;
386
+ }
387
+ };
388
+ const tracked = canonical.filter((entry) => entry.mode !== "160000").map((entry) => entry.file);
389
+ const existingTracked = [];
390
+ for (const file of tracked) {
391
+ const exists = await fs.lstat(path.join(cwd, file)).then(() => true, (error) => {
392
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return false;
318
393
  throw error;
319
394
  });
320
- if (exists) tracked.push(entry.file);
395
+ if (exists) existingTracked.push(file);
321
396
  }
322
- const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...tracked])].sort();
323
- if (files.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
397
+ const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...existingTracked])].filter((file) => !beneathGitlink(file)).sort();
398
+ if (files.length + gitlinks.size > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
324
399
  let size = 0;
325
- const records = [];
400
+ const records = [...gitlinks].map(([file, oid]) => ({
401
+ file,
402
+ value: `160000 ${oid} ${file}\0`
403
+ }));
326
404
  for (const file of files) {
327
405
  sourcePath(file);
328
406
  const bytes = await readRegular(path.join(cwd, file));
@@ -330,16 +408,17 @@ async function snapshotTree(repo, cwd, canonicalHead) {
330
408
  if ((size += bytes.length) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Source snapshot exceeds 128 MiB");
331
409
  const st = await fs.lstat(path.join(cwd, file));
332
410
  const oid = (await git(repo, ["hash-object", "-w", "--stdin", "--no-filters"], bytes)).toString().trim();
333
- records.push(`${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0`);
411
+ records.push({ file, value: `${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0` });
334
412
  }
335
413
  if (!files.includes(".gitignore")) throw new WorkspaceError("ignore_policy", "The .gitignore policy itself must be published");
336
- await git(repo, ["update-index", "-z", "--index-info"], records.join(""));
414
+ await git(repo, ["update-index", "-z", "--index-info"], records.sort((a, b) => a.file.localeCompare(b.file)).map((record) => record.value).join(""));
337
415
  return (await git(repo, ["write-tree"])).toString().trim();
338
416
  }
339
417
  export {
340
418
  durableJson,
341
419
  ensureAuthorityGitRepositoryLayout,
342
420
  git,
421
+ materializeTree,
343
422
  noSymlinkAncestors,
344
423
  privateRoot,
345
424
  readHostRegular,
@@ -20,9 +20,10 @@ export declare function sourcePath(file: string): void;
20
20
  export declare function sourceBytes(bytes: Buffer, file?: string): void;
21
21
  export type TreeEntry = {
22
22
  file: string;
23
- mode: string;
23
+ mode: "100644" | "100755" | "160000";
24
24
  oid: string;
25
25
  };
26
26
  export declare function selectedTree(repo: string, commit: string): Promise<TreeEntry[]>;
27
+ export declare function materializeTree(repo: string, destination: string, entries: TreeEntry[], limit: number, message: string): Promise<void>;
27
28
  export declare function verifyIgnore(repo: string, cwd: string): Promise<void>;
28
29
  export declare function snapshotTree(repo: string, cwd: string, canonicalHead?: string | null): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.146",
3
+ "version": "0.0.147",
4
4
  "type": "module",
5
5
  "main": "./dist/mjs/main.mjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -21,8 +21,8 @@
21
21
  "r5d-worker": "dist/mjs/main.mjs"
22
22
  },
23
23
  "dependencies": {
24
- "@ricsam/r5d-api": "^0.0.146",
25
- "@ricsam/r5dctl": "0.0.146",
24
+ "@ricsam/r5d-api": "^0.0.147",
25
+ "@ricsam/r5dctl": "0.0.147",
26
26
  "node-pty": "1.1.0",
27
27
  "zod": "^4.1.13",
28
28
  "picomatch": "^4.0.3"