@livestore/cli 0.0.0-snapshot-f32dde17378384e071007a6d0baf04b92066d7d5 → 0.0.0-snapshot-0f27d343553b9bb260543bf20de36d216f53c5d8

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 (50) hide show
  1. package/dist/__tests__/fixtures/mock-config.d.ts +56 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/mock-config.js +88 -0
  4. package/dist/__tests__/fixtures/mock-config.js.map +1 -0
  5. package/dist/__tests__/sync-operations.test.d.ts +2 -0
  6. package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
  7. package/dist/__tests__/sync-operations.test.js +167 -0
  8. package/dist/__tests__/sync-operations.test.js.map +1 -0
  9. package/dist/cli.d.ts +15 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +2 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/import-export.d.ts +34 -0
  14. package/dist/commands/import-export.d.ts.map +1 -0
  15. package/dist/commands/import-export.js +133 -0
  16. package/dist/commands/import-export.js.map +1 -0
  17. package/dist/commands/mcp-coach.d.ts +2 -2
  18. package/dist/commands/mcp-coach.d.ts.map +1 -1
  19. package/dist/commands/mcp-tool-handlers.d.ts +5 -1
  20. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
  21. package/dist/commands/mcp-tool-handlers.js +42 -4
  22. package/dist/commands/mcp-tool-handlers.js.map +1 -1
  23. package/dist/commands/mcp-tools-defs.d.ts +31 -1
  24. package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
  25. package/dist/commands/mcp-tools-defs.js +87 -5
  26. package/dist/commands/mcp-tools-defs.js.map +1 -1
  27. package/dist/commands/new-project.d.ts +1 -1
  28. package/dist/mcp-runtime/runtime.d.ts +4 -3
  29. package/dist/mcp-runtime/runtime.d.ts.map +1 -1
  30. package/dist/mcp-runtime/runtime.js +20 -53
  31. package/dist/mcp-runtime/runtime.js.map +1 -1
  32. package/dist/module-loader.d.ts +22 -0
  33. package/dist/module-loader.d.ts.map +1 -0
  34. package/dist/module-loader.js +75 -0
  35. package/dist/module-loader.js.map +1 -0
  36. package/dist/sync-operations.d.ts +121 -0
  37. package/dist/sync-operations.d.ts.map +1 -0
  38. package/dist/sync-operations.js +180 -0
  39. package/dist/sync-operations.js.map +1 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +15 -8
  42. package/src/__tests__/fixtures/mock-config.ts +104 -0
  43. package/src/__tests__/sync-operations.test.ts +230 -0
  44. package/src/cli.ts +2 -1
  45. package/src/commands/import-export.ts +278 -0
  46. package/src/commands/mcp-tool-handlers.ts +50 -5
  47. package/src/commands/mcp-tools-defs.ts +92 -4
  48. package/src/mcp-runtime/runtime.ts +32 -65
  49. package/src/module-loader.ts +93 -0
  50. package/src/sync-operations.ts +360 -0
@@ -1,14 +1,20 @@
1
- import { Effect } from '@livestore/utils/effect'
1
+ import { Effect, FetchHttpClient, Layer, type Toolkit } 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
 
