@livestore/cli 0.4.0-dev.9 → 0.4.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 (77) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts +65 -0
  3. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/mock-config.js +94 -0
  5. package/dist/__tests__/fixtures/mock-config.js.map +1 -0
  6. package/dist/__tests__/sync-operations.test.d.ts +2 -0
  7. package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
  8. package/dist/__tests__/sync-operations.test.js +167 -0
  9. package/dist/__tests__/sync-operations.test.js.map +1 -0
  10. package/dist/bin.js.map +1 -1
  11. package/dist/cli.d.ts +16 -2
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +3 -4
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/import-export.d.ts +34 -0
  16. package/dist/commands/import-export.d.ts.map +1 -0
  17. package/dist/commands/import-export.js +135 -0
  18. package/dist/commands/import-export.js.map +1 -0
  19. package/dist/commands/mcp-coach.d.ts +13 -5
  20. package/dist/commands/mcp-coach.d.ts.map +1 -1
  21. package/dist/commands/mcp-coach.js +41 -45
  22. package/dist/commands/mcp-coach.js.map +1 -1
  23. package/dist/commands/mcp-tool-handlers.d.ts +6 -0
  24. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
  25. package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +79 -45
  26. package/dist/commands/mcp-tool-handlers.js.map +1 -0
  27. package/dist/commands/mcp-tools-defs.d.ts +133 -0
  28. package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
  29. package/dist/commands/mcp-tools-defs.js +278 -0
  30. package/dist/commands/mcp-tools-defs.js.map +1 -0
  31. package/dist/commands/mcp.d.ts.map +1 -1
  32. package/dist/commands/mcp.js +2 -1
  33. package/dist/commands/mcp.js.map +1 -1
  34. package/dist/commands/new-project.d.ts +9 -2
  35. package/dist/commands/new-project.d.ts.map +1 -1
  36. package/dist/commands/new-project.js +72 -32
  37. package/dist/commands/new-project.js.map +1 -1
  38. package/dist/mcp-runtime/runtime.d.ts +48 -0
  39. package/dist/mcp-runtime/runtime.d.ts.map +1 -0
  40. package/dist/mcp-runtime/runtime.js +107 -0
  41. package/dist/mcp-runtime/runtime.js.map +1 -0
  42. package/dist/module-loader.d.ts +22 -0
  43. package/dist/module-loader.d.ts.map +1 -0
  44. package/dist/module-loader.js +75 -0
  45. package/dist/module-loader.js.map +1 -0
  46. package/dist/package-manager.d.ts +38 -0
  47. package/dist/package-manager.d.ts.map +1 -0
  48. package/dist/package-manager.js +33 -0
  49. package/dist/package-manager.js.map +1 -0
  50. package/dist/package-manager.test.d.ts +2 -0
  51. package/dist/package-manager.test.d.ts.map +1 -0
  52. package/dist/package-manager.test.js +57 -0
  53. package/dist/package-manager.test.js.map +1 -0
  54. package/dist/sync-operations.d.ts +121 -0
  55. package/dist/sync-operations.d.ts.map +1 -0
  56. package/dist/sync-operations.js +180 -0
  57. package/dist/sync-operations.js.map +1 -0
  58. package/package.json +71 -15
  59. package/src/__tests__/fixtures/mock-config.ts +112 -0
  60. package/src/__tests__/sync-operations.test.ts +232 -0
  61. package/src/bin.ts +1 -0
  62. package/src/cli.ts +4 -5
  63. package/src/commands/import-export.ts +283 -0
  64. package/src/commands/mcp-coach.ts +67 -67
  65. package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +93 -57
  66. package/src/commands/mcp-tools-defs.ts +318 -0
  67. package/src/commands/mcp.ts +3 -2
  68. package/src/commands/new-project.ts +90 -36
  69. package/src/mcp-runtime/runtime.ts +147 -0
  70. package/src/module-loader.ts +94 -0
  71. package/src/package-manager.test.ts +66 -0
  72. package/src/package-manager.ts +44 -0
  73. package/src/sync-operations.ts +360 -0
  74. package/dist/commands/mcp-tools.d.ts +0 -58
  75. package/dist/commands/mcp-tools.d.ts.map +0 -1
  76. package/dist/commands/mcp-tools.js.map +0 -1
  77. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,147 @@
