@livestore/cli 0.4.0-dev.9 → 0.5.0-dev.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 (136) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts +63 -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 +6 -5
  11. package/dist/bin.js.map +1 -1
  12. package/dist/cli.d.ts +2 -9
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +4 -5
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
  17. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
  18. package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
  19. package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
  20. package/dist/commands/import-export.d.ts +20 -0
  21. package/dist/commands/import-export.d.ts.map +1 -0
  22. package/dist/commands/import-export.js +135 -0
  23. package/dist/commands/import-export.js.map +1 -0
  24. package/dist/commands/mcp-coach.d.ts +21 -12
  25. package/dist/commands/mcp-coach.d.ts.map +1 -1
  26. package/dist/commands/mcp-coach.js +49 -53
  27. package/dist/commands/mcp-coach.js.map +1 -1
  28. package/dist/commands/mcp-protocols.d.ts +7 -0
  29. package/dist/commands/mcp-protocols.d.ts.map +1 -0
  30. package/dist/commands/mcp-protocols.js +12 -0
  31. package/dist/commands/mcp-protocols.js.map +1 -0
  32. package/dist/commands/mcp-protocols.test.d.ts +2 -0
  33. package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
  34. package/dist/commands/mcp-protocols.test.js +60 -0
  35. package/dist/commands/mcp-protocols.test.js.map +1 -0
  36. package/dist/commands/mcp-stdio.test.d.ts +2 -0
  37. package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
  38. package/dist/commands/mcp-stdio.test.js +79 -0
  39. package/dist/commands/mcp-stdio.test.js.map +1 -0
  40. package/dist/commands/mcp-tool-handlers.d.ts +6 -0
  41. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
  42. package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +80 -45
  43. package/dist/commands/mcp-tool-handlers.js.map +1 -0
  44. package/dist/commands/mcp-tools-defs.d.ts +134 -0
  45. package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
  46. package/dist/commands/mcp-tools-defs.js +285 -0
  47. package/dist/commands/mcp-tools-defs.js.map +1 -0
  48. package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
  49. package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
  50. package/dist/commands/mcp-tools-defs.test.js +11 -0
  51. package/dist/commands/mcp-tools-defs.test.js.map +1 -0
  52. package/dist/commands/mcp.d.ts +1 -3
  53. package/dist/commands/mcp.d.ts.map +1 -1
  54. package/dist/commands/mcp.js +7 -5
  55. package/dist/commands/mcp.js.map +1 -1
  56. package/dist/commands/new-project.d.ts +22 -23
  57. package/dist/commands/new-project.d.ts.map +1 -1
  58. package/dist/commands/new-project.js +85 -44
  59. package/dist/commands/new-project.js.map +1 -1
  60. package/dist/mcp-content/architecture.d.ts +1 -1
  61. package/dist/mcp-content/architecture.d.ts.map +1 -1
  62. package/dist/mcp-content/architecture.js +3 -3
  63. package/dist/mcp-content/features.d.ts +1 -1
  64. package/dist/mcp-content/features.d.ts.map +1 -1
  65. package/dist/mcp-content/features.js +1 -1
  66. package/dist/mcp-content/getting-started.d.ts +1 -1
  67. package/dist/mcp-content/getting-started.d.ts.map +1 -1
  68. package/dist/mcp-content/getting-started.js +37 -37
  69. package/dist/mcp-content/overview.d.ts +1 -1
  70. package/dist/mcp-content/overview.d.ts.map +1 -1
  71. package/dist/mcp-content/overview.js +6 -6
  72. package/dist/mcp-content/schemas/blog.d.ts +1 -1
  73. package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
  74. package/dist/mcp-content/schemas/blog.js +25 -25
  75. package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
  76. package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
  77. package/dist/mcp-content/schemas/ecommerce.js +67 -67
  78. package/dist/mcp-content/schemas/social.d.ts +1 -1
  79. package/dist/mcp-content/schemas/social.d.ts.map +1 -1
  80. package/dist/mcp-content/schemas/social.js +48 -48
  81. package/dist/mcp-content/schemas/todo.d.ts +1 -1
  82. package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
  83. package/dist/mcp-content/schemas/todo.js +35 -35
  84. package/dist/mcp-runtime/runtime.d.ts +48 -0
  85. package/dist/mcp-runtime/runtime.d.ts.map +1 -0
  86. package/dist/mcp-runtime/runtime.js +107 -0
  87. package/dist/mcp-runtime/runtime.js.map +1 -0
  88. package/dist/module-loader.d.ts +22 -0
  89. package/dist/module-loader.d.ts.map +1 -0
  90. package/dist/module-loader.js +75 -0
  91. package/dist/module-loader.js.map +1 -0
  92. package/dist/package-manager.d.ts +38 -0
  93. package/dist/package-manager.d.ts.map +1 -0
  94. package/dist/package-manager.js +33 -0
  95. package/dist/package-manager.js.map +1 -0
  96. package/dist/package-manager.test.d.ts +2 -0
  97. package/dist/package-manager.test.d.ts.map +1 -0
  98. package/dist/package-manager.test.js +57 -0
  99. package/dist/package-manager.test.js.map +1 -0
  100. package/dist/sync-operations.d.ts +115 -0
  101. package/dist/sync-operations.d.ts.map +1 -0
  102. package/dist/sync-operations.js +178 -0
  103. package/dist/sync-operations.js.map +1 -0
  104. package/package.json +57 -15
  105. package/src/__tests__/fixtures/mock-config.ts +112 -0
  106. package/src/__tests__/sync-operations.test.ts +218 -0
  107. package/src/bin.ts +8 -6
  108. package/src/cli.ts +5 -6
  109. package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
  110. package/src/commands/import-export.ts +283 -0
  111. package/src/commands/mcp-coach.ts +76 -75
  112. package/src/commands/mcp-protocols.test.ts +76 -0
  113. package/src/commands/mcp-protocols.ts +12 -0
  114. package/src/commands/mcp-stdio.test.ts +93 -0
  115. package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +100 -61
  116. package/src/commands/mcp-tools-defs.test.ts +13 -0
  117. package/src/commands/mcp-tools-defs.ts +320 -0
  118. package/src/commands/mcp.ts +10 -6
  119. package/src/commands/new-project.ts +124 -57
  120. package/src/mcp-content/architecture.ts +3 -3
  121. package/src/mcp-content/features.ts +1 -1
  122. package/src/mcp-content/getting-started.ts +37 -37
  123. package/src/mcp-content/overview.ts +6 -6
  124. package/src/mcp-content/schemas/blog.ts +25 -25
  125. package/src/mcp-content/schemas/ecommerce.ts +67 -67
  126. package/src/mcp-content/schemas/social.ts +48 -48
  127. package/src/mcp-content/schemas/todo.ts +35 -35
  128. package/src/mcp-runtime/runtime.ts +147 -0
  129. package/src/module-loader.ts +94 -0
  130. package/src/package-manager.test.ts +66 -0
  131. package/src/package-manager.ts +44 -0
  132. package/src/sync-operations.ts +360 -0
  133. package/dist/commands/mcp-tools.d.ts +0 -58
  134. package/dist/commands/mcp-tools.d.ts.map +0 -1
  135. package/dist/commands/mcp-tools.js.map +0 -1
  136. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,7 +1,10 @@