10
- // Tool handlers using Tim Smart's pattern
11
- export const toolHandlers: any = livestoreToolkit.of({
12
+ /** Layer providing FileSystem and HttpClient for sync operations */
13
+ const SyncOpsLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
14
+
15
+ type LivestoreToolHandlers = Toolkit.HandlersFrom<Toolkit.Tools<typeof livestoreToolkit>>
16
+
17
+ export const toolHandlers: LivestoreToolHandlers = livestoreToolkit.of({
12
18
  livestore_coach: coachToolHandler,
13
19
 
14
20
  livestore_generate_schema: Effect.fnUntraced(function* ({ schemaType, customDescription }) {
@@ -121,8 +127,13 @@ export const schema = Schema.create({
121
127
  }),
122
128
 
123
129
  // Connect the single in-process LiveStore instance from user module
124
- livestore_instance_connect: Effect.fnUntraced(function* ({ storePath, storeId, clientId, sessionId }) {
125
- const store = yield* Runtime.init({ storePath, storeId, clientId, sessionId }).pipe(Effect.orDie)
130
+ livestore_instance_connect: Effect.fnUntraced(function* ({ configPath, storeId, clientId, sessionId }) {
131
+ const store = yield* Runtime.init({
132
+ configPath,
133
+ storeId,
134
+ ...(clientId !== undefined ? { clientId } : {}),
135
+ ...(sessionId !== undefined ? { sessionId } : {}),
136
+ }).pipe(Effect.orDie)
126
137
  const eventNames = Array.from(store.schema.eventsDefsMap.keys())
127
138
  const tableNames = Array.from(store.schema.state.sqlite.tables.keys())
128
139
 
@@ -156,4 +167,38 @@ export const schema = Schema.create({
156
167
  livestore_instance_disconnect: Effect.fnUntraced(function* () {
157
168
  return yield* Runtime.disconnect
158
169
  }),
170
+
171
+ // Sync export - pull all events from sync backend
172
+ livestore_sync_export: Effect.fnUntraced(function* ({ configPath, storeId, clientId }) {
173
+ const result = yield* SyncOps.pullEventsFromSyncBackend({
174
+ configPath,
175
+ storeId,
176
+ clientId: clientId ?? 'mcp-export',
177
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
178
+
179
+ return {
180
+ storeId: result.storeId,
181
+ eventCount: result.eventCount,
182
+ exportedAt: result.exportedAt,
183
+ data: result.data,
184
+ }
185
+ }),
186
+
187
+ // Sync import - push events to sync backend
188
+ livestore_sync_import: Effect.fnUntraced(function* ({ configPath, storeId, clientId, data, force, dryRun }) {
189
+ const result = yield* SyncOps.pushEventsToSyncBackend({
190
+ configPath,
191
+ storeId,
192
+ clientId: clientId ?? 'mcp-import',
193
+ data,
194
+ force: force ?? false,
195
+ dryRun: dryRun ?? false,
196
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
197
+
198
+ return {
199
+ storeId: result.storeId,
200
+ eventCount: result.eventCount,
201
+ dryRun: result.dryRun,
202
+ }
203
+ }),
159
204
  })
@@ -43,7 +43,7 @@ export const livestoreToolkit = Toolkit.make(
43
43
  Notes:
44
44
  - Only one instance can be active at a time; calling connect again shuts down and replaces the previous instance.
45
45
  - Reconnecting creates a fresh, in-memory client database. The state visible to queries is populated by your backend's initial sync behavior; depending on configuration, you may briefly observe empty or partial data until sync completes.
46
- - \`storePath\` is resolved relative to the current working directory.
46
+ - \`configPath\` is resolved relative to the current working directory.
47
47
  - \`syncBackend\` must be a function (factory) that returns a backend; \`syncPayload\` must be JSON-serializable.
48
48
 
49
49
  Module contract (generic example):
@@ -63,13 +63,13 @@ export const syncPayload = { authToken: process.env.LIVESTORE_SYNC_AUTH_TOKEN ??
63
63
 
64
64
  Connect parameters:
65
65
  {
66
- "storePath": "<path-to-your-mcp-module>.ts",
66
+ "configPath": "livestore-cli.config.ts",
67
67
  "storeId": "<store-id>"
68
68
  }
69
69
 
70
70
  Optional identifiers to group client state on the server:
71
71
  {
72
- "storePath": "<path-to-your-mcp-module>.ts",
72
+ "configPath": "livestore-cli.config.ts",
73
73
  "storeId": "<store-id>",
74
74
  "clientId": "<client-id>",
75
75
  "sessionId": "<session-id>"
@@ -86,7 +86,7 @@ Returns on success:
86
86
  }
87
87
  }`,
88
88
  parameters: {
89
- storePath: Schema.String.annotations({
89
+ configPath: Schema.String.annotations({
90
90
  description: 'Path to a module that exports named variables: schema and syncBackend',
91
91
  }),
92
92
  storeId: Schema.String.annotations({ description: 'Required store id for the LiveStore instance.' }),
@@ -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
+ "configPath": "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
+ configPath: 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
+ "configPath": "livestore-cli.config.ts",
278
+ "storeId": "my-store",
279
+ "data": { "version": 1, "storeId": "my-store", "events": [...] }
280
+ }
281
+
282
+ With options:
283
+ {
284
+ "configPath": "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
+ configPath: 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
  )
@@ -1,77 +1,40 @@
1
- import path from 'node:path'
2
- import { pathToFileURL } from 'node:url'
3
1
  import { makeAdapter as makeNodeAdapter } from '@livestore/adapter-node'
4
- import { isLiveStoreSchema, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
2
+ import { UnknownError } from '@livestore/common'
3
+ import { LiveStoreEvent, SystemTables } from '@livestore/common/schema'
5
4
  import type { Store } from '@livestore/livestore'
6
5
  import { createStorePromise } from '@livestore/livestore'
7
- import { shouldNeverHappen } from '@livestore/utils'
8
- import { Effect, Option, Schema } from '@livestore/utils/effect'
6
+ import { Effect, FetchHttpClient, Layer, Option, Schema } from '@livestore/utils/effect'
7
+ import { PlatformNode } from '@livestore/utils/node'
8
+
9
+ import { loadModuleConfig } from '../module-loader.ts'
9
10
 
10
11
  /** Currently connected store */
11
12
  let store: Store<any> | undefined
12
13
 
14
+ /** Layer providing FileSystem and HttpClient for module loading */
15
+ const ModuleLoaderLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
16
+
13
17
  /**
14
18
  * Dynamically imports a module that exports a `makeStore({ storeId }): Promise<Store>` function,
15
19
  * calls it with the provided storeId, and caches the Store instance for subsequent tool calls.
16
20
  */
17
21
  export const init = ({
18
- storePath,
22
+ configPath,
19
23
  storeId,
20
24
  clientId,
21
25
  sessionId,
22
26
  }: {
23
- storePath: string
27
+ configPath: string
24
28
  storeId: string
25
29
  clientId?: string
26
30
  sessionId?: string
27
- }) =>
28
- Effect.promise(async () => {
31
+ }): Effect.Effect<Store<any>, UnknownError> =>
32
+ Effect.gen(function* () {
29
33
  if (!storeId || typeof storeId !== 'string') {
30
- throw new Error('Invalid storeId: expected a non-empty string')
31
- }
32
- // Resolve to absolute path and import as file URL
33
- const abs = path.isAbsolute(storePath) ? storePath : path.resolve(process.cwd(), storePath)
34
- const mod = await import(pathToFileURL(abs).href)
35
-
36
- // Validate required exports
37
- const schema = (mod as any)?.schema
38
- if (!isLiveStoreSchema(schema)) {
39
- throw new Error(
40
- `Module at ${abs} must export a valid LiveStore 'schema'. Ex: export { schema } from './src/livestore/schema.ts'`,
41
- )
34
+ return yield* UnknownError.make({ cause: new Error('Invalid storeId: expected a non-empty string') })
42
35
  }
43
36
 
44
- const syncBackend = (mod as any)?.syncBackend
45
- if (typeof syncBackend !== 'function') {
46
- throw new Error(
47
- `Module at ${abs} must export a 'syncBackend' constructor (e.g., makeWsSync({ url })). Ex: export const syncBackend = makeWsSync({ url })`,
48
- )
49
- }
50
-
51
- // Optional: syncPayload for authenticated backends
52
- const syncPayloadSchemaExport = (mod as any)?.syncPayloadSchema
53
- const syncPayloadSchema =
54
- syncPayloadSchemaExport === undefined
55
- ? Schema.JsonValue
56
- : Schema.isSchema(syncPayloadSchemaExport)
57
- ? (syncPayloadSchemaExport as Schema.Schema<any>)
58
- : shouldNeverHappen(
59
- `Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
60
- )
61
-
62
- const syncPayloadExport = (mod as any)?.syncPayload
63
- const syncPayload =
64
- syncPayloadExport === undefined
65
- ? undefined
66
- : (() => {
67
- try {
68
- return Schema.decodeSync(syncPayloadSchema)(syncPayloadExport)
69
- } catch (error) {
70
- throw new Error(
71
- `Failed to decode 'syncPayload' from ${abs} using the provided schema: ${(error as Error).message}`,
72
- )
73
- }
74
- })()
37
+ const { schema, syncBackendConstructor, syncPayloadSchema, syncPayload } = yield* loadModuleConfig({ configPath })
75
38
 
76
39
  // Build Node adapter internally
77
40
  const adapter = makeNodeAdapter({
@@ -79,32 +42,36 @@ export const init = ({
79
42
  ...(clientId ? { clientId } : {}),
80
43
  ...(sessionId ? { sessionId } : {}),
81
44
  sync: {
82
- backend: syncBackend as any,
45
+ backend: syncBackendConstructor,
83
46
  initialSyncOptions: { _tag: 'Blocking', timeout: 5000 },
84
47
  onSyncError: 'shutdown',
85
48
  },
86
49
  })
87
50
 
88
51
  // Create the store
89
- const s = await createStorePromise({
90
- schema,
91
- storeId,
92
- adapter,
93
- disableDevtools: true,
94
- syncPayload,
95
- syncPayloadSchema,
96
- })
52
+ const s = yield* Effect.promise(() =>
53
+ createStorePromise({
54
+ schema,
55
+ storeId,
56
+ adapter,
57
+ disableDevtools: true,
58
+ syncPayload,
59
+ syncPayloadSchema,
60
+ }),
61
+ )
97
62
 
98
63
  // Replace existing store if any
99
64
  if (store) {
100
- try {
101
- await store.shutdownPromise()
102
- } catch {}
65
+ yield* Effect.promise(async () => {
66
+ try {
67
+ await store!.shutdownPromise()
68
+ } catch {}
69
+ })
103
70
  }
104
71
 
105
72
  store = s
106
73
  return store
107
- })
74
+ }).pipe(Effect.provide(ModuleLoaderLayer), Effect.withSpan('mcp-runtime:init'))
108
75
 
109
76
  export const getStore = Effect.sync(() => Option.fromNullable(store))
110
77
 
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared module loading utility for CLI and MCP.
3
+ * Loads and validates user config modules that export schema, syncBackend, and optional syncPayload.
4
+ */
5
+ import path from 'node:path'
6
+ import { pathToFileURL } from 'node:url'
7
+ import type { SyncBackend } from '@livestore/common'
8
+ import { UnknownError } from '@livestore/common'
9
+ import { isLiveStoreSchema, type LiveStoreSchema } from '@livestore/common/schema'
10
+ import { shouldNeverHappen } from '@livestore/utils'
11
+ import { Effect, FileSystem, Schema } from '@livestore/utils/effect'
12
+
13
+ export interface ModuleConfig {
14
+ schema: LiveStoreSchema
15
+ syncBackendConstructor: SyncBackend.SyncBackendConstructor
16
+ syncPayloadSchema: Schema.Schema<any>
17
+ syncPayload: unknown
18
+ }
19
+
20
+ /**
21
+ * Loads and validates a user config module.
22
+ * The module must export:
23
+ * - `schema`: A valid LiveStore schema
24
+ * - `syncBackend`: A sync backend constructor function
25
+ * - `syncPayloadSchema` (optional): Schema for validating syncPayload
26
+ * - `syncPayload` (optional): Payload data for the sync backend
27
+ */
28
+ export const loadModuleConfig = ({
29
+ configPath,
30
+ }: {
31
+ configPath: string
32
+ }): Effect.Effect<ModuleConfig, UnknownError, FileSystem.FileSystem> =>
33
+ Effect.gen(function* () {
34
+ const abs = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath)
35
+
36
+ const fs = yield* FileSystem.FileSystem
37
+ const exists = yield* fs.exists(abs).pipe(UnknownError.mapToUnknownError)
38
+ if (!exists) {
39
+ return yield* UnknownError.make({
40
+ cause: `Store module not found at ${abs}`,
41
+ note: 'Make sure the path points to a valid LiveStore module',
42
+ })
43
+ }
44
+
45
+ const mod = yield* Effect.tryPromise({
46
+ try: () => import(pathToFileURL(abs).href),
47
+ catch: (cause) =>
48
+ UnknownError.make({
49
+ cause,
50
+ note: `Failed to import module at ${abs}`,
51
+ }),
52
+ })
53
+
54
+ const schema = (mod as any)?.schema
55
+ if (!isLiveStoreSchema(schema)) {
56
+ return yield* UnknownError.make({
57
+ cause: `Module at ${abs} must export a valid LiveStore 'schema'`,
58
+ note: `Ex: export { schema } from './src/livestore/schema.ts'`,
59
+ })
60
+ }
61
+
62
+ const syncBackendConstructor = (mod as any)?.syncBackend
63
+ if (typeof syncBackendConstructor !== 'function') {
64
+ return yield* UnknownError.make({
65
+ cause: `Module at ${abs} must export a 'syncBackend' constructor`,
66
+ note: `Ex: export const syncBackend = makeWsSync({ url })`,
67
+ })
68
+ }
69
+
70
+ const syncPayloadSchemaExport = (mod as any)?.syncPayloadSchema
71
+ const syncPayloadSchema =
72
+ syncPayloadSchemaExport === undefined
73
+ ? Schema.JsonValue
74
+ : Schema.isSchema(syncPayloadSchemaExport)
75
+ ? (syncPayloadSchemaExport as Schema.Schema<any>)
76
+ : shouldNeverHappen(
77
+ `Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
78
+ )
79
+
80
+ const syncPayloadExport = (mod as any)?.syncPayload
81
+ const syncPayload = yield* (
82
+ syncPayloadExport === undefined
83
+ ? Effect.succeed<unknown>(undefined)
84
+ : Schema.decodeUnknown(syncPayloadSchema)(syncPayloadExport)
85
+ ).pipe(UnknownError.mapToUnknownError)
86
+
87
+ return {
88
+ schema,
89
+ syncBackendConstructor,
90
+ syncPayloadSchema,
91
+ syncPayload,
92
+ }
93
+ }).pipe(Effect.withSpan('module-loader:loadModuleConfig'))