@storacha/clawracha 0.0.1 → 0.0.2

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/sync.js CHANGED
@@ -8,25 +8,26 @@
8
8
  * 5. Upload all blocks as CAR
9
9
  * 6. Apply remote changes to local filesystem
10
10
  */
11
- import * as fs from "node:fs/promises";
12
- import * as path from "node:path";
13
- import { CarWriter } from "@ipld/car/writer";
14
11
  // UCN Pail imports
15
- import { Agent, Name, Revision, Value } from "@storacha/ucn/pail";
16
- import * as Batch from "@storacha/ucn/pail/batch";
12
+ import { Agent, Name, NoValueError, Revision } from "@storacha/ucn/pail";
17
13
  import { createWorkspaceBlockstore, } from "./blockstore/index.js";
18
- import { encodeFiles } from "./encoder.js";
19
- import { diffRemoteChanges } from "./differ.js";
14
+ import { applyPendingOps } from "./handlers/apply.js";
15
+ import { applyRemoteChanges } from "./handlers/remote.js";
16
+ import { processChanges } from "./handlers/process.js";
17
+ import { diffRemoteChanges } from "./utils/differ.js";
18
+ import { makeTempCar } from "./utils/tempcar.js";
20
19
  export class SyncEngine {
21
20
  workspace;
22
21
  blocks;
23
22
  name = null;
24
23
  current = null;
25
24
  pendingOps = [];
26
- allBlocks = []; // Accumulate blocks for CAR upload
25
+ carFile = null;
27
26
  running = false;
28
27
  lastSync = null;
29
- constructor(workspace) {
28
+ storachaClient;
29
+ constructor(storachaClient, workspace) {
30
+ this.storachaClient = storachaClient;
30
31
  this.workspace = workspace;
31
32
  this.blocks = createWorkspaceBlockstore(workspace);
32
33
  }
@@ -36,10 +37,21 @@ export class SyncEngine {
36
37
  async init(config) {
37
38
  const agent = Agent.parse(config.agentKey);
38
39
  if (config.nameArchive) {
40
+ // Restore from previously saved archive (has full state)
39
41
  const archiveBytes = Buffer.from(config.nameArchive, "base64");
40
42
  this.name = await Name.extract(agent, archiveBytes);
41
43
  }
44
+ else if (config.nameDelegation) {
45
+ // Reconstruct from delegation (granted by another device)
46
+ const { extract } = await import("@storacha/client/delegation");
47
+ const nameBytes = Buffer.from(config.nameDelegation, "base64");
48
+ const { ok: delegation } = await extract(nameBytes);
49
+ if (!delegation)
50
+ throw new Error("Failed to extract name delegation");
51
+ this.name = Name.from(agent, [delegation]);
52
+ }
42
53
  else {
54
+ // First device — create a new name
43
55
  this.name = await Name.create(agent);
44
56
  }
45
57
  try {
@@ -48,7 +60,7 @@ export class SyncEngine {
48
60
  await this.storeBlocks(result.additions);
49
61
  }
50
62
  catch (err) {
51
- if (err.code === "ERR_NO_VALUE") {
63
+ if (err instanceof NoValueError) {
52
64
  this.current = null;
53
65
  }
54
66
  else {
@@ -60,34 +72,11 @@ export class SyncEngine {
60
72
  * Process a batch of file changes
61
73
  */
62
74
  async processChanges(changes) {
63
- const toEncode = changes
64
- .filter((c) => c.type !== "unlink")
65
- .map((c) => c.path);
66
- const toDelete = changes
67
- .filter((c) => c.type === "unlink")
68
- .map((c) => c.path);
69
- // Encode changed files
70
- const encoded = await encodeFiles(this.workspace, toEncode);
71
- // Accumulate file blocks for CAR upload only (not persisted to blockstore)
72
- // TODO: spill to temporary CAR file if memory becomes an issue
73
- for (const file of encoded) {
74
- this.allBlocks.push(...file.blocks);
75
- }
76
- // Check current pail state to skip no-ops
77
- const currentEntries = await this.getPailEntries();
78
- // Generate puts for changed files (skip if CID unchanged)
79
- for (const file of encoded) {
80
- const existing = currentEntries.get(file.path);
81
- if (!existing || !existing.equals(file.rootCID)) {
82
- this.pendingOps.push({ type: 'put', key: file.path, value: file.rootCID });
83
- }
84
- }
85
- // Generate dels for removed files (skip if not in pail)
86
- for (const deletePath of toDelete) {
87
- if (currentEntries.has(deletePath)) {
88
- this.pendingOps.push({ type: 'del', key: deletePath });
89
- }
75
+ if (!this.carFile) {
76
+ this.carFile = await makeTempCar();
90
77
  }
78
+ const pendingOps = await processChanges(changes, this.workspace, this.current, this.blocks, (block) => this.carFile.put(block), (block) => this.blocks.put(block));
79
+ this.pendingOps.push(...pendingOps);
91
80
  }
92
81
  /**
93
82
  * Execute sync: generate revision, publish, upload, apply remote changes
@@ -97,78 +86,19 @@ export class SyncEngine {
97
86
  throw new Error("Sync engine not initialized");
98
87
  }
99
88
  const beforeEntries = await this.getPailEntries();
100
- const revisionBlocks = [];
101
- // Separate puts and dels (batch only supports puts)
102
- const puts = this.pendingOps.filter((op) => op.type === "put" && op.value);
103
- const dels = this.pendingOps.filter((op) => op.type === "del");
104
- // Process puts with batch
105
- if (puts.length > 0) {
106
- if (this.current) {
107
- const batcher = await Batch.create(this.blocks, this.current);
108
- for (const op of puts) {
109
- if (op.value)
110
- await batcher.put(op.key, op.value);
111
- }
112
- const result = await batcher.commit();
113
- revisionBlocks.push(...result.additions);
114
- await this.storeBlocks(result.additions);
115
- const opResult = result.revision.operation;
116
- this.current = Value.create(this.name, opResult.root, [
117
- result.revision,
118
- ]);
89
+ if (this.pendingOps.length > 0) {
90
+ if (!this.carFile) {
91
+ throw new Error("CAR file not initialized");
119
92
  }
120
- else {
121
- // First revision - v0Put
122
- const firstPut = puts[0];
123
- if (firstPut.value) {
124
- const result = await Revision.v0Put(this.blocks, firstPut.key, firstPut.value);
125
- revisionBlocks.push(...result.additions);
126
- await this.storeBlocks(result.additions);
127
- const opResult = result.revision.operation;
128
- this.current = Value.create(this.name, opResult.root, [
129
- result.revision,
130
- ]);
131
- // Batch remaining puts
132
- if (puts.length > 1) {
133
- const batcher = await Batch.create(this.blocks, this.current);
134
- for (const op of puts.slice(1)) {
135
- if (op.value)
136
- await batcher.put(op.key, op.value);
137
- }
138
- const batchResult = await batcher.commit();
139
- revisionBlocks.push(...batchResult.additions);
140
- await this.storeBlocks(batchResult.additions);
141
- const batchOpResult = batchResult.revision.operation;
142
- this.current = Value.create(this.name, batchOpResult.root, [
143
- batchResult.revision,
144
- ]);
145
- }
146
- }
93
+ const result = await applyPendingOps(this.blocks, this.name, this.current, this.pendingOps);
94
+ this.current = result.current;
95
+ for (const block of result.revisionBlocks) {
96
+ await this.carFile.put(block);
147
97
  }
148
- }
149
- // Process dels individually (batch doesn't support del)
150
- if (dels.length > 0 && this.current) {
151
- for (const op of dels) {
152
- const result = await Revision.del(this.blocks, this.current, op.key);
153
- revisionBlocks.push(...result.additions);
154
- await this.storeBlocks(result.additions);
155
- const opResult = result.revision.operation;
156
- this.current = Value.create(this.name, opResult.root, [
157
- result.revision,
158
- ]);
159
- }
160
- }
161
- this.pendingOps = [];
162
- // Publish to network
163
- if (this.current && this.current.revision.length > 0) {
164
- const latestRevision = this.current.revision[this.current.revision.length - 1];
165
- const pubResult = await Revision.publish(this.blocks, this.name, latestRevision);
166
- revisionBlocks.push(...pubResult.additions);
167
- await this.storeBlocks(pubResult.additions);
168
- this.current = pubResult.value;
98
+ await this.storeBlocks(result.revisionBlocks);
169
99
  }
170
100
  else {
171
- // Just pull remote
101
+ // No pending ops — just pull remote
172
102
  try {
173
103
  const result = await Revision.resolve(this.blocks, this.name, {
174
104
  base: this.current ?? undefined,
@@ -177,16 +107,13 @@ export class SyncEngine {
177
107
  this.current = result.value;
178
108
  }
179
109
  catch (err) {
180
- if (err.code !== "ERR_NO_VALUE")
110
+ if (!(err instanceof NoValueError))
181
111
  throw err;
182
112
  }
183
113
  }
184
- // Combine all blocks and upload as CAR
185
- const allUploadBlocks = [...this.allBlocks, ...revisionBlocks];
186
- if (allUploadBlocks.length > 0) {
187
- await this.uploadCAR(allUploadBlocks);
188
- this.allBlocks = []; // Clear accumulated blocks
189
- }
114
+ this.pendingOps = [];
115
+ // Upload all accumulated blocks as CAR
116
+ await this.possiblyUploadCAR();
190
117
  // Apply remote changes
191
118
  const afterEntries = await this.getPailEntries();
192
119
  const remoteChanges = diffRemoteChanges(beforeEntries, afterEntries);
@@ -198,35 +125,19 @@ export class SyncEngine {
198
125
  /**
199
126
  * Create CAR and upload to Storacha
200
127
  */
201
- async uploadCAR(blocks) {
202
- if (blocks.length === 0)
203
- return;
204
- // Find root CID (last block is typically the root)
205
- const rootCID = blocks[blocks.length - 1].cid;
206
- // Create CAR
207
- const { writer, out } = CarWriter.create([rootCID]);
208
- // Collect CAR bytes
209
- const chunks = [];
210
- const collectPromise = (async () => {
211
- for await (const chunk of out) {
212
- chunks.push(chunk);
128
+ async possiblyUploadCAR() {
129
+ if (this.carFile) {
130
+ const readableCar = await this.carFile.switchToReadable();
131
+ this.carFile = null;
132
+ if (readableCar) {
133
+ try {
134
+ await this.storachaClient.uploadCAR(readableCar.readable);
135
+ }
136
+ finally {
137
+ await readableCar.cleanup();
138
+ }
213
139
  }
214
- })();
215
- // Write blocks
216
- for (const block of blocks) {
217
- await writer.put(block);
218
- }
219
- await writer.close();
220
- await collectPromise;
221
- const carBytes = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
222
- let offset = 0;
223
- for (const chunk of chunks) {
224
- carBytes.set(chunk, offset);
225
- offset += chunk.length;
226
140
  }
227
- // TODO: Upload to Storacha using client.uploadCAR
228
- // For now, just log
229
- console.log(`[storacha-sync] Would upload CAR: ${blocks.length} blocks, ${carBytes.length} bytes, root: ${rootCID}`);
230
141
  }
231
142
  /**
232
143
  * Get current pail entries as map
@@ -246,25 +157,39 @@ export class SyncEngine {
246
157
  * Apply remote changes to local filesystem
247
158
  */
248
159
  async applyRemoteChanges(changedPaths, entries) {
249
- for (const relativePath of changedPaths) {
250
- const cid = entries.get(relativePath);
251
- if (!cid) {
252
- // Deleted remotely
253
- const fullPath = path.join(this.workspace, relativePath);
254
- try {
255
- await fs.unlink(fullPath);
256
- console.log(`[storacha-sync] Deleted: ${relativePath}`);
257
- }
258
- catch (err) {
259
- if (err.code !== "ENOENT")
260
- throw err;
261
- }
262
- }
263
- else {
264
- // Changed remotely - TODO: fetch and reconstruct
265
- console.log(`[storacha-sync] Remote change: ${relativePath} → ${cid}`);
266
- }
160
+ await applyRemoteChanges(changedPaths, entries, this.workspace, {
161
+ blocks: this.blocks,
162
+ current: this.current ?? undefined,
163
+ });
164
+ }
165
+ /**
166
+ * Pull all remote state and write to local filesystem.
167
+ * Used by /storacha-join to overwrite local with remote before watcher starts.
168
+ */
169
+ async pullRemote() {
170
+ if (!this.name)
171
+ throw new Error("Sync engine not initialized");
172
+ try {
173
+ const result = await Revision.resolve(this.blocks, this.name, {
174
+ base: this.current ?? undefined,
175
+ });
176
+ await this.storeBlocks(result.additions);
177
+ this.current = result.value;
178
+ }
179
+ catch (err) {
180
+ if (!(err instanceof NoValueError))
181
+ throw err;
267
182
  }
183
+ const entries = await this.getPailEntries();
184
+ if (entries.size > 0) {
185
+ const allPaths = [...entries.keys()];
186
+ await applyRemoteChanges(allPaths, entries, this.workspace, {
187
+ blocks: this.blocks,
188
+ current: this.current ?? undefined,
189
+ });
190
+ }
191
+ this.lastSync = Date.now();
192
+ return entries.size;
268
193
  }
269
194
  async status() {
270
195
  const entries = await this.getPailEntries();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Type definitions for Storacha workspace sync
3
+ */
4
+ import type { UnknownLink } from "multiformats";
5
+ import type { Block } from "multiformats";
6
+ import type { CID } from "multiformats/cid";
7
+ /** Plugin configuration (from openclaw.plugin.json schema) */
8
+ export interface SyncPluginConfig {
9
+ enabled: boolean;
10
+ watchPatterns: string[];
11
+ ignorePatterns: string[];
12
+ }
13
+ /** Stored in .storacha/config.json — device-specific, not synced */
14
+ export interface DeviceConfig {
15
+ /** Ed25519 agent private key (base64) */
16
+ agentKey: string;
17
+ /** UCN name archive (base64 CAR) */
18
+ nameArchive?: string;
19
+ /** Space → agent upload delegation (base64 archive) */
20
+ uploadDelegation?: string;
21
+ /** Name → agent delegation for pail sync (base64 archive) */
22
+ nameDelegation?: string;
23
+ /** Space DID extracted from upload delegation */
24
+ spaceDID?: string;
25
+ /** Whether setup is complete (watcher won't start without this) */
26
+ setupComplete?: boolean;
27
+ }
28
+ /** Current sync state */
29
+ export interface SyncState {
30
+ /** Is sync currently running? */
31
+ running: boolean;
32
+ /** Last successful sync timestamp */
33
+ lastSync: number | null;
34
+ /** Current pail root CID */
35
+ root: UnknownLink | null;
36
+ /** Number of tracked entries */
37
+ entryCount: number;
38
+ /** Pending local changes not yet pushed */
39
+ pendingChanges: number;
40
+ }
41
+ /** File change event from watcher */
42
+ export interface FileChange {
43
+ type: "add" | "change" | "unlink";
44
+ path: string;
45
+ }
46
+ /** Encoded file result */
47
+ export interface EncodedFile {
48
+ path: string;
49
+ blocks: ReadableStream<Block>;
50
+ size: number;
51
+ }
52
+ /** Diff operation for pail */
53
+ export interface PailOp {
54
+ type: "put" | "del";
55
+ key: string;
56
+ value?: CID;
57
+ }
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAE5C,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,yBAAyB;AACzB,MAAM,WAAW,SAAS;IACxB,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4BAA4B;IAC5B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0BAA0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8BAA8B;AAC9B,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;CACb"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for Storacha workspace sync
3
+ */
4
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Create a @storacha/client instance from an agent private key and upload delegation.
3
+ *
4
+ * Uses the top-level `create()` factory which accepts principal + store directly,
5
+ * avoiding internal @storacha/access imports.
6
+ */
7
+ import type { DeviceConfig } from "../types/index.js";
8
+ import type { Client } from "@storacha/client";
9
+ /**
10
+ * Build a Storacha Client from device config.
11
+ * Requires agentKey and uploadDelegation to be present.
12
+ */
13
+ export declare function createStorachaClient(config: DeviceConfig): Promise<Client>;
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/utils/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,MAAM,CAAC,CA8BjB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Create a @storacha/client instance from an agent private key and upload delegation.
3
+ *
4
+ * Uses the top-level `create()` factory which accepts principal + store directly,
5
+ * avoiding internal @storacha/access imports.
6
+ */
7
+ import { Agent as PailAgent } from "@storacha/ucn/pail";
8
+ import { create as createClient } from "@storacha/client";
9
+ import { StoreMemory } from "@storacha/client/stores/memory";
10
+ import { extract } from "@storacha/client/delegation";
11
+ /**
12
+ * Build a Storacha Client from device config.
13
+ * Requires agentKey and uploadDelegation to be present.
14
+ */
15
+ export async function createStorachaClient(config) {
16
+ if (!config.uploadDelegation) {
17
+ throw new Error("No upload delegation in device config");
18
+ }
19
+ const agent = PailAgent.parse(config.agentKey);
20
+ const client = await createClient({
21
+ principal: agent,
22
+ store: new StoreMemory(),
23
+ });
24
+ // Import the upload delegation (space → agent)
25
+ const uploadBytes = Buffer.from(config.uploadDelegation, "base64");
26
+ const { ok: uploadDelegation, error: uploadErr } = await extract(uploadBytes);
27
+ if (!uploadDelegation) {
28
+ throw new Error(`Failed to extract upload delegation: ${uploadErr}`);
29
+ }
30
+ // addSpace registers the space and adds the delegation as a proof
31
+ await client.addSpace(uploadDelegation);
32
+ // Set the space as current so uploads target it
33
+ const spaceDID = uploadDelegation.capabilities[0]?.with;
34
+ if (spaceDID) {
35
+ await client.setCurrentSpace(spaceDID);
36
+ }
37
+ return client;
38
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Differ - compares local directory tree with pail entries
3
+ *
4
+ * Generates put/del operations to sync local state to pail.
5
+ */
6
+ import type { CID } from "multiformats/cid";
7
+ import type { PailOp } from "../types/index.js";
8
+ /** Map of path → CID from pail entries */
9
+ export type PailEntries = Map<string, CID>;
10
+ /** Map of path → CID from local encoded files */
11
+ export type LocalEntries = Map<string, CID>;
12
+ /**
13
+ * Compute diff between local files and pail entries
14
+ *
15
+ * @param local - Encoded local files (path → rootCID)
16
+ * @param pail - Current pail entries (path → CID)
17
+ * @returns Operations to apply to pail
18
+ */
19
+ export declare function diffEntries(local: LocalEntries, pail: PailEntries): PailOp[];
20
+ /**
21
+ * Diff two pail states to find files that changed remotely
22
+ * (Used after publish to determine what to download)
23
+ *
24
+ * @param before - Pail entries before publish
25
+ * @param after - Pail entries after publish (may include remote changes)
26
+ * @returns Paths that changed remotely (need to download)
27
+ */
28
+ export declare function diffRemoteChanges(before: PailEntries, after: PailEntries): string[];
29
+ //# sourceMappingURL=differ.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/utils/differ.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,0CAA0C;AAC1C,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE3C,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE5C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAmB5E;AAGD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,GACjB,MAAM,EAAE,CAWV"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Differ - compares local directory tree with pail entries
3
+ *
4
+ * Generates put/del operations to sync local state to pail.
5
+ */
6
+ /**
7
+ * Compute diff between local files and pail entries
8
+ *
9
+ * @param local - Encoded local files (path → rootCID)
10
+ * @param pail - Current pail entries (path → CID)
11
+ * @returns Operations to apply to pail
12
+ */
13
+ export function diffEntries(local, pail) {
14
+ const ops = [];
15
+ // Find puts: files in local that are new or changed
16
+ for (const [path, localCID] of local) {
17
+ const pailCID = pail.get(path);
18
+ if (!pailCID || !localCID.equals(pailCID)) {
19
+ ops.push({ type: "put", key: path, value: localCID });
20
+ }
21
+ }
22
+ // Find deletes: files in pail that aren't in local
23
+ for (const path of pail.keys()) {
24
+ if (!local.has(path)) {
25
+ ops.push({ type: "del", key: path });
26
+ }
27
+ }
28
+ return ops;
29
+ }
30
+ /**
31
+ * Diff two pail states to find files that changed remotely
32
+ * (Used after publish to determine what to download)
33
+ *
34
+ * @param before - Pail entries before publish
35
+ * @param after - Pail entries after publish (may include remote changes)
36
+ * @returns Paths that changed remotely (need to download)
37
+ */
38
+ export function diffRemoteChanges(before, after) {
39
+ const changed = [];
40
+ for (const [path, afterCID] of after) {
41
+ const beforeCID = before.get(path);
42
+ if (!beforeCID || !afterCID.equals(beforeCID)) {
43
+ changed.push(path);
44
+ }
45
+ }
46
+ return changed;
47
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * File encoder - converts files to UnixFS DAG with root CID
3
+ *
4
+ * Uses @storacha/upload-client's UnixFS encoding to generate
5
+ * content-addressed blocks for each file.
6
+ */
7
+ import type { EncodedFile } from "../types/index.js";
8
+ /**
9
+ * Encode a single file to UnixFS blocks
10
+ */
11
+ export declare function encodeWorkspaceFile(workspacePath: string, relativePath: string): Promise<EncodedFile>;
12
+ /**
13
+ * Encode multiple files, returning all encoded results
14
+ */
15
+ export declare function encodeFiles(workspacePath: string, relativePaths: string[]): Promise<EncodedFile[]>;
16
+ //# sourceMappingURL=encoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/utils/encoder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAkBtB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,WAAW,EAAE,CAAC,CAkBxB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * File encoder - converts files to UnixFS DAG with root CID
3
+ *
4
+ * Uses @storacha/upload-client's UnixFS encoding to generate
5
+ * content-addressed blocks for each file.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import * as stream from "node:stream";
10
+ import { createFileEncoderStream } from "@storacha/upload-client/unixfs";
11
+ /**
12
+ * Encode a single file to UnixFS blocks
13
+ */
14
+ export async function encodeWorkspaceFile(workspacePath, relativePath) {
15
+ const fullPath = path.join(workspacePath, relativePath);
16
+ const fileHandle = await fs.open(fullPath);
17
+ const stat = await fs.stat(fullPath);
18
+ // Encode to UnixFS - returns { cid, blocks }
19
+ const blocks = createFileEncoderStream({
20
+ // @ts-expect-error node web stream not type compatible with web stream
21
+ stream() {
22
+ return stream.Readable.toWeb(fileHandle.createReadStream());
23
+ },
24
+ });
25
+ return {
26
+ path: relativePath,
27
+ size: stat.size,
28
+ blocks: blocks,
29
+ };
30
+ }
31
+ /**
32
+ * Encode multiple files, returning all encoded results
33
+ */
34
+ export async function encodeFiles(workspacePath, relativePaths) {
35
+ const results = [];
36
+ for (const relativePath of relativePaths) {
37
+ try {
38
+ const encoded = await encodeWorkspaceFile(workspacePath, relativePath);
39
+ results.push(encoded);
40
+ }
41
+ catch (err) {
42
+ if (err.code === "ENOENT") {
43
+ // File was deleted between detection and encoding - skip
44
+ console.warn(`File not found during encoding: ${relativePath}`);
45
+ continue;
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+ return results;
51
+ }
@@ -0,0 +1,22 @@
1
+ import type { Block } from "multiformats";
2
+ import type { BlobLike } from "@storacha/client/types";
3
+ export interface WritableCar {
4
+ /**
5
+ * Write a block to the CAR file.
6
+ * Must not be called after close/switchToReadable.
7
+ */
8
+ put(block: Block): Promise<void>;
9
+ cleanup(): Promise<void>;
10
+ /**
11
+ * Close the writer and return a readable for upload.
12
+ * Returns null if nothing was written.
13
+ */
14
+ switchToReadable(): Promise<ReadableCar | null>;
15
+ }
16
+ interface ReadableCar {
17
+ readable: BlobLike;
18
+ cleanup(): Promise<void>;
19
+ }
20
+ export declare const makeTempCar: () => Promise<WritableCar>;
21
+ export {};
22
+ //# sourceMappingURL=tempcar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tempcar.d.ts","sourceRoot":"","sources":["../../src/utils/tempcar.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB;;;OAGG;IACH,gBAAgB,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACjD;AAED,UAAU,WAAW;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,WAAW,CAkDvD,CAAC"}