@risingwave/wavelet-cli 0.2.1 → 0.2.5

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/package.json CHANGED
@@ -1,17 +1,30 @@
1
1
  {
2
2
  "name": "@risingwave/wavelet-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "description": "Wavelet CLI - manage views, generate types, run dev server",
5
+ "homepage": "https://github.com/risingwavelabs/wavelet",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/risingwavelabs/wavelet.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/risingwavelabs/wavelet/issues"
13
+ },
5
14
  "bin": {
6
- "wavelet": "./dist/index.js"
15
+ "wavelet": "dist/index.js"
7
16
  },
17
+ "files": [
18
+ "dist"
19
+ ],
8
20
  "scripts": {
9
21
  "build": "tsc",
10
- "dev": "tsx src/index.ts"
22
+ "dev": "tsx src/index.ts",
23
+ "prepack": "npm --prefix ../config run build && npm --prefix ../server run build && npm run build"
11
24
  },
12
25
  "dependencies": {
13
- "@risingwave/wavelet": "0.2.1",
14
- "@risingwave/wavelet-server": "0.2.1",
26
+ "@risingwave/wavelet": "0.2.5",
27
+ "@risingwave/wavelet-server": "0.2.5",
15
28
  "pg": "^8.13.0",
16
29
  "tsx": "^4.0.0"
17
30
  },
