@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/LICENSE +661 -0
- package/README.md +150 -0
- package/bin/wdoublesync.js +121 -0
- package/lib/DoubleSyncFSFile.js +23 -0
- package/lib/DoubleSyncFSFolder.js +37 -0
- package/lib/commands.js +692 -0
- package/package.json +35 -0
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
|
+
}
|