meridian-cli 1.0.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.
Files changed (3) hide show
  1. package/package.json +32 -0
  2. package/src/main.ts +115 -0
  3. package/tsconfig.json +11 -0
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "meridian-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for inspecting and replaying Meridian CRDT state",
5
+ "type": "module",
6
+ "bin": {
7
+ "meridian": "./dist/main.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod +x dist/main.js",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "biome lint ./src",
13
+ "dev": "bun run src/main.ts"
14
+ },
15
+ "dependencies": {
16
+ "@effect/cli": "^0.75.0",
17
+ "@effect/platform": "^0.80.0",
18
+ "@effect/platform-node": "^0.106.0",
19
+ "effect": "^3.21.0",
20
+ "meridian-sdk": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "@biomejs/biome": "^2.4.7",
24
+ "typescript": "^5.8.0"
25
+ },
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Chahine-tech/meridian.git",
30
+ "directory": "packages/cli"
31
+ }
32
+ }
package/src/main.ts ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Options, Args } from "@effect/cli";
3
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
+ import { Console, Effect, Option } from "effect";
5
+ import { HttpClient } from "meridian-sdk";
6
+
7
+ const urlOption = Options.text("url").pipe(
8
+ Options.withDescription("Meridian server base URL"),
9
+ Options.withAlias("u"),
10
+ Options.withDefault("http://localhost:3000"),
11
+ );
12
+
13
+ const tokenOption = Options.text("token").pipe(
14
+ Options.withDescription("Bearer token for authentication"),
15
+ Options.withAlias("t"),
16
+ );
17
+
18
+ const limitOption = Options.integer("limit").pipe(
19
+ Options.withDescription("Number of history entries per page"),
20
+ Options.withDefault(50),
21
+ );
22
+
23
+ const namespaceArg = Args.text({ name: "namespace" }).pipe(
24
+ Args.withDescription("The Meridian namespace"),
25
+ );
26
+
27
+ const crdtIdArg = Args.text({ name: "crdt-id" }).pipe(
28
+ Args.withDescription("The CRDT identifier"),
29
+ );
30
+
31
+ const inspectCommand = Command.make(
32
+ "inspect",
33
+ { url: urlOption, token: tokenOption, namespace: namespaceArg, crdtId: crdtIdArg },
34
+ ({ url, token, namespace, crdtId }) =>
35
+ Effect.gen(function* () {
36
+ const http = new HttpClient({ baseUrl: url, token });
37
+
38
+ yield* Console.log(`Inspecting ${namespace}/${crdtId} at ${url}\n`);
39
+
40
+ const result = yield* http.getCrdt(namespace, crdtId).pipe(
41
+ Effect.mapError((e) => new Error(`Request failed: ${JSON.stringify(e)}`)),
42
+ );
43
+
44
+ yield* Console.log(JSON.stringify(result, null, 2));
45
+ }).pipe(Effect.catchAll((e) => Console.error(String(e)))),
46
+ );
47
+
48
+ const fromSeqOption = Options.integer("from-seq").pipe(
49
+ Options.withDescription("Start replaying from this WAL sequence number"),
50
+ Options.withDefault(0),
51
+ );
52
+
53
+ const replayCommand = Command.make(
54
+ "replay",
55
+ {
56
+ url: urlOption,
57
+ token: tokenOption,
58
+ namespace: namespaceArg,
59
+ crdtId: crdtIdArg,
60
+ fromSeq: fromSeqOption,
61
+ limit: limitOption,
62
+ },
63
+ ({ url, token, namespace, crdtId, fromSeq, limit }) =>
64
+ Effect.gen(function* () {
65
+ const http = new HttpClient({ baseUrl: url, token });
66
+
67
+ yield* Console.log(`Replaying WAL for ${namespace}/${crdtId} from seq=${fromSeq}\n`);
68
+ yield* Console.log("seq timestamp op");
69
+ yield* Console.log("─".repeat(72));
70
+
71
+ let currentSeq = fromSeq;
72
+ let hasMore = true;
73
+
74
+ while (hasMore) {
75
+ const response = yield* Effect.tryPromise({
76
+ try: () => http.getHistory(namespace, crdtId, currentSeq, limit),
77
+ catch: (e) => new Error(`Request failed: ${String(e)}`),
78
+ });
79
+
80
+ for (const entry of response.entries) {
81
+ const ts = new Date(entry.timestamp_ms).toISOString();
82
+ const op = JSON.stringify(entry.op);
83
+ const opTrunc = op.length > 45 ? `${op.slice(0, 44)}…` : op;
84
+ yield* Console.log(`#${String(entry.seq).padEnd(9)} ${ts} ${opTrunc}`);
85
+ }
86
+
87
+ if (response.next_seq !== null && Option.isSome(Option.fromNullable(response.next_seq))) {
88
+ currentSeq = response.next_seq;
89
+ } else {
90
+ hasMore = false;
91
+ }
92
+
93
+ if (response.entries.length === 0) {
94
+ hasMore = false;
95
+ }
96
+ }
97
+
98
+ yield* Console.log("\nDone.");
99
+ }).pipe(Effect.catchAll((e) => Console.error(String(e)))),
100
+ );
101
+
102
+ const meridianCommand = Command.make("meridian").pipe(
103
+ Command.withDescription("Meridian CRDT CLI — inspect state and replay WAL history"),
104
+ Command.withSubcommands([inspectCommand, replayCommand]),
105
+ );
106
+
107
+ const cli = Command.run(meridianCommand, {
108
+ name: "meridian",
109
+ version: "1.0.0",
110
+ });
111
+
112
+ cli(process.argv).pipe(
113
+ Effect.provide(NodeContext.layer),
114
+ NodeRuntime.runMain,
115
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src"
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["node_modules", "dist"]
11
+ }