@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,318 @@
1
+ import { Schema, Tool, Toolkit } from '@livestore/utils/effect'
2
+
3
+ import { coachTool } from './mcp-coach.ts'
4
+
5
+ export const livestoreToolkit = Toolkit.make(
6
+ coachTool,
7
+
8
+ Tool.make('livestore_generate_schema', {
9
+ description:
10
+ 'Generate a LiveStore schema for a specific use case. Choose from predefined types (todo, blog, social, ecommerce) or request a custom schema by providing a description.',
11
+ parameters: {
12
+ schemaType: Schema.String.annotations({
13
+ description: "Schema type: 'todo', 'blog', 'social', 'ecommerce', or 'custom'",
14
+ }),
15
+ customDescription: Schema.optional(
16
+ Schema.String.annotations({
17
+ description:
18
+ "For custom schemas: describe your data model needs (e.g., 'user management system with roles and permissions')",
19
+ }),
20
+ ),
21
+ },
22
+ success: Schema.Struct({
23
+ schemaCode: Schema.String.annotations({ description: 'The generated LiveStore schema TypeScript code' }),
24
+ explanation: Schema.String.annotations({ description: 'Brief explanation of the schema structure' }),
25
+ }),
26
+ }),
27
+
28
+ Tool.make('livestore_get_example_schema', {
29
+ description:
30
+ 'Get a complete example LiveStore schema with TypeScript code. Returns ready-to-use schema definitions for common application types.',
31
+ parameters: {
32
+ type: Schema.String.annotations({ description: "Example type: 'todo', 'blog', 'social', or 'ecommerce'" }),
33
+ },
34
+ success: Schema.Struct({
35
+ schemaCode: Schema.String.annotations({ description: 'The complete LiveStore schema code' }),
36
+ description: Schema.String.annotations({ description: 'Description of what this schema models' }),
37
+ }),
38
+ })
39
+ .annotate(Tool.Readonly, true)
40
+ .annotate(Tool.Destructive, false),
41
+
42
+ Tool.make('livestore_instance_connect', {
43
+ description: `Connect a LiveStore instance (one active per MCP session) by dynamically importing a user module that exports a LiveStore \`schema\` and a \`syncBackend\` factory (and optionally \`syncPayload\`).
44
+ Notes:
45
+ - Only one instance can be active at a time; calling connect again shuts down and replaces the previous instance.
46
+ - 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.
47
+ - \`configPath\` is resolved relative to the current working directory.
48
+ - \`syncBackend\` must be a function (factory) that returns a backend; \`syncPayload\` must be JSON-serializable.
49
+
50
+ Module contract (generic example):
51
+ \`\`\`ts
52
+ // Choose any supported sync provider for your deployment
53
+ import { makeWsSync } from '@livestore/sync-cf/client' // or your own provider
54
+
55
+ // Export your app's schema
56
+ export { schema } from './src/livestore/schema.ts'
57
+
58
+ // Provide a sync backend (e.g., WebSocket). Configure via env in practice.
59
+ export const syncBackend = makeWsSync({ url: process.env.LIVESTORE_SYNC_URL ?? 'ws://localhost:8787' })
60
+
61
+ // Optionally, pass an auth payload for your backend (must be JSON-serializable)
62
+ export const syncPayload = { authToken: process.env.LIVESTORE_SYNC_AUTH_TOKEN ?? 'insecure-token-change-me' }
63
+ \`\`\`
64
+
65
+ Connect parameters:
66
+ {
67
+ "configPath": "livestore-cli.config.ts",
68
+ "storeId": "<store-id>"
69
+ }
70
+
71
+ Optional identifiers to group client state on the server:
72
+ {
73
+ "configPath": "livestore-cli.config.ts",
74
+ "storeId": "<store-id>",
75
+ "clientId": "<client-id>",
76
+ "sessionId": "<session-id>"
77
+ }
78
+
79
+ Returns on success:
80
+ {
81
+ "storeId": "<store-id>",
82
+ "clientId": "<client-id>",
83
+ "sessionId": "<session-id>",
84
+ "schemaInfo": {
85
+ "tableNames": ["<table-1>", "<table-2>", "..."],
86
+ "eventNames": ["<event-name-1>", "<event-name-2>", "..."]
87
+ }
88
+ }`,
89
+ parameters: {
90
+ configPath: Schema.String.annotations({
91
+ description: 'Path to a module that exports named variables: schema and syncBackend',
92
+ }),
93
+ storeId: Schema.String.annotations({ description: 'Required store id for the LiveStore instance.' }),
94
+ clientId: Schema.optional(
95
+ Schema.String.annotations({ description: 'Optional client id for the LiveStore instance.' }),
96
+ ),
97
+ sessionId: Schema.optional(
98
+ Schema.String.annotations({ description: 'Optional session id for the LiveStore instance.' }),
99
+ ),
100
+ },
101
+ success: Schema.Struct({
102
+ storeId: Schema.String,
103
+ clientId: Schema.String,
104
+ sessionId: Schema.String,
105
+ schemaInfo: Schema.Struct({
106
+ tableNames: Schema.Array(Schema.String).annotations({
107
+ description: 'Non-system table names in the connected schema',
108
+ }),
109
+ eventNames: Schema.Array(Schema.String).annotations({
110
+ description: 'Canonical event names defined by the connected schema',
111
+ }),
112
+ }),
113
+ }),
114
+ }),
115
+
116
+ Tool.make('livestore_instance_query', {
117
+ description: `Execute a raw SQL query against the connected client's local database (read-only).
118
+ Notes:
119
+ - The client store runs SQLite under the hood; use valid SQLite syntax.
120
+ - Inspect your exported \`schema\` to learn table/column names.
121
+ - \`bindValues\` must be an array (positional "?") or a record (named "$key"); do not pass a stringified JSON value.
122
+
123
+ Examples (positional binds):
124
+ {
125
+ "sql": "SELECT * FROM my_table WHERE userId = ? LIMIT 5",
126
+ "bindValues": ["u1"]
127
+ }
128
+
129
+ Examples (named binds):
130
+ {
131
+ "sql": "SELECT * FROM my_table WHERE userId = $userId LIMIT 5",
132
+ "bindValues": { "userId": "u1" }
133
+ }
134
+
135
+ Returns on success:
136
+ {
137
+ "rows": [{ "col": "value" }],
138
+ "rowCount": 1
139
+ }`,
140
+ parameters: {
141
+ sql: Schema.String.annotations({ description: 'The SQL query to execute' }),
142
+ bindValues: Schema.Union(
143
+ Schema.Array(Schema.JsonValue),
144
+ Schema.Record({ key: Schema.String, value: Schema.JsonValue }),
145
+ ).annotations({
146
+ description: 'Bind values for the SQL query (array or record). Record keys must not start with $.',
147
+ }),
148
+ },
149
+ success: Schema.Struct({
150
+ rows: Schema.Array(Schema.Record({ key: Schema.String, value: Schema.JsonValue })),
151
+ rowCount: Schema.Number,
152
+ }),
153
+ }).annotate(Tool.Destructive, false),
154
+
155
+ Tool.make('livestore_instance_commit_events', {
156
+ description: `Commit one or more events defined by your connected LiveStore schema.
157
+ Notes:
158
+ - The \`name\` must match the event's canonical name declared in your schema (e.g., "v1.UserRegistered").
159
+ - \`args\` must be a JSON object matching the event schema; do not pass a stringified JSON.
160
+ - Use your app's own event names and fields; the example below is generic.
161
+ - Date fields typically accept ISO 8601 strings (e.g., "2024-01-01T00:00:00.000Z").
162
+
163
+ Example parameters:
164
+ {
165
+ "events": [
166
+ {
167
+ "name": "v1.EntityCreated",
168
+ "args": {
169
+ "id": "e1",
170
+ "title": "Hello World",
171
+ "createdAt": "2024-01-01T00:00:00.000Z"
172
+ }
173
+ }
174
+ ]
175
+ }
176
+
177
+ Returns on success:
178
+ { "committed": 1 }`,
179
+ parameters: {
180
+ events: Schema.Array(
181
+ Schema.Struct({
182
+ name: Schema.String.annotations({ description: 'The name of the event' }),
183
+ args: Schema.JsonValue.annotations({
184
+ description: 'The arguments for the event as a non-stringified JSON value',
185
+ }),
186
+ }),
187
+ ),
188
+ },
189
+ success: Schema.Struct({ committed: Schema.Number }),
190
+ }).annotate(Tool.Destructive, true),
191
+
192
+ Tool.make('livestore_instance_status', {
193
+ description: `Report the LiveStore runtime status for the current MCP session.
194
+
195
+ Returns when connected:
196
+ {
197
+ "_tag": "connected",
198
+ "storeId": "<store-id>",
199
+ "clientId": "<client-id>",
200
+ "sessionId": "<session-id>",
201
+ "tableCounts": { "<table>": 123 }
202
+ }
203
+
204
+ Returns when not connected:
205
+ {
206
+ "_tag": "disconnected"
207
+ }`,
208
+ parameters: {},
209
+ success: Schema.Union(
210
+ Schema.TaggedStruct('connected', {
211
+ storeId: Schema.String,
212
+ clientId: Schema.String,
213
+ sessionId: Schema.String,
214
+ tableCounts: Schema.Record({ key: Schema.String, value: Schema.Number }).annotations({
215
+ description: 'Tables in the LiveStore instance with their row count',
216
+ }),
217
+ }),
218
+ Schema.TaggedStruct('disconnected', {}),
219
+ ),
220
+ }).annotate(Tool.Readonly, true),
221
+
222
+ Tool.make('livestore_instance_disconnect', {
223
+ description: `Disconnect the current LiveStore instance and release resources.
224
+
225
+ Example success:
226
+ { "_tag": "disconnected" }`,
227
+ parameters: {},
228
+ success: Schema.TaggedStruct('disconnected', {}),
229
+ }),
230
+
231
+ Tool.make('livestore_sync_export', {
232
+ description: `Export all events from a sync backend to JSON data.
233
+
234
+ This tool connects directly to the sync backend (without creating a full LiveStore instance) and pulls all events. Useful for backup, migration, and debugging.
235
+
236
+ Module contract (same as livestore_instance_connect):
237
+ \`\`\`ts
238
+ export { schema } from './src/livestore/schema.ts'
239
+ export const syncBackend = makeWsSync({ url: process.env.LIVESTORE_SYNC_URL ?? 'ws://localhost:8787' })
240
+ export const syncPayload = { authToken: process.env.LIVESTORE_SYNC_AUTH_TOKEN }
241
+ \`\`\`
242
+
243
+ Example parameters:
244
+ {
245
+ "configPath": "livestore-cli.config.ts",
246
+ "storeId": "my-store"
247
+ }
248
+
249
+ Returns on success:
250
+ {
251
+ "storeId": "my-store",
252
+ "eventCount": 127,
253
+ "exportedAt": "2024-01-15T10:30:00.000Z",
254
+ "data": { "version": 1, "storeId": "my-store", ... }
255
+ }`,
256
+ parameters: {
257
+ configPath: Schema.String.annotations({
258
+ description: 'Path to a module that exports schema and syncBackend',
259
+ }),
260
+ storeId: Schema.String.annotations({ description: 'Store identifier' }),
261
+ clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-export)' })),
262
+ },
263
+ success: Schema.Struct({
264
+ storeId: Schema.String,
265
+ eventCount: Schema.Number,
266
+ exportedAt: Schema.String,
267
+ data: Schema.JsonValue.annotations({ description: 'The export file data (can be saved or passed to import)' }),
268
+ }),
269
+ }).annotate(Tool.Readonly, true),
270
+
271
+ Tool.make('livestore_sync_import', {
272
+ description: `Import events from export data to a sync backend.
273
+
274
+ This tool connects directly to the sync backend and pushes events. The sync backend must be empty.
275
+
276
+ Example parameters:
277
+ {
278
+ "configPath": "livestore-cli.config.ts",
279
+ "storeId": "my-store",
280
+ "data": { "version": 1, "storeId": "my-store", "events": [...] }
281
+ }
282
+
283
+ With options:
284
+ {
285
+ "configPath": "livestore-cli.config.ts",
286
+ "storeId": "my-store",
287
+ "data": { ... },
288
+ "force": true, // Import even if store ID doesn't match
289
+ "dryRun": true // Validate without importing
290
+ }
291
+
292
+ Returns on success:
293
+ {
294
+ "storeId": "my-store",
295
+ "eventCount": 127,
296
+ "dryRun": false
297
+ }`,
298
+ parameters: {
299
+ configPath: Schema.String.annotations({
300
+ description: 'Path to a module that exports schema and syncBackend',
301
+ }),
302
+ storeId: Schema.String.annotations({ description: 'Store identifier' }),
303
+ clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-import)' })),
304
+ data: Schema.JsonValue.annotations({
305
+ description: 'The export data to import (from livestore_sync_export or a file)',
306
+ }),
307
+ force: Schema.optional(
308
+ Schema.Boolean.annotations({ description: 'Force import even if store ID does not match' }),
309
+ ),
310
+ dryRun: Schema.optional(Schema.Boolean.annotations({ description: 'Validate without actually importing' })),
311
+ },
312
+ success: Schema.Struct({
313
+ storeId: Schema.String,
314
+ eventCount: Schema.Number,
315
+ dryRun: Schema.Boolean,
316
+ }),
317
+ }).annotate(Tool.Destructive, true),
318
+ )
@@ -1,5 +1,6 @@
1
1
  import { Effect, Layer, Logger, McpServer } from '@livestore/utils/effect'
