@fizzyflow/wdoublesync_cli 1.0.2 → 1.0.5

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.
@@ -0,0 +1,81 @@
1
+ import EndlessVector from '@fizzyflow/endless-vector';
2
+ import { WDoubleSync } from '@fizzyflow/wdoublesync';
3
+ import { CDCStore } from '@fizzyflow/doublesync';
4
+ import { DoubleSyncFSFolder } from '../DoubleSyncFSFolder.js';
5
+ import {
6
+ makeWalrusSealClient,
7
+ resolvePath,
8
+ makeExcludes,
9
+ formatBytes,
10
+ countTree,
11
+ } from './shared.js';
12
+
13
+ export async function rebate(args) {
14
+ if (!args.vectorId) {
15
+ throw new Error('vector-id is required for rebate');
16
+ }
17
+
18
+ const destPath = resolvePath(args.path);
19
+ const fsFolder = new DoubleSyncFSFolder(destPath, makeExcludes(args));
20
+
21
+ const counts = await countTree(fsFolder);
22
+ console.log(' scanning...', counts.files, 'files,', counts.folders, 'folders');
23
+
24
+ const wsc = await makeWalrusSealClient(args);
25
+
26
+ const evParams = {
27
+ suiClient: wsc.suiClient,
28
+ id: args.vectorId,
29
+ packageId: args.chain,
30
+ signAndExecuteTransaction: (tx) => wsc.signAndExecuteTransaction(tx),
31
+ walrusClient: wsc.walrusClient,
32
+ aggregatorUrl: wsc.aggregatorUrl,
33
+ senderAddress: wsc.suiMaster.address,
34
+ signer: wsc.suiMaster._keypair,
35
+ };
36
+ if (wsc.sealClient && wsc.suiMaster) {
37
+ evParams.sealClient = wsc.sealClient;
38
+ }
39
+
40
+ const ev = new EndlessVector(evParams);
41
+ await ev.initialize();
42
+
43
+ console.log('step 1: archiving history...');
44
+ await ev.archive();
45
+ console.log(' archived');
46
+
47
+ console.log('step 2: burning archives...');
48
+ await ev.burnArchive();
49
+ console.log(' burned');
50
+
51
+ // archive() and burnArchive() both call ev.reInitialize() — refetch fresh state
52
+ await ev.initialize();
53
+
54
+ console.log('step 3: pushing local state as fresh snapshot...');
55
+
56
+ const wdsync = new WDoubleSync({
57
+ endlessVector: ev,
58
+ compress: args.compress || 'gzip',
59
+ });
60
+
61
+ // Skip wdsync.initialize() replay entirely: rebate always pushes a full snapshot,
62
+ // so there's no need to replay the existing (now partly burned) chain.
63
+ // _lastSnapshot = null forces a full snapshot; _isInitialized = true bypasses replay.
64
+ wdsync._senderStore = new CDCStore({ copyBytes: false });
65
+ wdsync._receiverMirror = new CDCStore();
66
+ wdsync._lastSnapshot = null;
67
+ wdsync._replayedCount = ev.length;
68
+ wdsync._isInitialized = true;
69
+
70
+ const result = await wdsync.push(fsFolder);
71
+
72
+ ev.reInitialize();
73
+ await ev.initialize();
74
+
75
+ console.log(' pushed version', result.version);
76
+ console.log(' [debug] after push: length=', ev.length, 'burned=', ev.burnedArchiveCount, 'version=', result.version);
77
+
78
+ console.log('\nrebate complete:');
79
+ console.log(' new version:', result.version);
80
+ console.log(' new on-chain size:', formatBytes(ev.binaryLength));
81
+ }
@@ -0,0 +1,237 @@
1
+ import { mkdir, writeFile, readFile, readdir, rm } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+
6
+ import { SuiMaster } from 'suidouble';
7
+ import { DoubleSync, CDCStore, DoubleSyncSnapshot } from '@fizzyflow/doublesync';
8
+ import WalrusSealClient from '../WalrusSealClient.js';
9
+ import { DoubleSyncFSFolder } from '../DoubleSyncFSFolder.js';
10
+
11
+ const DEFAULT_FS_EXCLUDES = ['node_modules', '.git', '.env', '.DS_Store', '.wdoublesync', '.claude', 'pnpm-lock.yaml', 'package-lock.json'];
12
+
13
+ export function makeExcludes(args) {
14
+ if (!args.exclude) return undefined;
15
+ const extra = args.exclude.split(',').map(s => s.trim());
16
+ return [...DEFAULT_FS_EXCLUDES, ...extra];
17
+ }
18
+
19
+ export function resolvePath(p) {
20
+ if (p.startsWith('~')) return join(homedir(), p.slice(1));
21
+ return resolve(p);
22
+ }
23
+
24
+ export function formatBytes(bytes) {
25
+ if (bytes < 1024) return bytes + ' B';
26
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
27
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
28
+ }
29
+
30
+ export function formatPath(path) {
31
+ const home = homedir();
32
+ if (path.startsWith(home)) {
33
+ return '~' + path.slice(home.length);
34
+ }
35
+ return path;
36
+ }
37
+
38
+ export async function makeWalrusSealClient(args) {
39
+ const params = { client: args.chain };
40
+ const key = args.key || process.env.WDOUBLESYNC_KEY;
41
+ if (key) {
42
+ params.privateKey = key;
43
+ } else if (args.phrase) {
44
+ params.phrase = args.phrase;
45
+ } else {
46
+ throw new Error('No signing key. Use --key, --phrase, or WDOUBLESYNC_KEY env var');
47
+ }
48
+
49
+ const suiMaster = new SuiMaster(params);
50
+ await suiMaster.initialize();
51
+
52
+ const client = new WalrusSealClient({ network: args.chain, suiMaster });
53
+ await client.initialize();
54
+ return client;
55
+ }
56
+
57
+ export async function makeReadOnlyWalrusSealClient(chain) {
58
+ const client = new WalrusSealClient({ network: chain });
59
+ await client.initialize();
60
+ return client;
61
+ }
62
+
63
+ export async function makeClientForRead(args) {
64
+ const key = args.key || process.env.WDOUBLESYNC_KEY;
65
+ if (key || args.phrase) {
66
+ return makeWalrusSealClient(args);
67
+ }
68
+ return makeReadOnlyWalrusSealClient(args.chain);
69
+ }
70
+
71
+ export async function countTree(folder) {
72
+ let files = 0;
73
+ let folders = 0;
74
+ const children = await folder.list();
75
+ for (const child of children) {
76
+ if (child.getContent) {
77
+ files++;
78
+ } else {
79
+ folders++;
80
+ const sub = await countTree(child);
81
+ files += sub.files;
82
+ folders += sub.folders;
83
+ }
84
+ }
85
+ return { files, folders };
86
+ }
87
+
88
+ export function hashContent(bytes) {
89
+ return createHash('sha256').update(bytes).digest('hex');
90
+ }
91
+
92
+ export async function readManifest(destPath) {
93
+ try {
94
+ const raw = await readFile(join(destPath, '.wdoublesync'), 'utf8');
95
+ return JSON.parse(raw);
96
+ } catch { return null; }
97
+ }
98
+
99
+ export async function writeManifest(destPath, manifest) {
100
+ await writeFile(join(destPath, '.wdoublesync'), JSON.stringify(manifest, null, 2));
101
+ }
102
+
103
+ export const DISK_EXCLUDES = new Set(['.wdoublesync', '.git', '.DS_Store', 'node_modules']);
104
+
105
+ export async function hashDiskTree(dirPath, prefix = []) {
106
+ const files = {};
107
+ let entries;
108
+ try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return files; }
109
+ for (const entry of entries) {
110
+ if (DISK_EXCLUDES.has(entry.name) || entry.name.startsWith('.')) continue;
111
+ const relParts = [...prefix, entry.name];
112
+ const relPath = relParts.join('/');
113
+ const fullPath = join(dirPath, entry.name);
114
+ if (entry.isFile()) {
115
+ const buf = await readFile(fullPath);
116
+ files[relPath] = hashContent(buf);
117
+ } else if (entry.isDirectory()) {
118
+ Object.assign(files, await hashDiskTree(fullPath, relParts));
119
+ }
120
+ }
121
+ return files;
122
+ }
123
+
124
+ export async function localTreeHash(dirPath) {
125
+ const fsFolder = new DoubleSyncFSFolder(dirPath);
126
+ const children = await fsFolder.list();
127
+ if (children.length === 0) return null;
128
+
129
+ const sync = new DoubleSync();
130
+ const store = new CDCStore();
131
+ const snapshotBytes = await sync.buildSnapshot({ root: fsFolder, store });
132
+ return new DoubleSyncSnapshot(snapshotBytes).treeHash;
133
+ }
134
+
135
+ export async function syncMemoryFolderToDisk(folder, destPath, oldFiles) {
136
+ const newFiles = {};
137
+ const newDirs = new Set();
138
+ const stats = { written: 0, skipped: 0, deleted: 0, folders: 0, bytes: 0 };
139
+
140
+ await walkAndSync(folder, destPath, [], oldFiles, newFiles, newDirs, stats);
141
+
142
+ if (oldFiles) {
143
+ for (const relPath of Object.keys(oldFiles)) {
144
+ if (!(relPath in newFiles)) {
145
+ const absPath = join(destPath, ...relPath.split('/'));
146
+ await rm(absPath, { force: true });
147
+ stats.deleted++;
148
+ }
149
+ }
150
+ }
151
+
152
+ await removeStaleDirectories(destPath, newDirs);
153
+
154
+ return { stats, newFiles };
155
+ }
156
+
157
+ export function hashMapsEqual(a, b) {
158
+ const keysA = Object.keys(a).sort();
159
+ const keysB = Object.keys(b).sort();
160
+ if (keysA.length !== keysB.length) return false;
161
+ return keysA.every((k, i) => k === keysB[i] && a[k] === b[k]);
162
+ }
163
+
164
+ export async function hashLocalTree(fsFolder, prefix = []) {
165
+ const files = {};
166
+ const children = await fsFolder.list();
167
+ for (const child of children) {
168
+ const relParts = [...prefix, child.name];
169
+ const relPath = relParts.join('/');
170
+ if (child.getContent) {
171
+ const content = await child.getContent();
172
+ files[relPath] = hashContent(content);
173
+ } else {
174
+ Object.assign(files, await hashLocalTree(child, relParts));
175
+ }
176
+ }
177
+ return files;
178
+ }
179
+
180
+ async function walkAndSync(folder, destPath, prefix, oldFiles, newFiles, newDirs, stats) {
181
+ const children = await folder.list();
182
+
183
+ for (const child of children) {
184
+ const relParts = [...prefix, child.name];
185
+ const relPath = relParts.join('/');
186
+ const absPath = join(destPath, child.name);
187
+
188
+ if (child.getContent) {
189
+ const content = await child.getContent();
190
+ const hash = hashContent(content);
191
+ newFiles[relPath] = hash;
192
+
193
+ if (oldFiles && oldFiles[relPath] === hash) {
194
+ stats.skipped++;
195
+ } else {
196
+ await writeFile(absPath, content);
197
+ stats.written++;
198
+ stats.bytes += content.length;
199
+ }
200
+ } else {
201
+ await mkdir(absPath, { recursive: true });
202
+ newDirs.add(relPath);
203
+ stats.folders++;
204
+ await walkAndSync(child, absPath, relParts, oldFiles, newFiles, newDirs, stats);
205
+ }
206
+ }
207
+ }
208
+
209
+ async function removeStaleDirectories(destPath, newDirs) {
210
+ let entries;
211
+ try { entries = await readdir(destPath, { withFileTypes: true }); } catch { return; }
212
+ for (const entry of entries) {
213
+ if (!entry.isDirectory()) continue;
214
+ if (DISK_EXCLUDES.has(entry.name) || entry.name.startsWith('.')) continue;
215
+ const fullPath = join(destPath, entry.name);
216
+ if (!newDirs.has(entry.name)) {
217
+ await rm(fullPath, { recursive: true, force: true });
218
+ } else {
219
+ await removeStaleSubdirs(fullPath, entry.name, newDirs);
220
+ }
221
+ }
222
+ }
223
+
224
+ async function removeStaleSubdirs(dirPath, prefix, newDirs) {
225
+ let entries;
226
+ try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return; }
227
+ for (const entry of entries) {
228
+ if (!entry.isDirectory()) continue;
229
+ const relPath = prefix + '/' + entry.name;
230
+ const fullPath = join(dirPath, entry.name);
231
+ if (!newDirs.has(relPath)) {
232
+ await rm(fullPath, { recursive: true, force: true });
233
+ } else {
234
+ await removeStaleSubdirs(fullPath, relPath, newDirs);
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,181 @@
1
+ import { watch as fsWatch } from 'node:fs';
2
+ import EndlessVector from '@fizzyflow/endless-vector';
3
+ import { WDoubleSync } from '@fizzyflow/wdoublesync';
4
+ import { DoubleSyncFSFolder } from '../DoubleSyncFSFolder.js';
5
+ import {
6
+ makeWalrusSealClient,
7
+ makeClientForRead,
8
+ resolvePath,
9
+ makeExcludes,
10
+ formatPath,
11
+ localTreeHash,
12
+ hashDiskTree,
13
+ syncMemoryFolderToDisk,
14
+ DISK_EXCLUDES,
15
+ } from './shared.js';
16
+
17
+ function timestamp() {
18
+ return new Date().toTimeString().slice(0, 8);
19
+ }
20
+
21
+ function hashesEqual(a, b) {
22
+ if (a === null && b === null) return true;
23
+ if (a === null || b === null) return false;
24
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
25
+ }
26
+
27
+ function isEnsureLengthError(err) {
28
+ return err?.message?.includes('abort code: 98') ||
29
+ err?.message?.includes('ensure_length') ||
30
+ err?.message?.includes('EUnexpectedLength');
31
+ }
32
+
33
+ export async function watch(args) {
34
+ if (!args.vectorId) throw new Error('vector-id is required for watch');
35
+
36
+ const cwd = resolvePath(args.path);
37
+ const chain = args.chain;
38
+ const debounceMs = args.debounce ?? 5000;
39
+ const pollIntervalMs = (args.pollInterval ?? 2) * 1000;
40
+ const pushEnabled = !args.pullOnly;
41
+ const pullEnabled = !args.pushOnly;
42
+
43
+ const excludes = makeExcludes(args);
44
+
45
+ const wsc = pushEnabled
46
+ ? await makeWalrusSealClient(args)
47
+ : await makeClientForRead(args);
48
+
49
+ const evParams = {
50
+ suiClient: wsc.suiClient,
51
+ id: args.vectorId,
52
+ packageId: chain,
53
+ walrusClient: wsc.walrusClient,
54
+ aggregatorUrl: wsc.aggregatorUrl,
55
+ };
56
+ if (wsc.sealClient && wsc.suiMaster) {
57
+ evParams.sealClient = wsc.sealClient;
58
+ evParams.senderAddress = wsc.suiMaster.address;
59
+ evParams.signer = wsc.suiMaster._keypair;
60
+ if (pushEnabled) {
61
+ evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
62
+ }
63
+ }
64
+
65
+ const ev = new EndlessVector({ ...evParams });
66
+ const wdsync = new WDoubleSync({
67
+ endlessVector: ev,
68
+ compress: args.compress || 'gzip',
69
+ });
70
+
71
+ await wdsync.initialize();
72
+
73
+ let remoteVersion = ev.length;
74
+ let lastSyncedHash = await localTreeHash(cwd);
75
+ let pushInFlight = false;
76
+ let debounceTimer = null;
77
+ let dirty = false;
78
+
79
+ console.log(`watching ${formatPath(cwd)}`);
80
+ console.log(` vector: ${args.vectorId}`);
81
+ console.log(` remote version: ${remoteVersion}`);
82
+ console.log(` push: ${pushEnabled ? `enabled (debounce ${debounceMs}ms)` : 'disabled'}`);
83
+ console.log(` pull: ${pullEnabled ? `enabled (poll every ${pollIntervalMs / 1000}s)` : 'disabled'}`);
84
+ console.log('press Ctrl+C to stop\n');
85
+
86
+ async function runPush() {
87
+ if (!pushEnabled || pushInFlight) return;
88
+ dirty = false;
89
+ const newHash = await localTreeHash(cwd);
90
+ if (hashesEqual(newHash, lastSyncedHash)) return;
91
+
92
+ pushInFlight = true;
93
+ try {
94
+ const fsFolder = new DoubleSyncFSFolder(cwd, excludes);
95
+ console.log(`[${timestamp()}] change detected — pushing...`);
96
+ const result = await wdsync.push(fsFolder);
97
+ lastSyncedHash = newHash;
98
+ remoteVersion = result.version;
99
+ console.log(`[${timestamp()}] pushed version ${result.version}`);
100
+ } catch (err) {
101
+ if (isEnsureLengthError(err)) {
102
+ console.log(`[${timestamp()}] conflict: remote was updated while pushing — skipping push, will pull on next poll`);
103
+ dirty = true;
104
+ } else {
105
+ console.error(`[${timestamp()}] push error:`, err.message);
106
+ }
107
+ } finally {
108
+ pushInFlight = false;
109
+ }
110
+ }
111
+
112
+ async function runPoll() {
113
+ if (!pullEnabled || pushInFlight) return;
114
+ try {
115
+ ev.reInitialize();
116
+ await ev.initialize();
117
+ if (ev.length <= remoteVersion) return;
118
+
119
+ const newRemoteVersion = ev.length;
120
+
121
+ // Always resync wdsync when remote has advanced. Without this,
122
+ // wdsync._lastSnapshot stays at the old version while ev.length has
123
+ // advanced — the next push would build a diff against the wrong base
124
+ // snapshot, corrupting the chain.
125
+ wdsync.reInitialize();
126
+
127
+ const localNow = await localTreeHash(cwd);
128
+
129
+ if (!hashesEqual(localNow, lastSyncedHash)) {
130
+ console.log(`[${timestamp()}] remote has ${newRemoteVersion - remoteVersion} new version(s) but local has unpushed changes — skipping pull`);
131
+ return;
132
+ }
133
+
134
+ console.log(`[${timestamp()}] remote advanced to version ${newRemoteVersion} — pulling...`);
135
+ let folder;
136
+ try {
137
+ folder = await wdsync.restore();
138
+ } catch (err) {
139
+ console.error(`[${timestamp()}] pull failed (${err.message}) — skipping to version ${newRemoteVersion}; run \`pull\` manually`);
140
+ remoteVersion = newRemoteVersion;
141
+ return;
142
+ }
143
+ const oldFiles = await hashDiskTree(cwd);
144
+ const { stats } = await syncMemoryFolderToDisk(folder, cwd, oldFiles);
145
+ lastSyncedHash = await localTreeHash(cwd);
146
+ remoteVersion = newRemoteVersion;
147
+ console.log(`[${timestamp()}] pulled version ${remoteVersion} (${stats.written} written, ${stats.skipped} unchanged, ${stats.deleted} deleted)`);
148
+ } catch (err) {
149
+ console.error(`[${timestamp()}] poll error:`, err.message);
150
+ }
151
+ }
152
+
153
+ const watcher = fsWatch(cwd, { recursive: true }, (eventType, filename) => {
154
+ if (!pushEnabled) return;
155
+ if (!filename) return;
156
+ const base = filename.split('/')[0];
157
+ if (DISK_EXCLUDES.has(base) || base.startsWith('.')) return;
158
+ dirty = true;
159
+ clearTimeout(debounceTimer);
160
+ debounceTimer = setTimeout(runPush, debounceMs);
161
+ });
162
+
163
+ const poller = setInterval(runPoll, pollIntervalMs);
164
+
165
+ let shutdownResolve;
166
+ const shutdownPromise = new Promise((resolve) => {
167
+ shutdownResolve = resolve;
168
+ });
169
+
170
+ function shutdown() {
171
+ console.log('\nstopping watch...');
172
+ clearTimeout(debounceTimer);
173
+ clearInterval(poller);
174
+ watcher.close();
175
+ shutdownResolve();
176
+ }
177
+ process.once('SIGINT', shutdown);
178
+ process.once('SIGTERM', shutdown);
179
+
180
+ await shutdownPromise;
181
+ }
package/lib/commands.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { mkdir, writeFile, readFile, readdir, rm } from 'node:fs/promises';
2
2
  import { watch as fsWatch } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { homedir } from 'node:os';
6
+
7
+ function resolvePath(p) {
8
+ if (p.startsWith('~')) return join(homedir(), p.slice(1));
9
+ return resolve(p);
10
+ }
6
11
  import { SuiMaster } from 'suidouble';
7
12
  import EndlessVector from '@fizzyflow/endless-vector';
8
13
  import { WDoubleSync } from '@fizzyflow/wdoublesync';
9
14
  import { DoubleSync, CDCStore, DoubleSyncSnapshot } from '@fizzyflow/doublesync';
10
- import { WalrusSealClient } from 'walrus-seal-client-with-local';
15
+ import WalrusSealClient from './WalrusSealClient.js';
11
16
  import { DoubleSyncFSFolder } from './DoubleSyncFSFolder.js';
12
17
 
13
18
  async function makeWalrusSealClient(args) {
@@ -81,7 +86,7 @@ export async function push(args) {
81
86
  const excludes = args.exclude
82
87
  ? args.exclude.split(',').map(s => s.trim())
83
88
  : undefined;
84
- const destPath = process.cwd();
89
+ const destPath = resolvePath(args.path);
85
90
  const fsFolder = new DoubleSyncFSFolder(destPath, excludes);
86
91
 
87
92
  const counts = await countTree(fsFolder);
@@ -202,7 +207,7 @@ export async function pull(args) {
202
207
  throw err;
203
208
  }
204
209
 
205
- const destPath = process.cwd();
210
+ const destPath = resolvePath(args.path);
206
211
  const oldFiles = await hashDiskTree(destPath);
207
212
 
208
213
  const { stats, newFiles } = await syncMemoryFolderToDisk(folder, destPath, oldFiles);
@@ -218,12 +223,16 @@ export async function pull(args) {
218
223
  }
219
224
 
220
225
  export async function info(args) {
226
+ const wsc = await makeClientForRead(args);
227
+
228
+ console.log('wdoublesync');
229
+ console.log(' chain: ', args.chain);
230
+ console.log(' your wallet: ', wsc.suiMaster?.address || '(none)');
231
+
221
232
  if (!args.vectorId) {
222
- throw new Error('vector-id is required for info');
233
+ return;
223
234
  }
224
235
 
225
- const wsc = await makeClientForRead(args);
226
-
227
236
  const evParams = {
228
237
  suiClient: wsc.suiClient,
229
238
  id: args.vectorId,
@@ -244,14 +253,35 @@ export async function info(args) {
244
253
 
245
254
  const totalVersions = ev.length;
246
255
 
247
- console.log('EndlessVector:', args.vectorId);
248
- console.log(' chain: ', args.chain);
256
+ console.log('\nEndlessVector:', args.vectorId);
257
+
258
+ let owner = '(unknown)';
259
+ if (wsc.suiMaster) {
260
+ try {
261
+ const obj = await wsc.suiMaster.getObject(args.vectorId);
262
+ if (obj?._owner) {
263
+ const ownerData = obj._owner;
264
+ if (typeof ownerData === 'string') {
265
+ owner = ownerData;
266
+ } else if (ownerData.AddressOwner) {
267
+ owner = ownerData.AddressOwner;
268
+ } else if (ownerData.ObjectOwner) {
269
+ owner = ownerData.ObjectOwner;
270
+ }
271
+ }
272
+ } catch (err) {
273
+ // silently fail, owner stays unknown
274
+ }
275
+ }
276
+
277
+ console.log(' owner: ', owner);
278
+ console.log(' encrypted: ', ev.sealEncryptedKey ? 'yes (Seal)' : 'no');
249
279
  console.log(' versions: ', totalVersions);
250
280
  console.log(' binary size: ', formatBytes(ev.binaryLength));
251
281
  console.log(' history items: ', ev.historyItemsCount);
252
282
  console.log(' archives: ', ev.archiveItemsCount);
253
283
 
254
- const destPath = process.cwd();
284
+ const destPath = resolvePath(args.path);
255
285
  const localHash = await localTreeHash(destPath);
256
286
 
257
287
  if (!localHash) {
@@ -451,7 +481,7 @@ async function removeStaleSubdirs(dirPath, prefix, newDirs) {
451
481
  export async function watch(args) {
452
482
  if (!args.vectorId) throw new Error('vector-id is required for watch');
453
483
 
454
- const cwd = process.cwd();
484
+ const cwd = resolvePath(args.path);
455
485
  const chain = args.chain;
456
486
  const debounceMs = args.debounce ?? 5000;
457
487
  const pollIntervalMs = (args.pollInterval ?? 2) * 1000;
@@ -624,7 +654,7 @@ export async function rebate(args) {
624
654
  throw new Error('vector-id is required for rebate');
625
655
  }
626
656
 
627
- const destPath = process.cwd();
657
+ const destPath = resolvePath(args.path);
628
658
  const excludes = args.exclude
629
659
  ? args.exclude.split(',').map(s => s.trim())
630
660
  : undefined;
@@ -663,6 +693,9 @@ export async function rebate(args) {
663
693
  await ev.burnArchive();
664
694
  console.log(' burned');
665
695
 
696
+ // archive() and burnArchive() both call ev.reInitialize() — refetch fresh state
697
+ await ev.initialize();
698
+
666
699
  console.log('step 3: pushing local state as fresh snapshot...');
667
700
 
668
701
  const wdsync = new WDoubleSync({
@@ -670,12 +703,13 @@ export async function rebate(args) {
670
703
  compress: args.compress || 'gzip',
671
704
  });
672
705
 
673
- // Force fresh snapshot by resetting internal state to prevent replay of broken diffs
706
+ // Skip wdsync.initialize() replay: rebate pushes a full snapshot, no need to replay chain.
707
+ // _lastSnapshot = null forces a full snapshot; _isInitialized = true bypasses replay.
674
708
  wdsync._senderStore = new CDCStore({ copyBytes: false });
675
709
  wdsync._receiverMirror = new CDCStore();
676
710
  wdsync._lastSnapshot = null;
677
- wdsync._replayedCount = 0;
678
- wdsync._isInitialized = false;
711
+ wdsync._replayedCount = ev.length;
712
+ wdsync._isInitialized = true;
679
713
 
680
714
  const result = await wdsync.push(fsFolder);
681
715
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fizzyflow/wdoublesync_cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "CLI tool built on the wdoublesync library to sync local folders to Walrus decentralised storage on the Sui network. Stores versioned gzip-compressed snapshots and diffs inside an EndlessVector on-chain object. Supports Seal encryption, manifest-based fast change detection, and full version history with point-in-time restore.",
6
6
  "bin": {
@@ -27,9 +27,8 @@
27
27
  "@fizzyflow/wdoublesync": "^0.1.7",
28
28
  "@fizzyflow/endless-vector": "^0.0.13",
29
29
  "@mysten/seal": "^1.1.3",
30
+ "@mysten/sui": "^2.19.0",
31
+ "@mysten/walrus": "^1.2.1",
30
32
  "@noble/ciphers": "^2.2.0"
31
- },
32
- "devDependencies": {
33
- "walrus-seal-client-with-local": "file:../seal_walrus_localnet"
34
33
  }
35
34
  }