@memfork/cli 0.1.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/dist/cli.d.ts +21 -0
- package/dist/cli.js +154 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +170 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +286 -0
- package/dist/commands/install.d.ts +17 -0
- package/dist/commands/install.js +196 -0
- package/dist/commands/join.d.ts +17 -0
- package/dist/commands/join.js +125 -0
- package/dist/commands/ops.d.ts +56 -0
- package/dist/commands/ops.js +444 -0
- package/dist/commands/provision.d.ts +32 -0
- package/dist/commands/provision.js +173 -0
- package/dist/commands/ui-server.d.ts +15 -0
- package/dist/commands/ui-server.js +217 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.js +200 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/package.json +39 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @memfork/cli — entry point
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* memfork init — first-run interactive setup
|
|
7
|
+
* memfork doctor — verify the full setup
|
|
8
|
+
* memfork install cursor|codex — install IDE plugin
|
|
9
|
+
*
|
|
10
|
+
* memfork status — tree status
|
|
11
|
+
* memfork log [--branch <b>] [-n <n>] — commit log
|
|
12
|
+
* memfork recall <query> [--branch <b>] — semantic recall
|
|
13
|
+
* memfork commit -m <msg> --facts <...> — write facts on-chain
|
|
14
|
+
* memfork merge <from> <into> --resolver <id> — propose merge
|
|
15
|
+
* memfork proposals — list open proposals
|
|
16
|
+
* memfork ui — open DAG visualizer
|
|
17
|
+
*
|
|
18
|
+
* Config API re-exported for use by other packages:
|
|
19
|
+
* import { resolveConfig, toClientConfig } from "@memfork/cli"
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @memfork/cli — entry point
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* memfork init — first-run interactive setup
|
|
7
|
+
* memfork doctor — verify the full setup
|
|
8
|
+
* memfork install cursor|codex — install IDE plugin
|
|
9
|
+
*
|
|
10
|
+
* memfork status — tree status
|
|
11
|
+
* memfork log [--branch <b>] [-n <n>] — commit log
|
|
12
|
+
* memfork recall <query> [--branch <b>] — semantic recall
|
|
13
|
+
* memfork commit -m <msg> --facts <...> — write facts on-chain
|
|
14
|
+
* memfork merge <from> <into> --resolver <id> — propose merge
|
|
15
|
+
* memfork proposals — list open proposals
|
|
16
|
+
* memfork ui — open DAG visualizer
|
|
17
|
+
*
|
|
18
|
+
* Config API re-exported for use by other packages:
|
|
19
|
+
* import { resolveConfig, toClientConfig } from "@memfork/cli"
|
|
20
|
+
*/
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
import { cmdInit } from "./commands/init.js";
|
|
24
|
+
import { cmdDoctor } from "./commands/doctor.js";
|
|
25
|
+
import { cmdInstall } from "./commands/install.js";
|
|
26
|
+
import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
|
|
27
|
+
import { cmdJoin } from "./commands/join.js";
|
|
28
|
+
const program = new Command();
|
|
29
|
+
program
|
|
30
|
+
.name("memfork")
|
|
31
|
+
.description("MemForks CLI — on-chain, branch-aware agent memory")
|
|
32
|
+
.version("0.1.0");
|
|
33
|
+
// ─── Setup ────────────────────────────────────────────────────────────────────
|
|
34
|
+
program
|
|
35
|
+
.command("init")
|
|
36
|
+
.description("interactive first-run setup — create or link a memory tree")
|
|
37
|
+
.option("-q, --quick", "auto-provision: keygen → faucet → MemWal account → tree (no copy-paste needed)")
|
|
38
|
+
.action(wrap((opts) => cmdInit({ quick: opts.quick })));
|
|
39
|
+
program
|
|
40
|
+
.command("doctor")
|
|
41
|
+
.description("verify config, credentials, Sui connection, and MemWal")
|
|
42
|
+
.action(wrap(cmdDoctor));
|
|
43
|
+
program
|
|
44
|
+
.command("join")
|
|
45
|
+
.description("onboard to an existing tree (team member setup)")
|
|
46
|
+
.action(wrap(cmdJoin));
|
|
47
|
+
program
|
|
48
|
+
.command("install <target>")
|
|
49
|
+
.description("install an IDE plugin: cursor | codex")
|
|
50
|
+
.action((target) => cmdInstall(target));
|
|
51
|
+
// ─── Operations ───────────────────────────────────────────────────────────────
|
|
52
|
+
program
|
|
53
|
+
.command("branch <name>")
|
|
54
|
+
.description("create a new branch from the current (or specified) branch")
|
|
55
|
+
.option("-f, --from <branch>", "source branch (default: current branch)")
|
|
56
|
+
.action(wrap((name, opts) => cmdBranch(name, opts)));
|
|
57
|
+
program
|
|
58
|
+
.command("checkout <name>")
|
|
59
|
+
.description("switch the active branch")
|
|
60
|
+
.action(wrap((name) => cmdCheckout(name)));
|
|
61
|
+
program
|
|
62
|
+
.command("status")
|
|
63
|
+
.description("show current tree, network, branch, and signer")
|
|
64
|
+
.action(wrap(cmdStatus));
|
|
65
|
+
program
|
|
66
|
+
.command("log")
|
|
67
|
+
.description("show recent commits on a branch")
|
|
68
|
+
.option("-b, --branch <name>", "branch name (default: current git branch)")
|
|
69
|
+
.option("-n, --limit <n>", "number of commits", parseInt, 20)
|
|
70
|
+
.action(wrap((opts) => cmdLog(opts)));
|
|
71
|
+
program
|
|
72
|
+
.command("recall [query]")
|
|
73
|
+
.description("semantic recall from branch memory")
|
|
74
|
+
.option("-b, --branch <name>", "branch to recall from")
|
|
75
|
+
.option("-n, --limit <n>", "max results", parseInt, 5)
|
|
76
|
+
.option("--json", "output as JSON (used by plugin hooks)")
|
|
77
|
+
.action(wrap((query, opts) => cmdRecall(query ?? "", opts)));
|
|
78
|
+
program
|
|
79
|
+
.command("commit")
|
|
80
|
+
.description("commit facts to the current branch")
|
|
81
|
+
.requiredOption("-m, --message <msg>", "commit message")
|
|
82
|
+
.option("-b, --branch <name>", "branch (default: current git branch)")
|
|
83
|
+
.option("-f, --facts <facts...>", "one or more fact strings")
|
|
84
|
+
.option("--from-response <text>", "extract facts from a full response text")
|
|
85
|
+
.option("--auto-extract", "use LLM to extract durable facts (requires --from-response)")
|
|
86
|
+
.action(wrap((opts) => cmdCommit(opts)));
|
|
87
|
+
program
|
|
88
|
+
.command("merge <from> <into>")
|
|
89
|
+
.description("propose a merge from one branch into another")
|
|
90
|
+
.requiredOption("-r, --resolver <id>", "ResolverRef object ID")
|
|
91
|
+
.option("--ttl <ms>", "TTL in milliseconds", parseInt, 86_400_000)
|
|
92
|
+
.action(wrap((from, into, opts) => cmdMerge(from, into, opts)));
|
|
93
|
+
program
|
|
94
|
+
.command("proposals")
|
|
95
|
+
.description("list open merge proposals")
|
|
96
|
+
.action(wrap(cmdProposals));
|
|
97
|
+
program
|
|
98
|
+
.command("ui")
|
|
99
|
+
.description("open the MemForks DAG visualizer")
|
|
100
|
+
.option("--share", "build and publish to a Walrus Site (shareable URL)")
|
|
101
|
+
.option("-p, --port <n>", "local server port", (v) => parseInt(v, 10), 4242)
|
|
102
|
+
.action(wrap((opts) => cmdUi(opts)));
|
|
103
|
+
program
|
|
104
|
+
.command("show <commitId>")
|
|
105
|
+
.description("show details of a single commit")
|
|
106
|
+
.action(wrap((commitId) => cmdShow(commitId)));
|
|
107
|
+
program
|
|
108
|
+
.command("diff <from> <to>")
|
|
109
|
+
.description("show fact differences between two branches or commits")
|
|
110
|
+
.action(wrap((from, to) => cmdDiff(from, to)));
|
|
111
|
+
// ─── ACL ─────────────────────────────────────────────────────────────────────
|
|
112
|
+
program
|
|
113
|
+
.command("delegates")
|
|
114
|
+
.description("list all delegates for the current tree")
|
|
115
|
+
.action(wrap(cmdDelegates));
|
|
116
|
+
program
|
|
117
|
+
.command("grant <address>")
|
|
118
|
+
.description("grant a delegate key write access to the tree")
|
|
119
|
+
.option("-p, --permissions <hex>", "permission bitmask in hex (default: 0xFF = all)", "0xFF")
|
|
120
|
+
.option("--expiry <ms>", "expiry timestamp in epoch ms (default: never)", parseInt)
|
|
121
|
+
.option("-b, --branches <names...>", "restrict to specific branches")
|
|
122
|
+
.action(wrap((address, opts) => cmdGrant({ address, ...opts })));
|
|
123
|
+
program
|
|
124
|
+
.command("grant-memwal <address>")
|
|
125
|
+
.description("register a team member's MemWal key (run by the owner after `memfork join`)")
|
|
126
|
+
.requiredOption("--pubkey <hex>", "MemWal delegate public key (hex) — printed by `memfork join`")
|
|
127
|
+
.action(wrap((address, opts) => cmdGrantMemwal({ agent: address, pubkey: opts.pubkey })));
|
|
128
|
+
program
|
|
129
|
+
.command("revoke <address>")
|
|
130
|
+
.description("revoke a delegate key")
|
|
131
|
+
.action(wrap((address) => cmdRevoke(address)));
|
|
132
|
+
// ─── Error handling ───────────────────────────────────────────────────────────
|
|
133
|
+
program.configureOutput({
|
|
134
|
+
writeErr: (str) => process.stderr.write(chalk.red(str)),
|
|
135
|
+
});
|
|
136
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
137
|
+
console.error(chalk.red("Error: " + String(e)));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
140
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
141
|
+
function wrap(fn) {
|
|
142
|
+
return (...args) => {
|
|
143
|
+
fn(...args).catch((e) => {
|
|
144
|
+
if (e.name === "ConfigError") {
|
|
145
|
+
console.error(chalk.red("\n " + String(e.message)));
|
|
146
|
+
console.error(chalk.cyan(" → Run `memfork init` to configure.\n"));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error(chalk.red("\nError: " + String(e)));
|
|
150
|
+
}
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memfork doctor`
|
|
3
|
+
*
|
|
4
|
+
* Verifies the full setup end-to-end:
|
|
5
|
+
* ✓ / ✗ .memfork/config.json exists
|
|
6
|
+
* ✓ / ✗ credentials file exists and is chmod 600
|
|
7
|
+
* ✓ / ✗ Sui RPC reachable
|
|
8
|
+
* ✓ / ✗ MemoryTree object found on-chain
|
|
9
|
+
* ✓ / ✗ Signer address matches tree owner (or has delegate)
|
|
10
|
+
* ✓ / ✗ MemWal account reachable
|
|
11
|
+
*/
|
|
12
|
+
export declare function cmdDoctor(): Promise<void>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memfork doctor`
|
|
3
|
+
*
|
|
4
|
+
* Verifies the full setup end-to-end:
|
|
5
|
+
* ✓ / ✗ .memfork/config.json exists
|
|
6
|
+
* ✓ / ✗ credentials file exists and is chmod 600
|
|
7
|
+
* ✓ / ✗ Sui RPC reachable
|
|
8
|
+
* ✓ / ✗ MemoryTree object found on-chain
|
|
9
|
+
* ✓ / ✗ Signer address matches tree owner (or has delegate)
|
|
10
|
+
* ✓ / ✗ MemWal account reachable
|
|
11
|
+
*/
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import { resolveConfig, readProjectConfig, credentialsPath, } from "../config.js";
|
|
15
|
+
import { MemForksClient } from "@memfork/core";
|
|
16
|
+
function icon(s) {
|
|
17
|
+
return { ok: chalk.green("✓"), fail: chalk.red("✗"), warn: chalk.yellow("⚠"), skip: chalk.dim("·") }[s];
|
|
18
|
+
}
|
|
19
|
+
function printCheck(c) {
|
|
20
|
+
console.log(` ${icon(c.status)} ${c.label}` + (c.detail ? chalk.dim(" — " + c.detail) : ""));
|
|
21
|
+
if (c.fix && c.status !== "ok" && c.status !== "skip") {
|
|
22
|
+
console.log(` ${chalk.cyan("→")} ${c.fix}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function cmdDoctor() {
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(chalk.bold("memfork doctor"));
|
|
28
|
+
console.log("");
|
|
29
|
+
const checks = [];
|
|
30
|
+
let cfg;
|
|
31
|
+
// ── 1. Project config ──────────────────────────────────────────────────────
|
|
32
|
+
const project = readProjectConfig();
|
|
33
|
+
checks.push({
|
|
34
|
+
label: ".memfork/config.json",
|
|
35
|
+
status: project ? "ok" : "warn",
|
|
36
|
+
detail: project ? `tree: ${project.treeId?.slice(0, 10)}…` : "not found (credentials-only mode)",
|
|
37
|
+
fix: project ? undefined : "Run `memfork init` from the project root to create it",
|
|
38
|
+
});
|
|
39
|
+
// ── 2. Credentials file ─────────────────────────────────────────────────────
|
|
40
|
+
const credsPath = credentialsPath();
|
|
41
|
+
const credsExists = fs.existsSync(credsPath);
|
|
42
|
+
let credsPerms = "skip";
|
|
43
|
+
if (credsExists) {
|
|
44
|
+
const mode = fs.statSync(credsPath).mode & 0o777;
|
|
45
|
+
credsPerms = (mode === 0o600) ? "ok" : "warn";
|
|
46
|
+
}
|
|
47
|
+
checks.push({
|
|
48
|
+
label: "~/.memfork/credentials.json",
|
|
49
|
+
status: credsExists ? (credsPerms === "ok" ? "ok" : "warn") : "fail",
|
|
50
|
+
detail: credsExists ? (credsPerms === "ok" ? "chmod 600 ✓" : "permissions too open") : "not found",
|
|
51
|
+
fix: credsExists
|
|
52
|
+
? "Run: chmod 600 ~/.memfork/credentials.json"
|
|
53
|
+
: "Run `memfork init` to create it",
|
|
54
|
+
});
|
|
55
|
+
// ── 3. Config resolution ────────────────────────────────────────────────────
|
|
56
|
+
try {
|
|
57
|
+
cfg = resolveConfig();
|
|
58
|
+
checks.push({
|
|
59
|
+
label: "Config resolution",
|
|
60
|
+
status: "ok",
|
|
61
|
+
detail: `tree ${cfg.treeId.slice(0, 10)}… / ${cfg.network}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
checks.push({
|
|
66
|
+
label: "Config resolution",
|
|
67
|
+
status: "fail",
|
|
68
|
+
detail: e.message,
|
|
69
|
+
fix: "Run `memfork init`",
|
|
70
|
+
});
|
|
71
|
+
checks.forEach(printCheck);
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(chalk.red(" Setup incomplete. Run `memfork init` to fix."));
|
|
74
|
+
console.log("");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// ── 4. Sui RPC reachable ────────────────────────────────────────────────────
|
|
78
|
+
let client;
|
|
79
|
+
try {
|
|
80
|
+
client = await MemForksClient.connect({
|
|
81
|
+
treeId: cfg.treeId,
|
|
82
|
+
signer: cfg.privateKey,
|
|
83
|
+
network: cfg.network,
|
|
84
|
+
rpcUrl: cfg.rpcUrl,
|
|
85
|
+
packageId: cfg.packageId,
|
|
86
|
+
});
|
|
87
|
+
// Quick liveness ping — getChainIdentifier is cheap.
|
|
88
|
+
await client.suiClient.getChainIdentifier();
|
|
89
|
+
checks.push({ label: "Sui RPC", status: "ok", detail: cfg.rpcUrl ?? `${cfg.network} default` });
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
checks.push({
|
|
93
|
+
label: "Sui RPC",
|
|
94
|
+
status: "fail",
|
|
95
|
+
detail: String(e),
|
|
96
|
+
fix: "Check your network connection or set a custom MEMFORK_RPC_URL",
|
|
97
|
+
});
|
|
98
|
+
checks.forEach(printCheck);
|
|
99
|
+
console.log("");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
// ── 5. MemoryTree on-chain ──────────────────────────────────────────────────
|
|
103
|
+
try {
|
|
104
|
+
const tree = await client.getTree();
|
|
105
|
+
checks.push({
|
|
106
|
+
label: "MemoryTree on-chain",
|
|
107
|
+
status: "ok",
|
|
108
|
+
detail: `default branch: ${tree.default_branch}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
checks.push({
|
|
113
|
+
label: "MemoryTree on-chain",
|
|
114
|
+
status: "fail",
|
|
115
|
+
detail: `object not found: ${cfg.treeId.slice(0, 10)}…`,
|
|
116
|
+
fix: "Check the treeId in .memfork/config.json or run `memfork init`",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── 6. Signer balance (warn if low) ─────────────────────────────────────────
|
|
120
|
+
try {
|
|
121
|
+
const addr = client.keypair.toSuiAddress();
|
|
122
|
+
const balance = await client.suiClient.getBalance({ owner: addr });
|
|
123
|
+
const sui = Number(balance.totalBalance) / 1e9;
|
|
124
|
+
const low = sui < 0.1;
|
|
125
|
+
checks.push({
|
|
126
|
+
label: "Signer balance",
|
|
127
|
+
status: low ? "warn" : "ok",
|
|
128
|
+
detail: `${sui.toFixed(4)} SUI (${addr.slice(0, 10)}…)`,
|
|
129
|
+
fix: low ? (cfg.network === "mainnet" ? "Gas is sponsored — no SUI needed. If you need a balance, send SUI to the address above." : "Fund via faucet: sui client faucet or https://faucet.testnet.sui.io") : undefined,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
checks.push({ label: "Signer balance", status: "skip", detail: "could not fetch" });
|
|
134
|
+
}
|
|
135
|
+
// ── 7. MemWal reachable ──────────────────────────────────────────────────────
|
|
136
|
+
try {
|
|
137
|
+
const resp = await fetch(cfg.memwalRelayer + "/health", { signal: AbortSignal.timeout(5000) });
|
|
138
|
+
checks.push({
|
|
139
|
+
label: "MemWal relayer",
|
|
140
|
+
status: resp.ok ? "ok" : "warn",
|
|
141
|
+
detail: resp.ok ? cfg.memwalRelayer : `HTTP ${resp.status}`,
|
|
142
|
+
fix: resp.ok ? undefined : "Check MEMFORK_MEMWAL_RELAYER or try again",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
checks.push({
|
|
147
|
+
label: "MemWal relayer",
|
|
148
|
+
status: "warn",
|
|
149
|
+
detail: "could not reach " + cfg.memwalRelayer,
|
|
150
|
+
fix: "Check your network. MemWal read/write will be unavailable.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// ── Print all checks ─────────────────────────────────────────────────────────
|
|
154
|
+
console.log("");
|
|
155
|
+
checks.forEach(printCheck);
|
|
156
|
+
console.log("");
|
|
157
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
158
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
console.log(chalk.red(` ${failed} check(s) failed. Run \`memfork init\` to fix.`));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
else if (warned > 0) {
|
|
164
|
+
console.log(chalk.yellow(` ${warned} warning(s). Setup is functional but review the items above.`));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(chalk.green(" Everything looks good."));
|
|
168
|
+
}
|
|
169
|
+
console.log("");
|
|
170
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memfork init`
|
|
3
|
+
*
|
|
4
|
+
* Interactive first-run setup. Writes:
|
|
5
|
+
* .memfork/config.json — committable project config (treeId, network, branch)
|
|
6
|
+
* ~/.memfork/credentials.json — secrets, chmod 600 (privateKey, memwalKey, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: re-running updates values without destroying existing ones.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cmdInit(opts?: {
|
|
11
|
+
quick?: boolean;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memfork init`
|
|
3
|
+
*
|
|
4
|
+
* Interactive first-run setup. Writes:
|
|
5
|
+
* .memfork/config.json — committable project config (treeId, network, branch)
|
|
6
|
+
* ~/.memfork/credentials.json — secrets, chmod 600 (privateKey, memwalKey, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: re-running updates values without destroying existing ones.
|
|
9
|
+
*/
|
|
10
|
+
import { input, select, password, confirm } from "@inquirer/prompts";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { readProjectConfig, readCredentials, writeProjectConfig, upsertCredential, credentialsPath, } from "../config.js";
|
|
15
|
+
import { MemForksClient } from "@memfork/core";
|
|
16
|
+
import { autoProvision } from "./provision.js";
|
|
17
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
function dim(s) { return chalk.dim(s); }
|
|
19
|
+
function ok(s) { return chalk.green("✓") + " " + s; }
|
|
20
|
+
function err(s) { return chalk.red("✗") + " " + chalk.red(s); }
|
|
21
|
+
function tip(s) { return chalk.cyan("→") + " " + s; }
|
|
22
|
+
// ─── Command ──────────────────────────────────────────────────────────────────
|
|
23
|
+
export async function cmdInit(opts = {}) {
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(chalk.bold("MemForks init") + " " + dim("configure your memory tree"));
|
|
26
|
+
console.log("");
|
|
27
|
+
// ── Mode selection ──────────────────────────────────────────────────────────
|
|
28
|
+
const mode = opts.quick
|
|
29
|
+
? "quick"
|
|
30
|
+
: await select({
|
|
31
|
+
message: "How do you want to set up?",
|
|
32
|
+
choices: [
|
|
33
|
+
{
|
|
34
|
+
value: "quick",
|
|
35
|
+
name: "Quick setup " + chalk.dim("— auto-provision: keygen → faucet → MemWal → tree (recommended)"),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: "manual",
|
|
39
|
+
name: "Manual setup " + chalk.dim("— paste existing keys & IDs"),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
if (mode === "quick") {
|
|
44
|
+
await cmdInitQuick();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await cmdInitManual();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ─── Quick path ───────────────────────────────────────────────────────────────
|
|
51
|
+
async function cmdInitQuick() {
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log(dim(" We'll generate a fresh keypair, fund it from the faucet,"));
|
|
54
|
+
console.log(dim(" create your MemWal account and memory tree automatically."));
|
|
55
|
+
console.log(dim(" Nothing to copy-paste."));
|
|
56
|
+
console.log("");
|
|
57
|
+
const network = await select({
|
|
58
|
+
message: "Sui network",
|
|
59
|
+
choices: [
|
|
60
|
+
{ value: "mainnet", name: "mainnet (recommended — gas sponsored by MemForks)" },
|
|
61
|
+
{ value: "testnet", name: "testnet (free gas via faucet)" },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const existingKey = readProjectConfig()
|
|
65
|
+
? (await confirm({
|
|
66
|
+
message: "Reuse your existing Sui key instead of generating a new one?",
|
|
67
|
+
default: false,
|
|
68
|
+
}))
|
|
69
|
+
? await password({
|
|
70
|
+
message: "Sui private key (suiprivkey1… or 64-char hex)",
|
|
71
|
+
mask: "*",
|
|
72
|
+
})
|
|
73
|
+
: undefined
|
|
74
|
+
: undefined;
|
|
75
|
+
console.log("");
|
|
76
|
+
try {
|
|
77
|
+
const result = await autoProvision({
|
|
78
|
+
network,
|
|
79
|
+
existingKey: existingKey || undefined,
|
|
80
|
+
});
|
|
81
|
+
// Persist
|
|
82
|
+
writeProjectConfig({
|
|
83
|
+
treeId: result.treeId,
|
|
84
|
+
network: result.network,
|
|
85
|
+
defaultBranch: "main",
|
|
86
|
+
});
|
|
87
|
+
upsertCredential(result.treeId, {
|
|
88
|
+
privateKey: result.privateKey,
|
|
89
|
+
memwalAccountId: result.memwalAccountId,
|
|
90
|
+
memwalKey: result.memwalKey,
|
|
91
|
+
});
|
|
92
|
+
ensureGitignore();
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(chalk.green.bold(" Setup complete!"));
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log(ok(`Tree ID: ${chalk.bold(result.treeId)}`));
|
|
97
|
+
console.log(ok(`Address: ${chalk.dim("(saved to credentials)")}`));
|
|
98
|
+
console.log(ok(`Project cfg: ${chalk.bold(".memfork/config.json")} ${dim("(safe to commit)")}`));
|
|
99
|
+
console.log(ok(`Credentials: ${chalk.bold(credentialsPath())} ${dim("(chmod 600, gitignored)")}`));
|
|
100
|
+
printNextSteps();
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(err(String(e)));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ─── Manual path ──────────────────────────────────────────────────────────────
|
|
109
|
+
async function cmdInitManual() {
|
|
110
|
+
const existing = readProjectConfig();
|
|
111
|
+
const creds = readCredentials();
|
|
112
|
+
// ── Step 1: network ──────────────────────────────────────────────────────────
|
|
113
|
+
const network = await select({
|
|
114
|
+
message: "Sui network",
|
|
115
|
+
default: existing?.network ?? "mainnet",
|
|
116
|
+
choices: [
|
|
117
|
+
{ value: "mainnet", name: "mainnet (recommended — gas sponsored by MemForks)" },
|
|
118
|
+
{ value: "testnet", name: "testnet (free gas via faucet)" },
|
|
119
|
+
{ value: "devnet", name: "devnet" },
|
|
120
|
+
{ value: "localnet", name: "localnet" },
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
// ── Step 2: tree ID ──────────────────────────────────────────────────────────
|
|
124
|
+
const treeMode = await select({
|
|
125
|
+
message: "MemoryTree",
|
|
126
|
+
choices: [
|
|
127
|
+
{ value: "existing", name: "Use an existing tree (paste the object ID)" },
|
|
128
|
+
{ value: "new", name: "Create a new tree now" },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
let treeId = "";
|
|
132
|
+
if (treeMode === "existing") {
|
|
133
|
+
treeId = (await input({
|
|
134
|
+
message: "Tree object ID (0x…)",
|
|
135
|
+
default: existing?.treeId,
|
|
136
|
+
validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be a 64-char hex address starting with 0x",
|
|
137
|
+
})).trim();
|
|
138
|
+
}
|
|
139
|
+
// ── Step 3: Sui private key ───────────────────────────────────────────────────
|
|
140
|
+
const storedKey = treeMode === "existing" ? creds.trees[treeId]?.privateKey : undefined;
|
|
141
|
+
const privateKeyInput = await password({
|
|
142
|
+
message: storedKey
|
|
143
|
+
? "Sui private key (suiprivkey1… or hex — enter to keep existing)"
|
|
144
|
+
: "Sui private key (suiprivkey1… bech32 or 64-char hex)",
|
|
145
|
+
mask: "*",
|
|
146
|
+
validate: (v) => {
|
|
147
|
+
if (storedKey && v === "")
|
|
148
|
+
return true;
|
|
149
|
+
if (v.startsWith("suiprivkey"))
|
|
150
|
+
return true;
|
|
151
|
+
if (/^[0-9a-fA-F]{64}$/.test(v))
|
|
152
|
+
return true;
|
|
153
|
+
return "Expected suiprivkey1… (Sui CLI format) or 64-char hex";
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const resolvedKey = privateKeyInput === "" && storedKey ? storedKey : privateKeyInput;
|
|
157
|
+
// ── Step 4: create tree if needed ─────────────────────────────────────────────
|
|
158
|
+
if (treeMode === "new") {
|
|
159
|
+
console.log("");
|
|
160
|
+
const defaultBranch = await input({ message: "Default branch name", default: "main" });
|
|
161
|
+
const memwalAccId = (await input({
|
|
162
|
+
message: "MemWal account ID (0x…)",
|
|
163
|
+
validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be 0x… address",
|
|
164
|
+
})).trim();
|
|
165
|
+
process.stdout.write(chalk.dim(" Creating MemoryTree on " + network + "… "));
|
|
166
|
+
try {
|
|
167
|
+
const tempClient = await MemForksClient.connect({
|
|
168
|
+
treeId: "0x" + "0".repeat(64),
|
|
169
|
+
signer: resolvedKey,
|
|
170
|
+
network,
|
|
171
|
+
});
|
|
172
|
+
const { treeId: newId, digest } = await tempClient.initTree(memwalAccId, defaultBranch);
|
|
173
|
+
treeId = newId;
|
|
174
|
+
console.log(chalk.green("done"));
|
|
175
|
+
console.log(ok(`Tree created: ${chalk.bold(treeId)}`));
|
|
176
|
+
console.log(dim(` tx: ${digest}`));
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
console.log(chalk.red("failed"));
|
|
180
|
+
console.error(err(String(e)));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ── Step 5: MemWal credentials ────────────────────────────────────────────────
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(dim(" MemWal — decentralised blob storage for memory contents."));
|
|
187
|
+
console.log("");
|
|
188
|
+
const storedCred = creds.trees[treeId];
|
|
189
|
+
const memwalAccountId = (await input({
|
|
190
|
+
message: "MemWal account ID (0x…)",
|
|
191
|
+
default: storedCred?.memwalAccountId,
|
|
192
|
+
validate: (v) => /^0x[0-9a-fA-F]{64}$/.test(v.trim()) ? true : "Must be 0x… address",
|
|
193
|
+
})).trim();
|
|
194
|
+
const memwalKeyInput = await password({
|
|
195
|
+
message: storedCred?.memwalKey
|
|
196
|
+
? "MemWal delegate key (enter to keep existing)"
|
|
197
|
+
: "MemWal delegate key (64-char hex)",
|
|
198
|
+
mask: "*",
|
|
199
|
+
validate: (v) => {
|
|
200
|
+
if (storedCred?.memwalKey && v === "")
|
|
201
|
+
return true;
|
|
202
|
+
if (/^[0-9a-fA-F]{64}$/.test(v))
|
|
203
|
+
return true;
|
|
204
|
+
return "Expected 64-char hex";
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const resolvedMemwalKey = memwalKeyInput === "" && storedCred?.memwalKey
|
|
208
|
+
? storedCred.memwalKey
|
|
209
|
+
: memwalKeyInput;
|
|
210
|
+
// ── Step 6: optional overrides ────────────────────────────────────────────────
|
|
211
|
+
const advanced = await confirm({
|
|
212
|
+
message: "Configure advanced options (RPC URL, package ID override)?",
|
|
213
|
+
default: false,
|
|
214
|
+
});
|
|
215
|
+
let rpcUrl;
|
|
216
|
+
let packageId;
|
|
217
|
+
let defaultBranch = existing?.defaultBranch ?? "main";
|
|
218
|
+
if (advanced) {
|
|
219
|
+
rpcUrl = (await input({ message: "Custom RPC URL (leave blank for default)" })).trim() || undefined;
|
|
220
|
+
packageId = (await input({ message: "Package ID override (leave blank for default)" })).trim() || undefined;
|
|
221
|
+
defaultBranch = await input({ message: "Default branch", default: defaultBranch });
|
|
222
|
+
}
|
|
223
|
+
// ── Write ──────────────────────────────────────────────────────────────────────
|
|
224
|
+
writeProjectConfig({
|
|
225
|
+
treeId,
|
|
226
|
+
network: network ?? "testnet",
|
|
227
|
+
defaultBranch,
|
|
228
|
+
...(rpcUrl ? { rpcUrl } : {}),
|
|
229
|
+
...(packageId ? { packageId } : {}),
|
|
230
|
+
});
|
|
231
|
+
upsertCredential(treeId, {
|
|
232
|
+
privateKey: resolvedKey,
|
|
233
|
+
memwalAccountId,
|
|
234
|
+
memwalKey: resolvedMemwalKey,
|
|
235
|
+
});
|
|
236
|
+
ensureGitignore();
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(ok(`Project config written to ${chalk.bold(".memfork/config.json")} ${dim("(safe to commit)")}`));
|
|
239
|
+
console.log(ok(`Credentials stored in ${chalk.bold(credentialsPath())} ${dim("(chmod 600, gitignored)")}`));
|
|
240
|
+
console.log("");
|
|
241
|
+
// ── Verify ────────────────────────────────────────────────────────────────────
|
|
242
|
+
process.stdout.write(chalk.dim(" Verifying connection to Sui… "));
|
|
243
|
+
try {
|
|
244
|
+
const client = await MemForksClient.connect({
|
|
245
|
+
treeId,
|
|
246
|
+
signer: resolvedKey,
|
|
247
|
+
network: network ?? "testnet",
|
|
248
|
+
...(rpcUrl ? { rpcUrl } : {}),
|
|
249
|
+
...(packageId ? { packageId } : {}),
|
|
250
|
+
});
|
|
251
|
+
await client.getTree();
|
|
252
|
+
console.log(chalk.green("ok"));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
console.log(chalk.yellow("failed"));
|
|
256
|
+
console.log(chalk.yellow(" ⚠ Could not reach tree — run `memfork doctor` to diagnose."));
|
|
257
|
+
}
|
|
258
|
+
printNextSteps();
|
|
259
|
+
}
|
|
260
|
+
// ─── Shared footer ─────────────────────────────────────────────────────────────
|
|
261
|
+
function printNextSteps() {
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log(chalk.bold("Next steps:"));
|
|
264
|
+
console.log(tip("memfork doctor — verify the full setup"));
|
|
265
|
+
console.log(tip("memfork install cursor — install the Cursor plugin"));
|
|
266
|
+
console.log(tip("memfork install codex — install the Codex plugin"));
|
|
267
|
+
console.log(tip("memfork status — show tree status"));
|
|
268
|
+
console.log("");
|
|
269
|
+
}
|
|
270
|
+
// ─── .gitignore helper ────────────────────────────────────────────────────────
|
|
271
|
+
function ensureGitignore(cwd = process.cwd()) {
|
|
272
|
+
const lines = [
|
|
273
|
+
".memfork/credentials.json",
|
|
274
|
+
".memfork/*.local.json",
|
|
275
|
+
];
|
|
276
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
277
|
+
let existing = "";
|
|
278
|
+
if (fs.existsSync(gitignorePath)) {
|
|
279
|
+
existing = fs.readFileSync(gitignorePath, "utf8");
|
|
280
|
+
}
|
|
281
|
+
const toAdd = lines.filter((l) => !existing.includes(l));
|
|
282
|
+
if (toAdd.length === 0)
|
|
283
|
+
return;
|
|
284
|
+
const block = "\n# MemForks — never commit private keys\n" + toAdd.join("\n") + "\n";
|
|
285
|
+
fs.appendFileSync(gitignorePath, block, "utf8");
|
|
286
|
+
}
|