1
1
  import * as os from 'node:os'
2
2
  import * as nodePath from 'node:path'
3
+
4
+ import { sluggify } from '@livestore/utils'
3
5
  import {
4
- Command,
6
+ ChildProcess,
7
+ ChildProcessSpawner,
5
8
  Console,
6
9
  Effect,
7
10
  FileSystem,
@@ -12,60 +15,86 @@ import {
12
15
  } from '@livestore/utils/effect'
13
16
  import { Cli } from '@livestore/utils/node'
14
17
 
18
+ import { detectPackageManager, pmCommands } from '../package-manager.ts'
19
+
15
20
  // Schema for GitHub API response
16
21
  const GitHubContentSchema = Schema.Struct({
17
22
  name: Schema.String,
18
- type: Schema.Literal('dir', 'file'),
23
+ type: Schema.Literals(['dir', 'file']),
19
24
  path: Schema.String,
20
25
  download_url: Schema.NullOr(Schema.String),
21
26
  })
22
27
 
23
28
  const GitHubContentsResponseSchema = Schema.Array(GitHubContentSchema)
24
29
 
30
+ const githubRequest = (url: string) => {
31
+ const request = HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader('accept', 'application/vnd.github+json'))
32
+ const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
33
+ return token === undefined ? request : request.pipe(HttpClientRequest.setHeader('authorization', `Bearer ${token}`))
34
+ }
35
+
36
+ /** Schema for parsing package.json scripts (dev or start) */
37
+ const PackageJsonScriptsSchema = Schema.Struct({
38
+ scripts: Schema.Union([Schema.Struct({ dev: Schema.String }), Schema.Struct({ start: Schema.String })]),
39
+ })
40
+
25
41
  // Error types
