mnemonad-cli 0.1.0 → 0.2.0
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 +78 -13
- package/bin/mnemonad.js +54 -7
- package/lib/chainClient.js +8 -1
- package/lib/commands/burn.js +59 -0
- package/lib/commands/compact.js +6 -1
- package/lib/commands/diff.js +24 -5
- package/lib/commands/index.js +1 -0
- package/lib/commands/info.js +41 -1
- package/lib/commands/pull.js +12 -5
- package/lib/commands/push.js +17 -6
- package/lib/commands/shared.js +134 -9
- package/lib/commands/watch.js +8 -3
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,8 +12,23 @@ signer's own wallet key.
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
### From npm (recommended)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g mnemonad-cli
|
|
19
|
+
mnemonad --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
To pick up a new release later:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm update -g mnemonad-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### From this repo (for development)
|
|
29
|
+
|
|
30
|
+
This package lives in the monorepo alongside `mnemonad` and `monadsync`, linked through a
|
|
31
|
+
pnpm workspace at the repo root. From the repo root:
|
|
17
32
|
|
|
18
33
|
```bash
|
|
19
34
|
cd cli
|
|
@@ -21,7 +36,7 @@ pnpm install
|
|
|
21
36
|
node bin/mnemonad.js --help
|
|
22
37
|
```
|
|
23
38
|
|
|
24
|
-
Or link it for a global `mnemonad` command:
|
|
39
|
+
Or link it for a global `mnemonad` command that tracks your local checkout:
|
|
25
40
|
|
|
26
41
|
```bash
|
|
27
42
|
cd cli
|
|
@@ -47,15 +62,22 @@ mnemonad info [stream-id] [path] [options] Show chain/wallet info, or full s
|
|
|
47
62
|
mnemonad diff <stream-id> <path> [options] List files added/modified/deleted locally vs the on-chain version
|
|
48
63
|
mnemonad watch <stream-id> <path> [options] Watch folder: auto-push on changes, auto-pull on remote updates
|
|
49
64
|
mnemonad compact <stream-id> <path> [options] Truncate old history and push folder as a fresh snapshot
|
|
65
|
+
mnemonad burn <stream-id> --yes [options] Permanently destroy a stream — irreversible
|
|
50
66
|
```
|
|
51
67
|
|
|
52
68
|
`<path>` is always required for any command that touches a folder — there is no need to
|
|
53
69
|
`cd` into the folder first, but there's also no falling back to the current working
|
|
54
70
|
directory: an omitted or mistyped path fails fast with an error rather than silently
|
|
55
|
-
operating on wherever the CLI happened to be run from. The
|
|
56
|
-
|
|
57
|
-
chain/wallet). A stream id is recognized as a
|
|
58
|
-
|
|
71
|
+
operating on wherever the CLI happened to be run from. The commands that can run with no
|
|
72
|
+
path at all are `mnemonad info` with no stream id either (it just checks your configured
|
|
73
|
+
chain/wallet) and `mnemonad burn`, which never takes one. A stream id is recognized as a
|
|
74
|
+
bare decimal number (`123`), a
|
|
75
|
+
`0x`-prefixed hex string, or the network-qualified form the
|
|
76
|
+
[explorer dApp](https://mnemonad.vercel.app)'s own URLs use —
|
|
77
|
+
`testnet-0x648f18…` — pasted straight out of a `/stream/<id>` link. That form also sets
|
|
78
|
+
`--chain` to match; pass `--chain` too only if it agrees, or the command refuses rather than
|
|
79
|
+
guess which one you meant. Every command's own output always prints the bare id in hex
|
|
80
|
+
(`0x` + the full 32-byte token id), matching the explorer's own display.
|
|
59
81
|
|
|
60
82
|
Every command prints the installed CLI version as its first line of output.
|
|
61
83
|
|
|
@@ -76,12 +98,13 @@ Every command prints the installed CLI version as its first line of output.
|
|
|
76
98
|
| `--encrypt-with <method>` | How to key it: `password` (needs `--password`), or `wallet` — the `--key`/`--phrase` account signs for it, and only that account can ever decrypt it. Implied by `--password` |
|
|
77
99
|
| `--password <pw>` | Password to encrypt a new stream with, or decrypt an existing one |
|
|
78
100
|
| `--pinata-jwt <jwt>` | Pinata JWT for IPFS offload of large files (or `PINATA_JWT` env var) |
|
|
79
|
-
| `--manifest` | Write/use `.mnemonad` manifest for faster change detection |
|
|
101
|
+
| `--manifest` | Write/use `.mnemonad` manifest for faster change detection. With no stream-id given, every command recovers it from an existing `.mnemonad` in the folder instead of requiring it explicitly |
|
|
80
102
|
| `--force-snapshot` | Push a full snapshot regardless of prior history (repairs a corrupt stream) |
|
|
81
103
|
| `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
|
|
82
104
|
| `--debounce <ms>` | `watch`: quiet period in ms before pushing after a local change (default: `1000`) |
|
|
83
105
|
| `--push-only` | `watch`: disable auto-pull |
|
|
84
106
|
| `--pull-only` | `watch`: disable auto-push |
|
|
107
|
+
| `--yes` | Required by `burn` — explicit opt-in for an irreversible action, never assumed |
|
|
85
108
|
| `--help` | Show help |
|
|
86
109
|
|
|
87
110
|
### Authentication
|
|
@@ -116,13 +139,19 @@ chain.
|
|
|
116
139
|
```bash
|
|
117
140
|
mnemonad push ~/my-data
|
|
118
141
|
|
|
119
|
-
# prints the new stream id, e.g.:
|
|
120
|
-
# created:
|
|
142
|
+
# prints the new stream id in hex, e.g.:
|
|
143
|
+
# created: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
121
144
|
# version 1 pushed (full snapshot, gzip compressed)
|
|
145
|
+
|
|
146
|
+
# examples below use `123` as a stand-in for whatever id you actually got back —
|
|
147
|
+
# decimal or hex both work as input, see "Usage" above
|
|
122
148
|
```
|
|
123
149
|
|
|
124
150
|
By default a new stream is public (unencrypted). Encrypting one takes a method — the CLI
|
|
125
|
-
never picks for you, since both are reachable from the same invocation
|
|
151
|
+
never picks for you, since both are reachable from the same invocation. See
|
|
152
|
+
[`../docs/encryption/encryption.md`](../docs/encryption/encryption.md) (and its
|
|
153
|
+
[password](../docs/encryption/password.md)/[signature](../docs/encryption/signature.md)
|
|
154
|
+
pages) for how each actually works and the trade-off each makes:
|
|
126
155
|
|
|
127
156
|
```bash
|
|
128
157
|
# Keyed by a password: anyone who knows it can read the stream, from any machine.
|
|
@@ -172,7 +201,7 @@ mnemonad info 123 ~/my-data
|
|
|
172
201
|
chain: testnet
|
|
173
202
|
your wallet: 0x06bc6420b37a4898424429dcfa236f0065e12279
|
|
174
203
|
|
|
175
|
-
stream:
|
|
204
|
+
stream: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
176
205
|
owner: 0x06bc6420b37a4898424429dcfa236f0065e12279 (you)
|
|
177
206
|
encrypted: no
|
|
178
207
|
versions: 3
|
|
@@ -213,6 +242,23 @@ mnemonad push 123 ~/my-data --manifest
|
|
|
213
242
|
# subsequent pushes are skipped when nothing has changed locally
|
|
214
243
|
```
|
|
215
244
|
|
|
245
|
+
`--manifest` writes `.mnemonad` into the folder, recording the stream id, version and file
|
|
246
|
+
hashes. With it, later commands on the same folder don't need the id typed again:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
mnemonad push ~/my-data --manifest # no id — read from .mnemonad
|
|
250
|
+
mnemonad pull ~/my-data --manifest # same
|
|
251
|
+
mnemonad diff ~/my-data --manifest # same
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If no id is given and there's no `.mnemonad` yet (or `--manifest` wasn't passed), the usual
|
|
255
|
+
`stream-id is required` error is thrown.
|
|
256
|
+
|
|
257
|
+
`info` is the one exception that doesn't require `--manifest` to react to a manifest: being
|
|
258
|
+
read-only, it always peeks at `.mnemonad` in `<path>` and reports what it finds — a note that
|
|
259
|
+
one's available (with its id) if you left `--manifest` off, or a note if the id you gave
|
|
260
|
+
doesn't match the one recorded in the folder's own manifest.
|
|
261
|
+
|
|
216
262
|
### Watch a folder (auto push + pull)
|
|
217
263
|
|
|
218
264
|
```bash
|
|
@@ -246,6 +292,23 @@ just a faster-replay optimization.
|
|
|
246
292
|
mnemonad compact 123 ~/my-data
|
|
247
293
|
```
|
|
248
294
|
|
|
295
|
+
### Burn a stream
|
|
296
|
+
|
|
297
|
+
Permanently destroys the stream: the ERC-721 token and its metadata are gone, and — because
|
|
298
|
+
ids are deterministic — re-creating a stream with the same id afterward does **not** recover
|
|
299
|
+
the old data; the contract deliberately marks a burned id as permanently used. Deployed
|
|
300
|
+
CODE-tier item bytes physically survive on-chain regardless — this removes the stream as a
|
|
301
|
+
usable, readable thing, not necessarily every trace of its bytes. Requires **`--yes`**: run
|
|
302
|
+
without it first to see what would be destroyed (owner, version count, on-chain size) before
|
|
303
|
+
committing.
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
mnemonad burn 123
|
|
307
|
+
# prints stream/chain/versions/size, then refuses to proceed without --yes
|
|
308
|
+
|
|
309
|
+
mnemonad burn 123 --yes
|
|
310
|
+
```
|
|
311
|
+
|
|
249
312
|
### Repair a corrupt stream with a force snapshot
|
|
250
313
|
|
|
251
314
|
If a diff patch was pushed against a stale base (e.g. a race condition in `watch`),
|
|
@@ -266,8 +329,10 @@ export MNEMONAD_KEY=0xabc123...
|
|
|
266
329
|
|
|
267
330
|
# 1. first push — creates the stream
|
|
268
331
|
mnemonad push ~/my-data
|
|
269
|
-
# created:
|
|
332
|
+
# created: 0x68d9736257f0d2da3c848666a4b36c4b9f18eda7e4dfba655599c47560d8a4b5
|
|
270
333
|
# version 1 pushed (full snapshot, gzip compressed)
|
|
334
|
+
# (the rest of this walkthrough keeps using 123 as a stand-in for that id — both
|
|
335
|
+
# forms work as input, see "Usage" above)
|
|
271
336
|
|
|
272
337
|
# 2. verify: restore into a scratch folder and compare against the original
|
|
273
338
|
mnemonad pull 123 ~/verify-tmp
|
package/bin/mnemonad.js
CHANGED
|
@@ -17,7 +17,8 @@ import { readFileSync } from 'node:fs';
|
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
|
|
20
|
-
import { push, pull, info, diff, watch, compact } from '../lib/commands/index.js';
|
|
20
|
+
import { push, pull, info, diff, watch, compact, burn } from '../lib/commands/index.js';
|
|
21
|
+
import { parseQualifiedStreamId } from '../lib/commands/shared.js';
|
|
21
22
|
import defaultConfig from '../mnemonad.config.js';
|
|
22
23
|
|
|
23
24
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -56,10 +57,15 @@ Usage:
|
|
|
56
57
|
mnemonad diff <stream-id> <path> [options] List local files added/modified/deleted vs the on-chain version
|
|
57
58
|
mnemonad watch <stream-id> <path> [options] Watch folder and auto-push/pull
|
|
58
59
|
mnemonad compact <stream-id> <path> [options] Truncate old history, push a fresh snapshot
|
|
60
|
+
mnemonad burn <stream-id> --yes [options] Permanently destroy a stream (irreversible)
|
|
59
61
|
|
|
60
62
|
<path> is always required wherever a command touches a folder — the CLI never assumes the
|
|
61
|
-
current directory. The
|
|
62
|
-
|
|
63
|
+
current directory. The exceptions are \`mnemonad info\` with no stream-id (just checks your
|
|
64
|
+
configured chain/wallet) and \`mnemonad burn\`, which takes no folder at all.
|
|
65
|
+
|
|
66
|
+
<stream-id> also accepts the network-qualified form shown in the explorer's URLs —
|
|
67
|
+
testnet-0x648f18… — paste it straight out of a /stream/<id> link. It sets --chain to match;
|
|
68
|
+
pass --chain too only if it agrees, or the command refuses rather than guess which one you meant.
|
|
63
69
|
|
|
64
70
|
Options:
|
|
65
71
|
--chain <name> Chain: testnet, mainnet, local (default: testnet)
|
|
@@ -80,12 +86,15 @@ Options:
|
|
|
80
86
|
signs for it). Implied by --password; required otherwise
|
|
81
87
|
--password <pw> Password to encrypt a new stream with, or decrypt an existing one
|
|
82
88
|
--pinata-jwt <jwt> Pinata JWT for IPFS offload of large files (or PINATA_JWT env var)
|
|
83
|
-
--manifest Write/use .mnemonad manifest for faster change detection
|
|
89
|
+
--manifest Write/use .mnemonad manifest for faster change detection.
|
|
90
|
+
With no stream-id given, every command recovers it from an
|
|
91
|
+
existing .mnemonad in <path> instead of requiring it explicitly
|
|
84
92
|
--force-snapshot Push a full snapshot regardless of prior history (repairs corrupt streams)
|
|
85
93
|
--poll-interval <s> Watch: seconds between remote checks (default: 2)
|
|
86
94
|
--debounce <ms> Watch: ms quiet period before pushing after a change (default: 1000)
|
|
87
95
|
--push-only Watch: disable auto-pull
|
|
88
96
|
--pull-only Watch: disable auto-push
|
|
97
|
+
--yes Required by burn — explicit opt-in for an irreversible action
|
|
89
98
|
--help Show this help
|
|
90
99
|
`;
|
|
91
100
|
|
|
@@ -116,6 +125,7 @@ function parseArgs(argv) {
|
|
|
116
125
|
debounce: 1000,
|
|
117
126
|
pushOnly: false,
|
|
118
127
|
pullOnly: false,
|
|
128
|
+
yes: false,
|
|
119
129
|
};
|
|
120
130
|
|
|
121
131
|
const raw = argv.slice(2);
|
|
@@ -134,27 +144,48 @@ function parseArgs(argv) {
|
|
|
134
144
|
// command except a bare `info`) fails fast with a clear error instead of silently
|
|
135
145
|
// operating on whatever directory the CLI happened to be run from.
|
|
136
146
|
// A stream id is a decimal bigint or a 0x-prefixed hex string; anything else is a path.
|
|
137
|
-
|
|
147
|
+
//
|
|
148
|
+
// Also accepted: the network-qualified form the explorer's own URLs use —
|
|
149
|
+
// `<network>-0x<hex>`, e.g. `testnet-0x648f18…` — since that's what an agent naturally
|
|
150
|
+
// has in hand after pulling an id straight out of a /stream/<id> link. A qualified id
|
|
151
|
+
// names the chain the same way the URL does; parseQualifiedStreamId (shared.js, same
|
|
152
|
+
// split the explorer's own parseStreamRouteId does) records which one (chainFromId,
|
|
153
|
+
// below) so it can settle --chain once flags are parsed too.
|
|
154
|
+
const isStreamId = (s) => /^\d+$/.test(s) || /^0x[0-9a-fA-F]+$/.test(s) || !!parseQualifiedStreamId(s);
|
|
155
|
+
|
|
156
|
+
let chainFromId = null;
|
|
157
|
+
// Strips a recognized `<network>-` prefix and records the network it named, so every
|
|
158
|
+
// downstream command only ever sees the bare id it already knows how to parse
|
|
159
|
+
// (formatStreamId, `new Mnemonad({id})`, etc. never learn this form exists).
|
|
160
|
+
const takeStreamId = (arg) => {
|
|
161
|
+
const qualified = parseQualifiedStreamId(arg);
|
|
162
|
+
if (!qualified) return arg;
|
|
163
|
+
chainFromId = qualified.network;
|
|
164
|
+
return qualified.id;
|
|
165
|
+
};
|
|
166
|
+
|
|
138
167
|
const positionals = [];
|
|
139
168
|
while (i < raw.length && !raw[i].startsWith('--')) {
|
|
140
169
|
positionals.push(raw[i]);
|
|
141
170
|
i++;
|
|
142
171
|
}
|
|
143
172
|
if (positionals.length === 2) {
|
|
144
|
-
args.streamId = positionals[0];
|
|
173
|
+
args.streamId = takeStreamId(positionals[0]);
|
|
145
174
|
args.path = positionals[1];
|
|
146
175
|
} else if (positionals.length === 1) {
|
|
147
176
|
if (isStreamId(positionals[0])) {
|
|
148
|
-
args.streamId = positionals[0];
|
|
177
|
+
args.streamId = takeStreamId(positionals[0]);
|
|
149
178
|
} else {
|
|
150
179
|
args.path = positionals[0];
|
|
151
180
|
}
|
|
152
181
|
}
|
|
153
182
|
|
|
183
|
+
let sawChainFlag = false;
|
|
154
184
|
while (i < raw.length) {
|
|
155
185
|
const flag = raw[i];
|
|
156
186
|
if (flag === '--chain' && i + 1 < raw.length) {
|
|
157
187
|
args.chain = raw[++i];
|
|
188
|
+
sawChainFlag = true;
|
|
158
189
|
} else if (flag === '--rpc-url' && i + 1 < raw.length) {
|
|
159
190
|
args.rpcUrl = raw[++i];
|
|
160
191
|
} else if (flag === '--contract-address' && i + 1 < raw.length) {
|
|
@@ -193,10 +224,24 @@ function parseArgs(argv) {
|
|
|
193
224
|
args.pushOnly = true;
|
|
194
225
|
} else if (flag === '--pull-only') {
|
|
195
226
|
args.pullOnly = true;
|
|
227
|
+
} else if (flag === '--yes') {
|
|
228
|
+
args.yes = true;
|
|
196
229
|
}
|
|
197
230
|
i++;
|
|
198
231
|
}
|
|
199
232
|
|
|
233
|
+
if (chainFromId) {
|
|
234
|
+
if (sawChainFlag && args.chain !== chainFromId) {
|
|
235
|
+
console.error(
|
|
236
|
+
`Error: the stream id names network '${chainFromId}', but --chain '${args.chain}' was\n` +
|
|
237
|
+
` also given — they disagree. Drop --chain to use the id's network, or pass\n` +
|
|
238
|
+
` --chain ${chainFromId} to match it.`
|
|
239
|
+
);
|
|
240
|
+
_realExit(1);
|
|
241
|
+
}
|
|
242
|
+
args.chain = chainFromId;
|
|
243
|
+
}
|
|
244
|
+
|
|
200
245
|
return args;
|
|
201
246
|
}
|
|
202
247
|
|
|
@@ -221,6 +266,8 @@ try {
|
|
|
221
266
|
await watch(args);
|
|
222
267
|
} else if (args.command === 'compact') {
|
|
223
268
|
await compact(args);
|
|
269
|
+
} else if (args.command === 'burn') {
|
|
270
|
+
await burn(args);
|
|
224
271
|
} else {
|
|
225
272
|
console.error('Unknown command:', args.command);
|
|
226
273
|
console.log(USAGE);
|
package/lib/chainClient.js
CHANGED
|
@@ -107,6 +107,13 @@ export function makeChainClient(args) {
|
|
|
107
107
|
*/
|
|
108
108
|
export function requireWalletClient(client) {
|
|
109
109
|
if (!client.walletClient) {
|
|
110
|
-
|
|
110
|
+
// Marked as a user error so the CLI boundary prints the message alone: this is the
|
|
111
|
+
// first thing anyone hits before configuring a key, and a stack trace through
|
|
112
|
+
// chainClient makes an actionable one-liner look like a crash.
|
|
113
|
+
const err = new Error(
|
|
114
|
+
'No signing key. Set the MNEMONAD_KEY env var to a private key, or pass --key / --phrase.'
|
|
115
|
+
);
|
|
116
|
+
err._isUserError = true;
|
|
117
|
+
throw err;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Mnemonad from 'mnemonad';
|
|
2
|
+
import { makeChainClient, requireWalletClient } from '../chainClient.js';
|
|
3
|
+
import { assertStreamOwner, formatStreamId, formatBytes } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Destroys a stream permanently: burns the ERC-721 token and clears its metadata and
|
|
7
|
+
* wrapped encryption key (`contracts/src/Mnemonad.sol`'s own `burn()`). Irreversible, and
|
|
8
|
+
* re-creating a stream with the same id afterward does NOT resurrect the old data — ids are
|
|
9
|
+
* deterministic, so the contract deliberately marks a burned id as permanently used rather
|
|
10
|
+
* than letting a fresh `create()` silently inherit old item records.
|
|
11
|
+
*
|
|
12
|
+
* Deployed CODE-tier item bytes physically survive on-chain regardless of burn — this
|
|
13
|
+
* removes the stream as a usable, readable thing, not necessarily every trace of its bytes.
|
|
14
|
+
*
|
|
15
|
+
* Requires `--yes`: the one command here that destroys something with no way back, so it
|
|
16
|
+
* refuses to run without an explicit, unambiguous opt-in rather than an interactive prompt
|
|
17
|
+
* a script could accidentally click through.
|
|
18
|
+
*/
|
|
19
|
+
export async function burn(args) {
|
|
20
|
+
if (!args.streamId) {
|
|
21
|
+
throw new Error('stream-id is required for burn');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = makeChainClient(args);
|
|
25
|
+
requireWalletClient(client);
|
|
26
|
+
|
|
27
|
+
const mn = new Mnemonad({
|
|
28
|
+
publicClient: client.publicClient,
|
|
29
|
+
walletClient: client.walletClient,
|
|
30
|
+
contractAddress: client.contractAddress,
|
|
31
|
+
id: args.streamId,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Ownership checked before printing anything about the stream's contents — same
|
|
35
|
+
// reasoning as every other write command's own assertStreamOwner call.
|
|
36
|
+
await assertStreamOwner(mn, client.account.address);
|
|
37
|
+
await mn.initialize();
|
|
38
|
+
|
|
39
|
+
console.log('stream: ', formatStreamId(mn.id));
|
|
40
|
+
console.log('chain: ', args.chain);
|
|
41
|
+
console.log('versions: ', mn.length);
|
|
42
|
+
console.log('binary size: ', formatBytes(mn.binaryLength));
|
|
43
|
+
|
|
44
|
+
if (!args.yes) {
|
|
45
|
+
const err = new Error(
|
|
46
|
+
'This permanently destroys the stream above: the ERC-721 token and its metadata\n' +
|
|
47
|
+
' are gone, and re-creating a stream with this id afterward does NOT recover the\n' +
|
|
48
|
+
' old data — there is no undo. (Deployed CODE-tier chunks physically survive on\n' +
|
|
49
|
+
' chain regardless; this only removes the stream as a usable, readable thing.)\n' +
|
|
50
|
+
' Re-run with --yes to actually burn it.'
|
|
51
|
+
);
|
|
52
|
+
err._isUserError = true;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('burning...');
|
|
57
|
+
await mn.burn();
|
|
58
|
+
console.log(' done —', formatStreamId(mn.id), 'is burned.');
|
|
59
|
+
}
|
package/lib/commands/compact.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
assertStreamOwner,
|
|
11
11
|
unlockStream,
|
|
12
12
|
resolveOffloadParams,
|
|
13
|
+
discoverStreamId,
|
|
13
14
|
} from './shared.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -19,11 +20,15 @@ import {
|
|
|
19
20
|
* to it.
|
|
20
21
|
*/
|
|
21
22
|
export async function compact(args) {
|
|
23
|
+
const destPath = resolvePath(args.path, { mustExist: true });
|
|
24
|
+
|
|
25
|
+
// With --manifest and no id, a folder pulled/pushed before already has one recorded —
|
|
26
|
+
// no need to require it again.
|
|
27
|
+
await discoverStreamId(args, destPath);
|
|
22
28
|
if (!args.streamId) {
|
|
23
29
|
throw new Error('stream-id is required for compact');
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const destPath = resolvePath(args.path);
|
|
27
32
|
const fsFolder = new FSFolder(destPath, makeExcludes(args));
|
|
28
33
|
|
|
29
34
|
const counts = await countTree(fsFolder);
|
package/lib/commands/diff.js
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
formatPath,
|
|
9
9
|
hashLocalTree,
|
|
10
10
|
unlockStream,
|
|
11
|
+
formatStreamId,
|
|
12
|
+
folderExists,
|
|
13
|
+
discoverStreamId,
|
|
11
14
|
} from './shared.js';
|
|
12
15
|
|
|
13
16
|
export function compareFileMaps(localFiles, remoteFiles) {
|
|
@@ -36,13 +39,23 @@ export function compareFileMaps(localFiles, remoteFiles) {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export async function diff(args) {
|
|
39
|
-
if (!args.streamId) {
|
|
40
|
-
throw new Error('stream-id is required for diff');
|
|
41
|
-
}
|
|
42
42
|
// Resolved up front (throws if missing) so a missing path fails fast, before any chain
|
|
43
43
|
// work — see resolvePath()'s own comment for why this is never defaulted to cwd.
|
|
44
|
+
//
|
|
45
|
+
// Deliberately NOT `{ mustExist: true }`, unlike push/compact/watch: diffing a stream
|
|
46
|
+
// against a folder you haven't pulled yet is a legitimate question ("what would I get?"),
|
|
47
|
+
// and this command writes nothing either way. It is called out explicitly below though —
|
|
48
|
+
// "everything deleted" for a folder that never existed reads as data loss rather than as
|
|
49
|
+
// a wrong path.
|
|
44
50
|
const destPath = resolvePath(args.path);
|
|
45
51
|
|
|
52
|
+
// With --manifest and no id, a folder pulled/pushed before already has one recorded —
|
|
53
|
+
// no need to require it again.
|
|
54
|
+
await discoverStreamId(args, destPath);
|
|
55
|
+
if (!args.streamId) {
|
|
56
|
+
throw new Error('stream-id is required for diff');
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
const client = makeChainClient(args);
|
|
47
60
|
|
|
48
61
|
const mn = new Mnemonad({
|
|
@@ -61,7 +74,7 @@ export async function diff(args) {
|
|
|
61
74
|
const totalVersions = mn.length;
|
|
62
75
|
|
|
63
76
|
if (totalVersions === 0) {
|
|
64
|
-
console.log('stream', args.streamId, 'has no versions yet — nothing to diff');
|
|
77
|
+
console.log('stream', formatStreamId(args.streamId), 'has no versions yet — nothing to diff');
|
|
65
78
|
return;
|
|
66
79
|
}
|
|
67
80
|
|
|
@@ -70,7 +83,13 @@ export async function diff(args) {
|
|
|
70
83
|
throw new Error('version must be between 1 and ' + totalVersions);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
console.log('diff:', formatPath(destPath), 'vs', args.streamId, '(version ' + version + ' of ' + totalVersions + ')');
|
|
86
|
+
console.log('diff:', formatPath(destPath), 'vs', formatStreamId(args.streamId), '(version ' + version + ' of ' + totalVersions + ')');
|
|
87
|
+
// Said up front, before the file list: without it, a mistyped path produces a list of
|
|
88
|
+
// every file in the stream marked "deleted", which reads as data loss rather than as
|
|
89
|
+
// a folder that was never there.
|
|
90
|
+
if (!folderExists(destPath)) {
|
|
91
|
+
console.log(' note: local folder does not exist — everything below is listed as deleted');
|
|
92
|
+
}
|
|
74
93
|
console.log(' restoring chain version...');
|
|
75
94
|
|
|
76
95
|
let remoteFolder;
|
package/lib/commands/index.js
CHANGED
package/lib/commands/info.js
CHANGED
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
formatBytes,
|
|
7
7
|
localTreeHash,
|
|
8
8
|
unlockStream,
|
|
9
|
+
formatStreamId,
|
|
10
|
+
folderExists,
|
|
11
|
+
discoverStreamId,
|
|
12
|
+
readManifest,
|
|
13
|
+
streamIdsEqual,
|
|
9
14
|
} from './shared.js';
|
|
10
15
|
|
|
11
16
|
export async function info(args) {
|
|
@@ -14,6 +19,32 @@ export async function info(args) {
|
|
|
14
19
|
console.log(' chain: ', args.chain);
|
|
15
20
|
console.log(' your wallet: ', client.account?.address || '(none)');
|
|
16
21
|
|
|
22
|
+
// Peek at .mnemonad whenever a path is given, regardless of --manifest and regardless
|
|
23
|
+
// of whether an id was given — info is read-only, so surfacing what's there costs
|
|
24
|
+
// nothing, and it's the only command where "you have a manifest but forgot the flag" or
|
|
25
|
+
// "you gave an id that doesn't match your own manifest" are worth a note rather than a
|
|
26
|
+
// silent mismatch.
|
|
27
|
+
const manifest = args.path ? await readManifest(resolvePath(args.path)) : null;
|
|
28
|
+
|
|
29
|
+
if (manifest?.streamId) {
|
|
30
|
+
if (!args.streamId) {
|
|
31
|
+
if (args.manifest) {
|
|
32
|
+
// Logs its own "using stream id from .mnemonad manifest: ..." line.
|
|
33
|
+
await discoverStreamId(args, resolvePath(args.path));
|
|
34
|
+
} else {
|
|
35
|
+
console.log(
|
|
36
|
+
' manifest: ', formatStreamId(manifest.streamId),
|
|
37
|
+
'(.mnemonad found — pass --manifest to use it without an id)'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} else if (!streamIdsEqual(manifest.streamId, args.streamId)) {
|
|
41
|
+
console.log(
|
|
42
|
+
' manifest: ', formatStreamId(manifest.streamId),
|
|
43
|
+
'(differs from the id given:', formatStreamId(args.streamId) + ')'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
17
48
|
if (!args.streamId) {
|
|
18
49
|
return;
|
|
19
50
|
}
|
|
@@ -33,7 +64,7 @@ export async function info(args) {
|
|
|
33
64
|
|
|
34
65
|
const totalVersions = mn.length;
|
|
35
66
|
|
|
36
|
-
console.log('\nstream:', args.streamId);
|
|
67
|
+
console.log('\nstream:', formatStreamId(args.streamId));
|
|
37
68
|
|
|
38
69
|
const owner = await mn.owner().catch(() => null) || '(unknown)';
|
|
39
70
|
const isOwner = client.account && owner !== '(unknown)' && client.account.address.toLowerCase() === owner.toLowerCase();
|
|
@@ -63,7 +94,16 @@ export async function info(args) {
|
|
|
63
94
|
console.log(' history starts: ', 'index', mn.firstAvailableIndex, '(earlier history truncated — see `compact`)');
|
|
64
95
|
}
|
|
65
96
|
|
|
97
|
+
// Deliberately NOT `{ mustExist: true }` like every other command that reads a folder:
|
|
98
|
+
// `info` is a read-only report, and "you haven't pulled this anywhere yet" is a state
|
|
99
|
+
// worth describing rather than refusing over — the stream metadata above is already
|
|
100
|
+
// printed and still useful on its own.
|
|
66
101
|
const destPath = resolvePath(args.path);
|
|
102
|
+
if (!folderExists(destPath)) {
|
|
103
|
+
console.log('\n local folder: missing (run `mnemonad pull` to fetch it)');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
const localHash = await localTreeHash(destPath);
|
|
68
108
|
|
|
69
109
|
if (!localHash) {
|
package/lib/commands/pull.js
CHANGED
|
@@ -8,16 +8,22 @@ import {
|
|
|
8
8
|
syncMemoryFolderToDisk,
|
|
9
9
|
writeManifest,
|
|
10
10
|
unlockStream,
|
|
11
|
+
formatStreamId,
|
|
12
|
+
discoverStreamId,
|
|
11
13
|
} from './shared.js';
|
|
12
14
|
|
|
13
15
|
export async function pull(args) {
|
|
14
|
-
if (!args.streamId) {
|
|
15
|
-
throw new Error('stream-id is required for pull');
|
|
16
|
-
}
|
|
17
16
|
// Resolved up front (throws if missing) so a missing path fails fast, before any chain
|
|
18
17
|
// work — see resolvePath()'s own comment for why this is never defaulted to cwd.
|
|
19
18
|
const destPath = resolvePath(args.path);
|
|
20
19
|
|
|
20
|
+
// With --manifest and no id, a folder pulled/pushed before already has one recorded —
|
|
21
|
+
// no need to require it again.
|
|
22
|
+
await discoverStreamId(args, destPath);
|
|
23
|
+
if (!args.streamId) {
|
|
24
|
+
throw new Error('stream-id is required for pull');
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const client = makeChainClient(args);
|
|
22
28
|
|
|
23
29
|
const mn = new Mnemonad({
|
|
@@ -35,7 +41,7 @@ export async function pull(args) {
|
|
|
35
41
|
await unlockStream(mn, { password: args.password, signer: client.walletClient });
|
|
36
42
|
|
|
37
43
|
const version = args.version != null ? Number(args.version) : undefined;
|
|
38
|
-
console.log('restoring', args.streamId, version != null ? 'at version ' + version : '(latest)', '...');
|
|
44
|
+
console.log('restoring', formatStreamId(args.streamId), version != null ? 'at version ' + version : '(latest)', '...');
|
|
39
45
|
|
|
40
46
|
let folder;
|
|
41
47
|
try {
|
|
@@ -56,7 +62,8 @@ export async function pull(args) {
|
|
|
56
62
|
if (args.manifest) {
|
|
57
63
|
await mn.initialize();
|
|
58
64
|
const pulledVersion = version ?? mn.length;
|
|
59
|
-
|
|
65
|
+
// Canonical form stored — see push.js's own writeManifest call for why.
|
|
66
|
+
await writeManifest(destPath, { streamId: formatStreamId(args.streamId), version: pulledVersion, files: newFiles });
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
console.log(' ', stats.written, 'written,', stats.skipped, 'unchanged,',
|
package/lib/commands/push.js
CHANGED
|
@@ -15,12 +15,20 @@ import {
|
|
|
15
15
|
unlockStream,
|
|
16
16
|
resolveEncryptMethod,
|
|
17
17
|
resolveOffloadParams,
|
|
18
|
+
formatStreamId,
|
|
19
|
+
streamIdsEqual,
|
|
20
|
+
discoverStreamId,
|
|
18
21
|
} from './shared.js';
|
|
19
22
|
|
|
20
23
|
export async function push(args) {
|
|
24
|
+
const destPath = resolvePath(args.path, { mustExist: true });
|
|
25
|
+
|
|
26
|
+
// With --manifest and no id, a folder pushed before already has one recorded — reuse it
|
|
27
|
+
// rather than falling into the "no id" branch below and creating a second, unrelated
|
|
28
|
+
// stream for the same folder.
|
|
29
|
+
await discoverStreamId(args, destPath);
|
|
21
30
|
let streamId = args.streamId;
|
|
22
31
|
|
|
23
|
-
const destPath = resolvePath(args.path);
|
|
24
32
|
const fsFolder = new FSFolder(destPath, makeExcludes(args));
|
|
25
33
|
|
|
26
34
|
const counts = await countTree(fsFolder);
|
|
@@ -29,7 +37,7 @@ export async function push(args) {
|
|
|
29
37
|
if (args.manifest) {
|
|
30
38
|
const manifest = await readManifest(destPath);
|
|
31
39
|
const localFiles = await hashLocalTree(fsFolder);
|
|
32
|
-
if (streamId && manifest && manifest.streamId
|
|
40
|
+
if (streamId && manifest && streamIdsEqual(manifest.streamId, streamId) && manifest.files) {
|
|
33
41
|
if (hashMapsEqual(localFiles, manifest.files)) {
|
|
34
42
|
console.log(' no changes since last pull, nothing to push');
|
|
35
43
|
return;
|
|
@@ -73,7 +81,7 @@ export async function push(args) {
|
|
|
73
81
|
console.log('creating new stream on', args.chain, how, '...');
|
|
74
82
|
mn = await Mnemonad.create(createParams);
|
|
75
83
|
streamId = mn.id.toString();
|
|
76
|
-
console.log(' created:', streamId);
|
|
84
|
+
console.log(' created:', formatStreamId(streamId));
|
|
77
85
|
} else {
|
|
78
86
|
mn = new Mnemonad({ ...baseParams, id: streamId });
|
|
79
87
|
await assertStreamOwner(mn, client.account.address);
|
|
@@ -83,7 +91,7 @@ export async function push(args) {
|
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
console.log('syncing ./ →', streamId);
|
|
94
|
+
console.log('syncing ./ →', formatStreamId(streamId));
|
|
87
95
|
|
|
88
96
|
const monadSync = new MonadSync({ mnemonad: mn, compress: args.compress || 'gzip' });
|
|
89
97
|
|
|
@@ -118,10 +126,13 @@ export async function push(args) {
|
|
|
118
126
|
const patchType = args.forceSnapshot || existingVersions === 0 ? 'full snapshot' : 'diff';
|
|
119
127
|
const compressedNote = args.compress !== false ? ', gzip compressed' : '';
|
|
120
128
|
console.log(' version', result.version, 'pushed (' + patchType + compressedNote + ')');
|
|
121
|
-
console.log(' stream:', streamId);
|
|
129
|
+
console.log(' stream:', formatStreamId(streamId));
|
|
122
130
|
|
|
123
131
|
if (args.manifest) {
|
|
124
132
|
const localFiles = await hashLocalTree(fsFolder);
|
|
125
|
-
|
|
133
|
+
// Stored canonical (see formatStreamId) rather than whatever form the caller typed —
|
|
134
|
+
// so a manifest written by a hex-id push and read back by a decimal-id one (or vice
|
|
135
|
+
// versa) still matches; streamIdsEqual() above is the belt to this suspenders.
|
|
136
|
+
await writeManifest(destPath, { streamId: formatStreamId(streamId), version: result.version, files: localFiles });
|
|
126
137
|
}
|
|
127
138
|
}
|
package/lib/commands/shared.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
2
3
|
import { join, resolve } from 'node:path';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
@@ -56,15 +57,112 @@ export function resolveOffloadParams(args, account) {
|
|
|
56
57
|
* `p` is missing — every command that touches a folder requires the caller to name it
|
|
57
58
|
* explicitly, so a typo'd or omitted `path` never silently operates on whatever directory
|
|
58
59
|
* the CLI happened to be run from.
|
|
60
|
+
*
|
|
61
|
+
* `mustExist` additionally requires the folder to be there already. Every command that
|
|
62
|
+
* *reads* the local folder passes it, because the alternative is worse than an error in
|
|
63
|
+
* both directions: `push`/`compact` used to surface a raw ENOENT stack trace from deep
|
|
64
|
+
* inside `readdir`, and `diff` used to treat a missing folder as an empty one and report
|
|
65
|
+
* every file in the stream as locally deleted — which reads as "your data is gone" rather
|
|
66
|
+
* than "you typed the wrong path". `pull` is the one command that leaves it off, since
|
|
67
|
+
* creating the destination is exactly its job.
|
|
68
|
+
*
|
|
69
|
+
* @param {?string} p
|
|
70
|
+
* @param {Object} [options]
|
|
71
|
+
* @param {boolean} [options.mustExist=false]
|
|
59
72
|
*/
|
|
60
|
-
export function resolvePath(p) {
|
|
73
|
+
export function resolvePath(p, { mustExist = false } = {}) {
|
|
61
74
|
if (!p) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
throw userError('a folder path is required (e.g. `mnemonad <command> ... /path/to/folder`)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolved = p.startsWith('~') ? join(homedir(), p.slice(1)) : resolve(p);
|
|
79
|
+
if (!mustExist) return resolved;
|
|
80
|
+
|
|
81
|
+
const stat = statOrNull(resolved);
|
|
82
|
+
if (!stat) {
|
|
83
|
+
throw userError(
|
|
84
|
+
`no such folder: ${resolved}\n` +
|
|
85
|
+
' Create it first, or use `mnemonad pull <stream-id> <path>` to fetch one into it.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!stat.isDirectory()) {
|
|
89
|
+
throw userError(`not a folder: ${resolved}\n This command syncs a directory, not a single file.`);
|
|
90
|
+
}
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @returns {?import('node:fs').Stats} null when the path doesn't exist or isn't readable. */
|
|
95
|
+
function statOrNull(path) {
|
|
96
|
+
try {
|
|
97
|
+
return statSync(path);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
65
100
|
}
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether `path` is an existing directory. For a command that reports on one rather than
|
|
104
|
+
* refusing to run without it (see info.js). */
|
|
105
|
+
export function folderExists(path) {
|
|
106
|
+
return statOrNull(path)?.isDirectory() === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Canonical display form for a stream id: `0x` + the full 32-byte token id, zero-padded,
|
|
111
|
+
* lowercase. Matches the dApp's own formatting (explorer/src/includes/formatStreamId.js),
|
|
112
|
+
* unelided — a CLI command needs the whole id to actually reuse, where the dApp's UI only
|
|
113
|
+
* needs enough of it to recognize at a glance.
|
|
114
|
+
*
|
|
115
|
+
* Takes whatever a stream id shows up as in this codebase — a bigint (`mn.id`), or a raw
|
|
116
|
+
* CLI arg that's either a decimal or a `0x`-hex string — since `BigInt()` parses all three
|
|
117
|
+
* natively; see bin/mnemonad.js's own `isStreamId` for why both string forms are accepted
|
|
118
|
+
* as input in the first place.
|
|
119
|
+
*
|
|
120
|
+
* @param {bigint|string} id
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function formatStreamId(id) {
|
|
124
|
+
return '0x' + BigInt(id).toString(16).padStart(64, '0');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether two stream ids refer to the same token, regardless of which form either was
|
|
129
|
+
* written in (decimal, hex, differently-padded hex) — a plain `===` on the raw strings
|
|
130
|
+
* would wrongly call `123` and `0x7b` different streams.
|
|
131
|
+
*/
|
|
132
|
+
export function streamIdsEqual(a, b) {
|
|
133
|
+
if (a == null || b == null) return a === b;
|
|
134
|
+
return BigInt(a) === BigInt(b);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Networks a qualified stream id can name — see `parseQualifiedStreamId` below. Kept in
|
|
138
|
+
* sync by hand with `chainClient.js`'s own `CHAINS` map (which also lists `local`, never
|
|
139
|
+
* meaningful here — no URL is ever built with it) and with the explorer's
|
|
140
|
+
* `src/settings.js` `NETWORKS` map, the source these prefixes actually come from. */
|
|
141
|
+
export const NETWORK_PREFIXES = ['testnet', 'mainnet'];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Splits the network-qualified stream-id form the explorer's URLs use — `<network>-0x<hex>`
|
|
145
|
+
* (or `<network>-<decimal>`), e.g. `testnet-0x648f18…` — into its network and bare id. Same
|
|
146
|
+
* split the explorer's own `streamRouteId()`/`parseStreamRouteId()`
|
|
147
|
+
* (`explorer/src/includes/streamRoute.js`) do, so an id copied straight out of a
|
|
148
|
+
* `/stream/<id>` link parses here too, instead of failing `BigInt()` on the whole string.
|
|
149
|
+
*
|
|
150
|
+
* Returns null for anything that isn't this exact shape — an unqualified id, a malformed
|
|
151
|
+
* one, or a prefix that isn't a known network (not a stream on some network this build
|
|
152
|
+
* hasn't heard of, just not this form).
|
|
153
|
+
*
|
|
154
|
+
* @param {string} s
|
|
155
|
+
* @returns {{network: string, id: string}|null}
|
|
156
|
+
*/
|
|
157
|
+
export function parseQualifiedStreamId(s) {
|
|
158
|
+
if (typeof s !== 'string') return null;
|
|
159
|
+
const dash = s.indexOf('-');
|
|
160
|
+
if (dash === -1) return null;
|
|
161
|
+
const prefix = s.slice(0, dash).toLowerCase();
|
|
162
|
+
if (!NETWORK_PREFIXES.includes(prefix)) return null;
|
|
163
|
+
const rest = s.slice(dash + 1);
|
|
164
|
+
if (!/^0x[0-9a-fA-F]+$/.test(rest) && !/^\d+$/.test(rest)) return null;
|
|
165
|
+
return { network: prefix, id: rest };
|
|
68
166
|
}
|
|
69
167
|
|
|
70
168
|
export function formatBytes(bytes) {
|
|
@@ -107,9 +205,10 @@ export async function assertStreamOwner(mn, signerAddress) {
|
|
|
107
205
|
if (getAddress(owner) === getAddress(signerAddress)) return;
|
|
108
206
|
|
|
109
207
|
const err = new Error(
|
|
110
|
-
`stream ${mn.id} is owned by ${owner}\n` +
|
|
208
|
+
`stream ${formatStreamId(mn.id)} is owned by ${owner}\n` +
|
|
111
209
|
` but you are signing as ${signerAddress}.\n` +
|
|
112
|
-
` Nothing was uploaded. Use the owner's key (--key
|
|
210
|
+
` Nothing was uploaded. Use the owner's key (--key, --phrase, or the MNEMONAD_KEY\n` +
|
|
211
|
+
` env var, set to the private key),\n` +
|
|
113
212
|
` or run push without a stream id to create your own stream.`
|
|
114
213
|
);
|
|
115
214
|
err._isUserError = true;
|
|
@@ -199,7 +298,7 @@ export async function unlockStream(mn, credentials = {}) {
|
|
|
199
298
|
if (!signer) {
|
|
200
299
|
throw userError(
|
|
201
300
|
'This stream is encrypted to its owner\'s wallet. Provide the owner\'s key with\n' +
|
|
202
|
-
' --key
|
|
301
|
+
' --key, --phrase, or the MNEMONAD_KEY env var (set it to the private key) to decrypt it.'
|
|
203
302
|
);
|
|
204
303
|
}
|
|
205
304
|
await mn.unlockWithSignature(signer);
|
|
@@ -244,6 +343,32 @@ export async function writeManifest(destPath, manifest) {
|
|
|
244
343
|
await writeFile(join(destPath, '.mnemonad'), JSON.stringify(manifest, null, 2));
|
|
245
344
|
}
|
|
246
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Recovers a stream id from the folder's own `.mnemonad` manifest when `--manifest` was
|
|
348
|
+
* passed and no id was given on the command line — the whole point of writing the manifest
|
|
349
|
+
* is that the id doesn't have to be remembered or re-typed for every later command on the
|
|
350
|
+
* same folder. Mutates `args.streamId` in place (every call site reads it back off `args`
|
|
351
|
+
* afterward) and is a no-op whenever an id was already given or `--manifest` wasn't passed —
|
|
352
|
+
* so it's safe to call unconditionally right after resolving the folder path.
|
|
353
|
+
*
|
|
354
|
+
* Silent when there's nothing to find (no `.mnemonad` yet, e.g. a folder's very first push,
|
|
355
|
+
* or a manifest with no `streamId`) — the caller's own "stream-id is required" error covers
|
|
356
|
+
* that case with the wording appropriate to its command.
|
|
357
|
+
*
|
|
358
|
+
* @param {Object} args - parsed CLI args, mutated in place
|
|
359
|
+
* @param {string} destPath - resolved folder path to look for `.mnemonad` in
|
|
360
|
+
* @returns {Promise<?string>} the resolved stream id, or whatever `args.streamId` already was
|
|
361
|
+
*/
|
|
362
|
+
export async function discoverStreamId(args, destPath) {
|
|
363
|
+
if (args.streamId || !args.manifest) return args.streamId;
|
|
364
|
+
const manifest = await readManifest(destPath);
|
|
365
|
+
if (manifest?.streamId) {
|
|
366
|
+
console.log(' using stream id from .mnemonad manifest:', formatStreamId(manifest.streamId));
|
|
367
|
+
args.streamId = manifest.streamId;
|
|
368
|
+
}
|
|
369
|
+
return args.streamId;
|
|
370
|
+
}
|
|
371
|
+
|
|
247
372
|
export const DISK_EXCLUDES = new Set(['.mnemonad', '.git', '.DS_Store', 'node_modules']);
|
|
248
373
|
|
|
249
374
|
export async function hashDiskTree(dirPath, prefix = []) {
|
package/lib/commands/watch.js
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
assertStreamOwner,
|
|
15
15
|
unlockStream,
|
|
16
16
|
resolveOffloadParams,
|
|
17
|
+
formatStreamId,
|
|
18
|
+
discoverStreamId,
|
|
17
19
|
} from './shared.js';
|
|
18
20
|
|
|
19
21
|
function timestamp() {
|
|
@@ -27,9 +29,12 @@ function hashesEqual(a, b) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export async function watch(args) {
|
|
30
|
-
|
|
32
|
+
const cwd = resolvePath(args.path, { mustExist: true });
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
// With --manifest and no id, a folder pulled/pushed before already has one recorded —
|
|
35
|
+
// no need to require it again.
|
|
36
|
+
await discoverStreamId(args, cwd);
|
|
37
|
+
if (!args.streamId) throw new Error('stream-id is required for watch');
|
|
33
38
|
const debounceMs = args.debounce ?? 5000;
|
|
34
39
|
const pollIntervalMs = (args.pollInterval ?? 2) * 1000;
|
|
35
40
|
const pushEnabled = !args.pullOnly;
|
|
@@ -70,7 +75,7 @@ export async function watch(args) {
|
|
|
70
75
|
let debounceTimer = null;
|
|
71
76
|
|
|
72
77
|
console.log(`watching ${formatPath(cwd)}`);
|
|
73
|
-
console.log(` stream: ${args.streamId}`);
|
|
78
|
+
console.log(` stream: ${formatStreamId(args.streamId)}`);
|
|
74
79
|
console.log(` remote version: ${remoteVersion}`);
|
|
75
80
|
console.log(` push: ${pushEnabled ? `enabled (debounce ${debounceMs}ms)` : 'disabled'}`);
|
|
76
81
|
console.log(` pull: ${pullEnabled ? `enabled (poll every ${pollIntervalMs / 1000}s)` : 'disabled'}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mnemonad-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI to sync local folders to versioned, diffed on-chain streams on Monad, backed by Mnemonad + monadsync.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/jeka911/mnemonad.git",
|
|
8
|
+
"directory": "cli"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"license": "AGPL-3.0",
|
|
7
12
|
"bin": {
|