package/src/codegen.ts DELETED
@@ -1,214 +0,0 @@
1
- import { writeFileSync, mkdirSync } from 'node:fs'
2
- import pg from 'pg'
3
- import type { WaveletConfig, SqlFragment, QueryDef, ColumnType } from '@risingwave/wavelet'
4
-
5
- const { Client } = pg
6
-
7
- export async function generateClient(config: WaveletConfig): Promise<void> {
8
- // Event types are always known from config
9
- const eventTypes: TypedEntity[] = []
10
- const events = config.events ?? config.streams ?? {}
11
- for (const [eventName, eventDef] of Object.entries(events)) {
12
- const columns = Object.entries(eventDef.columns).map(([name, type]) => ({
13
- name,
14
- tsType: columnTypeToTs(type),
15
- }))
16
- eventTypes.push({ name: eventName, columns })
17
- }
18
-
19
- // Query types: try config-declared columns first, then RisingWave introspection, then generic
20
- const queryTypes: TypedEntity[] = []
21
- let dbClient: InstanceType<typeof Client> | null = null
22
- const queries = config.queries ?? config.views ?? {}
23
-
24
- for (const [queryName, queryDef] of Object.entries(queries)) {
25
- // Tier 1: explicit columns in config
26
- const declaredColumns = getQueryColumns(queryDef)
27
- if (declaredColumns) {
28
- queryTypes.push({ name: queryName, columns: declaredColumns })
29
- continue
30
- }
31
-
32
- // Tier 2: introspect from RisingWave (if reachable)
33
- if (!dbClient) {
34
- dbClient = await tryConnect(config.database)
35
- }
36
-
37
- if (dbClient) {
38
- try {
39
- const result = await dbClient.query(
40
- `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`,
41
- [queryName]
42
- )
43
- if (result.rows.length > 0) {
44
- queryTypes.push({
45
- name: queryName,
46
- columns: result.rows.map((row: any) => ({
47
- name: row.column_name,
48
- tsType: pgTypeToTs(row.data_type),
49
- })),
50
- })
51
- continue
52
- }
53
- } catch {
54
- // Fall through to tier 3
55
- }
56
- }
57
-
58
- // Tier 3: generic fallback
59
- queryTypes.push({ name: queryName, columns: [] })
60
- }
61
-
62
- if (dbClient) {
63
- await dbClient.end().catch(() => {})
64
- }
65
-
66
- const code = generateCode(queryTypes, eventTypes)
67
-
68
- mkdirSync('.wavelet', { recursive: true })
69
- writeFileSync('.wavelet/client.ts', code)
70
- }
71
-
72
- interface TypedEntity {
73
- name: string
74
- columns: { name: string; tsType: string }[]
75
- }
76
-
77
- function getQueryColumns(queryDef: QueryDef | SqlFragment): { name: string; tsType: string }[] | null {
78
- // SqlFragment has no columns
79
- if ('_tag' in queryDef && queryDef._tag === 'sql') return null
80
-
81
- // QueryDef might have explicit columns
82
- const qd = queryDef as QueryDef
83
- if (!qd.columns) return null
84
-
85
- return Object.entries(qd.columns).map(([name, type]) => ({
86
- name,
87
- tsType: columnTypeToTs(type),
88
- }))
89
- }
90
-
91
- async function tryConnect(connectionString: string): Promise<InstanceType<typeof Client> | null> {
92
- const client = new Client({ connectionString, connectionTimeoutMillis: 3000 })
93
- try {
94
- await client.connect()
95
- return client
96
- } catch {
97
- return null
98
- }
99
- }
100
-
101
- function generateCode(queries: TypedEntity[], events: TypedEntity[]): string {
102
- let code = `// Auto-generated by wavelet generate - do not edit\n`
103
- code += `// Run 'npx wavelet generate' to regenerate\n\n`
104
- code += `import { WaveletClient } from '@risingwave/wavelet-sdk'\n`
105
- code += `import type { QueryHandle, EventHandle, Diff, WaveletClientOptions } from '@risingwave/wavelet-sdk'\n\n`
106
-
107
- // Query row types
108
- for (const q of queries) {
109
- const typeName = pascalCase(q.name) + 'Row'
110
- if (q.columns.length > 0) {
111
- code += `export interface ${typeName} {\n`
112
- for (const col of q.columns) {
113
- code += ` ${col.name}: ${col.tsType}\n`
114
- }
115
- code += `}\n\n`
116
- } else {
117
- code += `export type ${typeName} = Record<string, unknown>\n\n`
118
- }
119
- }
120
-
121
- // Event types
122
- for (const ev of events) {
123
- const typeName = pascalCase(ev.name) + 'Event'
124
- code += `export interface ${typeName} {\n`
125
- for (const col of ev.columns) {
126
- code += ` ${col.name}: ${col.tsType}\n`
127
- }
128
- code += `}\n\n`
129
- }
130
-
131
- // Typed client class
132
- code += `export class TypedWaveletClient {\n`
133
- code += ` private client: WaveletClient\n\n`
134
- code += ` constructor(options: WaveletClientOptions) {\n`
135
- code += ` this.client = new WaveletClient(options)\n`
136
- code += ` }\n\n`
137
-
138
- // Query accessors
139
- code += ` queries = {\n`
140
- for (const q of queries) {
141
- const typeName = pascalCase(q.name) + 'Row'
142
- code += ` ${q.name}: this.client.query<${typeName}>('${q.name}'),\n`
143
- }
144
- code += ` }\n\n`
145
-
146
- // Event accessors
147
- code += ` events = {\n`
148
- for (const ev of events) {
149
- const typeName = pascalCase(ev.name) + 'Event'
150
- code += ` ${ev.name}: this.client.event<${typeName}>('${ev.name}'),\n`
151
- }
152
- code += ` }\n`
153
- code += `}\n\n`
154
-
155
- // Query name literal type
156
- const queryNames = queries.map(q => `'${q.name}'`).join(' | ')
157
- code += `export type QueryName = ${queryNames || 'never'}\n\n`
158
-
159
- // React hook overloads
160
- code += `// React hook overloads for type-safe useWavelet\n`
161
- code += `import type { UseWaveletResult } from '@risingwave/wavelet-sdk/react'\n\n`
162
- for (const q of queries) {
163
- const typeName = pascalCase(q.name) + 'Row'
164
- code += `export declare function useWavelet(query: '${q.name}'): UseWaveletResult<${typeName}>\n`
165
- }
166
-
167
- return code
168
- }
169
-
170
- function pgTypeToTs(pgType: string): string {
171
- switch (pgType.toLowerCase()) {
172
- case 'integer':
173
- case 'bigint':
174
- case 'smallint':
175
- case 'real':
176
- case 'double precision':
177
- case 'numeric':
178
- case 'float':
179
- case 'int':
180
- return 'number'
181
- case 'boolean':
182
- return 'boolean'
183
- case 'jsonb':
184
- case 'json':
185
- return 'unknown'
186
- case 'timestamp with time zone':
187
- case 'timestamp without time zone':
188
- case 'timestamptz':
189
- return 'string'
190
- default:
191
- return 'string'
192
- }
193
- }
194
-
195
- function columnTypeToTs(colType: string): string {
196
- switch (colType) {
197
- case 'int':
198
- case 'float':
199
- return 'number'
200
- case 'boolean':
201
- return 'boolean'
202
- case 'json':
203
- return 'unknown'
204
- default:
205
- return 'string'
206
- }
207
- }
208
-
209
- function pascalCase(str: string): string {
210
- return str
211
- .split(/[_\s-]+/)
212
- .map(s => s.charAt(0).toUpperCase() + s.slice(1))
213
- .join('')
214
- }
@@ -1,69 +0,0 @@
1
- import { resolve } from 'node:path'
2
- import { execSync } from 'node:child_process'
3
- import type { WaveletConfig } from '@risingwave/wavelet'
4
-
5
- export async function loadConfig(configPath: string): Promise<WaveletConfig> {
6
- const abs = resolve(configPath)
7
-
8
- // For .ts files, use tsx to evaluate the config
9
- if (abs.endsWith('.ts')) {
10
- return loadTsConfig(abs)
11
- }
12
-
13
- // For .js/.mjs files, use dynamic import directly
14
- const { pathToFileURL } = await import('node:url')
15
- const mod = await import(pathToFileURL(abs).href)
16
- return unwrapConfig(mod)
17
- }
18
-
19
- function loadTsConfig(absPath: string): WaveletConfig {
20
- // Use tsx to evaluate the TypeScript config and extract the result as JSON
21
- const script = `
22
- import('${absPath}').then(mod => {
23
- let config = mod.default ?? mod;
24
- if (config && typeof config === 'object' && 'default' in config) config = config.default;
25
- process.stdout.write(JSON.stringify(config));
26
- }).catch(err => {
27
- process.stderr.write(err.message);
28
- process.exit(1);
29
- });
30
- `
31
-
32
- try {
33
- const result = execSync(`npx tsx -e "${script.replace(/"/g, '\\"')}"`, {
34
- cwd: resolve(absPath, '..'),
35
- encoding: 'utf-8',
36
- timeout: 15000,
37
- stdio: ['pipe', 'pipe', 'pipe'],
38
- })
39
- const config = JSON.parse(result)
40
- if (!config || !config.database) {
41
- throw new Error('Config missing database field')
42
- }
43
- return config
44
- } catch (err: any) {
45
- throw new Error(
46
- `Failed to load ${absPath}.\n` +
47
- `Make sure tsx is installed: npm install tsx\n` +
48
- `Error: ${err.stderr || err.message}`
49
- )
50
- }
51
- }
52
-
53
- function unwrapConfig(mod: any): WaveletConfig {
54
- let config = mod.default ?? mod
55
- if (config && typeof config === 'object' && 'default' in config) {
56
- config = config.default
57
- }
58
-
59
- if (!config || !config.database) {
60
- throw new Error(
61
- `wavelet.config.ts must export a config with a 'database' field.\n` +
62
- `Example:\n` +
63
- ` import { defineConfig } from '@risingwave/wavelet'\n` +
64
- ` export default defineConfig({ database: 'postgres://...' })`
65
- )
66
- }
67
-
68
- return config
69
- }
package/src/index.ts DELETED
@@ -1,233 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const command = process.argv[2]
4
-
5
- const HELP = `
6
- wavelet - Subscribe to computed results, not raw rows.
7
-
8
- Usage:
9
- wavelet <command> [options]
10
-
11
- Commands:
12
- dev Start local development server
13
- generate Generate typed client from query definitions
14
- push Sync query definitions to Wavelet server
15
- status Show current configuration and connection status
16
- init Initialize a new Wavelet project
17
-
18
- Options:
19
- --config Path to wavelet.config.ts (default: ./wavelet.config.ts)
20
- --json Output in JSON format
21
- --help Show this help message
22
-
23
- Examples:
24
- wavelet init
25
- wavelet dev
26
- wavelet generate
27
- wavelet push
28
- `
29
-
30
- async function main() {
31
- switch (command) {
32
- case 'init':
33
- await runInit()
34
- break
35
- case 'dev':
36
- await runDev()
37
- break
38
- case 'generate':
39
- await runGenerate()
40
- break
41
- case 'push':
42
- await runPush()
43
- break
44
- case 'status':
45
- await runStatus()
46
- break
47
- case '--help':
48
- case '-h':
49
- case undefined:
50
- console.log(HELP)
51
- break
52
- default:
53
- console.error(`Unknown command: '${command}'`)
54
- console.error(`Run 'wavelet --help' for available commands.`)
55
- process.exit(1)
56
- }
57
- }
58
-
59
- async function runInit() {
60
- const { writeFileSync, existsSync } = await import('node:fs')
61
-
62
- if (existsSync('wavelet.config.ts')) {
63
- console.log('wavelet.config.ts already exists. Skipping.')
64
- return
65
- }
66
-
67
- writeFileSync('wavelet.config.ts', `import { defineConfig, sql } from '@risingwave/wavelet'
68
-
69
- export default defineConfig({
70
- database: process.env.WAVELET_DATABASE_URL ?? 'postgres://root@localhost:4566/dev',
71
-
72
- events: {
73
- // Define your events here
74
- // game_events: {
75
- // columns: {
76
- // user_id: 'string',
77
- // action: 'string',
78
- // value: 'int',
79
- // }
80
- // }
81
- },
82
-
83
- queries: {
84
- // Define your queries (materialized views) here
85
- // leaderboard: sql\`
86
- // SELECT user_id, SUM(value) as total
87
- // FROM game_events
88
- // GROUP BY user_id
89
- // ORDER BY total DESC
90
- // LIMIT 100
91
- // \`,
92
- },
93
- })
94
- `)
95
-
96
- console.log('Created wavelet.config.ts')
97
- console.log('')
98
- console.log('Next steps:')
99
- console.log(' 1. Edit wavelet.config.ts to define your events and queries')
100
- console.log(' 2. Run: wavelet dev')
101
- }
102
-
103
- async function runDev() {
104
- const { loadConfig } = await import('./config-loader.js')
105
- const { ensureRisingWave } = await import('./risingwave-launcher.js')
106
- const configPath = getConfigPath()
107
-
108
- console.log(`Loading config from ${configPath}...`)
109
- const config = await loadConfig(configPath)
110
-
111
- // Ensure RisingWave is running
112
- const rwProcess = await ensureRisingWave(config.database)
113
-
114
- // Sync DDL before starting server
115
- const { DdlManager, WaveletServer } = await import('@risingwave/wavelet-server')
116
- const ddl = new DdlManager(config.database)
117
- await ddl.connect()
118
-
119
- console.log('\nSyncing events and queries...')
120
- const actions = await ddl.sync(config)
121
- printDdlActions(actions)
122
- await ddl.close()
123
-
124
- // Start server
125
- const server = new WaveletServer(config)
126
- await server.start()
127
-
128
- const shutdown = async () => {
129
- console.log('\nShutting down...')
130
- await server.stop()
131
- if (rwProcess) {
132
- console.log('Stopping RisingWave...')
133
- rwProcess.kill()
134
- }
135
- process.exit(0)
136
- }
137
- process.on('SIGINT', shutdown)
138
- process.on('SIGTERM', shutdown)
139
- }
140
-
141
- async function runGenerate() {
142
- const { loadConfig } = await import('./config-loader.js')
143
- const { generateClient } = await import('./codegen.js')
144
- const configPath = getConfigPath()
145
-
146
- console.log(`Loading config from ${configPath}...`)
147
- const config = await loadConfig(configPath)
148
-
149
- await generateClient(config)
150
- console.log('Generated .wavelet/client.ts')
151
- }
152
-
153
- async function runPush() {
154
- const { loadConfig } = await import('./config-loader.js')
155
- const configPath = getConfigPath()
156
-
157
- console.log(`Loading config from ${configPath}...`)
158
- const config = await loadConfig(configPath)
159
-
160
- const { DdlManager } = await import('@risingwave/wavelet-server')
161
- const ddl = new DdlManager(config.database)
162
- await ddl.connect()
163
-
164
- const actions = await ddl.sync(config)
165
- await ddl.close()
166
-
167
- const isJson = process.argv.includes('--json')
168
- if (isJson) {
169
- console.log(JSON.stringify({ actions }))
170
- } else {
171
- printDdlActions(actions)
172
- }
173
- }
174
-
175
- async function runStatus() {
176
- const { loadConfig } = await import('./config-loader.js')
177
- const configPath = getConfigPath()
178
-
179
- try {
180
- const config = await loadConfig(configPath)
181
- const eventCount = Object.keys(config.events ?? config.streams ?? {}).length
182
- const queryCount = Object.keys(config.queries ?? config.views ?? {}).length
183
-
184
- console.log(`Config: ${configPath}`)
185
- console.log(`Database: ${config.database.replace(/\/\/[^@]+@/, '//***@')}`)
186
- console.log(`Events: ${eventCount}`)
187
- console.log(`Queries: ${queryCount}`)
188
-
189
- if (queryCount > 0) {
190
- console.log('\nQueries:')
191
- for (const name of Object.keys(config.queries ?? config.views ?? {})) {
192
- console.log(` - ${name}`)
193
- }
194
- }
195
- } catch (err: any) {
196
- console.error(`Error: ${err.message}`)
197
- process.exit(1)
198
- }
199
- }
200
-
201
- function printDdlActions(actions: { type: string; resource: string; name: string; detail?: string }[]): void {
202
- const changed = actions.filter(a => a.type !== 'unchanged')
203
- const unchanged = actions.filter(a => a.type === 'unchanged')
204
-
205
- for (const action of actions) {
206
- const icon = action.type === 'create' ? '+' : action.type === 'delete' ? '-' : ' '
207
- const label = `${action.resource} '${action.name}'`
208
- const detail = action.detail ? ` (${action.detail})` : ''
209
-
210
- if (action.type === 'unchanged') {
211
- console.log(` ${icon} ${label}`)
212
- } else if (action.type === 'create') {
213
- console.log(` ${icon} ${label} - created${detail}`)
214
- } else if (action.type === 'delete') {
215
- console.log(` ${icon} ${label} - removed${detail}`)
216
- }
217
- }
218
-
219
- console.log(`\n${changed.length} changed, ${unchanged.length} unchanged`)
220
- }
221
-
222
- function getConfigPath(): string {
223
- const idx = process.argv.indexOf('--config')
224
- if (idx !== -1 && process.argv[idx + 1]) {
225
- return process.argv[idx + 1]
226
- }
227
- return './wavelet.config.ts'
228
- }
229
-
230
- main().catch((err) => {
231
- console.error('Error:', err.message)
232
- process.exit(1)
233
- })
@@ -1,177 +0,0 @@
1
- import { execSync, spawn, type ChildProcess } from 'node:child_process'
2
- import pg from 'pg'
3
-
4
- const { Client } = pg
5
-
6
- export async function ensureRisingWave(connectionString: string): Promise<ChildProcess | null> {
7
- // Try to connect to existing RisingWave
8
- if (await isReachable(connectionString)) {
9
- console.log('RisingWave is already running.')
10
- return null
11
- }
12
-
13
- console.log('RisingWave is not reachable. Attempting to start...')
14
-
15
- // Try native binary first, then docker
16
- const binary = findBinary()
17
- if (binary) {
18
- return startNative(binary)
19
- }
20
-
21
- if (hasDocker()) {
22
- return startDocker()
23
- }
24
-
25
- console.error(
26
- 'Could not start RisingWave.\n\n' +
27
- 'Install one of:\n' +
28
- ' brew tap risingwavelabs/risingwave && brew install risingwave\n' +
29
- ' docker pull risingwavelabs/risingwave:latest\n\n' +
30
- 'Or start RisingWave manually and re-run wavelet dev.'
31
- )
32
- process.exit(1)
33
- }
34
-
35
- async function isReachable(connectionString: string): Promise<boolean> {
36
- const client = new Client({ connectionString, connectionTimeoutMillis: 3000 })
37
- try {
38
- await client.connect()
39
- await client.query('SELECT 1')
40
- await client.end()
41
- return true
42
- } catch {
43
- return false
44
- }
45
- }
46
-
47
- function findBinary(): string | null {
48
- try {
49
- const path = execSync('which risingwave', { encoding: 'utf-8' }).trim()
50
- return path || null
51
- } catch {
52
- return null
53
- }
54
- }
55
-
56
- function hasDocker(): boolean {
57
- try {
58
- execSync('docker info', { stdio: 'ignore' })
59
- return true
60
- } catch {
61
- return false
62
- }
63
- }
64
-
65
- function startNative(binaryPath: string): Promise<ChildProcess> {
66
- return new Promise((resolve, reject) => {
67
- console.log(`Starting RisingWave (${binaryPath})...`)
68
- const child = spawn(binaryPath, ['playground'], {
69
- stdio: ['ignore', 'pipe', 'pipe'],
70
- detached: false,
71
- })
72
-
73
- let started = false
74
-
75
- const onData = (data: Buffer) => {
76
- const text = data.toString()
77
- if (!started && text.includes('ready to accept connections')) {
78
- started = true
79
- console.log('RisingWave started (playground mode).')
80
- resolve(child)
81
- }
82
- }
83
-
84
- child.stdout?.on('data', onData)
85
- child.stderr?.on('data', onData)
86
-
87
- child.on('error', (err) => {
88
- if (!started) reject(err)
89
- })
90
-
91
- child.on('exit', (code) => {
92
- if (!started) reject(new Error(`RisingWave exited with code ${code}`))
93
- })
94
-
95
- // Fallback: poll for connectivity
96
- const poll = setInterval(async () => {
97
- if (started) {
98
- clearInterval(poll)
99
- return
100
- }
101
- if (await isReachable('postgres://root@localhost:4566/dev')) {
102
- started = true
103
- clearInterval(poll)
104
- console.log('RisingWave started (playground mode).')
105
- resolve(child)
106
- }
107
- }, 1000)
108
-
109
- // Timeout after 30 seconds
110
- setTimeout(() => {
111
- if (!started) {
112
- clearInterval(poll)
113
- child.kill()
114
- reject(new Error('RisingWave failed to start within 30 seconds.'))
115
- }
116
- }, 30000)
117
- })
118
- }
119
-
120
- function startDocker(): Promise<ChildProcess> {
121
- return new Promise((resolve, reject) => {
122
- console.log('Starting RisingWave via Docker...')
123
-
124
- // Remove stale container if exists
125
- try {
126
- execSync('docker rm -f wavelet-risingwave', { stdio: 'ignore' })
127
- } catch {}
128
-
129
- const child = spawn('docker', [
130
- 'run', '--name', 'wavelet-risingwave',
131
- '-p', '4566:4566',
132
- 'risingwavelabs/risingwave:latest',
133
- 'playground',
134
- ], {
135
- stdio: ['ignore', 'pipe', 'pipe'],
136
- detached: false,
137
- })
138
-
139
- let started = false
140
-
141
- // Poll for connectivity
142
- const poll = setInterval(async () => {
143
- if (started) {
144
- clearInterval(poll)
145
- return
146
- }
147
- if (await isReachable('postgres://root@localhost:4566/dev')) {
148
- started = true
149
- clearInterval(poll)
150
- console.log('RisingWave started via Docker.')
151
- resolve(child)
152
- }
153
- }, 1000)
154
-
155
- child.on('error', (err) => {
156
- if (!started) {
157
- clearInterval(poll)
158
- reject(err)
159
- }
160
- })
161
-
162
- child.on('exit', (code) => {
163
- if (!started) {
164
- clearInterval(poll)
165
- reject(new Error(`Docker exited with code ${code}`))
166
- }
167
- })
168
-
169
- setTimeout(() => {
170
- if (!started) {
171
- clearInterval(poll)
172
- child.kill()
173
- reject(new Error('RisingWave Docker container failed to start within 60 seconds.'))
174
- }
175
- }, 60000)
176
- })
177
- }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src"]
8
- }