@memfork/cli 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ops.js +3 -23
- package/dist/commands/provision.js +109 -54
- package/package.json +2 -2
package/dist/commands/ops.js
CHANGED
|
@@ -14,26 +14,6 @@ async function getClient() {
|
|
|
14
14
|
const client = await MemForksClient.connect(toClientConfig(cfg));
|
|
15
15
|
return { client, cfg };
|
|
16
16
|
}
|
|
17
|
-
function isTransientSuiError(e) {
|
|
18
|
-
const msg = String(e);
|
|
19
|
-
return (msg.includes("needs to be rebuilt") ||
|
|
20
|
-
msg.includes("unavailable for consumption") ||
|
|
21
|
-
msg.includes("object version"));
|
|
22
|
-
}
|
|
23
|
-
async function withRetry(fn, retries = 2, delayMs = 1500) {
|
|
24
|
-
for (let attempt = 1;; attempt++) {
|
|
25
|
-
try {
|
|
26
|
-
return await fn();
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
if (isTransientSuiError(e) && attempt < retries) {
|
|
30
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
throw e;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
17
|
function currentGitBranch() {
|
|
38
18
|
try {
|
|
39
19
|
return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
|
|
@@ -132,7 +112,7 @@ export async function cmdCommit(opts) {
|
|
|
132
112
|
console.error(chalk.red("No facts to commit. Pass --facts or --from-response."));
|
|
133
113
|
process.exit(1);
|
|
134
114
|
}
|
|
135
|
-
const { blobId } = await
|
|
115
|
+
const { blobId } = await client.commit(branch, { facts, message: opts.message });
|
|
136
116
|
const out = { blobId, branch };
|
|
137
117
|
if (process.stdout.isTTY) {
|
|
138
118
|
console.log("");
|
|
@@ -158,7 +138,7 @@ export async function cmdMerge(from, into, opts) {
|
|
|
158
138
|
process.stdout.write(chalk.dim(`Merging ${chalk.green(from)} → ${chalk.green(into)}`) +
|
|
159
139
|
chalk.dim(governed ? " (governed — awaiting resolver…)" : " (LWW — self-finalizing…)") +
|
|
160
140
|
" ");
|
|
161
|
-
const { digest, mergedCount, blobId, proposalId } = await
|
|
141
|
+
const { digest, mergedCount, blobId, proposalId } = await client.merge(from, into);
|
|
162
142
|
console.log(chalk.green("done"));
|
|
163
143
|
console.log("");
|
|
164
144
|
console.log(chalk.dim(` facts merged: ${mergedCount}`));
|
|
@@ -594,7 +574,7 @@ export async function cmdBranch(name, opts = {}) {
|
|
|
594
574
|
const { client, cfg } = await getClient();
|
|
595
575
|
const from = opts.from ?? cfg.defaultBranch ?? currentGitBranch();
|
|
596
576
|
process.stdout.write(chalk.dim(`Creating branch ${chalk.green(name)} from ${chalk.green(from)} … `));
|
|
597
|
-
const digest = await
|
|
577
|
+
const digest = await client.branch(name, { from });
|
|
598
578
|
console.log(chalk.green("done"));
|
|
599
579
|
console.log("");
|
|
600
580
|
console.log(chalk.dim(` tx: ${digest}`));
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* to SuiJsonRpcClient, and the MemWal SDK's internal auto-init would fail on v2.
|
|
20
20
|
*/
|
|
21
21
|
import chalk from "chalk";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
22
25
|
import { confirm } from "@inquirer/prompts";
|
|
23
26
|
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
24
27
|
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
@@ -31,20 +34,48 @@ function step(n, msg) {
|
|
|
31
34
|
}
|
|
32
35
|
function done() { console.log(chalk.green("done")); }
|
|
33
36
|
function skip(reason) { console.log(chalk.dim("skip " + reason)); }
|
|
37
|
+
function checkpointPath() {
|
|
38
|
+
return path.join(os.homedir(), ".memfork", ".pending-init.json");
|
|
39
|
+
}
|
|
40
|
+
function readCheckpoint() {
|
|
41
|
+
const p = checkpointPath();
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveCheckpoint(data) {
|
|
50
|
+
const p = checkpointPath();
|
|
51
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
52
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
function clearCheckpoint() {
|
|
55
|
+
try {
|
|
56
|
+
fs.unlinkSync(checkpointPath());
|
|
57
|
+
}
|
|
58
|
+
catch { /* already gone */ }
|
|
59
|
+
}
|
|
34
60
|
export async function autoProvision(opts) {
|
|
35
61
|
const network = opts.network;
|
|
36
62
|
const consts = MEMWAL_CONSTANTS[network];
|
|
37
63
|
const rpcUrl = getJsonRpcFullnodeUrl(network);
|
|
38
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
65
|
const suiClient = new SuiJsonRpcClient({ transport: new JsonRpcHTTPTransport({ url: rpcUrl }), network });
|
|
66
|
+
// Load any checkpoint from a previous failed attempt.
|
|
67
|
+
const ckpt = readCheckpoint();
|
|
68
|
+
// If the checkpoint is for a different network, ignore it.
|
|
69
|
+
const cp = (ckpt?.network === network) ? ckpt : { network };
|
|
40
70
|
// ── 1. Keypair ──────────────────────────────────────────────────────────────
|
|
41
71
|
step(1, "Generating Sui keypair");
|
|
42
72
|
let keypair;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const resumeKey = opts.existingKey ?? cp.privateKey;
|
|
74
|
+
if (resumeKey) {
|
|
75
|
+
keypair = resumeKey.startsWith("suiprivkey")
|
|
76
|
+
? Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(resumeKey).secretKey)
|
|
77
|
+
: Ed25519Keypair.fromSecretKey(Uint8Array.from(Buffer.from(resumeKey, "hex")));
|
|
78
|
+
skip(cp.privateKey && !opts.existingKey ? "resuming from checkpoint" : "reusing existing key");
|
|
48
79
|
}
|
|
49
80
|
else {
|
|
50
81
|
keypair = new Ed25519Keypair();
|
|
@@ -53,6 +84,11 @@ export async function autoProvision(opts) {
|
|
|
53
84
|
const address = keypair.toSuiAddress();
|
|
54
85
|
const privateKey = keypair.getSecretKey(); // bech32 suiprivkey1…
|
|
55
86
|
console.log(chalk.dim(` address: ${address}`));
|
|
87
|
+
// Save keypair to checkpoint so a retry can reuse the same key.
|
|
88
|
+
if (!cp.privateKey) {
|
|
89
|
+
cp.privateKey = privateKey;
|
|
90
|
+
saveCheckpoint(cp);
|
|
91
|
+
}
|
|
56
92
|
// ── 2. Fund wallet ───────────────────────────────────────────────────────────
|
|
57
93
|
// sponsorBase = root URL, used for /drip and /sponsor paths.
|
|
58
94
|
// sponsorEndpoint = full URL the MemForksClient POSTs to (no path appended internally).
|
|
@@ -73,7 +109,6 @@ export async function autoProvision(opts) {
|
|
|
73
109
|
// Mainnet: drip covers the two MemWal calls (createAccount + addDelegateKey).
|
|
74
110
|
// initTree (step 6) goes through /sponsor — the MemForksClient handles that path.
|
|
75
111
|
step(2, "Requesting mainnet gas from MemForks sponsor");
|
|
76
|
-
// Normalise: strip a trailing /sponsor so the base URL is always path-free.
|
|
77
112
|
sponsorBase = (process.env.MEMFORK_SPONSOR_URL ?? "https://memforks-sponsor-production.up.railway.app")
|
|
78
113
|
.replace(/\/sponsor\/?$/, "");
|
|
79
114
|
sponsorEndpoint = `${sponsorBase}/sponsor`;
|
|
@@ -104,57 +139,79 @@ export async function autoProvision(opts) {
|
|
|
104
139
|
}
|
|
105
140
|
}
|
|
106
141
|
// ── 3. MemWal account ────────────────────────────────────────────────────────
|
|
107
|
-
step(3, "Creating MemWal account on-chain");
|
|
108
142
|
let accountId;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
suiPrivateKey: privateKey,
|
|
114
|
-
suiClient,
|
|
115
|
-
});
|
|
116
|
-
accountId = result.accountId;
|
|
117
|
-
done();
|
|
143
|
+
if (cp.accountId) {
|
|
144
|
+
step(3, "Creating MemWal account on-chain");
|
|
145
|
+
skip("resuming from checkpoint");
|
|
146
|
+
accountId = cp.accountId;
|
|
118
147
|
console.log(chalk.dim(` accountId: ${accountId}`));
|
|
119
148
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
else {
|
|
150
|
+
step(3, "Creating MemWal account on-chain");
|
|
151
|
+
try {
|
|
152
|
+
const result = await createAccount({
|
|
153
|
+
packageId: consts.packageId,
|
|
154
|
+
registryId: consts.registryId,
|
|
155
|
+
suiPrivateKey: privateKey,
|
|
156
|
+
suiClient,
|
|
157
|
+
});
|
|
158
|
+
accountId = result.accountId;
|
|
159
|
+
done();
|
|
126
160
|
console.log(chalk.dim(` accountId: ${accountId}`));
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
catch (e) {
|
|
163
|
+
const msg = String(e);
|
|
164
|
+
if (msg.includes("EAccountAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 3)")) {
|
|
165
|
+
console.log(chalk.dim("already exists"));
|
|
166
|
+
accountId = await resolveExistingMemwalAccount(suiClient, consts.packageId, address);
|
|
167
|
+
console.log(chalk.dim(` accountId: ${accountId}`));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk.red("failed"));
|
|
171
|
+
throw new Error(`MemWal account creation failed: ${msg}`);
|
|
172
|
+
}
|
|
131
173
|
}
|
|
174
|
+
cp.accountId = accountId;
|
|
175
|
+
saveCheckpoint(cp);
|
|
132
176
|
}
|
|
133
177
|
// ── 4 + 5. Delegate key ───────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
accountId,
|
|
142
|
-
publicKey: delegate.publicKey,
|
|
143
|
-
label: `memfork-cli-${new Date().toISOString().slice(0, 10)}`,
|
|
144
|
-
suiPrivateKey: privateKey,
|
|
145
|
-
suiClient,
|
|
146
|
-
});
|
|
147
|
-
done();
|
|
178
|
+
let delegatePrivateKey;
|
|
179
|
+
if (cp.delegateKey) {
|
|
180
|
+
step(4, "Generating MemWal delegate key");
|
|
181
|
+
skip("resuming from checkpoint");
|
|
182
|
+
step(5, "Registering delegate key with MemWal");
|
|
183
|
+
skip("resuming from checkpoint");
|
|
184
|
+
delegatePrivateKey = cp.delegateKey;
|
|
148
185
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
186
|
+
else {
|
|
187
|
+
step(4, "Generating MemWal delegate key");
|
|
188
|
+
const delegate = await generateDelegateKey();
|
|
189
|
+
done();
|
|
190
|
+
delegatePrivateKey = delegate.privateKey;
|
|
191
|
+
step(5, "Registering delegate key with MemWal");
|
|
192
|
+
try {
|
|
193
|
+
await addDelegateKey({
|
|
194
|
+
packageId: consts.packageId,
|
|
195
|
+
accountId,
|
|
196
|
+
publicKey: delegate.publicKey,
|
|
197
|
+
label: `memfork-cli-${new Date().toISOString().slice(0, 10)}`,
|
|
198
|
+
suiPrivateKey: privateKey,
|
|
199
|
+
suiClient,
|
|
200
|
+
});
|
|
201
|
+
done();
|
|
153
202
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
catch (e) {
|
|
204
|
+
const msg = String(e);
|
|
205
|
+
if (msg.includes("EDelegateKeyAlreadyExists") || msg.includes("MoveAbort") && msg.includes(", 0)")) {
|
|
206
|
+
skip("key already registered");
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(chalk.red("failed"));
|
|
210
|
+
throw new Error(`Failed to register delegate key: ${msg}`);
|
|
211
|
+
}
|
|
157
212
|
}
|
|
213
|
+
cp.delegateKey = delegatePrivateKey;
|
|
214
|
+
saveCheckpoint(cp);
|
|
158
215
|
}
|
|
159
216
|
// ── 6. MemoryTree ────────────────────────────────────────────────────────────
|
|
160
217
|
step(6, "Creating MemoryTree on Sui");
|
|
@@ -163,19 +220,15 @@ export async function autoProvision(opts) {
|
|
|
163
220
|
signer: privateKey,
|
|
164
221
|
network,
|
|
165
222
|
packageId: consts.memforksPackageId,
|
|
166
|
-
// sponsorEndpoint is the full POST URL — MemForksClient appends no path.
|
|
167
223
|
...(sponsorEndpoint ? { sponsorUrl: sponsorEndpoint } : {}),
|
|
168
224
|
memwal: {
|
|
169
225
|
accountId,
|
|
170
|
-
delegateKey:
|
|
226
|
+
delegateKey: delegatePrivateKey,
|
|
171
227
|
serverUrl: consts.relayer,
|
|
172
228
|
},
|
|
173
229
|
});
|
|
174
230
|
let treeId;
|
|
175
231
|
let digest;
|
|
176
|
-
// Retry once on a transient gas-coin version race: the sponsor refreshes its
|
|
177
|
-
// pool after the drip, but if initTree lands before that completes the
|
|
178
|
-
// fullnode may report a stale-object error. A short wait + retry clears it.
|
|
179
232
|
const maxAttempts = 2;
|
|
180
233
|
for (let attempt = 1;; attempt++) {
|
|
181
234
|
try {
|
|
@@ -194,8 +247,8 @@ export async function autoProvision(opts) {
|
|
|
194
247
|
console.log(chalk.red("failed"));
|
|
195
248
|
if (msg.includes("429") || msg.toLowerCase().includes("rate limit")) {
|
|
196
249
|
throw new Error("Sponsor rate limit hit for init_tree (1/IP/day).\n" +
|
|
197
|
-
"Wait 24 h and run `memfork init --quick` again
|
|
198
|
-
|
|
250
|
+
"Wait 24 h and run `memfork init --quick` again to resume — your keypair and\n" +
|
|
251
|
+
"MemWal account are already saved and will be reused automatically.");
|
|
199
252
|
}
|
|
200
253
|
if (msg.includes("Sponsor error: 404")) {
|
|
201
254
|
throw new Error(`Sponsor endpoint not reachable (${sponsorEndpoint}).\n` +
|
|
@@ -210,11 +263,13 @@ export async function autoProvision(opts) {
|
|
|
210
263
|
done();
|
|
211
264
|
console.log(chalk.dim(` treeId: ${treeId}`));
|
|
212
265
|
console.log(chalk.dim(` tx: ${digest}`));
|
|
266
|
+
// All steps succeeded — clear the checkpoint.
|
|
267
|
+
clearCheckpoint();
|
|
213
268
|
return {
|
|
214
269
|
treeId,
|
|
215
270
|
privateKey,
|
|
216
271
|
memwalAccountId: accountId,
|
|
217
|
-
memwalKey:
|
|
272
|
+
memwalKey: delegatePrivateKey,
|
|
218
273
|
network,
|
|
219
274
|
};
|
|
220
275
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memfork/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "MemForks CLI — init, commit, recall, merge, install plugins",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@inquirer/prompts": "^8.5.2",
|
|
38
|
-
"@memfork/core": "^0.1.
|
|
38
|
+
"@memfork/core": "^0.1.11",
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"commander": "^15.0.0"
|
|
41
41
|
},
|