26
- export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>()('ExampleNotFoundError', {
42
+ export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>(
43
+ '~@livestore/cli/ExampleNotFoundError',
44
+ )('ExampleNotFoundError', {
27
45
  exampleName: Schema.String,
28
46
  availableExamples: Schema.Array(Schema.String),
29
47
  message: Schema.String,
30
48
  }) {}
31
49
 
32
- export class NetworkError extends Schema.TaggedError<NetworkError>()('NetworkError', {
50
+ export class NetworkError extends Schema.TaggedError<NetworkError>('~@livestore/cli/NetworkError')('NetworkError', {
33
51
  cause: Schema.Unknown,
34
52
  message: Schema.String,
35
53
  }) {}
36
54
 
37
- export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>()('DirectoryExistsError', {
55
+ export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>(
56
+ '~@livestore/cli/DirectoryExistsError',
57
+ )('DirectoryExistsError', {
38
58
  path: Schema.String,
39
59
  message: Schema.String,
40
60
  }) {}
41
61
 
62
+ export class NoExamplesError extends Schema.TaggedError<NoExamplesError>('~@livestore/cli/NoExamplesError')(
63
+ 'NoExamplesError',
64
+ {
65
+ message: Schema.String,
66
+ },
67
+ ) {}
68
+
42
69
  // Fetch available examples from GitHub
43
- const fetchExamples = (branch: string) =>
70
+ const fetchExamples = (ref: string) =>
44
71
  Effect.gen(function* () {
45
- const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${branch}`
72
+ const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${ref}`
46
73
 
47
- yield* Effect.log(`Fetching examples from branch: ${branch}`)
74
+ yield* Effect.log(`Fetching examples from ref: ${ref}`)
48
75
 
49
- const request = HttpClientRequest.get(url)
76
+ const request = githubRequest(url)
50
77
  const response = yield* HttpClient.execute(request).pipe(
51
78
  Effect.scoped,
52
- Effect.catchAll(
79
+ Effect.catch(
53
80
  (error) =>
54
81
  new NetworkError({
55
82
  cause: error,
56
- message: `Failed to fetch examples from GitHub: ${error}`,
83
+ message: `Failed to fetch examples from GitHub: ${String(error)}`,
57
84
  }),
58
85
  ),
59
86
  )
60
87
 
61
88
  const responseText = yield* response.text
62
89
 
63
- const examples = yield* Schema.decodeUnknown(GitHubContentsResponseSchema)(JSON.parse(responseText)).pipe(
64
- Effect.catchAll(
90
+ const examples = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GitHubContentsResponseSchema))(
91
+ responseText,
92
+ ).pipe(
93
+ Effect.catch(
65
94
  (error) =>
66
95
  new NetworkError({
67
96
  cause: error,
68
- message: `Failed to parse GitHub API response: ${error}`,
97
+ message: `Failed to parse GitHub API response: ${String(error)}`,
69
98
  }),
70
99
  ),
71
100
  )
@@ -73,7 +102,7 @@ const fetchExamples = (branch: string) =>
73
102
  const exampleNames = examples
74
103
  .filter((item) => item.type === 'dir')
75
104
  .map((item) => item.name)
76
- .sort()
105
+ .toSorted()
77
106
 
78
107
  yield* Effect.log(`Found ${exampleNames.length} examples: ${exampleNames.join(', ')}`)
79
108
 
@@ -84,7 +113,7 @@ const fetchExamples = (branch: string) =>
84
113
  const selectExample = (examples: string[]) =>
85
114
  Effect.gen(function* () {
86
115
  if (examples.length === 0) {
87
- return yield* Effect.fail(new Error('No examples available'))
116
+ return yield* new NoExamplesError({ message: 'No examples available' })
88
117
  }
89
118
 
90
119
  const prompt = Cli.Prompt.select({
@@ -100,24 +129,26 @@ const selectExample = (examples: string[]) =>
100
129
  })
101
130
 
102
131
  // Download and extract example using tiged approach
103
- const downloadExample = (exampleName: string, branch: string, destinationPath: string) =>
132
+ const downloadExample = (exampleName: string, ref: string, destinationPath: string) =>
104
133
  Effect.gen(function* () {
105
- yield* Console.log(`📥 Downloading example "${exampleName}" from branch "${branch}"...`)
134
+ yield* Console.log(`📥 Downloading example "${exampleName}" from ref "${ref}"...`)
135
+
136
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
106
137
 
107
138
  const tempDir = yield* Effect.sync(() => os.tmpdir())
108
- const tarballPath = nodePath.join(tempDir, `livestore-${branch}-${Date.now()}.tar.gz`)
109
- const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${branch}`
139
+ const tarballPath = nodePath.join(tempDir, `livestore-${sluggify(ref)}-${Date.now()}.tar.gz`)
140
+ const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${ref}`
110
141
 
111
142
  // Download tarball directly
112
- const request = HttpClientRequest.get(tarballUrl)
143
+ const request = githubRequest(tarballUrl)
113
144
 
114
145
  const response = yield* HttpClient.execute(request).pipe(
115
146
  Effect.scoped,
116
- Effect.catchAll(
147
+ Effect.catch(
117
148
  (error) =>
118
149
  new NetworkError({
119
150
  cause: error,
120
- message: `Failed to download tarball: ${error}`,
151
+ message: `Failed to download tarball: ${String(error)}`,
121
152
  }),
122
153
  ),
123
154
  )
@@ -135,14 +166,13 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
135
166
  const extractDir = nodePath.join(tempDir, `extract-${Date.now()}`)
136
167
  yield* fs.makeDirectory(extractDir, { recursive: true })
137
168
 
138
- // Extract tarball using Effect Command
139
- yield* Command.make('tar', '-xzf', tarballPath, '-C', extractDir).pipe(
140
- Command.exitCode,
141
- Effect.catchAll(
169
+ // Extract tarball using a child process
170
+ yield* spawner.exitCode(ChildProcess.make('tar', ['-xzf', tarballPath, '-C', extractDir])).pipe(
171
+ Effect.catch(
142
172
  (error) =>
143
173
  new NetworkError({
144
174
  cause: error,
145
- message: `Failed to extract tarball: ${error}`,
175
+ message: `Failed to extract tarball: ${String(error)}`,
146
176
  }),
147
177
  ),
148
178
  )
@@ -163,7 +193,7 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
163
193
  // Check if the example exists
164
194
  const exampleExists = yield* fs.exists(exampleSourcePath)
165
195
 
166
- if (!exampleExists) {
196
+ if (exampleExists === false) {
167
197
  return yield* new ExampleNotFoundError({
168
198
  exampleName,
169
199
  availableExamples: [],
@@ -171,56 +201,60 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
171
201
  })
172
202
  }
173
203
 
174
- // Copy the example directory contents to the destination using Effect Command
175
- yield* Command.make('cp', '-r', `${exampleSourcePath}/.`, destinationPath).pipe(
176
- Command.exitCode,
177
- Effect.catchAll(
204
+ // Copy the example directory contents to the destination using a child process
205
+ yield* spawner.exitCode(ChildProcess.make('cp', ['-r', `${exampleSourcePath}/.`, destinationPath])).pipe(
206
+ Effect.catch(
178
207
  (error) =>
179
208
  new NetworkError({
180
209
  cause: error,
181
- message: `Failed to copy example files: ${error}`,
210
+ message: `Failed to copy example files: ${String(error)}`,
182
211
  }),
183
212
  ),
184
213
  )
185
214
 
186
215
  // Clean up extract directory
187
- yield* fs.remove(extractDir, { recursive: true }).pipe(Effect.catchAll(() => Effect.void))
216
+ yield* fs.remove(extractDir, { recursive: true }).pipe(Effect.catch(() => Effect.void))
188
217
 
189
218
  // Clean up tarball
190
- yield* fs.remove(tarballPath).pipe(Effect.catchAll(() => Effect.void))
219
+ yield* fs.remove(tarballPath).pipe(Effect.catch(() => Effect.void))
191
220
 
192
221
  yield* Console.log(`✅ Example "${exampleName}" created successfully at: ${destinationPath}`)
193
222
  })
194
223
 
195
- export const newProjectCommand = Cli.Command.make(
196
- 'new-project',
224
+ export const createCommand = Cli.Command.make(
225
+ 'create',
197
226
  {
198
- example: Cli.Options.text('example').pipe(
199
- Cli.Options.optional,
200
- Cli.Options.withDescription('Example name to create (bypasses interactive selection)'),
227
+ example: Cli.Flag.string('example').pipe(
228
+ Cli.Flag.optional,
229
+ Cli.Flag.withDescription('Example name to create (bypasses interactive selection)'),
201
230
  ),
202
- branch: Cli.Options.text('branch').pipe(
203
- Cli.Options.withDefault('dev'),
204
- Cli.Options.withDescription('Branch to fetch examples from'),
231
+ ref: Cli.Flag.string('ref').pipe(
232
+ Cli.Flag.withAlias('commit'),
233
+ Cli.Flag.withAlias('branch'),
234
+ Cli.Flag.withAlias('tag'),
235
+ Cli.Flag.withDefault('main'),
236
+ Cli.Flag.withDescription(
237
+ 'The name of the commit/branch/tag to fetch examples from. Pull requests refs must be fully-formed (e.g., `refs/pull/123/merge`).',
238
+ ),
205
239
  ),
206
- path: Cli.Args.text({ name: 'path' }).pipe(
207
- Cli.Args.optional,
208
- Cli.Args.withDescription('Destination path for the new project'),
240
+ path: Cli.Argument.string('path').pipe(
241
+ Cli.Argument.optional,
242
+ Cli.Argument.withDescription('Destination path for the new project'),
209
243
  ),
210
244
  },
211
245
  Effect.fn(function* ({
212
246
  example,
213
- branch,
247
+ ref,
214
248
  path,
215
249
  }: {
216
250
  example: Option.Option<string>
217
- branch: string
251
+ ref: string
218
252
  path: Option.Option<string>
219
253
  }) {
220
254
  yield* Effect.log('🚀 Creating new LiveStore project...')
221
255
 
222
256
  // Fetch available examples
223
- const examples = yield* fetchExamples(branch)
257
+ const examples = yield* fetchExamples(ref)
224
258
 
225
259
  if (examples.length === 0) {
226
260
  yield* Console.log('❌ No examples found in the repository')
@@ -232,10 +266,10 @@ export const newProjectCommand = Cli.Command.make(
232
266
  }
233
267
 
234
268
  // Select example (from CLI option or interactive prompt)
235
- const selectedExample = Option.isSome(example) ? example.value : yield* selectExample(examples)
269
+ const selectedExample = Option.isSome(example) === true ? example.value : yield* selectExample(examples)
236
270
 
237
271
  // Validate selected example exists
238
- if (!examples.includes(selectedExample)) {
272
+ if (examples.includes(selectedExample) === false) {
239
273
  yield* Console.log(`❌ Example "${selectedExample}" not found`)
240
274
  yield* Console.log(`Available examples: ${examples.join(', ')}`)
241
275
  return yield* new ExampleNotFoundError({
@@ -246,18 +280,51 @@ export const newProjectCommand = Cli.Command.make(
246
280
  }
247
281
 
248
282
  // Determine destination path
249
- const destinationPath = Option.isSome(path) ? nodePath.resolve(path.value) : nodePath.resolve(selectedExample)
283
+ const destinationPath =
284
+ Option.isSome(path) === true ? nodePath.resolve(path.value) : nodePath.resolve(selectedExample)
250
285
 
251
286
  // Download and extract the example
252
- yield* downloadExample(selectedExample, branch, destinationPath)
287
+ yield* downloadExample(selectedExample, ref, destinationPath)
288
+
289
+ // Detect available run script (dev or start) from the created project's package.json.
290
+ // Some examples use "dev" (web projects), others use "start" (Expo projects),
291
+ // and some have no run script at all (e.g., node-effect-cli).
292
+ const fs = yield* FileSystem.FileSystem
293
+ const packageJsonPath = nodePath.join(destinationPath, 'package.json')
294
+ const packageJsonContent = yield* fs.readFileString(packageJsonPath)
295
+ const runScript = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(PackageJsonScriptsSchema))(
296
+ packageJsonContent,
297
+ ).pipe(
298
+ Effect.map((pkg) => ('dev' in pkg.scripts ? ('dev' as const) : ('start' as const))),
299
+ Effect.orElseSucceed(() => undefined),
300
+ )
301
+
302
+ // Detect which package manager was used to invoke the CLI (via npm_config_user_agent).
303
+ // This ensures the "next steps" instructions match how the user ran the create command.
304
+ const pmResult = detectPackageManager()
253
305
 
254
- // Success message
255
306
  yield* Console.log('\n🎉 Project created successfully!')
256
307
  yield* Console.log(`📁 Location: ${destinationPath}`)
257
308
  yield* Console.log('\n📋 Next steps:')
258
309
  yield* Console.log(` cd ${nodePath.basename(destinationPath)}`)
259
- yield* Console.log(' pnpm install # Install dependencies')
260
- yield* Console.log(' pnpm dev # Start development server')
310
+
311
+ // Yarn is not recommended for LiveStore projects. When detected, show a warning
312
+ // and suggest using bun instead for the next steps.
313
+ if (pmResult._tag === 'unsupported') {
314
+ yield* Console.log(' bun install # Install dependencies (yarn is not recommended)')
315
+ if (runScript !== undefined) {
316
+ yield* Console.log(` bun ${runScript} # Start development server`)
317
+ }
318
+ yield* Console.log('\n⚠️ Yarn is not recommended for LiveStore projects.')
319
+ yield* Console.log(' We recommend using bun, pnpm, or npm instead.')
320
+ yield* Console.log(' The commands above use bun by default.')
321
+ } else {
322
+ const pm = pmResult.pm
323
+ yield* Console.log(` ${pmCommands.install[pm]} # Install dependencies`)
324
+ if (runScript !== undefined) {
325
+ yield* Console.log(` ${pmCommands.run[pm](runScript)} # Start development server`)
326
+ }
327
+ }
261
328
  yield* Console.log('\n💡 Tip: Run `git init` if you want to initialize version control')
262
329
  }),
263
330
  )
@@ -28,9 +28,9 @@ LiveStore implements distributed systems principles from Martin Kleppmann's rese
28
28
  \`\`\`typescript
29
29
  // Materialized views from event log
30
30
  const materializers = State.SQLite.materializers(events, {
31
- 'TodoCreated': ({ id, text, createdAt }) =>
31
+ 'TodoCreated': ({ id, text, createdAt }) =>
32
32
  tables.todos.insert({ id, text, completed: false, createdAt }),
33
- 'TodoCompleted': ({ id }) =>
33
+ 'TodoCompleted': ({ id }) =>
34
34
  tables.todos.update({ completed: true }).where({ id })
35
35
  })
36
36
  \`\`\`
@@ -46,7 +46,7 @@ const materializers = State.SQLite.materializers(events, {
46
46
  const events = {
47
47
  todoCompleted: Events.synced({
48
48
  name: 'v1.TodoCompleted',
49
- schema: Schema.Struct({
49
+ schema: Schema.Struct({
50
50
  id: Schema.String,
51
51
  completedAt: Schema.Date // For conflict resolution
52
52
  })
@@ -149,7 +149,7 @@ await testStore.replay([
149
149
 
150
150
  ### Developer Tools
151
151
  - **Event Inspector**: Real-time event log visualization
152
- - **Query Profiler**: Performance analysis for slow queries
152
+ - **Query Profiler**: Performance analysis for slow queries
153
153
  - **Sync Monitor**: Network synchronization health dashboard
154
154
  - **Schema Explorer**: Interactive database schema browsing
155
155
 
@@ -36,13 +36,13 @@ export const tables = {
36
36
  createdAt: State.SQLite.integer({ schema: Schema.DateFromNumber }),
37
37
  },
38
38
  }),
39
-
39
+
40
40
  // Client-only state (not synced)
41
41
  uiState: State.SQLite.clientDocument({
42
42
  name: 'uiState',
43
- schema: Schema.Struct({
44
- newTodoText: Schema.String,
45
- filter: Schema.Literal('all', 'active', 'completed')
43
+ schema: Schema.Struct({
44
+ newTodoText: Schema.String,
45
+ filter: Schema.Literal('all', 'active', 'completed')
46
46
  }),
47
47
  default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } },
48
48
  }),
@@ -52,10 +52,10 @@ export const tables = {
52
52
  export const events = {
53
53
  todoCreated: Events.synced({
54
54
  name: 'v1.TodoCreated',
55
- schema: Schema.Struct({
56
- id: Schema.String,
55
+ schema: Schema.Struct({
56
+ id: Schema.String,
57
57
  text: Schema.String,
58
- createdAt: Schema.Date
58
+ createdAt: Schema.Date
59
59
  }),
60
60
  }),
61
61
  todoCompleted: Events.synced({
@@ -70,23 +70,23 @@ export const events = {
70
70
  name: 'v1.TodoDeleted',
71
71
  schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
72
72
  }),
73
-
73
+
74
74
  // UI state events (local only)
75
75
  uiStateSet: tables.uiState.set,
76
76
  }
77
77
 
78
78
  // Materializers map events to state changes
79
79
  const materializers = State.SQLite.materializers(events, {
80
- 'v1.TodoCreated': ({ id, text, createdAt }) =>
80
+ 'v1.TodoCreated': ({ id, text, createdAt }) =>
81
81
  tables.todos.insert({ id, text, completed: false, createdAt }),
82
-
83
- 'v1.TodoCompleted': ({ id }) =>
82
+
83
+ 'v1.TodoCompleted': ({ id }) =>
84
84
  tables.todos.update({ completed: true }).where({ id }),
85
-
86
- 'v1.TodoUncompleted': ({ id }) =>
85
+
86
+ 'v1.TodoUncompleted': ({ id }) =>
87
87
  tables.todos.update({ completed: false }).where({ id }),
88
-
89
- 'v1.TodoDeleted': ({ id, deletedAt }) =>
88
+
89
+ 'v1.TodoDeleted': ({ id, deletedAt }) =>
90
90
  tables.todos.update({ deletedAt }).where({ id }),
91
91
  })
92
92
 
@@ -144,28 +144,28 @@ const TodoApp: React.FC = () => {
144
144
  const activeTodos = useLiveQuery(activeTodos$)
145
145
  const completedTodos = useLiveQuery(completedTodos$)
146
146
  const uiState = useLiveQuery(uiState$)
147
-
147
+
148
148
  const createTodo = (text: string) => {
149
149
  const id = crypto.randomUUID()
150
150
  dispatchEvent(events.todoCreated({ id, text, createdAt: new Date() }))
151
151
  }
152
-
152
+
153
153
  const toggleTodo = (id: string, completed: boolean) => {
154
154
  dispatchEvent(
155
- completed
155
+ completed
156
156
  ? events.todoCompleted({ id })
157
157
  : events.todoUncompleted({ id })
158
158
  )
159
159
  }
160
-
160
+
161
161
  const deleteTodo = (id: string) => {
162
162
  dispatchEvent(events.todoDeleted({ id, deletedAt: new Date() }))
163
163
  }
164
-
164
+
165
165
  return (
166
166
  <div className="todo-app">
167
167
  <h1>Local-First Todos</h1>
168
-
168
+
169
169
  {/* Add todo form */}
170
170
  <form onSubmit={(e) => {
171
171
  e.preventDefault()
@@ -179,13 +179,13 @@ const TodoApp: React.FC = () => {
179
179
  <input name="todo" placeholder="What needs to be done?" />
180
180
  <button type="submit">Add Todo</button>
181
181
  </form>
182
-
182
+
183
183
  {/* Active todos */}
184
184
  <div className="todo-list">
185
185
  <h2>Active ({activeTodos.length})</h2>
186
186
  {activeTodos.map(todo => (
187
187
  <div key={todo.id} className="todo-item">
188
- <input
188
+ <input
189
189
  type="checkbox"
190
190
  checked={false}
191
191
  onChange={() => toggleTodo(todo.id, true)}
@@ -195,13 +195,13 @@ const TodoApp: React.FC = () => {
195
195
  </div>
196
196
  ))}
197
197
  </div>
198
-
198
+
199
199
  {/* Completed todos */}
200
200
  <div className="todo-list">
201
201
  <h2>Completed ({completedTodos.length})</h2>
202
202
  {completedTodos.map(todo => (
203
203
  <div key={todo.id} className="todo-item completed">
204
- <input
204
+ <input
205
205
  type="checkbox"
206
206
  checked={true}
207
207
  onChange={() => toggleTodo(todo.id, false)}
@@ -225,16 +225,16 @@ export default TodoApp
225
225
  <template>
226
226
  <div class="todo-app">
227
227
  <h1>Local-First Todos</h1>
228
-
228
+
229
229
  <form @submit.prevent="createTodo">
230
230
  <input v-model="newTodoText" placeholder="What needs to be done?" />
231
231
  <button type="submit">Add Todo</button>
232
232
  </form>
233
-
233
+
234
234
  <div class="todo-list">
235
235
  <h2>Active ({{ activeTodos.length }})</h2>
236
236
  <div v-for="todo in activeTodos" :key="todo.id" class="todo-item">
237
- <input
237
+ <input
238
238
  type="checkbox"
239
239
  :checked="false"
240
240
  @change="toggleTodo(todo.id, true)"
@@ -258,10 +258,10 @@ const newTodoText = ref('')
258
258
  const createTodo = () => {
259
259
  if (newTodoText.value.trim()) {
260
260
  const id = crypto.randomUUID()
261
- dispatchEvent(events.todoCreated({
262
- id,
263
- text: newTodoText.value.trim(),
264
- createdAt: new Date()
261
+ dispatchEvent(events.todoCreated({
262
+ id,
263
+ text: newTodoText.value.trim(),
264
+ createdAt: new Date()
265
265
  }))
266
266
  newTodoText.value = ''
267
267
  }
@@ -269,7 +269,7 @@ const createTodo = () => {
269
269
 
270
270
  const toggleTodo = (id: string, completed: boolean) => {
271
271
  dispatchEvent(
272
- completed
272
+ completed
273
273
  ? events.todoCompleted({ id })
274
274
  : events.todoUncompleted({ id })
275
275
  )
@@ -332,7 +332,7 @@ Multiple users can collaborate in real-time. Conflicts are automatically resolve
332
332
  \`\`\`typescript
333
333
  // Custom conflict resolution
334
334
  const materializers = State.SQLite.materializers(events, {
335
- 'v1.TodoTextChanged': ({ id, text, editedAt }) =>
335
+ 'v1.TodoTextChanged': ({ id, text, editedAt }) =>
336
336
  // Use timestamp for conflict resolution
337
337
  tables.todos
338
338
  .update({ text, editedAt })
@@ -350,18 +350,18 @@ import { schema, events } from './schema.js'
350
350
 
351
351
  test('creating and completing todos', async () => {
352
352
  const store = createTestStore(schema)
353
-
353
+
354
354
  // Dispatch events
355
355
  await store.dispatch([
356
356
  events.todoCreated({ id: '1', text: 'Test todo', createdAt: new Date() }),
357
357
  events.todoCompleted({ id: '1' })
358
358
  ])
359
-
359
+
360
360
  // Query final state
361
361
  const completedTodos = await store.query(
362
362
  tables.todos.select().where({ completed: true })
363
363
  )
364
-
364
+
365
365
  expect(completedTodos).toHaveLength(1)
366
366
  expect(completedTodos[0].text).toBe('Test todo')
367
367
  })
@@ -26,7 +26,7 @@ LiveStore is a production-ready local-first data platform that combines the imme
26
26
 
27
27
  ### 🔐 End-to-End Type Safety
28
28
  - **Schema-first development** with Effect-based validation
29
- - **Compile-time query validation** prevents runtime errors
29
+ - **Compile-time query validation** prevents runtime errors
30
30
  - **Automatic TypeScript generation** from schema definitions
31
31
  - **Runtime safety** with comprehensive input validation
32
32
 
@@ -43,7 +43,7 @@ LiveStore is a production-ready local-first data platform that combines the imme
43
43
  - **Design tools** with multiplayer editing capabilities
44
44
  - **Chat applications** with offline message queuing
45
45
 
46
- ### 📱 Mobile-First Applications
46
+ ### 📱 Mobile-First Applications
47
47
  - **Field service apps** that work in areas with poor connectivity
48
48
  - **Healthcare applications** with sensitive data that must stay local
49
49
  - **Educational apps** for students in low-connectivity environments
@@ -61,16 +61,16 @@ LiveStore is a production-ready local-first data platform that combines the imme
61
61
  graph TB
62
62
  UI[UI Framework] --> Queries[Reactive Queries]
63
63
  UI --> Events[Event Dispatch]
64
-
64
+
65
65
  Events --> EventLog[Event Log]
66
66
  EventLog --> Materializers[Materializers]
67
67
  Materializers --> SQLite[SQLite Database]
68
68
  Queries --> SQLite
69
-
69
+
70
70
  EventLog --> Sync[Sync Engine]
71
71
  Sync --> Network[Network Layer]
72
72
  Network --> Server[Sync Server]
73
-
73
+
74
74
  Server --> OtherClients[Other Clients]
75
75
  OtherClients --> Server
76
76
  Server --> Network
@@ -95,7 +95,7 @@ Unlike traditional ORMs that hide change tracking, LiveStore makes all state cha
95
95
  ### Sophisticated Conflict Resolution
96
96
  LiveStore handles conflicts intelligently using multiple strategies:
97
97
  - **Last-Write-Wins**: Simple timestamp-based resolution
98
- - **Semantic Merging**: Application-specific conflict resolution logic
98
+ - **Semantic Merging**: Application-specific conflict resolution logic
99
99
  - **Operational Transforms**: For real-time collaborative text editing
100
100
  - **CRDT Integration**: Conflict-free data types for specific use cases
101
101