@ricsam/r5d-worker 0.0.145 → 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.
package/README.md CHANGED
@@ -8,3 +8,5 @@ r5d-worker manager /absolute/private/manager-config.json
8
8
  ```
9
9
 
10
10
  The executor retains processes and durable receipts independently of application releases. The manager authenticates the approved workspace keeper and retains old adapters until their delivery receipts are settled. Configuration and host roots belong exclusively to one installation. Never share an executor's state directory, credentials or workspace root between environments.
11
+
12
+ The published worker has an exact-version dependency on `@ricsam/r5dctl`. A personal worker resolves that bundled CLI before the host `PATH`, verifies that its package version equals the running worker version, and exposes it to agent shells through a private worker-owned wrapper. Each worker remains bound to one server origin and installation, so stage, production, and review workers cannot accidentally select another environment's global CLI or credentials.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.145",
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.145" : "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.145" : "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.145",
3
+ "version": "0.0.147",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,59 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { promises as fs } from "node:fs";
4
+ const PACKAGE_NAME = "@ricsam/r5dctl";
5
+ function packageMain(moduleUrl) {
6
+ const module = fileURLToPath(moduleUrl);
7
+ return path.join(path.dirname(module), path.extname(module) === ".cjs" ? "main.cjs" : "main.mjs");
8
+ }
9
+ async function inspectPackage(candidate) {
10
+ const entrypoint = await fs.realpath(candidate);
11
+ const entrypointStat = await fs.lstat(entrypoint);
12
+ if (!entrypointStat.isFile() || entrypointStat.nlink !== 1 || entrypointStat.mode & 18 || ![0, process.getuid?.()].includes(entrypointStat.uid))
13
+ throw new Error("Untrusted installed r5dctl entrypoint");
14
+ let directory = path.dirname(entrypoint);
15
+ for (let depth = 0; depth < 6; depth += 1) {
16
+ const manifest = path.join(directory, "package.json");
17
+ try {
18
+ const stat = await fs.lstat(manifest);
19
+ if (!stat.isFile() || stat.nlink !== 1 || stat.mode & 18 || ![0, process.getuid?.()].includes(stat.uid))
20
+ throw new Error("Untrusted installed r5dctl package manifest");
21
+ const parsed = JSON.parse(await fs.readFile(manifest, "utf8"));
22
+ if (parsed?.name === PACKAGE_NAME) {
23
+ if (typeof parsed.version !== "string" || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(parsed.version))
24
+ throw new Error("Invalid installed r5dctl package version");
25
+ return { entrypoint, version: parsed.version };
26
+ }
27
+ } catch (error) {
28
+ if (error.code !== "ENOENT") throw error;
29
+ }
30
+ const parent = path.dirname(directory);
31
+ if (parent === directory) break;
32
+ directory = parent;
33
+ }
34
+ throw new Error("Resolved r5dctl entrypoint is not inside an @ricsam/r5dctl package");
35
+ }
36
+ async function resolvePersonalCliEntrypoint(explicit, workerVersion, dependencies = {}) {
37
+ let source, candidate;
38
+ if (explicit) {
39
+ source = "explicit";
40
+ candidate = explicit;
41
+ } else {
42
+ try {
43
+ const resolvePackage = dependencies.resolvePackage ?? ((specifier) => import.meta.resolve(specifier));
44
+ candidate = packageMain(resolvePackage(`${PACKAGE_NAME}/cli`));
45
+ source = "bundled";
46
+ } catch {
47
+ source = "path";
48
+ candidate = (dependencies.which ?? Bun.which)("r5dctl");
49
+ }
50
+ }
51
+ if (!candidate) throw new Error("Install @ricsam/r5dctl before starting this worker");
52
+ const inspected = await inspectPackage(candidate);
53
+ if (workerVersion !== "development" && inspected.version !== workerVersion)
54
+ throw new Error(`r5d-worker ${workerVersion} requires its bundled r5dctl ${workerVersion}; resolved ${inspected.version}`);
55
+ return { ...inspected, source };
56
+ }
57
+ export {
58
+ resolvePersonalCliEntrypoint
59
+ };
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
2
  import os from "node:os";
4
3
  import { createHash, randomUUID } from "node:crypto";
5
4
  import { promises as fs, openSync, closeSync } from "node:fs";
@@ -7,9 +6,10 @@ import { Database } from "bun:sqlite";
7
6
  import { canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
8
7
  import { privateDirectory, readPrivateJson } from "../runtime/storage.mjs";
9
8
  import { openPersonalWorkerRuntime, PersonalWorkerGrant } from "./runtime.mjs";
10
- import { installCliUpdate, readInstalledCliVersion } from "../cli-update.mjs";
9
+ import { installCliUpdate } from "../cli-update.mjs";
11
10
  import { WorkspaceError } from "../runtime/workspace/contracts.mjs";
12
11
  import { PersonalActionScheduler } from "./action-scheduler.mjs";
12
+ import { resolvePersonalCliEntrypoint } from "./cli-entrypoint.mjs";
13
13
  class PersonalResponseError extends Error {
14
14
  constructor(code, status, rejectedBeforeAdmission) {
15
15
  super(`Personal worker request failed (${status})`);
@@ -95,9 +95,11 @@ async function startPersonalWorker(options, version) {
95
95
  }
96
96
  return data;
97
97
  }
98
+ const cli = await resolvePersonalCliEntrypoint(options.cliEntrypoint, version);
98
99
  const metadata = () => ({
99
100
  version,
100
- r5dctlVersion: readInstalledCliVersion("r5dctl"),
101
+ r5dctlVersion: cli.version,
102
+ r5dctlSource: cli.source,
101
103
  platform: process.platform,
102
104
  arch: process.arch,
103
105
  hostname: os.hostname(),
@@ -106,18 +108,9 @@ async function startPersonalWorker(options, version) {
106
108
  const grant = PersonalWorkerGrant.parse(
107
109
  await request("/api/personal/resources/register", { kind: "worker", ...identity, label: options.label, metadata: metadata() })
108
110
  );
109
- let cliEntrypoint = options.cliEntrypoint ?? Bun.which("r5dctl");
110
- if (!cliEntrypoint) {
111
- try {
112
- const module = fileURLToPath(import.meta.resolve("@ricsam/r5dctl/cli"));
113
- cliEntrypoint = path.join(path.dirname(module), path.extname(module) === ".cjs" ? "main.cjs" : "main.mjs");
114
- } catch {
115
- }
116
- }
117
- if (!cliEntrypoint) throw new Error("Install @ricsam/r5dctl before starting this worker");
118
111
  const runtime = await openPersonalWorkerRuntime({
119
112
  root,
120
- cliEntrypoint,
113
+ cliEntrypoint: cli.entrypoint,
121
114
  grant,
122
115
  storage: (sessionId) => async (storageRequest) => {
123
116
  try {
@@ -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,
@@ -0,0 +1,13 @@
1
+ type ResolutionDependencies = {
2
+ resolvePackage?: (specifier: string) => string;
3
+ which?: (command: string) => string | null;
4
+ };
5
+ export type PersonalCliEntrypoint = {
6
+ entrypoint: string;
7
+ version: string;
8
+ source: "explicit" | "bundled" | "path";
9
+ };
10
+ /** Resolve the CLI that agent shells receive. Published workers always prefer
11
+ * their exact npm dependency; PATH is only a development/source fallback. */
12
+ export declare function resolvePersonalCliEntrypoint(explicit: string | undefined, workerVersion: string, dependencies?: ResolutionDependencies): Promise<PersonalCliEntrypoint>;
13
+ export {};
@@ -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.145",
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.145",
25
- "@ricsam/r5dctl": "0.0.145",
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"