2
2
  import { Cli, PlatformNode } from '@livestore/utils/node'
3
+
3
4
  import { architectureContent } from '../mcp-content/architecture.ts'
4
5
  import { featuresContent } from '../mcp-content/features.ts'
5
6
  import { gettingStartedContent } from '../mcp-content/getting-started.ts'
@@ -9,9 +10,9 @@ import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
9
10
  import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
10
11
  import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
11
12
  import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
12
-
13
+ import { toolHandlers } from './mcp-tool-handlers.ts'
13
14
  // Tools imports
14
- import { livestoreToolkit, toolHandlers } from './mcp-tools.ts'
15
+ import { livestoreToolkit } from './mcp-tools-defs.ts'
15
16
 
16
17
  const LivestoreResources = Layer.mergeAll(
17
18
  McpServer.resource({
@@ -1,5 +1,7 @@
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
6
  Command,
5
7
  Console,
@@ -12,6 +14,8 @@ import {
12
14
  } from '@livestore/utils/effect'
13
15
  import { Cli } from '@livestore/utils/node'
14
16
 
17
+ import { detectPackageManager, pmCommands } from '../package-manager.ts'
18
+
15
19
  // Schema for GitHub API response
16
20
  const GitHubContentSchema = Schema.Struct({
17
21
  name: Schema.String,
@@ -22,50 +26,65 @@ const GitHubContentSchema = Schema.Struct({
22
26
 
23
27
  const GitHubContentsResponseSchema = Schema.Array(GitHubContentSchema)
24
28
 
29
+ const githubRequest = (url: string) => {
30
+ const request = HttpClientRequest.get(url).pipe(HttpClientRequest.setHeader('accept', 'application/vnd.github+json'))
31
+ const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
32
+ return token === undefined ? request : request.pipe(HttpClientRequest.setHeader('authorization', `Bearer ${token}`))
33
+ }
34
+
35
+ /** Schema for parsing package.json scripts (dev or start) */
36
+ const PackageJsonScriptsSchema = Schema.Struct({
37
+ scripts: Schema.Union(Schema.Struct({ dev: Schema.String }), Schema.Struct({ start: Schema.String })),
38
+ })
39
+
25
40
  // Error types
26
- export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>()('ExampleNotFoundError', {
41
+ export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>('~@livestore/cli/ExampleNotFoundError')('ExampleNotFoundError', {
27
42
  exampleName: Schema.String,
28
43
  availableExamples: Schema.Array(Schema.String),
29
44
  message: Schema.String,
30
45
  }) {}
31
46
 
32
- export class NetworkError extends Schema.TaggedError<NetworkError>()('NetworkError', {
47
+ export class NetworkError extends Schema.TaggedError<NetworkError>('~@livestore/cli/NetworkError')('NetworkError', {
33
48
  cause: Schema.Unknown,
34
49
  message: Schema.String,
35
50
  }) {}
36
51
 
37
- export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>()('DirectoryExistsError', {
52
+ export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>('~@livestore/cli/DirectoryExistsError')('DirectoryExistsError', {
38
53
  path: Schema.String,
39
54
  message: Schema.String,
40
55
  }) {}
41
56
 
57
+ export class NoExamplesError extends Schema.TaggedError<NoExamplesError>('~@livestore/cli/NoExamplesError')('NoExamplesError', {
58
+ message: Schema.String,
59
+ }) {}
60
+
42
61
  // Fetch available examples from GitHub
43
- const fetchExamples = (branch: string) =>
62
+ const fetchExamples = (ref: string) =>
44
63
  Effect.gen(function* () {
45
- const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${branch}`
64
+ const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${ref}`
46
65
 
47
- yield* Effect.log(`Fetching examples from branch: ${branch}`)
66
+ yield* Effect.log(`Fetching examples from ref: ${ref}`)
48
67
 
49
- const request = HttpClientRequest.get(url)
68
+ const request = githubRequest(url)
50
69
  const response = yield* HttpClient.execute(request).pipe(
51
70
  Effect.scoped,
52
71
  Effect.catchAll(
53
72
  (error) =>
54
73
  new NetworkError({
55
74
  cause: error,
56
- message: `Failed to fetch examples from GitHub: ${error}`,
75
+ message: `Failed to fetch examples from GitHub: ${String(error)}`,
57
76
  }),
58
77
  ),
59
78
  )
60
79
 
61
80
  const responseText = yield* response.text
62
81
 
63
- const examples = yield* Schema.decodeUnknown(GitHubContentsResponseSchema)(JSON.parse(responseText)).pipe(
82
+ const examples = yield* Schema.decodeUnknown(Schema.parseJson(GitHubContentsResponseSchema))(responseText).pipe(
64
83
  Effect.catchAll(
65
84
  (error) =>
66
85
  new NetworkError({
67
86
  cause: error,
68
- message: `Failed to parse GitHub API response: ${error}`,
87
+ message: `Failed to parse GitHub API response: ${String(error)}`,
69
88
  }),
70
89
  ),
71
90
  )
@@ -73,7 +92,7 @@ const fetchExamples = (branch: string) =>
73
92
  const exampleNames = examples
74
93
  .filter((item) => item.type === 'dir')
75
94
  .map((item) => item.name)
76
- .sort()
95
+ .toSorted()
77
96
 
78
97
  yield* Effect.log(`Found ${exampleNames.length} examples: ${exampleNames.join(', ')}`)
79
98
 
@@ -84,7 +103,7 @@ const fetchExamples = (branch: string) =>
84
103
  const selectExample = (examples: string[]) =>
85
104
  Effect.gen(function* () {
86
105
  if (examples.length === 0) {
87
- return yield* Effect.fail(new Error('No examples available'))
106
+ return yield* new NoExamplesError({ message: 'No examples available' })
88
107
  }
89
108
 
90
109
  const prompt = Cli.Prompt.select({
@@ -100,16 +119,16 @@ const selectExample = (examples: string[]) =>
100
119
  })
101
120
 
102
121
  // Download and extract example using tiged approach
103
- const downloadExample = (exampleName: string, branch: string, destinationPath: string) =>
122
+ const downloadExample = (exampleName: string, ref: string, destinationPath: string) =>
104
123
  Effect.gen(function* () {
105
- yield* Console.log(`📥 Downloading example "${exampleName}" from branch "${branch}"...`)
124
+ yield* Console.log(`📥 Downloading example "${exampleName}" from ref "${ref}"...`)
106
125
 
107
126
  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}`
127
+ const tarballPath = nodePath.join(tempDir, `livestore-${sluggify(ref)}-${Date.now()}.tar.gz`)
128
+ const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${ref}`
110
129
 
111
130
  // Download tarball directly
112
- const request = HttpClientRequest.get(tarballUrl)
131
+ const request = githubRequest(tarballUrl)
113
132
 
114
133
  const response = yield* HttpClient.execute(request).pipe(
115
134
  Effect.scoped,
@@ -117,7 +136,7 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
117
136
  (error) =>
118
137
  new NetworkError({
119
138
  cause: error,
120
- message: `Failed to download tarball: ${error}`,
139
+ message: `Failed to download tarball: ${String(error)}`,
121
140
  }),
122
141
  ),
123
142
  )
@@ -142,7 +161,7 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
142
161
  (error) =>
143
162
  new NetworkError({
144
163
  cause: error,
145
- message: `Failed to extract tarball: ${error}`,
164
+ message: `Failed to extract tarball: ${String(error)}`,
146
165
  }),
147
166
  ),
148
167
  )
@@ -163,7 +182,7 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
163
182
  // Check if the example exists
164
183
  const exampleExists = yield* fs.exists(exampleSourcePath)
165
184
 
166
- if (!exampleExists) {
185
+ if (exampleExists === false) {
167
186
  return yield* new ExampleNotFoundError({
168
187
  exampleName,
169
188
  availableExamples: [],
@@ -178,7 +197,7 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
178
197
  (error) =>
179
198
  new NetworkError({
180
199
  cause: error,
181
- message: `Failed to copy example files: ${error}`,
200
+ message: `Failed to copy example files: ${String(error)}`,
182
201
  }),
183
202
  ),
184
203
  )
@@ -192,16 +211,21 @@ const downloadExample = (exampleName: string, branch: string, destinationPath: s
192
211
  yield* Console.log(`✅ Example "${exampleName}" created successfully at: ${destinationPath}`)
193
212
  })
194
213
 
195
- export const newProjectCommand = Cli.Command.make(
196
- 'new-project',
214
+ export const createCommand = Cli.Command.make(
215
+ 'create',
197
216
  {
198
217
  example: Cli.Options.text('example').pipe(
199
218
  Cli.Options.optional,
200
219
  Cli.Options.withDescription('Example name to create (bypasses interactive selection)'),
201
220
  ),
202
- branch: Cli.Options.text('branch').pipe(
203
- Cli.Options.withDefault('dev'),
204
- Cli.Options.withDescription('Branch to fetch examples from'),
221
+ ref: Cli.Options.text('ref').pipe(
222
+ Cli.Options.withAlias('commit'),
223
+ Cli.Options.withAlias('branch'),
224
+ Cli.Options.withAlias('tag'),
225
+ Cli.Options.withDefault('main'),
226
+ Cli.Options.withDescription(
227
+ 'The name of the commit/branch/tag to fetch examples from. Pull requests refs must be fully-formed (e.g., `refs/pull/123/merge`).',
228
+ ),
205
229
  ),
206
230
  path: Cli.Args.text({ name: 'path' }).pipe(
207
231
  Cli.Args.optional,
@@ -210,17 +234,17 @@ export const newProjectCommand = Cli.Command.make(
210
234
  },
211
235
  Effect.fn(function* ({
212
236
  example,
213
- branch,
237
+ ref,
214
238
  path,
215
239
  }: {
216
240
  example: Option.Option<string>
217
- branch: string
241
+ ref: string
218
242
  path: Option.Option<string>
219
243
  }) {
220
244
  yield* Effect.log('🚀 Creating new LiveStore project...')
221
245
 
222
246
  // Fetch available examples
223
- const examples = yield* fetchExamples(branch)
247
+ const examples = yield* fetchExamples(ref)
224
248
 
225
249
  if (examples.length === 0) {
226
250
  yield* Console.log('❌ No examples found in the repository')
@@ -232,10 +256,10 @@ export const newProjectCommand = Cli.Command.make(
232
256
  }
233
257
 
234
258
  // Select example (from CLI option or interactive prompt)
235
- const selectedExample = Option.isSome(example) ? example.value : yield* selectExample(examples)
259
+ const selectedExample = Option.isSome(example) === true ? example.value : yield* selectExample(examples)
236
260
 
237
261
  // Validate selected example exists
238
- if (!examples.includes(selectedExample)) {
262
+ if (examples.includes(selectedExample) === false) {
239
263
  yield* Console.log(`❌ Example "${selectedExample}" not found`)
240
264
  yield* Console.log(`Available examples: ${examples.join(', ')}`)
241
265
  return yield* new ExampleNotFoundError({
@@ -246,18 +270,48 @@ export const newProjectCommand = Cli.Command.make(
246
270
  }
247
271
 
248
272
  // Determine destination path
249
- const destinationPath = Option.isSome(path) ? nodePath.resolve(path.value) : nodePath.resolve(selectedExample)
273
+ const destinationPath = Option.isSome(path) === true ? nodePath.resolve(path.value) : nodePath.resolve(selectedExample)
250
274
 
251
275
  // Download and extract the example
252
- yield* downloadExample(selectedExample, branch, destinationPath)
276
+ yield* downloadExample(selectedExample, ref, destinationPath)
277
+
278
+ // Detect available run script (dev or start) from the created project's package.json.
279
+ // Some examples use "dev" (web projects), others use "start" (Expo projects),
280
+ // and some have no run script at all (e.g., node-effect-cli).
281
+ const fs = yield* FileSystem.FileSystem
282
+ const packageJsonPath = nodePath.join(destinationPath, 'package.json')
283
+ const packageJsonContent = yield* fs.readFileString(packageJsonPath)
284
+ const runScript = yield* Schema.decodeUnknown(Schema.parseJson(PackageJsonScriptsSchema))(packageJsonContent).pipe(
285
+ Effect.map((pkg) => ('dev' in pkg.scripts ? ('dev' as const) : ('start' as const))),
286
+ Effect.orElseSucceed(() => undefined),
287
+ )
288
+
289
+ // Detect which package manager was used to invoke the CLI (via npm_config_user_agent).
290
+ // This ensures the "next steps" instructions match how the user ran the create command.
291
+ const pmResult = detectPackageManager()
253
292
 
254
- // Success message
255
293
  yield* Console.log('\n🎉 Project created successfully!')
256
294
  yield* Console.log(`📁 Location: ${destinationPath}`)
257
295
  yield* Console.log('\n📋 Next steps:')
258
296
  yield* Console.log(` cd ${nodePath.basename(destinationPath)}`)
259
- yield* Console.log(' pnpm install # Install dependencies')
260
- yield* Console.log(' pnpm dev # Start development server')
297
+
298
+ // Yarn is not recommended for LiveStore projects. When detected, show a warning
299
+ // and suggest using bun instead for the next steps.
300
+ if (pmResult._tag === 'unsupported') {
301
+ yield* Console.log(' bun install # Install dependencies (yarn is not recommended)')
302
+ if (runScript !== undefined) {
303
+ yield* Console.log(` bun ${runScript} # Start development server`)
304
+ }
305
+ yield* Console.log('\n⚠️ Yarn is not recommended for LiveStore projects.')
306
+ yield* Console.log(' We recommend using bun, pnpm, or npm instead.')
307
+ yield* Console.log(' The commands above use bun by default.')
308
+ } else {
309
+ const pm = pmResult.pm
310
+ yield* Console.log(` ${pmCommands.install[pm]} # Install dependencies`)
311
+ if (runScript !== undefined) {
312
+ yield* Console.log(` ${pmCommands.run[pm](runScript)} # Start development server`)
313
+ }
314
+ }
261
315
  yield* Console.log('\n💡 Tip: Run `git init` if you want to initialize version control')
262
316
  }),
263
317
  )