@livestore/cli 0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/cli",
3
- "version": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c",
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-b38bc9fa61134ae035254562c7155b3f381d5f6c",
15
- "@livestore/livestore": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c",
16
- "@livestore/common": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c",
17
- "@livestore/peer-deps": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c",
18
- "@livestore/sync-cf": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c",
19
- "@livestore/utils": "0.0.0-snapshot-b38bc9fa61134ae035254562c7155b3f381d5f6c"
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-b38bc9fa61134ae035254562c7155b3f381d5f6c"
24
+ "@livestore/utils-dev": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b"
25
25
  },
26
26
  "files": [
27
27
  "package.json",
package/src/cli.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Cli } from '@livestore/utils/node'
2
+ import { syncCommand } from './commands/import-export.ts'
2
3
  import { mcpCommand } from './commands/mcp.ts'
3
4
  import { createCommand } from './commands/new-project.ts'
4
5
 
5
6
  export const command = Cli.Command.make('livestore', {
6
7
  verbose: Cli.Options.boolean('verbose').pipe(Cli.Options.withDefault(false)),
7
- }).pipe(Cli.Command.withSubcommands([mcpCommand, createCommand]))
8
+ }).pipe(Cli.Command.withSubcommands([mcpCommand, createCommand, syncCommand]))
@@ -0,0 +1,262 @@
1
+ import path from 'node:path'
2
+ import type { UnknownError } from '@livestore/common'
3
+ import { Console, Effect, FileSystem, type HttpClient, type Scope } from '@livestore/utils/effect'
4
+ import { Cli } from '@livestore/utils/node'
5
+ import * as SyncOps from '../sync-operations.ts'
6
+
7
+ /**
8
+ * Export events from the sync backend to a JSON file.
9
+ */
10
+ const exportEvents = ({
11
+ storePath,
12
+ storeId,
13
+ clientId,
14
+ outputPath,
15
+ }: {
16
+ storePath: string
17
+ storeId: string
18
+ clientId: string
19
+ outputPath: string
20
+ }): Effect.Effect<
21
+ void,
22
+ SyncOps.ExportError | SyncOps.ConnectionError | UnknownError,
23
+ FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
24
+ > =>
25
+ Effect.gen(function* () {
26
+ yield* Console.log(`Connecting to sync backend...`)
27
+
28
+ const result = yield* SyncOps.pullEventsFromSyncBackend({ storePath, storeId, clientId })
29
+
30
+ yield* Console.log(`✓ Connected to sync backend`)
31
+ yield* Console.log(`Pulled ${result.eventCount} events`)
32
+
33
+ const fs = yield* FileSystem.FileSystem
34
+ const absOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)
35
+
36
+ yield* fs.writeFileString(absOutputPath, JSON.stringify(result.data, null, 2)).pipe(
37
+ Effect.mapError(
38
+ (cause) =>
39
+ new SyncOps.ExportError({
40
+ cause,
41
+ note: `Failed to write export file: ${cause}`,
42
+ }),
43
+ ),
44
+ )
45
+
46
+ yield* Console.log(`Exported ${result.eventCount} events to ${absOutputPath}`)
47
+ }).pipe(Effect.withSpan('cli:export'))
48
+
49
+ /**
50
+ * Import events from a JSON file to the sync backend.
51
+ */
52
+ const importEvents = ({
53
+ storePath,
54
+ storeId,
55
+ clientId,
56
+ inputPath,
57
+ force,
58
+ dryRun,
59
+ }: {
60
+ storePath: string
61
+ storeId: string
62
+ clientId: string
63
+ inputPath: string
64
+ force: boolean
65
+ dryRun: boolean
66
+ }): Effect.Effect<
67
+ void,
68
+ SyncOps.ImportError | SyncOps.ConnectionError | UnknownError,
69
+ FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
70
+ > =>
71
+ Effect.gen(function* () {
72
+ const fs = yield* FileSystem.FileSystem
73
+ const absInputPath = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath)
74
+
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
+ )
84
+ if (!exists) {
85
+ return yield* new SyncOps.ImportError({
86
+ cause: new Error(`File not found: ${absInputPath}`),
87
+ note: `Import file does not exist at ${absInputPath}`,
88
+ })
89
+ }
90
+
91
+ yield* Console.log(`Reading import file...`)
92
+
93
+ const fileContent = yield* fs.readFileString(absInputPath).pipe(
94
+ Effect.mapError(
95
+ (cause) =>
96
+ new SyncOps.ImportError({
97
+ cause,
98
+ note: `Failed to read import file: ${cause}`,
99
+ }),
100
+ ),
101
+ )
102
+
103
+ const parsedContent = yield* Effect.try({
104
+ try: () => JSON.parse(fileContent),
105
+ catch: (error) =>
106
+ new SyncOps.ImportError({
107
+ cause: new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`),
108
+ note: `Invalid JSON in import file: ${error instanceof Error ? error.message : String(error)}`,
109
+ }),
110
+ })
111
+
112
+ /** Validate export file format before proceeding */
113
+ const validation = yield* SyncOps.validateExportData({ data: parsedContent, targetStoreId: storeId })
114
+
115
+ if (validation.storeIdMismatch) {
116
+ if (!force) {
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
+ })
121
+ }
122
+ yield* Console.log(
123
+ `Store ID mismatch: file has '${validation.sourceStoreId}', importing to '${storeId}' (--force)`,
124
+ )
125
+ }
126
+
127
+ yield* Console.log(`Found ${validation.eventCount} events in export file`)
128
+
129
+ if (dryRun) {
130
+ yield* Console.log(`Dry run - validating import file...`)
131
+ yield* Console.log(`Dry run complete. ${validation.eventCount} events would be imported.`)
132
+ return
133
+ }
134
+
135
+ yield* Console.log(`Connecting to sync backend...`)
136
+
137
+ const result = yield* SyncOps.pushEventsToSyncBackend({
138
+ storePath,
139
+ storeId,
140
+ clientId,
141
+ data: parsedContent,
142
+ force,
143
+ dryRun: false,
144
+ })
145
+
146
+ yield* Console.log(`✓ Connected to sync backend`)
147
+ yield* Console.log(`Successfully imported ${result.eventCount} events`)
148
+ }).pipe(Effect.withSpan('cli:import'))
149
+
150
+ export const exportCommand = Cli.Command.make(
151
+ 'export',
152
+ {
153
+ store: Cli.Options.text('store').pipe(
154
+ Cli.Options.withAlias('s'),
155
+ Cli.Options.withDescription('Path to the store module that exports schema and syncBackend'),
156
+ ),
157
+ storeId: Cli.Options.text('store-id').pipe(
158
+ Cli.Options.withAlias('i'),
159
+ Cli.Options.withDescription('Store identifier'),
160
+ ),
161
+ clientId: Cli.Options.text('client-id').pipe(
162
+ Cli.Options.withDefault('cli-export'),
163
+ Cli.Options.withDescription('Client identifier for the sync connection'),
164
+ ),
165
+ output: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Output JSON file path')),
166
+ },
167
+ Effect.fn(function* ({
168
+ store,
169
+ storeId,
170
+ clientId,
171
+ output,
172
+ }: {
173
+ store: string
174
+ storeId: string
175
+ clientId: string
176
+ output: string
177
+ }) {
178
+ yield* Console.log(`Exporting events from LiveStore...`)
179
+ yield* Console.log(` Store: ${store}`)
180
+ yield* Console.log(` Store ID: ${storeId}`)
181
+ yield* Console.log(` Output: ${output}`)
182
+ yield* Console.log('')
183
+
184
+ yield* exportEvents({
185
+ storePath: store,
186
+ storeId,
187
+ clientId,
188
+ outputPath: output,
189
+ }).pipe(Effect.scoped)
190
+ }),
191
+ ).pipe(
192
+ Cli.Command.withDescription(
193
+ 'Export all events from the sync backend to a JSON file. Useful for backup and migration.',
194
+ ),
195
+ )
196
+
197
+ export const importCommand = Cli.Command.make(
198
+ 'import',
199
+ {
200
+ store: Cli.Options.text('store').pipe(
201
+ Cli.Options.withAlias('s'),
202
+ Cli.Options.withDescription('Path to the store module that exports schema and syncBackend'),
203
+ ),
204
+ storeId: Cli.Options.text('store-id').pipe(
205
+ Cli.Options.withAlias('i'),
206
+ Cli.Options.withDescription('Store identifier'),
207
+ ),
208
+ clientId: Cli.Options.text('client-id').pipe(
209
+ Cli.Options.withDefault('cli-import'),
210
+ Cli.Options.withDescription('Client identifier for the sync connection'),
211
+ ),
212
+ force: Cli.Options.boolean('force').pipe(
213
+ Cli.Options.withAlias('f'),
214
+ Cli.Options.withDefault(false),
215
+ Cli.Options.withDescription('Force import even if store ID does not match'),
216
+ ),
217
+ dryRun: Cli.Options.boolean('dry-run').pipe(
218
+ Cli.Options.withDefault(false),
219
+ Cli.Options.withDescription('Validate the import file without actually importing'),
220
+ ),
221
+ input: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Input JSON file to import')),
222
+ },
223
+ Effect.fn(function* ({
224
+ store,
225
+ storeId,
226
+ clientId,
227
+ force,
228
+ dryRun,
229
+ input,
230
+ }: {
231
+ store: string
232
+ storeId: string
233
+ clientId: string
234
+ force: boolean
235
+ dryRun: boolean
236
+ input: string
237
+ }) {
238
+ yield* Console.log(`Importing events to LiveStore...`)
239
+ yield* Console.log(` Store: ${store}`)
240
+ yield* Console.log(` Store ID: ${storeId}`)
241
+ yield* Console.log(` Input: ${input}`)
242
+ if (force) yield* Console.log(` Force: enabled`)
243
+ if (dryRun) yield* Console.log(` Dry run: enabled`)
244
+ yield* Console.log('')
245
+
246
+ yield* importEvents({
247
+ storePath: store,
248
+ storeId,
249
+ clientId,
250
+ inputPath: input,
251
+ force,
252
+ dryRun,
253
+ }).pipe(Effect.scoped)
254
+ }),
255
+ ).pipe(
256
+ Cli.Command.withDescription('Import events from a JSON file to the sync backend. The sync backend must be empty.'),
257
+ )
258
+
259
+ export const syncCommand = Cli.Command.make('sync').pipe(
260
+ Cli.Command.withSubcommands([exportCommand, importCommand]),
261
+ Cli.Command.withDescription('Import and export events from the sync backend'),
262
+ )
@@ -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
  })
@@ -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
+ "storePath": "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
+ "storePath": "livestore-cli.config.ts",
73
73
  "storeId": "<store-id>",
74
74
  "clientId": "<client-id>",
75
75
  "sessionId": "<session-id>"
@@ -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
  )