@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.
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/fixtures/mock-config.d.ts +63 -0
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-config.js +94 -0
- package/dist/__tests__/fixtures/mock-config.js.map +1 -0
- package/dist/__tests__/sync-operations.test.d.ts +2 -0
- package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
- package/dist/__tests__/sync-operations.test.js +167 -0
- package/dist/__tests__/sync-operations.test.js.map +1 -0
- package/dist/bin.js +6 -5
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +2 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
- package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
- package/dist/commands/import-export.d.ts +20 -0
- package/dist/commands/import-export.d.ts.map +1 -0
- package/dist/commands/import-export.js +135 -0
- package/dist/commands/import-export.js.map +1 -0
- package/dist/commands/mcp-coach.d.ts +21 -12
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +49 -53
- package/dist/commands/mcp-coach.js.map +1 -1
- package/dist/commands/mcp-protocols.d.ts +7 -0
- package/dist/commands/mcp-protocols.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.js +12 -0
- package/dist/commands/mcp-protocols.js.map +1 -0
- package/dist/commands/mcp-protocols.test.d.ts +2 -0
- package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
- package/dist/commands/mcp-protocols.test.js +60 -0
- package/dist/commands/mcp-protocols.test.js.map +1 -0
- package/dist/commands/mcp-stdio.test.d.ts +2 -0
- package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
- package/dist/commands/mcp-stdio.test.js +79 -0
- package/dist/commands/mcp-stdio.test.js.map +1 -0
- package/dist/commands/mcp-tool-handlers.d.ts +6 -0
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
- package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +80 -45
- package/dist/commands/mcp-tool-handlers.js.map +1 -0
- package/dist/commands/mcp-tools-defs.d.ts +134 -0
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.js +285 -0
- package/dist/commands/mcp-tools-defs.js.map +1 -0
- package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
- package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.test.js +11 -0
- package/dist/commands/mcp-tools-defs.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +1 -3
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +7 -5
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +22 -23
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +85 -44
- package/dist/commands/new-project.js.map +1 -1
- package/dist/mcp-content/architecture.d.ts +1 -1
- package/dist/mcp-content/architecture.d.ts.map +1 -1
- package/dist/mcp-content/architecture.js +3 -3
- package/dist/mcp-content/features.d.ts +1 -1
- package/dist/mcp-content/features.d.ts.map +1 -1
- package/dist/mcp-content/features.js +1 -1
- package/dist/mcp-content/getting-started.d.ts +1 -1
- package/dist/mcp-content/getting-started.d.ts.map +1 -1
- package/dist/mcp-content/getting-started.js +37 -37
- package/dist/mcp-content/overview.d.ts +1 -1
- package/dist/mcp-content/overview.d.ts.map +1 -1
- package/dist/mcp-content/overview.js +6 -6
- package/dist/mcp-content/schemas/blog.d.ts +1 -1
- package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
- package/dist/mcp-content/schemas/blog.js +25 -25
- package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
- package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
- package/dist/mcp-content/schemas/ecommerce.js +67 -67
- package/dist/mcp-content/schemas/social.d.ts +1 -1
- package/dist/mcp-content/schemas/social.d.ts.map +1 -1
- package/dist/mcp-content/schemas/social.js +48 -48
- package/dist/mcp-content/schemas/todo.d.ts +1 -1
- package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
- package/dist/mcp-content/schemas/todo.js +35 -35
- package/dist/mcp-runtime/runtime.d.ts +48 -0
- package/dist/mcp-runtime/runtime.d.ts.map +1 -0
- package/dist/mcp-runtime/runtime.js +107 -0
- package/dist/mcp-runtime/runtime.js.map +1 -0
- package/dist/module-loader.d.ts +22 -0
- package/dist/module-loader.d.ts.map +1 -0
- package/dist/module-loader.js +75 -0
- package/dist/module-loader.js.map +1 -0
- package/dist/package-manager.d.ts +38 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +33 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/package-manager.test.d.ts +2 -0
- package/dist/package-manager.test.d.ts.map +1 -0
- package/dist/package-manager.test.js +57 -0
- package/dist/package-manager.test.js.map +1 -0
- package/dist/sync-operations.d.ts +115 -0
- package/dist/sync-operations.d.ts.map +1 -0
- package/dist/sync-operations.js +178 -0
- package/dist/sync-operations.js.map +1 -0
- package/package.json +57 -15
- package/src/__tests__/fixtures/mock-config.ts +112 -0
- package/src/__tests__/sync-operations.test.ts +218 -0
- package/src/bin.ts +8 -6
- package/src/cli.ts +5 -6
- package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
- package/src/commands/import-export.ts +283 -0
- package/src/commands/mcp-coach.ts +76 -75
- package/src/commands/mcp-protocols.test.ts +76 -0
- package/src/commands/mcp-protocols.ts +12 -0
- package/src/commands/mcp-stdio.test.ts +93 -0
- package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +100 -61
- package/src/commands/mcp-tools-defs.test.ts +13 -0
- package/src/commands/mcp-tools-defs.ts +320 -0
- package/src/commands/mcp.ts +10 -6
- package/src/commands/new-project.ts +124 -57
- package/src/mcp-content/architecture.ts +3 -3
- package/src/mcp-content/features.ts +1 -1
- package/src/mcp-content/getting-started.ts +37 -37
- package/src/mcp-content/overview.ts +6 -6
- package/src/mcp-content/schemas/blog.ts +25 -25
- package/src/mcp-content/schemas/ecommerce.ts +67 -67
- package/src/mcp-content/schemas/social.ts +48 -48
- package/src/mcp-content/schemas/todo.ts +35 -35
- package/src/mcp-runtime/runtime.ts +147 -0
- package/src/module-loader.ts +94 -0
- package/src/package-manager.test.ts +66 -0
- package/src/package-manager.ts +44 -0
- package/src/sync-operations.ts +360 -0
- package/dist/commands/mcp-tools.d.ts +0 -58
- package/dist/commands/mcp-tools.d.ts.map +0 -1
- package/dist/commands/mcp-tools.js.map +0 -1
- 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
|
-
|
|
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.
|
|
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>(
|
|
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>(
|
|
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 = (
|
|
70
|
+
const fetchExamples = (ref: string) =>
|
|
44
71
|
Effect.gen(function* () {
|
|
45
|
-
const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${
|
|
72
|
+
const url = `https://api.github.com/repos/livestorejs/livestore/contents/examples?ref=${ref}`
|
|
46
73
|
|
|
47
|
-
yield* Effect.log(`Fetching examples from
|
|
74
|
+
yield* Effect.log(`Fetching examples from ref: ${ref}`)
|
|
48
75
|
|
|
49
|
-
const request =
|
|
76
|
+
const request = githubRequest(url)
|
|
50
77
|
const response = yield* HttpClient.execute(request).pipe(
|
|
51
78
|
Effect.scoped,
|
|
52
|
-
Effect.
|
|
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.
|
|
64
|
-
|
|
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
|
-
.
|
|
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*
|
|
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,
|
|
132
|
+
const downloadExample = (exampleName: string, ref: string, destinationPath: string) =>
|
|
104
133
|
Effect.gen(function* () {
|
|
105
|
-
yield* Console.log(`📥 Downloading example "${exampleName}" from
|
|
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-${
|
|
109
|
-
const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${
|
|
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 =
|
|
143
|
+
const request = githubRequest(tarballUrl)
|
|
113
144
|
|
|
114
145
|
const response = yield* HttpClient.execute(request).pipe(
|
|
115
146
|
Effect.scoped,
|
|
116
|
-
Effect.
|
|
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
|
|
139
|
-
yield*
|
|
140
|
-
|
|
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 (
|
|
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
|
|
175
|
-
yield*
|
|
176
|
-
|
|
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.
|
|
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.
|
|
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
|
|
196
|
-
'
|
|
224
|
+
export const createCommand = Cli.Command.make(
|
|
225
|
+
'create',
|
|
197
226
|
{
|
|
198
|
-
example: Cli.
|
|
199
|
-
Cli.
|
|
200
|
-
Cli.
|
|
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
|
-
|
|
203
|
-
Cli.
|
|
204
|
-
Cli.
|
|
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.
|
|
207
|
-
Cli.
|
|
208
|
-
Cli.
|
|
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
|
-
|
|
247
|
+
ref,
|
|
214
248
|
path,
|
|
215
249
|
}: {
|
|
216
250
|
example: Option.Option<string>
|
|
217
|
-
|
|
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(
|
|
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 (
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|