@fizzyflow/wdoublesync_cli 1.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/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # wdoublesync cli
2
+
3
+ CLI tool to sync local folders to [Walrus](https://walrus.xyz) decentralised storage on the Sui network.
4
+
5
+ Built on top of the [`wdoublesync`](https://github.com/fizzyFlow/wdoublesync) library.
6
+
7
+ Each `push` stores a versioned, gzip-compressed snapshot (or diff) inside an [EndlessVector](https://github.com/fizzyFlow/endless_vector) on-chain object. Any past version can be restored at any time with `pull`. Folders can optionally be encrypted with [Seal](https://github.com/MystenLabs/seal).
8
+
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pnpm install
14
+ # make the binary globally available (optional)
15
+ pnpm link --global
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```
21
+ wdoublesync push [vector-id] [options] Sync current folder to a vector (creates one if no id given)
22
+ wdoublesync pull <vector-id> [options] Restore vector contents to the current folder
23
+ wdoublesync info <vector-id> [options] Show vector metadata and local sync status
24
+ wdoublesync watch <vector-id> [options] Watch folder and auto-push on changes, auto-pull on remote updates
25
+ ```
26
+
27
+ ### Options
28
+
29
+ | Flag | Description |
30
+ |---|---|
31
+ | `--chain <name>` | Chain: `mainnet`, `testnet`, `devnet`, `localnet` (default: `testnet`) |
32
+ | `--key <suiprivkey>` | Sui private key (or set `WDOUBLESYNC_KEY` env var) |
33
+ | `--phrase <mnemonic>` | Mnemonic phrase instead of a raw key |
34
+ | `--version <n>` | Version to restore (`pull` only, default: latest) |
35
+ | `--exclude <p1,p2>` | Extra exclude patterns (comma-separated) |
36
+ | `--no-compress` | Disable gzip compression |
37
+ | `--manifest` | Write/use `.wdoublesync` manifest for faster change detection |
38
+ | `--force-snapshot` | Push a full snapshot regardless of prior history (repairs a corrupt vector) |
39
+ | `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
40
+ | `--debounce <ms>` | `watch`: quiet period in ms before pushing after a local change (default: `1000`) |
41
+ | `--push-only` | `watch`: disable auto-pull |
42
+ | `--pull-only` | `watch`: disable auto-push |
43
+ | `--help` | Show help |
44
+
45
+ ### Authentication
46
+
47
+ Supply a signing key in one of three ways (checked in order):
48
+
49
+ 1. `--key suiprivkey1...`
50
+ 2. `--phrase "word1 word2 ..."`
51
+ 3. `WDOUBLESYNC_KEY` environment variable
52
+
53
+ `pull` and `info` work without a key for public (unencrypted) vectors. A key is required for Seal-encrypted vectors and for any `push`.
54
+
55
+ ## Examples
56
+
57
+ ### Push current folder (first time)
58
+
59
+ ```bash
60
+ cd my-project
61
+ wdoublesync push --chain testnet --key suiprivkey1...
62
+ # prints the new vector id, e.g.:
63
+ # created: 0xabc123...
64
+ # version 1 pushed (full snapshot, gzip compressed)
65
+ ```
66
+
67
+ ### Push an update
68
+
69
+ ```bash
70
+ wdoublesync push 0xabc123... --chain testnet --key suiprivkey1...
71
+ # version 2 pushed (diff, gzip compressed)
72
+ ```
73
+
74
+ ### Pull the latest version into the current folder
75
+
76
+ ```bash
77
+ mkdir restored && cd restored
78
+ wdoublesync pull 0xabc123... --chain testnet
79
+ ```
80
+
81
+ ### Pull a specific version
82
+
83
+ ```bash
84
+ wdoublesync pull 0xabc123... --chain testnet --version 1
85
+ ```
86
+
87
+ ### Inspect a vector without touching the local folder
88
+
89
+ ```bash
90
+ wdoublesync info 0xabc123... --chain testnet
91
+ ```
92
+
93
+ ### Fast incremental pushes with a manifest
94
+
95
+ ```bash
96
+ wdoublesync push 0xabc123... --chain testnet --key suiprivkey1... --manifest
97
+ # subsequent pushes are skipped when nothing has changed locally
98
+ ```
99
+
100
+ ### Watch a folder (auto push + pull)
101
+
102
+ ```bash
103
+ wdoublesync watch 0xabc123... --chain testnet --key suiprivkey1...
104
+ # pushes local changes 1 s after the last edit
105
+ # pulls remote updates every 2 s
106
+ # Ctrl-C to stop
107
+ ```
108
+
109
+ Tune the timing:
110
+
111
+ ```bash
112
+ wdoublesync watch 0xabc123... --debounce 3000 --poll-interval 5
113
+ ```
114
+
115
+ Watch in push-only or pull-only mode:
116
+
117
+ ```bash
118
+ wdoublesync watch 0xabc123... --push-only # no auto-pull
119
+ wdoublesync watch 0xabc123... --pull-only # no auto-push (read-only mirror)
120
+ ```
121
+
122
+ ### Repair a corrupt vector with a force snapshot
123
+
124
+ If a diff patch was pushed against a stale base (e.g. a race condition in `watch`), subsequent pulls will fail. Fix it by pushing a new full snapshot:
125
+
126
+ ```bash
127
+ wdoublesync push 0xabc123... --chain testnet --key suiprivkey1... --force-snapshot
128
+ # skips chain replay and pushes the current folder as a self-contained full snapshot
129
+ # restore() will recover from this snapshot, skipping any corrupt diffs before it
130
+ ```
131
+
132
+ ## How it works
133
+
134
+ 1. **push** — scans the current directory, computes a tree hash, and compares it against the last stored version. If changes are detected, a compressed diff (or full snapshot on the first push) is uploaded to Walrus and appended to the EndlessVector on-chain.
135
+ 2. **pull** — reads the requested version from the EndlessVector, decrypts it if Seal-encrypted, and writes only changed files to disk. Files absent from the stored version are deleted.
136
+ 3. **info** — reads EndlessVector metadata from the chain (version count, binary size, history) and checks whether the local folder matches any stored version.
137
+ 4. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced push on local changes. A poll interval checks the remote vector for new versions and pulls them if found. Push and pull never run concurrently.
138
+
139
+ ### Default excludes
140
+
141
+ The following are always excluded from snapshots: `node_modules`, `.git`, `.env`, `.DS_Store`, `.wdoublesync`, `pnpm-lock.yaml`, `package-lock.json`. Add more with `--exclude`.
142
+
143
+ ## Dependencies
144
+
145
+ | Package | Role |
146
+ |---|---|
147
+ | `@fizzyflow/wdoublesync` | Folder-diff / snapshot layer |
148
+ | `@fizzyflow/doublesync` | Core CDC store and snapshot primitives |
149
+ | `@fizzyflow/endless-vector` | On-chain EndlessVector (Sui + Walrus + Seal) |
150
+ | `suidouble` | Sui client / key management |
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { push, pull, info, watch, rebate } from '../lib/commands.js';
4
+
5
+ const USAGE = `wdoublesync — sync local folders to EndlessVector on Sui+Walrus
6
+
7
+ Usage:
8
+ wdoublesync push [vector-id] [options] Sync current folder to vector (creates if no id)
9
+ wdoublesync pull <vector-id> [options] Restore vector contents to current folder
10
+ wdoublesync info <vector-id> [options] Show vector metadata
11
+ wdoublesync watch <vector-id> [options] Watch folder and auto-push/pull
12
+ wdoublesync rebate <vector-id> [options] Archive history, burn archives, push fresh snapshot
13
+
14
+ Options:
15
+ --chain <name> Chain: mainnet, testnet, devnet, localnet (default: testnet)
16
+ --key <suiprivkey> Sui private key (or set WDOUBLESYNC_KEY env var)
17
+ --phrase <mnemonic> Mnemonic phrase
18
+ --version <n> Version to restore (pull only, default: latest)
19
+ --exclude <p1,p2> Extra exclude patterns (comma-separated)
20
+ --no-compress Disable gzip compression
21
+ --manifest Write/use .wdoublesync manifest for faster change detection
22
+ --force-snapshot Push a full snapshot regardless of prior history (repairs corrupt vectors)
23
+ --poll-interval <s> Watch: seconds between remote checks (default: 2)
24
+ --debounce <ms> Watch: ms quiet period before pushing after a change (default: 1000)
25
+ --push-only Watch: disable auto-pull
26
+ --pull-only Watch: disable auto-push
27
+ --help Show this help
28
+ `;
29
+
30
+ function parseArgs(argv) {
31
+ const args = {
32
+ command: null,
33
+ vectorId: null,
34
+ chain: 'testnet',
35
+ key: null,
36
+ phrase: null,
37
+ version: null,
38
+ exclude: null,
39
+ compress: 'gzip',
40
+ manifest: false,
41
+ forceSnapshot: false,
42
+ pollInterval: 2,
43
+ debounce: 1000,
44
+ pushOnly: false,
45
+ pullOnly: false,
46
+ };
47
+
48
+ const raw = argv.slice(2);
49
+
50
+ if (raw.length === 0 || raw.includes('--help')) {
51
+ console.log(USAGE);
52
+ process.exit(0);
53
+ }
54
+
55
+ args.command = raw[0];
56
+
57
+ let i = 1;
58
+
59
+ // Next positional arg is vector-id if it looks like one (starts with 0x)
60
+ if (i < raw.length && !raw[i].startsWith('--')) {
61
+ args.vectorId = raw[i];
62
+ i++;
63
+ }
64
+
65
+ while (i < raw.length) {
66
+ const flag = raw[i];
67
+ if (flag === '--chain' && i + 1 < raw.length) {
68
+ args.chain = raw[++i];
69
+ } else if (flag === '--key' && i + 1 < raw.length) {
70
+ args.key = raw[++i];
71
+ } else if (flag === '--phrase' && i + 1 < raw.length) {
72
+ args.phrase = raw[++i];
73
+ } else if (flag === '--version' && i + 1 < raw.length) {
74
+ args.version = raw[++i];
75
+ } else if (flag === '--exclude' && i + 1 < raw.length) {
76
+ args.exclude = raw[++i];
77
+ } else if (flag === '--no-compress') {
78
+ args.compress = false;
79
+ } else if (flag === '--manifest') {
80
+ args.manifest = true;
81
+ } else if (flag === '--force-snapshot') {
82
+ args.forceSnapshot = true;
83
+ } else if (flag === '--poll-interval' && i + 1 < raw.length) {
84
+ args.pollInterval = Number(raw[++i]);
85
+ } else if (flag === '--debounce' && i + 1 < raw.length) {
86
+ args.debounce = Number(raw[++i]);
87
+ } else if (flag === '--push-only') {
88
+ args.pushOnly = true;
89
+ } else if (flag === '--pull-only') {
90
+ args.pullOnly = true;
91
+ }
92
+ i++;
93
+ }
94
+
95
+ return args;
96
+ }
97
+
98
+ const args = parseArgs(process.argv);
99
+
100
+ try {
101
+ if (args.command === 'push') {
102
+ await push(args);
103
+ } else if (args.command === 'pull') {
104
+ await pull(args);
105
+ } else if (args.command === 'info') {
106
+ await info(args);
107
+ } else if (args.command === 'watch') {
108
+ await watch(args);
109
+ } else if (args.command === 'rebate') {
110
+ await rebate(args);
111
+ } else {
112
+ console.error('Unknown command:', args.command);
113
+ console.log(USAGE);
114
+ process.exit(1);
115
+ }
116
+ process.exit(0);
117
+ } catch (err) {
118
+ console.error('Error:', err.message);
119
+ if (err.stack) console.error(err.stack);
120
+ process.exit(1);
121
+ }
@@ -0,0 +1,23 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { DoubleSyncFile } from '@fizzyflow/doublesync';
4
+
5
+ export class DoubleSyncFSFile extends DoubleSyncFile {
6
+ constructor(filePath) {
7
+ super();
8
+ this._path = filePath;
9
+ this._name = basename(filePath);
10
+ }
11
+
12
+ get name() { return this._name; }
13
+
14
+ async getContent() {
15
+ const buf = await readFile(this._path);
16
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
17
+ }
18
+
19
+ async getSize() {
20
+ const s = await stat(this._path);
21
+ return s.size;
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join, basename } from 'node:path';
3
+ import { DoubleSyncFolder } from '@fizzyflow/doublesync';
4
+ import { DoubleSyncFSFile } from './DoubleSyncFSFile.js';
5
+
6
+ const DEFAULT_EXCLUDES = ['node_modules', '.git', '.env', '.DS_Store', '.wdoublesync', 'pnpm-lock.yaml', 'package-lock.json'];
7
+
8
+ export class DoubleSyncFSFolder extends DoubleSyncFolder {
9
+ constructor(dirPath, excludes = DEFAULT_EXCLUDES) {
10
+ super();
11
+ this._path = dirPath;
12
+ this._name = basename(dirPath);
13
+ this._excludes = excludes;
14
+ }
15
+
16
+ get name() { return this._name; }
17
+
18
+ async list() {
19
+ const entries = await readdir(this._path, { withFileTypes: true });
20
+ const children = [];
21
+
22
+ for (const entry of entries) {
23
+ if (this._excludes.includes(entry.name)) continue;
24
+ if (entry.name.startsWith('.')) continue;
25
+
26
+ const fullPath = join(this._path, entry.name);
27
+
28
+ if (entry.isDirectory()) {
29
+ children.push(new DoubleSyncFSFolder(fullPath, this._excludes));
30
+ } else if (entry.isFile()) {
31
+ children.push(new DoubleSyncFSFile(fullPath));
32
+ }
33
+ }
34
+
35
+ return children;
36
+ }
37
+ }