@livestore/cli 0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c → 0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b
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/import-export.d.ts +7 -31
- package/dist/commands/import-export.d.ts.map +1 -1
- package/dist/commands/import-export.js +34 -194
- package/dist/commands/import-export.js.map +1 -1
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
- package/dist/commands/mcp-tool-handlers.js +30 -1
- package/dist/commands/mcp-tool-handlers.js.map +1 -1
- package/dist/commands/mcp-tools-defs.d.ts +30 -0
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
- package/dist/commands/mcp-tools-defs.js +83 -1
- package/dist/commands/mcp-tools-defs.js.map +1 -1
- package/dist/sync-operations.d.ts +111 -0
- package/dist/sync-operations.d.ts.map +1 -0
- package/dist/sync-operations.js +230 -0
- package/dist/sync-operations.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/commands/import-export.ts +48 -316
- package/src/commands/mcp-tool-handlers.ts +34 -1
- package/src/commands/mcp-tools-defs.ts +88 -0
- package/src/sync-operations.ts +413 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/cli",
|
|
3
|
-
"version": "0.0.0-snapshot-
|
|
3
|
+
"version": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/ai-openai": "0.32.0",
|
|
14
|
-
"@livestore/adapter-node": "0.0.0-snapshot-
|
|
15
|
-
"@livestore/
|
|
16
|
-
"@livestore/
|
|
17
|
-
"@livestore/
|
|
18
|
-
"@livestore/sync-cf": "0.0.0-snapshot-
|
|
19
|
-
"@livestore/utils": "0.0.0-snapshot-
|
|
14
|
+
"@livestore/adapter-node": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
15
|
+
"@livestore/common": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
16
|
+
"@livestore/peer-deps": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
17
|
+
"@livestore/livestore": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
18
|
+
"@livestore/sync-cf": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
|
|
19
|
+
"@livestore/utils": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "24.10.1",
|
|
23
23
|
"typescript": "5.9.2",
|
|
24
|
-
"@livestore/utils-dev": "0.0.0-snapshot-
|
|
24
|
+
"@livestore/utils-dev": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"package.json",
|
|
@@ -1,200 +1,8 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
4
|
-
import { UnknownError } from '@livestore/common'
|
|
5
|
-
import { isLiveStoreSchema, LiveStoreEvent } from '@livestore/common/schema'
|
|
6
|
-
import { shouldNeverHappen } from '@livestore/utils'
|
|
7
|
-
import {
|
|
8
|
-
Cause,
|
|
9
|
-
Console,
|
|
10
|
-
Effect,
|
|
11
|
-
FileSystem,
|
|
12
|
-
type HttpClient,
|
|
13
|
-
KeyValueStore,
|
|
14
|
-
Layer,
|
|
15
|
-
Option,
|
|
16
|
-
Schema,
|
|
17
|
-
type Scope,
|
|
18
|
-
Stream,
|
|
19
|
-
} from '@livestore/utils/effect'
|
|
2
|
+
import type { UnknownError } from '@livestore/common'
|
|
3
|
+
import { Console, Effect, FileSystem, type HttpClient, type Scope } from '@livestore/utils/effect'
|
|
20
4
|
import { Cli } from '@livestore/utils/node'
|
|
21
|
-
|
|
22
|
-
/** Connection timeout for sync backend ping (5 seconds) */
|
|
23
|
-
const CONNECTION_TIMEOUT_MS = 5000
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Schema for the export file format.
|
|
27
|
-
* Contains metadata about the export and an array of events in global encoded format.
|
|
28
|
-
*/
|
|
29
|
-
const ExportFileSchema = Schema.Struct({
|
|
30
|
-
/** Format version for future compatibility */
|
|
31
|
-
version: Schema.Literal(1),
|
|
32
|
-
/** Store identifier */
|
|
33
|
-
storeId: Schema.String,
|
|
34
|
-
/** ISO timestamp of when the export was created */
|
|
35
|
-
exportedAt: Schema.String,
|
|
36
|
-
/** Total number of events in the export */
|
|
37
|
-
eventCount: Schema.Number,
|
|
38
|
-
/** Array of events in global encoded format */
|
|
39
|
-
events: Schema.Array(LiveStoreEvent.Global.Encoded),
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
type ExportFile = typeof ExportFileSchema.Type
|
|
43
|
-
|
|
44
|
-
class ConnectionError extends Schema.TaggedError<ConnectionError>()('ConnectionError', {
|
|
45
|
-
cause: Schema.Defect,
|
|
46
|
-
note: Schema.String,
|
|
47
|
-
}) {}
|
|
48
|
-
|
|
49
|
-
class ExportError extends Schema.TaggedError<ExportError>()('ExportError', {
|
|
50
|
-
cause: Schema.Defect,
|
|
51
|
-
note: Schema.String,
|
|
52
|
-
}) {}
|
|
53
|
-
|
|
54
|
-
class ImportError extends Schema.TaggedError<ImportError>()('ImportError', {
|
|
55
|
-
cause: Schema.Defect,
|
|
56
|
-
note: Schema.String,
|
|
57
|
-
}) {}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Creates a sync backend connection from a user module and verifies connectivity.
|
|
61
|
-
* This is a simplified version of the MCP runtime that only creates the sync backend.
|
|
62
|
-
*/
|
|
63
|
-
const makeSyncBackend = ({
|
|
64
|
-
storePath,
|
|
65
|
-
storeId,
|
|
66
|
-
clientId,
|
|
67
|
-
}: {
|
|
68
|
-
storePath: string
|
|
69
|
-
storeId: string
|
|
70
|
-
clientId: string
|
|
71
|
-
}): Effect.Effect<
|
|
72
|
-
SyncBackend.SyncBackend,
|
|
73
|
-
UnknownError | ConnectionError,
|
|
74
|
-
FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
|
|
75
|
-
> =>
|
|
76
|
-
Effect.gen(function* () {
|
|
77
|
-
const abs = path.isAbsolute(storePath) ? storePath : path.resolve(process.cwd(), storePath)
|
|
78
|
-
|
|
79
|
-
const fs = yield* FileSystem.FileSystem
|
|
80
|
-
const exists = yield* fs.exists(abs).pipe(UnknownError.mapToUnknownError)
|
|
81
|
-
if (!exists) {
|
|
82
|
-
return yield* Effect.fail(
|
|
83
|
-
UnknownError.make({
|
|
84
|
-
cause: `Store module not found at ${abs}`,
|
|
85
|
-
note: 'Make sure the path points to a valid LiveStore module',
|
|
86
|
-
}),
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const mod = yield* Effect.tryPromise({
|
|
91
|
-
try: () => import(pathToFileURL(abs).href),
|
|
92
|
-
catch: (cause) =>
|
|
93
|
-
UnknownError.make({
|
|
94
|
-
cause,
|
|
95
|
-
note: `Failed to import module at ${abs}`,
|
|
96
|
-
}),
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const schema = (mod as any)?.schema
|
|
100
|
-
if (!isLiveStoreSchema(schema)) {
|
|
101
|
-
return yield* Effect.fail(
|
|
102
|
-
UnknownError.make({
|
|
103
|
-
cause: `Module at ${abs} must export a valid LiveStore 'schema'`,
|
|
104
|
-
note: `Ex: export { schema } from './src/livestore/schema.ts'`,
|
|
105
|
-
}),
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const syncBackendConstructor = (mod as any)?.syncBackend
|
|
110
|
-
if (typeof syncBackendConstructor !== 'function') {
|
|
111
|
-
return yield* Effect.fail(
|
|
112
|
-
UnknownError.make({
|
|
113
|
-
cause: `Module at ${abs} must export a 'syncBackend' constructor`,
|
|
114
|
-
note: `Ex: export const syncBackend = makeWsSync({ url })`,
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const syncPayloadSchemaExport = (mod as any)?.syncPayloadSchema
|
|
120
|
-
const syncPayloadSchema =
|
|
121
|
-
syncPayloadSchemaExport === undefined
|
|
122
|
-
? Schema.JsonValue
|
|
123
|
-
: Schema.isSchema(syncPayloadSchemaExport)
|
|
124
|
-
? (syncPayloadSchemaExport as Schema.Schema<any>)
|
|
125
|
-
: shouldNeverHappen(
|
|
126
|
-
`Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
const syncPayloadExport = (mod as any)?.syncPayload
|
|
130
|
-
const syncPayload = yield* (
|
|
131
|
-
syncPayloadExport === undefined
|
|
132
|
-
? Effect.succeed<unknown>(undefined)
|
|
133
|
-
: Schema.decodeUnknown(syncPayloadSchema)(syncPayloadExport)
|
|
134
|
-
).pipe(UnknownError.mapToUnknownError)
|
|
135
|
-
|
|
136
|
-
/** Simple in-memory key-value store for sync backend state */
|
|
137
|
-
const kvStore: { backendId: string | undefined } = { backendId: undefined }
|
|
138
|
-
|
|
139
|
-
const syncBackend = yield* (syncBackendConstructor as SyncBackend.SyncBackendConstructor)({
|
|
140
|
-
storeId,
|
|
141
|
-
clientId,
|
|
142
|
-
payload: syncPayload,
|
|
143
|
-
}).pipe(
|
|
144
|
-
Effect.provide(
|
|
145
|
-
Layer.succeed(
|
|
146
|
-
KeyValueStore.KeyValueStore,
|
|
147
|
-
KeyValueStore.makeStringOnly({
|
|
148
|
-
get: (_key) => Effect.succeed(Option.fromNullable(kvStore.backendId)),
|
|
149
|
-
set: (_key, value) =>
|
|
150
|
-
Effect.sync(() => {
|
|
151
|
-
kvStore.backendId = value
|
|
152
|
-
}),
|
|
153
|
-
clear: Effect.dieMessage('Not implemented'),
|
|
154
|
-
remove: () => Effect.dieMessage('Not implemented'),
|
|
155
|
-
size: Effect.dieMessage('Not implemented'),
|
|
156
|
-
}),
|
|
157
|
-
),
|
|
158
|
-
),
|
|
159
|
-
UnknownError.mapToUnknownError,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
/** Connect to the sync backend */
|
|
163
|
-
yield* syncBackend.connect.pipe(
|
|
164
|
-
Effect.mapError(
|
|
165
|
-
(cause) =>
|
|
166
|
-
new ConnectionError({
|
|
167
|
-
cause,
|
|
168
|
-
note: `Failed to connect to sync backend: ${cause._tag === 'IsOfflineError' ? 'Backend is offline or unreachable' : String(cause)}`,
|
|
169
|
-
}),
|
|
170
|
-
),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
/** Verify connectivity with a ping (with timeout) */
|
|
174
|
-
yield* syncBackend.ping.pipe(
|
|
175
|
-
Effect.timeout(CONNECTION_TIMEOUT_MS),
|
|
176
|
-
Effect.catchAll((cause) => {
|
|
177
|
-
if (Cause.isTimeoutException(cause)) {
|
|
178
|
-
return Effect.fail(
|
|
179
|
-
new ConnectionError({
|
|
180
|
-
cause,
|
|
181
|
-
note: `Connection timeout: Sync backend did not respond within ${CONNECTION_TIMEOUT_MS}ms`,
|
|
182
|
-
}),
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
return Effect.fail(
|
|
186
|
-
new ConnectionError({
|
|
187
|
-
cause,
|
|
188
|
-
note: `Failed to ping sync backend: ${cause._tag === 'IsOfflineError' ? 'Backend is offline or unreachable' : String(cause)}`,
|
|
189
|
-
}),
|
|
190
|
-
)
|
|
191
|
-
}),
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
yield* Console.log(`✓ Connected to sync backend: ${syncBackend.metadata.name}`)
|
|
195
|
-
|
|
196
|
-
return syncBackend
|
|
197
|
-
})
|
|
5
|
+
import * as SyncOps from '../sync-operations.ts'
|
|
198
6
|
|
|
199
7
|
/**
|
|
200
8
|
* Export events from the sync backend to a JSON file.
|
|
@@ -211,61 +19,31 @@ const exportEvents = ({
|
|
|
211
19
|
outputPath: string
|
|
212
20
|
}): Effect.Effect<
|
|
213
21
|
void,
|
|
214
|
-
ExportError |
|
|
22
|
+
SyncOps.ExportError | SyncOps.ConnectionError | UnknownError,
|
|
215
23
|
FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
|
|
216
24
|
> =>
|
|
217
25
|
Effect.gen(function* () {
|
|
218
26
|
yield* Console.log(`Connecting to sync backend...`)
|
|
219
27
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
yield* Console.log(`Pulling events from sync backend...`)
|
|
28
|
+
const result = yield* SyncOps.pullEventsFromSyncBackend({ storePath, storeId, clientId })
|
|
223
29
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
yield* syncBackend.pull(Option.none(), { live: false }).pipe(
|
|
227
|
-
Stream.tap((item) =>
|
|
228
|
-
Effect.sync(() => {
|
|
229
|
-
for (const { eventEncoded } of item.batch) {
|
|
230
|
-
events.push(eventEncoded)
|
|
231
|
-
}
|
|
232
|
-
}),
|
|
233
|
-
),
|
|
234
|
-
Stream.takeUntil((item) => item.pageInfo._tag === 'NoMore'),
|
|
235
|
-
Stream.runDrain,
|
|
236
|
-
Effect.mapError(
|
|
237
|
-
(cause) =>
|
|
238
|
-
new ExportError({
|
|
239
|
-
cause,
|
|
240
|
-
note: `Failed to pull events from sync backend: ${cause}`,
|
|
241
|
-
}),
|
|
242
|
-
),
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
yield* Console.log(`Pulled ${events.length} events`)
|
|
246
|
-
|
|
247
|
-
const exportData: ExportFile = {
|
|
248
|
-
version: 1,
|
|
249
|
-
storeId,
|
|
250
|
-
exportedAt: new Date().toISOString(),
|
|
251
|
-
eventCount: events.length,
|
|
252
|
-
events,
|
|
253
|
-
}
|
|
30
|
+
yield* Console.log(`✓ Connected to sync backend`)
|
|
31
|
+
yield* Console.log(`Pulled ${result.eventCount} events`)
|
|
254
32
|
|
|
255
33
|
const fs = yield* FileSystem.FileSystem
|
|
256
34
|
const absOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)
|
|
257
35
|
|
|
258
|
-
yield* fs.writeFileString(absOutputPath, JSON.stringify(
|
|
36
|
+
yield* fs.writeFileString(absOutputPath, JSON.stringify(result.data, null, 2)).pipe(
|
|
259
37
|
Effect.mapError(
|
|
260
38
|
(cause) =>
|
|
261
|
-
new ExportError({
|
|
39
|
+
new SyncOps.ExportError({
|
|
262
40
|
cause,
|
|
263
41
|
note: `Failed to write export file: ${cause}`,
|
|
264
42
|
}),
|
|
265
43
|
),
|
|
266
44
|
)
|
|
267
45
|
|
|
268
|
-
yield* Console.log(`Exported ${
|
|
46
|
+
yield* Console.log(`Exported ${result.eventCount} events to ${absOutputPath}`)
|
|
269
47
|
}).pipe(Effect.withSpan('cli:export'))
|
|
270
48
|
|
|
271
49
|
/**
|
|
@@ -287,21 +65,27 @@ const importEvents = ({
|
|
|
287
65
|
dryRun: boolean
|
|
288
66
|
}): Effect.Effect<
|
|
289
67
|
void,
|
|
290
|
-
ImportError |
|
|
68
|
+
SyncOps.ImportError | SyncOps.ConnectionError | UnknownError,
|
|
291
69
|
FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
|
|
292
70
|
> =>
|
|
293
71
|
Effect.gen(function* () {
|
|
294
72
|
const fs = yield* FileSystem.FileSystem
|
|
295
73
|
const absInputPath = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath)
|
|
296
74
|
|
|
297
|
-
const exists = yield* fs.exists(absInputPath).pipe(
|
|
75
|
+
const exists = yield* fs.exists(absInputPath).pipe(
|
|
76
|
+
Effect.mapError(
|
|
77
|
+
(cause) =>
|
|
78
|
+
new SyncOps.ImportError({
|
|
79
|
+
cause,
|
|
80
|
+
note: `Failed to check file existence: ${cause}`,
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
298
84
|
if (!exists) {
|
|
299
|
-
return yield*
|
|
300
|
-
new
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}),
|
|
304
|
-
)
|
|
85
|
+
return yield* new SyncOps.ImportError({
|
|
86
|
+
cause: new Error(`File not found: ${absInputPath}`),
|
|
87
|
+
note: `Import file does not exist at ${absInputPath}`,
|
|
88
|
+
})
|
|
305
89
|
}
|
|
306
90
|
|
|
307
91
|
yield* Console.log(`Reading import file...`)
|
|
@@ -309,7 +93,7 @@ const importEvents = ({
|
|
|
309
93
|
const fileContent = yield* fs.readFileString(absInputPath).pipe(
|
|
310
94
|
Effect.mapError(
|
|
311
95
|
(cause) =>
|
|
312
|
-
new ImportError({
|
|
96
|
+
new SyncOps.ImportError({
|
|
313
97
|
cause,
|
|
314
98
|
note: `Failed to read import file: ${cause}`,
|
|
315
99
|
}),
|
|
@@ -319,100 +103,48 @@ const importEvents = ({
|
|
|
319
103
|
const parsedContent = yield* Effect.try({
|
|
320
104
|
try: () => JSON.parse(fileContent),
|
|
321
105
|
catch: (error) =>
|
|
322
|
-
new ImportError({
|
|
106
|
+
new SyncOps.ImportError({
|
|
323
107
|
cause: new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`),
|
|
324
108
|
note: `Invalid JSON in import file: ${error instanceof Error ? error.message : String(error)}`,
|
|
325
109
|
}),
|
|
326
110
|
})
|
|
327
111
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
(cause) =>
|
|
331
|
-
new ImportError({
|
|
332
|
-
cause: new Error(`Invalid export file format: ${cause}`),
|
|
333
|
-
note: `Invalid export file format: ${cause}`,
|
|
334
|
-
}),
|
|
335
|
-
),
|
|
336
|
-
)
|
|
112
|
+
/** Validate export file format before proceeding */
|
|
113
|
+
const validation = yield* SyncOps.validateExportData({ data: parsedContent, targetStoreId: storeId })
|
|
337
114
|
|
|
338
|
-
if (
|
|
115
|
+
if (validation.storeIdMismatch) {
|
|
339
116
|
if (!force) {
|
|
340
|
-
return yield*
|
|
341
|
-
new
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}),
|
|
345
|
-
)
|
|
117
|
+
return yield* new SyncOps.ImportError({
|
|
118
|
+
cause: new Error(`Store ID mismatch: file has '${validation.sourceStoreId}', expected '${storeId}'`),
|
|
119
|
+
note: `The export file was created for a different store. Use --force to import anyway.`,
|
|
120
|
+
})
|
|
346
121
|
}
|
|
347
|
-
yield* Console.log(
|
|
122
|
+
yield* Console.log(
|
|
123
|
+
`Store ID mismatch: file has '${validation.sourceStoreId}', importing to '${storeId}' (--force)`,
|
|
124
|
+
)
|
|
348
125
|
}
|
|
349
126
|
|
|
350
|
-
yield* Console.log(`Found ${
|
|
127
|
+
yield* Console.log(`Found ${validation.eventCount} events in export file`)
|
|
351
128
|
|
|
352
129
|
if (dryRun) {
|
|
353
130
|
yield* Console.log(`Dry run - validating import file...`)
|
|
354
|
-
yield* Console.log(`Dry run complete. ${
|
|
131
|
+
yield* Console.log(`Dry run complete. ${validation.eventCount} events would be imported.`)
|
|
355
132
|
return
|
|
356
133
|
}
|
|
357
134
|
|
|
358
135
|
yield* Console.log(`Connecting to sync backend...`)
|
|
359
136
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
Effect.sync(() => {
|
|
369
|
-
existingEventCount += item.batch.length
|
|
370
|
-
}),
|
|
371
|
-
),
|
|
372
|
-
Stream.takeUntil((item) => item.pageInfo._tag === 'NoMore'),
|
|
373
|
-
Stream.runDrain,
|
|
374
|
-
Effect.mapError(
|
|
375
|
-
(cause) =>
|
|
376
|
-
new ImportError({
|
|
377
|
-
cause,
|
|
378
|
-
note: `Failed to check existing events: ${cause}`,
|
|
379
|
-
}),
|
|
380
|
-
),
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
if (existingEventCount > 0) {
|
|
384
|
-
return yield* Effect.fail(
|
|
385
|
-
new ImportError({
|
|
386
|
-
cause: new Error(`Sync backend already contains ${existingEventCount} events`),
|
|
387
|
-
note: `Cannot import into a non-empty sync backend. The sync backend must be empty.`,
|
|
388
|
-
}),
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
yield* Console.log(`Pushing ${exportData.events.length} events to sync backend...`)
|
|
393
|
-
|
|
394
|
-
/** Push events in batches of 100 (sync backend constraint) */
|
|
395
|
-
const batchSize = 100
|
|
396
|
-
let pushed = 0
|
|
397
|
-
|
|
398
|
-
for (let i = 0; i < exportData.events.length; i += batchSize) {
|
|
399
|
-
const batch = exportData.events.slice(i, i + batchSize)
|
|
400
|
-
|
|
401
|
-
yield* syncBackend.push(batch).pipe(
|
|
402
|
-
Effect.mapError(
|
|
403
|
-
(cause) =>
|
|
404
|
-
new ImportError({
|
|
405
|
-
cause,
|
|
406
|
-
note: `Failed to push events at position ${i}: ${cause}`,
|
|
407
|
-
}),
|
|
408
|
-
),
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
pushed += batch.length
|
|
412
|
-
yield* Console.log(` Pushed ${pushed}/${exportData.events.length} events`)
|
|
413
|
-
}
|
|
137
|
+
const result = yield* SyncOps.pushEventsToSyncBackend({
|
|
138
|
+
storePath,
|
|
139
|
+
storeId,
|
|
140
|
+
clientId,
|
|
141
|
+
data: parsedContent,
|
|
142
|
+
force,
|
|
143
|
+
dryRun: false,
|
|
144
|
+
})
|
|
414
145
|
|
|
415
|
-
yield* Console.log(
|
|
146
|
+
yield* Console.log(`✓ Connected to sync backend`)
|
|
147
|
+
yield* Console.log(`Successfully imported ${result.eventCount} events`)
|
|
416
148
|
}).pipe(Effect.withSpan('cli:import'))
|
|
417
149
|
|
|
418
150
|
export const exportCommand = Cli.Command.make(
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { Effect } from '@livestore/utils/effect'
|
|
1
|
+
import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
|
|
2
|
+
import { PlatformNode } from '@livestore/utils/node'
|
|
2
3
|
import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
|
|
3
4
|
import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
|
|
4
5
|
import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
|
|
5
6
|
import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
|
|
6
7
|
import * as Runtime from '../mcp-runtime/runtime.ts'
|
|
8
|
+
import * as SyncOps from '../sync-operations.ts'
|
|
7
9
|
import { coachToolHandler } from './mcp-coach.ts'
|
|
8
10
|
import { livestoreToolkit } from './mcp-tools-defs.ts'
|
|
9
11
|
|
|
12
|
+
/** Layer providing FileSystem and HttpClient for sync operations */
|
|
13
|
+
const SyncOpsLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
|
|
14
|
+
|
|
10
15
|
// Tool handlers using Tim Smart's pattern
|
|
11
16
|
export const toolHandlers: any = livestoreToolkit.of({
|
|
12
17
|
livestore_coach: coachToolHandler,
|
|
@@ -156,4 +161,32 @@ export const schema = Schema.create({
|
|
|
156
161
|
livestore_instance_disconnect: Effect.fnUntraced(function* () {
|
|
157
162
|
return yield* Runtime.disconnect
|
|
158
163
|
}),
|
|
164
|
+
|
|
165
|
+
// Sync export - pull all events from sync backend
|
|
166
|
+
livestore_sync_export: Effect.fnUntraced(function* ({ storePath, storeId, clientId }) {
|
|
167
|
+
const result = yield* SyncOps.pullEventsFromSyncBackend({
|
|
168
|
+
storePath,
|
|
169
|
+
storeId,
|
|
170
|
+
clientId: clientId ?? 'mcp-export',
|
|
171
|
+
}).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
storeId: result.storeId,
|
|
175
|
+
eventCount: result.eventCount,
|
|
176
|
+
exportedAt: result.exportedAt,
|
|
177
|
+
data: result.data,
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
// Sync import - push events to sync backend
|
|
182
|
+
livestore_sync_import: Effect.fnUntraced(function* ({ storePath, storeId, clientId, data, force, dryRun }) {
|
|
183
|
+
return yield* SyncOps.pushEventsToSyncBackend({
|
|
184
|
+
storePath,
|
|
185
|
+
storeId,
|
|
186
|
+
clientId: clientId ?? 'mcp-import',
|
|
187
|
+
data,
|
|
188
|
+
force: force ?? false,
|
|
189
|
+
dryRun: dryRun ?? false,
|
|
190
|
+
}).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
|
|
191
|
+
}),
|
|
159
192
|
})
|
|
@@ -226,4 +226,92 @@ Example success:
|
|
|
226
226
|
parameters: {},
|
|
227
227
|
success: Schema.TaggedStruct('disconnected', {}),
|
|
228
228
|
}),
|
|
229
|
+
|
|
230
|
+
Tool.make('livestore_sync_export', {
|
|
231
|
+
description: `Export all events from a sync backend to JSON data.
|
|
232
|
+
|
|
233
|
+
This tool connects directly to the sync backend (without creating a full LiveStore instance) and pulls all events. Useful for backup, migration, and debugging.
|
|
234
|
+
|
|
235
|
+
Module contract (same as livestore_instance_connect):
|
|
236
|
+
\`\`\`ts
|
|
237
|
+
export { schema } from './src/livestore/schema.ts'
|
|
238
|
+
export const syncBackend = makeWsSync({ url: process.env.LIVESTORE_SYNC_URL ?? 'ws://localhost:8787' })
|
|
239
|
+
export const syncPayload = { authToken: process.env.LIVESTORE_SYNC_AUTH_TOKEN }
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
Example parameters:
|
|
243
|
+
{
|
|
244
|
+
"storePath": "livestore-cli.config.ts",
|
|
245
|
+
"storeId": "my-store"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Returns on success:
|
|
249
|
+
{
|
|
250
|
+
"storeId": "my-store",
|
|
251
|
+
"eventCount": 127,
|
|
252
|
+
"exportedAt": "2024-01-15T10:30:00.000Z",
|
|
253
|
+
"data": { "version": 1, "storeId": "my-store", ... }
|
|
254
|
+
}`,
|
|
255
|
+
parameters: {
|
|
256
|
+
storePath: Schema.String.annotations({
|
|
257
|
+
description: 'Path to a module that exports schema and syncBackend',
|
|
258
|
+
}),
|
|
259
|
+
storeId: Schema.String.annotations({ description: 'Store identifier' }),
|
|
260
|
+
clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-export)' })),
|
|
261
|
+
},
|
|
262
|
+
success: Schema.Struct({
|
|
263
|
+
storeId: Schema.String,
|
|
264
|
+
eventCount: Schema.Number,
|
|
265
|
+
exportedAt: Schema.String,
|
|
266
|
+
data: Schema.JsonValue.annotations({ description: 'The export file data (can be saved or passed to import)' }),
|
|
267
|
+
}),
|
|
268
|
+
}).annotate(Tool.Readonly, true),
|
|
269
|
+
|
|
270
|
+
Tool.make('livestore_sync_import', {
|
|
271
|
+
description: `Import events from export data to a sync backend.
|
|
272
|
+
|
|
273
|
+
This tool connects directly to the sync backend and pushes events. The sync backend must be empty.
|
|
274
|
+
|
|
275
|
+
Example parameters:
|
|
276
|
+
{
|
|
277
|
+
"storePath": "livestore-cli.config.ts",
|
|
278
|
+
"storeId": "my-store",
|
|
279
|
+
"data": { "version": 1, "storeId": "my-store", "events": [...] }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
With options:
|
|
283
|
+
{
|
|
284
|
+
"storePath": "livestore-cli.config.ts",
|
|
285
|
+
"storeId": "my-store",
|
|
286
|
+
"data": { ... },
|
|
287
|
+
"force": true, // Import even if store ID doesn't match
|
|
288
|
+
"dryRun": true // Validate without importing
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Returns on success:
|
|
292
|
+
{
|
|
293
|
+
"storeId": "my-store",
|
|
294
|
+
"eventCount": 127,
|
|
295
|
+
"dryRun": false
|
|
296
|
+
}`,
|
|
297
|
+
parameters: {
|
|
298
|
+
storePath: Schema.String.annotations({
|
|
299
|
+
description: 'Path to a module that exports schema and syncBackend',
|
|
300
|
+
}),
|
|
301
|
+
storeId: Schema.String.annotations({ description: 'Store identifier' }),
|
|
302
|
+
clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-import)' })),
|
|
303
|
+
data: Schema.JsonValue.annotations({
|
|
304
|
+
description: 'The export data to import (from livestore_sync_export or a file)',
|
|
305
|
+
}),
|
|
306
|
+
force: Schema.optional(
|
|
307
|
+
Schema.Boolean.annotations({ description: 'Force import even if store ID does not match' }),
|
|
308
|
+
),
|
|
309
|
+
dryRun: Schema.optional(Schema.Boolean.annotations({ description: 'Validate without actually importing' })),
|
|
310
|
+
},
|
|
311
|
+
success: Schema.Struct({
|
|
312
|
+
storeId: Schema.String,
|
|
313
|
+
eventCount: Schema.Number,
|
|
314
|
+
dryRun: Schema.Boolean,
|
|
315
|
+
}),
|
|
316
|
+
}).annotate(Tool.Destructive, true),
|
|
229
317
|
)
|