1
+ import { makeAdapter as makeNodeAdapter } from '@livestore/adapter-node'
2
+ import { UnknownError } from '@livestore/common'
3
+ import { LiveStoreEvent, SystemTables } from '@livestore/common/schema'
4
+ import type { Store } from '@livestore/livestore'
5
+ import { createStorePromise } from '@livestore/livestore'
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'
10
+
11
+ /** Currently connected store */
12
+ let store: Store<any> | undefined
13
+
14
+ /** Layer providing FileSystem and HttpClient for module loading */
15
+ const ModuleLoaderLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
16
+
17
+ /**
18
+ * Dynamically imports a module that exports a `makeStore({ storeId }): Promise<Store>` function,
19
+ * calls it with the provided storeId, and caches the Store instance for subsequent tool calls.
20
+ */
21
+ export const init = ({
22
+ configPath,
23
+ storeId,
24
+ clientId,
25
+ sessionId,
26
+ }: {
27
+ configPath: string
28
+ storeId: string
29
+ clientId?: string
30
+ sessionId?: string
31
+ }): Effect.Effect<Store<any>, UnknownError> =>
32
+ Effect.gen(function* () {
33
+ if (storeId === '' || typeof storeId !== 'string') {
34
+ return yield* UnknownError.make({ cause: new Error('Invalid storeId: expected a non-empty string') })
35
+ }
36
+
37
+ const { schema, syncBackendConstructor, syncPayloadSchema, syncPayload } = yield* loadModuleConfig({ configPath })
38
+
39
+ // Build Node adapter internally
40
+ const adapter = makeNodeAdapter({
41
+ storage: { type: 'in-memory' },
42
+ ...(clientId !== undefined && clientId !== '' ? { clientId } : {}),
43
+ ...(sessionId !== undefined && sessionId !== '' ? { sessionId } : {}),
44
+ sync: {
45
+ backend: syncBackendConstructor,
46
+ initialSyncOptions: { _tag: 'Blocking', timeout: 5000 },
47
+ onSyncError: 'shutdown',
48
+ },
49
+ })
50
+
51
+ // Create the store
52
+ const s = yield* Effect.promise(() =>
53
+ createStorePromise({
54
+ schema,
55
+ storeId,
56
+ adapter,
57
+ disableDevtools: true,
58
+ syncPayload,
59
+ syncPayloadSchema,
60
+ }),
61
+ )
62
+
63
+ // Replace existing store if any
64
+ if (store !== undefined) {
65
+ yield* Effect.promise(async () => {
66
+ try {
67
+ await store!.shutdownPromise()
68
+ } catch {}
69
+ })
70
+ }
71
+
72
+ store = s
73
+ return store
74
+ }).pipe(Effect.provide(ModuleLoaderLayer), Effect.withSpan('mcp-runtime:init'))
75
+
76
+ export const getStore = Effect.sync(() => Option.fromNullable(store))
77
+
78
+ export const status = Effect.gen(function* () {
79
+ const opt = yield* getStore
80
+ if (opt._tag === 'None') {
81
+ return {
82
+ _tag: 'disconnected' as const,
83
+ }
84
+ }
85
+ const s = opt.value
86
+ const tableCounts = Array.from(s.schema.state.sqlite.tables.keys())
87
+ .filter((name) => !SystemTables.isStateSystemTable(name))
88
+ .reduce(
89
+ (acc, name) => {
90
+ acc[name] = s.query(s.schema.state.sqlite.tables.get(name)!.count())
91
+ return acc
92
+ },
93
+ {} as Record<string, number>,
94
+ )
95
+
96
+ return {
97
+ _tag: 'connected' as const,
98
+ storeId: s.storeId,
99
+ clientId: s.clientId,
100
+ sessionId: s.sessionId,
101
+ tableCounts,
102
+ }
103
+ }).pipe(Effect.withSpan('mcp-runtime:status'))
104
+
105
+ export const query = Effect.fn('mcp-runtime:query')(function* ({
106
+ sql,
107
+ bindValues,
108
+ }: {
109
+ sql: string
110
+ bindValues?: readonly any[] | Record<string, unknown>
111
+ }) {
112
+ const opt = yield* getStore
113
+ if (opt._tag === 'None') {
114
+ return yield* Effect.dieMessage('LiveStore not connected. Call livestore_instance_connect first.')
115
+ }
116
+ const s = opt.value
117
+
118
+ const rows = s.query<Array<Record<string, unknown>>>({ query: sql, bindValues: (bindValues as any) ?? [] })
119
+ const jsonRows = rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, v as Schema.JsonValue])))
120
+ return { rows: jsonRows, rowCount: jsonRows.length }
121
+ })
122
+
123
+ export const commit = Effect.fn('mcp-runtime:commit')(function* ({
124
+ events,
125
+ }: {
126
+ events: ReadonlyArray<{ name: string; args: Schema.JsonValue }>
127
+ }) {
128
+ const opt = yield* getStore
129
+ if (opt._tag === 'None') {
130
+ return yield* Effect.dieMessage('LiveStore not connected. Call livestore_instance_connect first.')
131
+ }
132
+ const s = opt.value
133
+ const InputEventSchema = LiveStoreEvent.Input.makeSchema(s.schema) as Schema.Schema<any>
134
+ const decoded = events.map((e) => Schema.decodeSync(InputEventSchema)(e))
135
+ s.commit(...decoded)
136
+ return { committed: decoded.length }
137
+ })
138
+
139
+ export const disconnect = Effect.promise(async () => {
140
+ if (store !== undefined) {
141
+ try {
142
+ await store.shutdownPromise()
143
+ } catch {}
144
+ store = undefined
145
+ }
146
+ return { _tag: 'disconnected' as const }
147
+ }).pipe(Effect.withSpan('mcp-runtime:disconnect'))
@@ -0,0 +1,94 @@
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
+
8
+ import type { SyncBackend } from '@livestore/common'
9
+ import { UnknownError } from '@livestore/common'
10
+ import { isLiveStoreSchema, type LiveStoreSchema } from '@livestore/common/schema'
11
+ import { shouldNeverHappen } from '@livestore/utils'
12
+ import { Effect, FileSystem, Schema } from '@livestore/utils/effect'
13
+
14
+ export interface ModuleConfig {
15
+ schema: LiveStoreSchema
16
+ syncBackendConstructor: SyncBackend.SyncBackendConstructor
17
+ syncPayloadSchema: Schema.Schema<any>
18
+ syncPayload: unknown
19
+ }
20
+
21
+ /**
22
+ * Loads and validates a user config module.
23
+ * The module must export:
24
+ * - `schema`: A valid LiveStore schema
25
+ * - `syncBackend`: A sync backend constructor function
26
+ * - `syncPayloadSchema` (optional): Schema for validating syncPayload
27
+ * - `syncPayload` (optional): Payload data for the sync backend
28
+ */
29
+ export const loadModuleConfig = ({
30
+ configPath,
31
+ }: {
32
+ configPath: string
33
+ }): Effect.Effect<ModuleConfig, UnknownError, FileSystem.FileSystem> =>
34
+ Effect.gen(function* () {
35
+ const abs = path.isAbsolute(configPath) === true ? configPath : path.resolve(process.cwd(), configPath)
36
+
37
+ const fs = yield* FileSystem.FileSystem
38
+ const exists = yield* fs.exists(abs).pipe(UnknownError.mapToUnknownError)
39
+ if (exists === false) {
40
+ return yield* UnknownError.make({
41
+ cause: `Store module not found at ${abs}`,
42
+ note: 'Make sure the path points to a valid LiveStore module',
43
+ })
44
+ }
45
+
46
+ const mod = yield* Effect.tryPromise({
47
+ try: () => import(pathToFileURL(abs).href),
48
+ catch: (cause) =>
49
+ UnknownError.make({
50
+ cause,
51
+ note: `Failed to import module at ${abs}`,
52
+ }),
53
+ })
54
+
55
+ const schema = (mod)?.schema
56
+ if (isLiveStoreSchema(schema) === false) {
57
+ return yield* UnknownError.make({
58
+ cause: `Module at ${abs} must export a valid LiveStore 'schema'`,
59
+ note: `Ex: export { schema } from './src/livestore/schema.ts'`,
60
+ })
61
+ }
62
+
63
+ const syncBackendConstructor = (mod)?.syncBackend
64
+ if (typeof syncBackendConstructor !== 'function') {
65
+ return yield* UnknownError.make({
66
+ cause: `Module at ${abs} must export a 'syncBackend' constructor`,
67
+ note: `Ex: export const syncBackend = makeWsSync({ url })`,
68
+ })
69
+ }
70
+
71
+ const syncPayloadSchemaExport = (mod)?.syncPayloadSchema
72
+ const syncPayloadSchema =
73
+ syncPayloadSchemaExport === undefined
74
+ ? Schema.JsonValue
75
+ : Schema.isSchema(syncPayloadSchemaExport) === true
76
+ ? (syncPayloadSchemaExport as Schema.Schema<any>)
77
+ : shouldNeverHappen(
78
+ `Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
79
+ )
80
+
81
+ const syncPayloadExport = (mod)?.syncPayload
82
+ const syncPayload = yield* (
83
+ syncPayloadExport === undefined
84
+ ? Effect.succeed<unknown>(undefined)
85
+ : Schema.decodeUnknown(syncPayloadSchema)(syncPayloadExport)
86
+ ).pipe(UnknownError.mapToUnknownError)
87
+
88
+ return {
89
+ schema,
90
+ syncBackendConstructor,
91
+ syncPayloadSchema,
92
+ syncPayload,
93
+ }
94
+ }).pipe(Effect.withSpan('module-loader:loadModuleConfig'))
@@ -0,0 +1,66 @@
1
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
2
+
3
+ import { detectPackageManager, pmCommands } from './package-manager.ts'
4
+
5
+ Vitest.describe('detectPackageManager', () => {
6
+ Vitest.it('detects npm from user agent', () => {
7
+ const result = detectPackageManager('npm/10.2.4 node/v20.11.0 darwin arm64 workspaces/false')
8
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'npm' })
9
+ })
10
+
11
+ Vitest.it('detects pnpm from user agent', () => {
12
+ const result = detectPackageManager('pnpm/9.0.6 npm/? node/v22.0.0 darwin arm64')
13
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'pnpm' })
14
+ })
15
+
16
+ Vitest.it('detects yarn as unsupported', () => {
17
+ const result = detectPackageManager('yarn/1.22.19 npm/? node/v20.11.0 darwin arm64')
18
+ Vitest.expect(result).toEqual({ _tag: 'unsupported', pm: 'yarn' })
19
+ })
20
+
21
+ Vitest.it('detects bun from user agent', () => {
22
+ const result = detectPackageManager('bun/1.1.7')
23
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'bun' })
24
+ })
25
+
26
+ Vitest.it('falls back to bun for empty user agent', () => {
27
+ const result = detectPackageManager('')
28
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'bun' })
29
+ })
30
+
31
+ Vitest.it('uses process.env.npm_config_user_agent when no argument provided', () => {
32
+ const originalEnv = process.env.npm_config_user_agent
33
+ try {
34
+ process.env.npm_config_user_agent = 'pnpm/9.0.6'
35
+ const result = detectPackageManager()
36
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'pnpm' })
37
+ } finally {
38
+ process.env.npm_config_user_agent = originalEnv
39
+ }
40
+ })
41
+
42
+ Vitest.it('falls back to bun for unknown user agent', () => {
43
+ const result = detectPackageManager('some-unknown-tool/1.0.0')
44
+ Vitest.expect(result).toEqual({ _tag: 'supported', pm: 'bun' })
45
+ })
46
+ })
47
+
48
+ Vitest.describe('pmCommands', () => {
49
+ Vitest.it('provides correct install commands for each package manager', () => {
50
+ Vitest.expect(pmCommands.install.npm).toBe('npm install')
51
+ Vitest.expect(pmCommands.install.pnpm).toBe('pnpm install')
52
+ Vitest.expect(pmCommands.install.bun).toBe('bun install')
53
+ })
54
+
55
+ Vitest.it('provides correct run commands for dev script', () => {
56
+ Vitest.expect(pmCommands.run.npm('dev')).toBe('npm run dev')
57
+ Vitest.expect(pmCommands.run.pnpm('dev')).toBe('pnpm dev')
58
+ Vitest.expect(pmCommands.run.bun('dev')).toBe('bun dev')
59
+ })
60
+
61
+ Vitest.it('provides correct run commands for start script', () => {
62
+ Vitest.expect(pmCommands.run.npm('start')).toBe('npm run start')
63
+ Vitest.expect(pmCommands.run.pnpm('start')).toBe('pnpm start')
64
+ Vitest.expect(pmCommands.run.bun('start')).toBe('bun start')
65
+ })
66
+ })
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Supported package managers for LiveStore projects.
3
+ * Yarn is explicitly not supported due to compatibility issues.
4
+ */
5
+ export type PackageManager = 'npm' | 'pnpm' | 'bun'
6
+
7
+ /**
8
+ * Result of package manager detection.
9
+ * Returns 'unsupported' for yarn to allow the CLI to show a warning message.
10
+ */
11
+ export type DetectPackageManagerResult = { _tag: 'supported'; pm: PackageManager } | { _tag: 'unsupported'; pm: 'yarn' }
12
+
13
+ /**
14
+ * Detects the package manager used to invoke the CLI based on the `npm_config_user_agent`
15
+ * environment variable. This env var is set by npm, pnpm, yarn, and bun when running scripts.
16
+ *
17
+ * - Returns 'unsupported' for yarn so the CLI can show a recommendation to use bun instead
18
+ * - Falls back to 'bun' when detection fails (e.g., when run directly without a package manager)
19
+ */
20
+ export const detectPackageManager = (
21
+ userAgent = process.env.npm_config_user_agent ?? '',
22
+ ): DetectPackageManagerResult => {
23
+ if (userAgent.startsWith('bun/') === true) return { _tag: 'supported', pm: 'bun' }
24
+ if (userAgent.startsWith('pnpm/') === true) return { _tag: 'supported', pm: 'pnpm' }
25
+ if (userAgent.startsWith('npm/') === true) return { _tag: 'supported', pm: 'npm' }
26
+ if (userAgent.startsWith('yarn/') === true) return { _tag: 'unsupported', pm: 'yarn' }
27
+
28
+ // Default to bun when the package manager can't be detected
29
+ return { _tag: 'supported', pm: 'bun' }
30
+ }
31
+
32
+ /** Package manager command templates */
33
+ export const pmCommands = {
34
+ install: {
35
+ npm: 'npm install',
36
+ pnpm: 'pnpm install',
37
+ bun: 'bun install',
38
+ },
39
+ run: {
40
+ npm: (script: string) => `npm run ${script}`,
41
+ pnpm: (script: string) => `pnpm ${script}`,
42
+ bun: (script: string) => `bun ${script}`,
43
+ },
44
+ } as const