@livestore/cli 0.4.0-dev.22 → 0.4.0-dev.24
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 +13 -4
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mock-config.js +8 -2
- package/dist/__tests__/fixtures/mock-config.js.map +1 -1
- package/dist/__tests__/sync-operations.test.js +2 -2
- package/dist/__tests__/sync-operations.test.js.map +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/import-export.d.ts.map +1 -1
- package/dist/commands/import-export.js +16 -14
- package/dist/commands/import-export.js.map +1 -1
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +3 -3
- package/dist/commands/mcp-coach.js.map +1 -1
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -1
- package/dist/commands/mcp-tool-handlers.js +4 -4
- package/dist/commands/mcp-tool-handlers.js.map +1 -1
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -1
- package/dist/commands/mcp-tools-defs.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +8 -1
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +27 -18
- package/dist/commands/new-project.js.map +1 -1
- package/dist/mcp-runtime/runtime.d.ts +2 -2
- package/dist/mcp-runtime/runtime.d.ts.map +1 -1
- package/dist/mcp-runtime/runtime.js +9 -9
- package/dist/mcp-runtime/runtime.js.map +1 -1
- package/dist/module-loader.d.ts.map +1 -1
- package/dist/module-loader.js +8 -8
- package/dist/module-loader.js.map +1 -1
- package/dist/package-manager.js +4 -4
- package/dist/package-manager.js.map +1 -1
- package/dist/sync-operations.d.ts +1 -1
- package/dist/sync-operations.d.ts.map +1 -1
- package/dist/sync-operations.js +15 -15
- package/dist/sync-operations.js.map +1 -1
- package/package.json +71 -27
- package/src/__tests__/fixtures/mock-config.ts +10 -2
- package/src/__tests__/sync-operations.test.ts +4 -2
- package/src/bin.ts +1 -0
- package/src/cli.ts +1 -0
- package/src/commands/import-export.ts +19 -14
- package/src/commands/mcp-coach.ts +4 -3
- package/src/commands/mcp-tool-handlers.ts +5 -4
- package/src/commands/mcp-tools-defs.ts +1 -0
- package/src/commands/mcp.ts +1 -0
- package/src/commands/new-project.ts +29 -18
- package/src/mcp-runtime/runtime.ts +37 -29
- package/src/module-loader.ts +9 -8
- package/src/package-manager.ts +4 -4
- package/src/sync-operations.ts +19 -19
- package/dist/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,42 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/cli",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.24",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/livestorejs/livestore.git"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"livestore": "./dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"package.json",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
4
17
|
"type": "module",
|
|
5
18
|
"sideEffects": false,
|
|
6
19
|
"exports": {
|
|
7
20
|
".": "./dist/mod.js"
|
|
8
21
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
11
24
|
},
|
|
12
25
|
"dependencies": {
|
|
13
|
-
"@effect/ai": "0.
|
|
14
|
-
"@effect/ai-openai": "0.
|
|
15
|
-
"@effect/experimental": "0.
|
|
16
|
-
"@effect/opentelemetry": "0.
|
|
17
|
-
"@effect/platform": "0.
|
|
18
|
-
"@effect/rpc": "0.
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"@livestore/
|
|
22
|
-
"@livestore/
|
|
23
|
-
"@livestore/
|
|
24
|
-
"@livestore/
|
|
25
|
-
"@livestore/adapter-node": "0.4.0-dev.22",
|
|
26
|
-
"@livestore/utils": "0.4.0-dev.22"
|
|
26
|
+
"@effect/ai": "0.35.0",
|
|
27
|
+
"@effect/ai-openai": "0.39.0",
|
|
28
|
+
"@effect/experimental": "0.60.0",
|
|
29
|
+
"@effect/opentelemetry": "0.63.0",
|
|
30
|
+
"@effect/platform": "0.96.1",
|
|
31
|
+
"@effect/rpc": "0.75.1",
|
|
32
|
+
"effect": "3.21.2",
|
|
33
|
+
"@livestore/adapter-node": "^0.4.0-dev.24",
|
|
34
|
+
"@livestore/peer-deps": "^0.4.0-dev.24",
|
|
35
|
+
"@livestore/common": "^0.4.0-dev.24",
|
|
36
|
+
"@livestore/utils": "^0.4.0-dev.24",
|
|
37
|
+
"@livestore/livestore": "^0.4.0-dev.24"
|
|
27
38
|
},
|
|
28
39
|
"devDependencies": {
|
|
29
|
-
"@types/node": "
|
|
30
|
-
"typescript": "5.9.
|
|
31
|
-
"
|
|
40
|
+
"@types/node": "25.3.3",
|
|
41
|
+
"typescript": "5.9.3",
|
|
42
|
+
"vitest": "3.2.4",
|
|
43
|
+
"@livestore/utils-dev": "^0.4.0-dev.24"
|
|
32
44
|
},
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@effect/ai": "^0.35.0",
|
|
47
|
+
"@effect/cli": "^0.75.1",
|
|
48
|
+
"@effect/cluster": "^0.58.2",
|
|
49
|
+
"@effect/experimental": "^0.60.0",
|
|
50
|
+
"@effect/opentelemetry": "^0.63.0",
|
|
51
|
+
"@effect/platform": "^0.96.1",
|
|
52
|
+
"@effect/platform-browser": "^0.76.0",
|
|
53
|
+
"@effect/platform-bun": "^0.89.0",
|
|
54
|
+
"@effect/platform-node": "^0.106.0",
|
|
55
|
+
"@effect/printer": "^0.49.0",
|
|
56
|
+
"@effect/printer-ansi": "^0.49.0",
|
|
57
|
+
"@effect/rpc": "^0.75.1",
|
|
58
|
+
"@effect/sql": "^0.51.1",
|
|
59
|
+
"@effect/typeclass": "^0.40.0",
|
|
60
|
+
"@effect/vitest": "^0.29.0",
|
|
61
|
+
"@opentelemetry/api": "^1.9.0",
|
|
62
|
+
"@opentelemetry/resources": "^2.2.0",
|
|
63
|
+
"@standard-schema/spec": "^1.1.0",
|
|
64
|
+
"effect": "^3.21.2"
|
|
65
|
+
},
|
|
66
|
+
"$genie": {
|
|
67
|
+
"source": "package.json.genie.ts",
|
|
68
|
+
"warning": "DO NOT EDIT - changes will be overwritten",
|
|
69
|
+
"workspaceClosureDirs": [
|
|
70
|
+
"packages/@livestore/adapter-node",
|
|
71
|
+
"packages/@livestore/adapter-web",
|
|
72
|
+
"packages/@livestore/cli",
|
|
73
|
+
"packages/@livestore/common",
|
|
74
|
+
"packages/@livestore/common-cf",
|
|
75
|
+
"packages/@livestore/devtools-web-common",
|
|
76
|
+
"packages/@livestore/livestore",
|
|
77
|
+
"packages/@livestore/peer-deps",
|
|
78
|
+
"packages/@livestore/sqlite-wasm",
|
|
79
|
+
"packages/@livestore/utils",
|
|
80
|
+
"packages/@livestore/utils-dev",
|
|
81
|
+
"packages/@livestore/wa-sqlite",
|
|
82
|
+
"packages/@livestore/webmesh"
|
|
83
|
+
]
|
|
40
84
|
},
|
|
41
85
|
"scripts": {
|
|
42
86
|
"build": "tsc",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
3
4
|
import { Events, makeSchema, State } from '@livestore/common/schema'
|
|
4
5
|
import type { MockSyncBackend } from '@livestore/common/sync'
|
|
5
6
|
import { EventFactory } from '@livestore/common/testing'
|
|
@@ -7,6 +8,14 @@ import { Effect, FileSystem, type Mailbox, Schema } from '@livestore/utils/effec
|
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
|
|
11
|
+
class DynamicImportError extends Schema.TaggedError<DynamicImportError>()('DynamicImportError', {
|
|
12
|
+
cause: Schema.Defect,
|
|
13
|
+
path: Schema.String,
|
|
14
|
+
}) {}
|
|
15
|
+
|
|
16
|
+
// Use package-local temp directory for test config files to ensure proper module resolution
|
|
17
|
+
const tmpDir = path.join(__dirname, '.tmp-test-configs')
|
|
18
|
+
|
|
10
19
|
const items = State.SQLite.table({
|
|
11
20
|
name: 'items',
|
|
12
21
|
columns: {
|
|
@@ -33,7 +42,6 @@ const state = State.SQLite.makeState({
|
|
|
33
42
|
|
|
34
43
|
export const schema = makeSchema({ state, events })
|
|
35
44
|
|
|
36
|
-
const tmpDir = path.join(process.cwd(), 'tmp', 'cli-sync-tests')
|
|
37
45
|
const schemaModuleUrl = pathToFileURL(path.join(__dirname, 'mock-config.ts')).href
|
|
38
46
|
|
|
39
47
|
/** Generates a per-test config module exporting schema, a mock backend, and connection event taps. */
|
|
@@ -81,7 +89,7 @@ export const useMockConfig = Effect.acquireRelease(
|
|
|
81
89
|
|
|
82
90
|
const mod = (yield* Effect.tryPromise({
|
|
83
91
|
try: () => import(pathToFileURL(tempPath).href),
|
|
84
|
-
catch: (cause) => (
|
|
92
|
+
catch: (cause) => new DynamicImportError({ cause, path: tempPath }),
|
|
85
93
|
})) as {
|
|
86
94
|
mockBackend: MockSyncBackend
|
|
87
95
|
connectionEvents: Mailbox.Mailbox<'connect' | 'disconnect'>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
|
|
1
3
|
import type { LiveStoreEvent } from '@livestore/common/schema'
|
|
4
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
2
5
|
import { Chunk, Effect, FetchHttpClient, Layer, Mailbox, Stream } from '@livestore/utils/effect'
|
|
3
6
|
import { PlatformNode } from '@livestore/utils/node'
|
|
4
|
-
|
|
5
|
-
import { expect } from 'vitest'
|
|
7
|
+
|
|
6
8
|
import { pullEventsFromSyncBackend, pushEventsToSyncBackend } from '../sync-operations.ts'
|
|
7
9
|
import { makeEventFactory, useMockConfig } from './fixtures/mock-config.ts'
|
|
8
10
|
|
package/src/bin.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { liveStoreVersion } from '@livestore/common'
|
|
4
4
|
import { Console, Effect, FetchHttpClient, Layer, Logger, LogLevel } from '@livestore/utils/effect'
|
|
5
5
|
import { Cli, PlatformNode } from '@livestore/utils/node'
|
|
6
|
+
|
|
6
7
|
import { command } from './cli.ts'
|
|
7
8
|
|
|
8
9
|
const cli = Cli.Command.run(command, {
|
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
|
|
2
3
|
import type { UnknownError } from '@livestore/common'
|
|
3
|
-
import { Console, Effect, FileSystem, type HttpClient, type Scope } from '@livestore/utils/effect'
|
|
4
|
+
import { Console, Effect, FileSystem, type HttpClient, Schema, type Scope } from '@livestore/utils/effect'
|
|
4
5
|
import { Cli } from '@livestore/utils/node'
|
|
6
|
+
|
|
5
7
|
import * as SyncOps from '../sync-operations.ts'
|
|
6
8
|
|
|
9
|
+
const jsonStringifyPretty = Schema.encodeSync(Schema.parseJson({ space: 2 }))
|
|
10
|
+
const jsonParse = Schema.decodeUnknownSync(Schema.parseJson())
|
|
11
|
+
|
|
7
12
|
const LARGE_EVENT_WARNING_THRESHOLD = 100_000
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -38,14 +43,14 @@ const exportEvents = ({
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const fs = yield* FileSystem.FileSystem
|
|
41
|
-
const absOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)
|
|
46
|
+
const absOutputPath = path.isAbsolute(outputPath) === true ? outputPath : path.resolve(process.cwd(), outputPath)
|
|
42
47
|
|
|
43
|
-
yield* fs.writeFileString(absOutputPath,
|
|
48
|
+
yield* fs.writeFileString(absOutputPath, jsonStringifyPretty(result.data)).pipe(
|
|
44
49
|
Effect.mapError(
|
|
45
50
|
(cause) =>
|
|
46
51
|
new SyncOps.ExportError({
|
|
47
52
|
cause,
|
|
48
|
-
note: `Failed to write export file: ${cause}`,
|
|
53
|
+
note: `Failed to write export file: ${String(cause)}`,
|
|
49
54
|
}),
|
|
50
55
|
),
|
|
51
56
|
)
|
|
@@ -77,18 +82,18 @@ const importEvents = ({
|
|
|
77
82
|
> =>
|
|
78
83
|
Effect.gen(function* () {
|
|
79
84
|
const fs = yield* FileSystem.FileSystem
|
|
80
|
-
const absInputPath = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath)
|
|
85
|
+
const absInputPath = path.isAbsolute(inputPath) === true ? inputPath : path.resolve(process.cwd(), inputPath)
|
|
81
86
|
|
|
82
87
|
const exists = yield* fs.exists(absInputPath).pipe(
|
|
83
88
|
Effect.mapError(
|
|
84
89
|
(cause) =>
|
|
85
90
|
new SyncOps.ImportError({
|
|
86
91
|
cause,
|
|
87
|
-
note: `Failed to check file existence: ${cause}`,
|
|
92
|
+
note: `Failed to check file existence: ${String(cause)}`,
|
|
88
93
|
}),
|
|
89
94
|
),
|
|
90
95
|
)
|
|
91
|
-
if (
|
|
96
|
+
if (exists === false) {
|
|
92
97
|
return yield* new SyncOps.ImportError({
|
|
93
98
|
cause: new Error(`File not found: ${absInputPath}`),
|
|
94
99
|
note: `Import file does not exist at ${absInputPath}`,
|
|
@@ -102,13 +107,13 @@ const importEvents = ({
|
|
|
102
107
|
(cause) =>
|
|
103
108
|
new SyncOps.ImportError({
|
|
104
109
|
cause,
|
|
105
|
-
note: `Failed to read import file: ${cause}`,
|
|
110
|
+
note: `Failed to read import file: ${String(cause)}`,
|
|
106
111
|
}),
|
|
107
112
|
),
|
|
108
113
|
)
|
|
109
114
|
|
|
110
115
|
const parsedContent = yield* Effect.try({
|
|
111
|
-
try: () =>
|
|
116
|
+
try: () => jsonParse(fileContent),
|
|
112
117
|
catch: (error) =>
|
|
113
118
|
new SyncOps.ImportError({
|
|
114
119
|
cause: new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`),
|
|
@@ -119,8 +124,8 @@ const importEvents = ({
|
|
|
119
124
|
/** Validate export file format before proceeding */
|
|
120
125
|
const validation = yield* SyncOps.validateExportData({ data: parsedContent, targetStoreId: storeId })
|
|
121
126
|
|
|
122
|
-
if (validation.storeIdMismatch) {
|
|
123
|
-
if (
|
|
127
|
+
if (validation.storeIdMismatch === true) {
|
|
128
|
+
if (force !== true) {
|
|
124
129
|
return yield* new SyncOps.ImportError({
|
|
125
130
|
cause: new Error(`Store ID mismatch: file has '${validation.sourceStoreId}', expected '${storeId}'`),
|
|
126
131
|
note: `The export file was created for a different store. Use --force to import anyway.`,
|
|
@@ -138,7 +143,7 @@ const importEvents = ({
|
|
|
138
143
|
)
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
if (dryRun) {
|
|
146
|
+
if (dryRun === true) {
|
|
142
147
|
yield* Console.log(`Dry run - validating import file...`)
|
|
143
148
|
yield* Console.log(`Dry run complete. ${validation.eventCount} events would be imported.`)
|
|
144
149
|
return
|
|
@@ -255,8 +260,8 @@ export const importCommand = Cli.Command.make(
|
|
|
255
260
|
yield* Console.log(` Config: ${config}`)
|
|
256
261
|
yield* Console.log(` Store ID: ${storeId}`)
|
|
257
262
|
yield* Console.log(` Input: ${input}`)
|
|
258
|
-
if (force) yield* Console.log(` Force: enabled`)
|
|
259
|
-
if (dryRun) yield* Console.log(` Dry run: enabled`)
|
|
263
|
+
if (force === true) yield* Console.log(` Force: enabled`)
|
|
264
|
+
if (dryRun === true) yield* Console.log(` Dry run: enabled`)
|
|
260
265
|
yield* Console.log('')
|
|
261
266
|
|
|
262
267
|
yield* importEvents({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LanguageModel } from '@effect/ai'
|
|
2
2
|
import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'
|
|
3
|
+
|
|
3
4
|
import { AiError, Config, Effect, FetchHttpClient, Layer, Prompt, Schema, Tool } from '@livestore/utils/effect'
|
|
4
5
|
|
|
5
6
|
// Define the coach tool that analyzes LiveStore usage
|
|
@@ -57,7 +58,7 @@ type CoachToolResult = {
|
|
|
57
58
|
export const coachToolHandler: (input: CoachToolHandlerInput) => Effect.Effect<CoachToolResult, AiError.AiError> =
|
|
58
59
|
Effect.fn('mcp-coach-handler')(({ code, codeType }) => {
|
|
59
60
|
const effect = Effect.gen(function* () {
|
|
60
|
-
const codeTypeContext = codeType ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
|
|
61
|
+
const codeTypeContext = codeType !== undefined ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
|
|
61
62
|
|
|
62
63
|
const prompt = Prompt.makeMessage('user', {
|
|
63
64
|
content: [
|
|
@@ -113,11 +114,11 @@ Format your response as constructive feedback that helps developers improve thei
|
|
|
113
114
|
.slice(0, 5)
|
|
114
115
|
|
|
115
116
|
const scoreMatch = feedback.match(/(?:score|rating|quality).*?(\d+(?:\.\d+)?)/i)
|
|
116
|
-
const score = scoreMatch ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
|
|
117
|
+
const score = scoreMatch !== null ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
|
|
117
118
|
|
|
118
119
|
return {
|
|
119
120
|
feedback,
|
|
120
|
-
score: score && score >= 1 && score <= 10 ? score : undefined,
|
|
121
|
+
score: score !== undefined && score >= 1 && score <= 10 ? score : undefined,
|
|
121
122
|
suggestions,
|
|
122
123
|
}
|
|
123
124
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect, FetchHttpClient, Layer, type Toolkit } from '@livestore/utils/effect'
|
|
2
2
|
import { PlatformNode } from '@livestore/utils/node'
|
|
3
|
+
|
|
3
4
|
import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
|
|
4
5
|
import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
|
|
5
6
|
import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
|
|
@@ -39,7 +40,7 @@ export const toolHandlers: LivestoreToolHandlers = livestoreToolkit.of({
|
|
|
39
40
|
explanation = 'E-commerce schema with products, categories, orders, and inventory'
|
|
40
41
|
break
|
|
41
42
|
case 'custom': {
|
|
42
|
-
if (
|
|
43
|
+
if (customDescription == null) {
|
|
43
44
|
schemaCode = `// Custom schema requested but no description provided
|
|
44
45
|
import { Schema } from '@livestore/livestore'
|
|
45
46
|
|
|
@@ -58,11 +59,11 @@ export const schema = Schema.create({
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Generate a basic custom schema based on description
|
|
61
|
-
const tableName = customDescription.toLowerCase().includes('user')
|
|
62
|
+
const tableName = customDescription.toLowerCase().includes('user') === true
|
|
62
63
|
? 'users'
|
|
63
|
-
: customDescription.toLowerCase().includes('product')
|
|
64
|
+
: customDescription.toLowerCase().includes('product') === true
|
|
64
65
|
? 'products'
|
|
65
|
-
: customDescription.toLowerCase().includes('post')
|
|
66
|
+
: customDescription.toLowerCase().includes('post') === true
|
|
66
67
|
? 'posts'
|
|
67
68
|
: 'items'
|
|
68
69
|
|
package/src/commands/mcp.ts
CHANGED
|
@@ -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'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as os from 'node:os'
|
|
2
2
|
import * as nodePath from 'node:path'
|
|
3
|
+
|
|
3
4
|
import { sluggify } from '@livestore/utils'
|
|
4
5
|
import {
|
|
5
6
|
Command,
|
|
@@ -25,28 +26,38 @@ const GitHubContentSchema = Schema.Struct({
|
|
|
25
26
|
|
|
26
27
|
const GitHubContentsResponseSchema = Schema.Array(GitHubContentSchema)
|
|
27
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
|
+
|
|
28
35
|
/** Schema for parsing package.json scripts (dev or start) */
|
|
29
36
|
const PackageJsonScriptsSchema = Schema.Struct({
|
|
30
37
|
scripts: Schema.Union(Schema.Struct({ dev: Schema.String }), Schema.Struct({ start: Schema.String })),
|
|
31
38
|
})
|
|
32
39
|
|
|
33
40
|
// Error types
|
|
34
|
-
export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>()('ExampleNotFoundError', {
|
|
41
|
+
export class ExampleNotFoundError extends Schema.TaggedError<ExampleNotFoundError>('~@livestore/cli/ExampleNotFoundError')('ExampleNotFoundError', {
|
|
35
42
|
exampleName: Schema.String,
|
|
36
43
|
availableExamples: Schema.Array(Schema.String),
|
|
37
44
|
message: Schema.String,
|
|
38
45
|
}) {}
|
|
39
46
|
|
|
40
|
-
export class NetworkError extends Schema.TaggedError<NetworkError>()('NetworkError', {
|
|
47
|
+
export class NetworkError extends Schema.TaggedError<NetworkError>('~@livestore/cli/NetworkError')('NetworkError', {
|
|
41
48
|
cause: Schema.Unknown,
|
|
42
49
|
message: Schema.String,
|
|
43
50
|
}) {}
|
|
44
51
|
|
|
45
|
-
export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>()('DirectoryExistsError', {
|
|
52
|
+
export class DirectoryExistsError extends Schema.TaggedError<DirectoryExistsError>('~@livestore/cli/DirectoryExistsError')('DirectoryExistsError', {
|
|
46
53
|
path: Schema.String,
|
|
47
54
|
message: Schema.String,
|
|
48
55
|
}) {}
|
|
49
56
|
|
|
57
|
+
export class NoExamplesError extends Schema.TaggedError<NoExamplesError>('~@livestore/cli/NoExamplesError')('NoExamplesError', {
|
|
58
|
+
message: Schema.String,
|
|
59
|
+
}) {}
|
|
60
|
+
|
|
50
61
|
// Fetch available examples from GitHub
|
|
51
62
|
const fetchExamples = (ref: string) =>
|
|
52
63
|
Effect.gen(function* () {
|
|
@@ -54,26 +65,26 @@ const fetchExamples = (ref: string) =>
|
|
|
54
65
|
|
|
55
66
|
yield* Effect.log(`Fetching examples from ref: ${ref}`)
|
|
56
67
|
|
|
57
|
-
const request =
|
|
68
|
+
const request = githubRequest(url)
|
|
58
69
|
const response = yield* HttpClient.execute(request).pipe(
|
|
59
70
|
Effect.scoped,
|
|
60
71
|
Effect.catchAll(
|
|
61
72
|
(error) =>
|
|
62
73
|
new NetworkError({
|
|
63
74
|
cause: error,
|
|
64
|
-
message: `Failed to fetch examples from GitHub: ${error}`,
|
|
75
|
+
message: `Failed to fetch examples from GitHub: ${String(error)}`,
|
|
65
76
|
}),
|
|
66
77
|
),
|
|
67
78
|
)
|
|
68
79
|
|
|
69
80
|
const responseText = yield* response.text
|
|
70
81
|
|
|
71
|
-
const examples = yield* Schema.decodeUnknown(GitHubContentsResponseSchema)(
|
|
82
|
+
const examples = yield* Schema.decodeUnknown(Schema.parseJson(GitHubContentsResponseSchema))(responseText).pipe(
|
|
72
83
|
Effect.catchAll(
|
|
73
84
|
(error) =>
|
|
74
85
|
new NetworkError({
|
|
75
86
|
cause: error,
|
|
76
|
-
message: `Failed to parse GitHub API response: ${error}`,
|
|
87
|
+
message: `Failed to parse GitHub API response: ${String(error)}`,
|
|
77
88
|
}),
|
|
78
89
|
),
|
|
79
90
|
)
|
|
@@ -81,7 +92,7 @@ const fetchExamples = (ref: string) =>
|
|
|
81
92
|
const exampleNames = examples
|
|
82
93
|
.filter((item) => item.type === 'dir')
|
|
83
94
|
.map((item) => item.name)
|
|
84
|
-
.
|
|
95
|
+
.toSorted()
|
|
85
96
|
|
|
86
97
|
yield* Effect.log(`Found ${exampleNames.length} examples: ${exampleNames.join(', ')}`)
|
|
87
98
|
|
|
@@ -92,7 +103,7 @@ const fetchExamples = (ref: string) =>
|
|
|
92
103
|
const selectExample = (examples: string[]) =>
|
|
93
104
|
Effect.gen(function* () {
|
|
94
105
|
if (examples.length === 0) {
|
|
95
|
-
return yield*
|
|
106
|
+
return yield* new NoExamplesError({ message: 'No examples available' })
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
const prompt = Cli.Prompt.select({
|
|
@@ -117,7 +128,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
117
128
|
const tarballUrl = `https://api.github.com/repos/livestorejs/livestore/tarball/${ref}`
|
|
118
129
|
|
|
119
130
|
// Download tarball directly
|
|
120
|
-
const request =
|
|
131
|
+
const request = githubRequest(tarballUrl)
|
|
121
132
|
|
|
122
133
|
const response = yield* HttpClient.execute(request).pipe(
|
|
123
134
|
Effect.scoped,
|
|
@@ -125,7 +136,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
125
136
|
(error) =>
|
|
126
137
|
new NetworkError({
|
|
127
138
|
cause: error,
|
|
128
|
-
message: `Failed to download tarball: ${error}`,
|
|
139
|
+
message: `Failed to download tarball: ${String(error)}`,
|
|
129
140
|
}),
|
|
130
141
|
),
|
|
131
142
|
)
|
|
@@ -150,7 +161,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
150
161
|
(error) =>
|
|
151
162
|
new NetworkError({
|
|
152
163
|
cause: error,
|
|
153
|
-
message: `Failed to extract tarball: ${error}`,
|
|
164
|
+
message: `Failed to extract tarball: ${String(error)}`,
|
|
154
165
|
}),
|
|
155
166
|
),
|
|
156
167
|
)
|
|
@@ -171,7 +182,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
171
182
|
// Check if the example exists
|
|
172
183
|
const exampleExists = yield* fs.exists(exampleSourcePath)
|
|
173
184
|
|
|
174
|
-
if (
|
|
185
|
+
if (exampleExists === false) {
|
|
175
186
|
return yield* new ExampleNotFoundError({
|
|
176
187
|
exampleName,
|
|
177
188
|
availableExamples: [],
|
|
@@ -186,7 +197,7 @@ const downloadExample = (exampleName: string, ref: string, destinationPath: stri
|
|
|
186
197
|
(error) =>
|
|
187
198
|
new NetworkError({
|
|
188
199
|
cause: error,
|
|
189
|
-
message: `Failed to copy example files: ${error}`,
|
|
200
|
+
message: `Failed to copy example files: ${String(error)}`,
|
|
190
201
|
}),
|
|
191
202
|
),
|
|
192
203
|
)
|
|
@@ -211,7 +222,7 @@ export const createCommand = Cli.Command.make(
|
|
|
211
222
|
Cli.Options.withAlias('commit'),
|
|
212
223
|
Cli.Options.withAlias('branch'),
|
|
213
224
|
Cli.Options.withAlias('tag'),
|
|
214
|
-
Cli.Options.withDefault('
|
|
225
|
+
Cli.Options.withDefault('main'),
|
|
215
226
|
Cli.Options.withDescription(
|
|
216
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`).',
|
|
217
228
|
),
|
|
@@ -245,10 +256,10 @@ export const createCommand = Cli.Command.make(
|
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
// Select example (from CLI option or interactive prompt)
|
|
248
|
-
const selectedExample = Option.isSome(example) ? example.value : yield* selectExample(examples)
|
|
259
|
+
const selectedExample = Option.isSome(example) === true ? example.value : yield* selectExample(examples)
|
|
249
260
|
|
|
250
261
|
// Validate selected example exists
|
|
251
|
-
if (
|
|
262
|
+
if (examples.includes(selectedExample) === false) {
|
|
252
263
|
yield* Console.log(`❌ Example "${selectedExample}" not found`)
|
|
253
264
|
yield* Console.log(`Available examples: ${examples.join(', ')}`)
|
|
254
265
|
return yield* new ExampleNotFoundError({
|
|
@@ -259,7 +270,7 @@ export const createCommand = Cli.Command.make(
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
// Determine destination path
|
|
262
|
-
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)
|
|
263
274
|
|
|
264
275
|
// Download and extract the example
|
|
265
276
|
yield* downloadExample(selectedExample, ref, destinationPath)
|
|
@@ -30,7 +30,7 @@ export const init = ({
|
|
|
30
30
|
sessionId?: string
|
|
31
31
|
}): Effect.Effect<Store<any>, UnknownError> =>
|
|
32
32
|
Effect.gen(function* () {
|
|
33
|
-
if (
|
|
33
|
+
if (storeId === '' || typeof storeId !== 'string') {
|
|
34
34
|
return yield* UnknownError.make({ cause: new Error('Invalid storeId: expected a non-empty string') })
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -39,8 +39,8 @@ export const init = ({
|
|
|
39
39
|
// Build Node adapter internally
|
|
40
40
|
const adapter = makeNodeAdapter({
|
|
41
41
|
storage: { type: 'in-memory' },
|
|
42
|
-
...(clientId ? { clientId } : {}),
|
|
43
|
-
...(sessionId ? { sessionId } : {}),
|
|
42
|
+
...(clientId !== undefined && clientId !== '' ? { clientId } : {}),
|
|
43
|
+
...(sessionId !== undefined && sessionId !== '' ? { sessionId } : {}),
|
|
44
44
|
sync: {
|
|
45
45
|
backend: syncBackendConstructor,
|
|
46
46
|
initialSyncOptions: { _tag: 'Blocking', timeout: 5000 },
|
|
@@ -61,7 +61,7 @@ export const init = ({
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
// Replace existing store if any
|
|
64
|
-
if (store) {
|
|
64
|
+
if (store !== undefined) {
|
|
65
65
|
yield* Effect.promise(async () => {
|
|
66
66
|
try {
|
|
67
67
|
await store!.shutdownPromise()
|
|
@@ -83,7 +83,7 @@ export const status = Effect.gen(function* () {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
const s = opt.value
|
|
86
|
-
const tableCounts =
|
|
86
|
+
const tableCounts = Array.from(s.schema.state.sqlite.tables.keys())
|
|
87
87
|
.filter((name) => !SystemTables.isStateSystemTable(name))
|
|
88
88
|
.reduce(
|
|
89
89
|
(acc, name) => {
|
|
@@ -102,34 +102,42 @@ export const status = Effect.gen(function* () {
|
|
|
102
102
|
}
|
|
103
103
|
}).pipe(Effect.withSpan('mcp-runtime:status'))
|
|
104
104
|
|
|
105
|
-
export const query = (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
export const query = Effect.fn('mcp-runtime:query')(function* ({
|
|
106
|
+
sql,
|
|
107
|
+
bindValues,
|
|
108
|
+
}: {
|
|
109
|
+
sql: string
|
|
110
|
+
bindValues?: readonly any[] | Record<string, unknown>
|
|
111
|
+
}) {
|
|
112
|
+
const opt = yield* getStore
|
|
113
|
+
if (opt._tag === 'None') {
|
|
114
|
+
return yield* Effect.dieMessage('LiveStore not connected. Call livestore_instance_connect first.')
|
|
115
|
+
}
|
|
116
|
+
const s = opt.value
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const rows = s.query<Array<Record<string, unknown>>>({ query: sql, bindValues: (bindValues as any) ?? [] })
|
|
119
|
+
const jsonRows = rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, v as Schema.JsonValue])))
|
|
120
|
+
return { rows: jsonRows, rowCount: jsonRows.length }
|
|
121
|
+
})
|
|
117
122
|
|
|
118
|
-
export const commit = (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
export const commit = Effect.fn('mcp-runtime:commit')(function* ({
|
|
124
|
+
events,
|
|
125
|
+
}: {
|
|
126
|
+
events: ReadonlyArray<{ name: string; args: Schema.JsonValue }>
|
|
127
|
+
}) {
|
|
128
|
+
const opt = yield* getStore
|
|
129
|
+
if (opt._tag === 'None') {
|
|
130
|
+
return yield* Effect.dieMessage('LiveStore not connected. Call livestore_instance_connect first.')
|
|
131
|
+
}
|
|
132
|
+
const s = opt.value
|
|
133
|
+
const InputEventSchema = LiveStoreEvent.Input.makeSchema(s.schema) as Schema.Schema<any>
|
|
134
|
+
const decoded = events.map((e) => Schema.decodeSync(InputEventSchema)(e))
|
|
135
|
+
s.commit(...decoded)
|
|
136
|
+
return { committed: decoded.length }
|
|
137
|
+
})
|
|
130
138
|
|
|
131
139
|
export const disconnect = Effect.promise(async () => {
|
|
132
|
-
if (store) {
|
|
140
|
+
if (store !== undefined) {
|
|
133
141
|
try {
|
|
134
142
|
await store.shutdownPromise()
|
|
135
143
|
} catch {}
|