@korajs/cli 0.3.2 → 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 (50) hide show
  1. package/dist/bin.cjs +385 -288
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +25 -11
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-KTSRAPSE.js → chunk-CMSX76KM.js} +108 -65
  6. package/dist/chunk-CMSX76KM.js.map +1 -0
  7. package/dist/{chunk-ZGYRDYXS.js → chunk-MVP5PDT4.js} +22 -4
  8. package/dist/chunk-MVP5PDT4.js.map +1 -0
  9. package/dist/{chunk-E7OCVRYL.js → chunk-YGVO4POI.js} +242 -215
  10. package/dist/chunk-YGVO4POI.js.map +1 -0
  11. package/dist/create.cjs +122 -66
  12. package/dist/create.cjs.map +1 -1
  13. package/dist/create.js +2 -2
  14. package/dist/index.cjs +348 -278
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +31 -26
  17. package/dist/index.d.ts +31 -26
  18. package/dist/index.js +2 -2
  19. package/package.json +2 -2
  20. package/templates/tauri-react/.env.example +7 -0
  21. package/templates/tauri-react/.github/workflows/release-desktop.yml +107 -0
  22. package/templates/tauri-react/README.md.hbs +148 -0
  23. package/templates/tauri-react/dev.ts +37 -0
  24. package/templates/tauri-react/index.html.hbs +16 -0
  25. package/templates/tauri-react/kora.config.ts +17 -0
  26. package/templates/tauri-react/package.json.hbs +44 -0
  27. package/templates/tauri-react/server.ts +23 -0
  28. package/templates/tauri-react/src/App.tsx +578 -0
  29. package/templates/tauri-react/src/AppShell.tsx +116 -0
  30. package/templates/tauri-react/src/SetupScreen.tsx +239 -0
  31. package/templates/tauri-react/src/main.tsx +9 -0
  32. package/templates/tauri-react/src/schema.ts +15 -0
  33. package/templates/tauri-react/src/sync-config.ts +103 -0
  34. package/templates/tauri-react/src/updater.ts +38 -0
  35. package/templates/tauri-react/src-tauri/Cargo.toml +19 -0
  36. package/templates/tauri-react/src-tauri/build.rs +3 -0
  37. package/templates/tauri-react/src-tauri/capabilities/default.json +12 -0
  38. package/templates/tauri-react/src-tauri/icons/128x128.png +0 -0
  39. package/templates/tauri-react/src-tauri/icons/128x128@2x.png +0 -0
  40. package/templates/tauri-react/src-tauri/icons/32x32.png +0 -0
  41. package/templates/tauri-react/src-tauri/icons/icon.icns +0 -0
  42. package/templates/tauri-react/src-tauri/icons/icon.ico +0 -0
  43. package/templates/tauri-react/src-tauri/src/lib.rs +8 -0
  44. package/templates/tauri-react/src-tauri/src/main.rs +6 -0
  45. package/templates/tauri-react/src-tauri/tauri.conf.json +44 -0
  46. package/templates/tauri-react/tsconfig.json +15 -0
  47. package/templates/tauri-react/vite.config.ts +15 -0
  48. package/dist/chunk-E7OCVRYL.js.map +0 -1
  49. package/dist/chunk-KTSRAPSE.js.map +0 -1
  50. package/dist/chunk-ZGYRDYXS.js.map +0 -1
@@ -0,0 +1,239 @@
1
+ import { useState } from 'react'
2
+ import { Monitor, Wifi, ArrowRight, Loader2, AlertCircle } from 'lucide-react'
3
+ import { testConnection } from './sync-config'
4
+
5
+ interface SetupScreenProps {
6
+ onConnect: (syncUrl: string) => void
7
+ onSkip: () => void
8
+ }
9
+
10
+ /**
11
+ * First-launch setup screen.
12
+ *
13
+ * Shown when no sync server URL is configured. The user enters their
14
+ * organization's sync server URL, or skips to use the app in local-only mode.
15
+ *
16
+ * The connection test is advisory — if the server is unreachable (e.g.,
17
+ * during maintenance), the user can still save the URL and the app will
18
+ * connect when the server comes back.
19
+ */
20
+ export function SetupScreen({ onConnect, onSkip }: SetupScreenProps) {
21
+ const [url, setUrl] = useState('')
22
+ const [testing, setTesting] = useState(false)
23
+ const [error, setError] = useState<string | null>(null)
24
+ const [unreachable, setUnreachable] = useState(false)
25
+
26
+ const validateUrl = (value: string): string | null => {
27
+ if (!value.startsWith('ws://') && !value.startsWith('wss://')) {
28
+ return 'URL must start with ws:// or wss://'
29
+ }
30
+ try {
31
+ new URL(value.replace(/^ws/, 'http'))
32
+ } catch {
33
+ return 'Invalid URL format'
34
+ }
35
+ return null
36
+ }
37
+
38
+ const handleSubmit = async (e: React.FormEvent) => {
39
+ e.preventDefault()
40
+ const syncUrl = url.trim()
41
+ if (!syncUrl) return
42
+
43
+ const validationError = validateUrl(syncUrl)
44
+ if (validationError) {
45
+ setError(validationError)
46
+ return
47
+ }
48
+
49
+ setTesting(true)
50
+ setError(null)
51
+ setUnreachable(false)
52
+
53
+ const reachable = await testConnection(syncUrl)
54
+ setTesting(false)
55
+
56
+ if (reachable) {
57
+ onConnect(syncUrl)
58
+ } else {
59
+ // Server unreachable — let the user save anyway
60
+ setUnreachable(true)
61
+ }
62
+ }
63
+
64
+ const handleSaveAnyway = () => {
65
+ onConnect(url.trim())
66
+ }
67
+
68
+ return (
69
+ <div style={{ minHeight: '100vh', background: '#0a0a0a', color: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
70
+ <div style={{ maxWidth: '480px', width: '100%', padding: '0 24px' }}>
71
+ {/* Logo */}
72
+ <div style={{ textAlign: 'center', marginBottom: '48px' }}>
73
+ <Monitor style={{ width: '48px', height: '48px', color: '#818cf8', margin: '0 auto 16px' }} />
74
+ <h1 style={{ fontSize: '28px', fontWeight: 'bold', marginBottom: '8px' }}>Welcome</h1>
75
+ <p style={{ color: '#6b7280', fontSize: '15px' }}>
76
+ Connect to your organization's sync server to sync data across devices.
77
+ </p>
78
+ </div>
79
+
80
+ {/* Connection form */}
81
+ <form onSubmit={handleSubmit}>
82
+ <div style={{ marginBottom: '16px' }}>
83
+ <label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px', color: '#d1d5db' }}>
84
+ Sync server URL
85
+ </label>
86
+ <div style={{ position: 'relative' }}>
87
+ <Wifi style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', width: '18px', height: '18px', color: '#6b7280' }} />
88
+ <input
89
+ type="text"
90
+ value={url}
91
+ onChange={(e) => { setUrl(e.target.value); setError(null); setUnreachable(false) }}
92
+ placeholder="wss://your-server.example.com/kora-sync"
93
+ autoFocus
94
+ style={{
95
+ width: '100%',
96
+ borderRadius: '8px',
97
+ border: `1px solid ${error ? '#ef4444' : '#374151'}`,
98
+ background: '#111827',
99
+ padding: '12px 16px 12px 40px',
100
+ color: '#f3f4f6',
101
+ outline: 'none',
102
+ fontSize: '14px',
103
+ boxSizing: 'border-box',
104
+ }}
105
+ />
106
+ </div>
107
+ {error && (
108
+ <p style={{ color: '#ef4444', fontSize: '13px', marginTop: '8px' }}>{error}</p>
109
+ )}
110
+ </div>
111
+
112
+ {/* Server unreachable — offer to save anyway */}
113
+ {unreachable && (
114
+ <div style={{
115
+ marginBottom: '16px',
116
+ padding: '12px 16px',
117
+ borderRadius: '8px',
118
+ border: '1px solid #92400e',
119
+ background: '#451a03',
120
+ }}>
121
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
122
+ <AlertCircle style={{ width: '16px', height: '16px', color: '#fbbf24', flexShrink: 0, marginTop: '2px' }} />
123
+ <div>
124
+ <p style={{ fontSize: '13px', color: '#fde68a', marginBottom: '8px' }}>
125
+ Server is not reachable right now. This could be temporary (maintenance, network issues).
126
+ </p>
127
+ <div style={{ display: 'flex', gap: '8px' }}>
128
+ <button
129
+ type="button"
130
+ onClick={handleSaveAnyway}
131
+ style={{
132
+ fontSize: '13px',
133
+ color: '#fbbf24',
134
+ background: 'none',
135
+ border: '1px solid #92400e',
136
+ borderRadius: '6px',
137
+ padding: '4px 12px',
138
+ cursor: 'pointer',
139
+ }}
140
+ >
141
+ Save anyway — connect later
142
+ </button>
143
+ <button
144
+ type="submit"
145
+ style={{
146
+ fontSize: '13px',
147
+ color: '#9ca3af',
148
+ background: 'none',
149
+ border: '1px solid #374151',
150
+ borderRadius: '6px',
151
+ padding: '4px 12px',
152
+ cursor: 'pointer',
153
+ }}
154
+ >
155
+ Retry
156
+ </button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {!unreachable && (
164
+ <button
165
+ type="submit"
166
+ disabled={testing || !url.trim()}
167
+ style={{
168
+ width: '100%',
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ gap: '8px',
173
+ borderRadius: '8px',
174
+ background: '#4f46e5',
175
+ padding: '12px 20px',
176
+ fontWeight: '500',
177
+ color: 'white',
178
+ border: 'none',
179
+ cursor: testing || !url.trim() ? 'default' : 'pointer',
180
+ opacity: testing || !url.trim() ? 0.5 : 1,
181
+ fontSize: '14px',
182
+ marginBottom: '12px',
183
+ }}
184
+ >
185
+ {testing ? (
186
+ <>
187
+ <Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }} />
188
+ Testing connection...
189
+ </>
190
+ ) : (
191
+ <>
192
+ Connect
193
+ <ArrowRight style={{ width: '16px', height: '16px' }} />
194
+ </>
195
+ )}
196
+ </button>
197
+ )}
198
+ </form>
199
+
200
+ {/* Skip option */}
201
+ <div style={{ textAlign: 'center', marginTop: '24px' }}>
202
+ <button
203
+ onClick={onSkip}
204
+ style={{
205
+ background: 'none',
206
+ border: 'none',
207
+ color: '#6b7280',
208
+ cursor: 'pointer',
209
+ fontSize: '14px',
210
+ textDecoration: 'underline',
211
+ textUnderlineOffset: '3px',
212
+ }}
213
+ >
214
+ Skip — use offline only
215
+ </button>
216
+ <p style={{ color: '#374151', fontSize: '12px', marginTop: '8px' }}>
217
+ You can connect to a sync server later from settings.
218
+ </p>
219
+ </div>
220
+
221
+ {/* Help text */}
222
+ <div style={{
223
+ marginTop: '48px',
224
+ padding: '16px',
225
+ borderRadius: '8px',
226
+ border: '1px solid #1f2937',
227
+ background: '#111827',
228
+ }}>
229
+ <p style={{ fontSize: '13px', color: '#9ca3af', lineHeight: '1.6' }}>
230
+ <strong style={{ color: '#d1d5db' }}>Don't have a server URL?</strong>
231
+ <br />
232
+ Ask your administrator for the sync server address. For local development,
233
+ use <code style={{ color: '#818cf8' }}>ws://localhost:3001/kora-sync</code>.
234
+ </p>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ )
239
+ }
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { AppShell } from './AppShell'
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <StrictMode>
7
+ <AppShell />
8
+ </StrictMode>,
9
+ )
@@ -0,0 +1,15 @@
1
+ import { defineSchema, t } from '@korajs/core'
2
+
3
+ export default defineSchema({
4
+ version: 1,
5
+ collections: {
6
+ todos: {
7
+ fields: {
8
+ title: t.string(),
9
+ completed: t.boolean().default(false),
10
+ createdAt: t.timestamp().auto(),
11
+ },
12
+ indexes: ['completed'],
13
+ },
14
+ },
15
+ })
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Runtime sync server configuration.
3
+ *
4
+ * On first launch, the user enters their sync server URL (or organization URL).
5
+ * This is stored in localStorage and used for all subsequent launches.
6
+ *
7
+ * This allows a single binary to be distributed to multiple organizations,
8
+ * each pointing to their own sync server.
9
+ */
10
+
11
+ const SYNC_URL_KEY = 'kora-sync-url'
12
+ const SYNC_CONFIGURED_KEY = 'kora-sync-configured'
13
+
14
+ /**
15
+ * Get the configured sync server URL.
16
+ * Priority: localStorage → VITE_SYNC_URL env var → null (needs setup).
17
+ */
18
+ export function getSyncUrl(): string | null {
19
+ const stored = localStorage.getItem(SYNC_URL_KEY)
20
+ if (stored) return stored
21
+
22
+ // Compile-time default (set via VITE_SYNC_URL when building)
23
+ const envUrl = import.meta.env.VITE_SYNC_URL
24
+ if (envUrl) return envUrl
25
+
26
+ return null
27
+ }
28
+
29
+ /** Save a sync server URL to persistent local config. */
30
+ export function setSyncUrl(url: string): void {
31
+ localStorage.setItem(SYNC_URL_KEY, url)
32
+ localStorage.setItem(SYNC_CONFIGURED_KEY, 'true')
33
+ }
34
+
35
+ /** Clear the stored sync server URL (returns to setup screen). */
36
+ export function clearSyncUrl(): void {
37
+ localStorage.removeItem(SYNC_URL_KEY)
38
+ localStorage.removeItem(SYNC_CONFIGURED_KEY)
39
+ }
40
+
41
+ /**
42
+ * Whether the user has completed initial setup (connected or explicitly skipped).
43
+ * This is separate from getSyncUrl() — a user who skipped still completed setup.
44
+ */
45
+ export function hasCompletedSetup(): boolean {
46
+ // If there's a compile-time URL, setup is not needed
47
+ if (import.meta.env.VITE_SYNC_URL) return true
48
+ return localStorage.getItem(SYNC_CONFIGURED_KEY) === 'true'
49
+ }
50
+
51
+ /** Mark setup as completed (used when user skips sync). */
52
+ export function markSetupComplete(): void {
53
+ localStorage.setItem(SYNC_CONFIGURED_KEY, 'true')
54
+ }
55
+
56
+ /**
57
+ * Full factory reset: clears sync config and all local data.
58
+ * The app reloads with a fresh state, showing the setup screen.
59
+ */
60
+ export function factoryReset(): void {
61
+ // Clear sync config
62
+ clearSyncUrl()
63
+ localStorage.removeItem(SYNC_CONFIGURED_KEY)
64
+
65
+ // Clear all Kora local data by removing IndexedDB databases
66
+ // and any other localStorage keys the app may have set.
67
+ const keysToRemove: string[] = []
68
+ for (let i = 0; i < localStorage.length; i++) {
69
+ const key = localStorage.key(i)
70
+ if (key && key.startsWith('kora-')) {
71
+ keysToRemove.push(key)
72
+ }
73
+ }
74
+ for (const key of keysToRemove) {
75
+ localStorage.removeItem(key)
76
+ }
77
+
78
+ // Reload to reinitialize everything from scratch
79
+ window.location.reload()
80
+ }
81
+
82
+ /** Test if a WebSocket URL is reachable by attempting a brief connection. */
83
+ export function testConnection(url: string, timeoutMs = 5000): Promise<boolean> {
84
+ return new Promise((resolve) => {
85
+ const timeout = setTimeout(() => {
86
+ ws.close()
87
+ resolve(false)
88
+ }, timeoutMs)
89
+
90
+ const ws = new WebSocket(url)
91
+
92
+ ws.onopen = () => {
93
+ clearTimeout(timeout)
94
+ ws.close()
95
+ resolve(true)
96
+ }
97
+
98
+ ws.onerror = () => {
99
+ clearTimeout(timeout)
100
+ resolve(false)
101
+ }
102
+ })
103
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auto-updater integration via tauri-plugin-updater.
3
+ *
4
+ * Checks for updates on app launch and periodically. When an update is
5
+ * available, the user is notified and can install it with a restart.
6
+ *
7
+ * Configure the update endpoint in src-tauri/tauri.conf.json under
8
+ * plugins.updater.endpoints. For GitHub Releases, use:
9
+ * "endpoints": ["https://github.com/YOUR_ORG/YOUR_REPO/releases/latest/download/latest.json"]
10
+ *
11
+ * To generate signing keys: `pnpm tauri signer generate -w ~/.tauri/myapp.key`
12
+ * Set the pubkey in tauri.conf.json and TAURI_SIGNING_PRIVATE_KEY in CI.
13
+ */
14
+
15
+ let updateChecked = false
16
+
17
+ /**
18
+ * Check for updates once on app startup.
19
+ * Silently skips if the updater is not configured (empty endpoints/pubkey).
20
+ */
21
+ export async function checkForUpdates(): Promise<void> {
22
+ if (updateChecked) return
23
+ updateChecked = true
24
+
25
+ try {
26
+ const { check } = await import('@tauri-apps/plugin-updater')
27
+ const update = await check()
28
+ if (update) {
29
+ console.log(`Update available: v${update.version}`)
30
+ // Download and install — the app will restart automatically
31
+ await update.downloadAndInstall()
32
+ }
33
+ } catch (err) {
34
+ // Updater not configured or network unavailable — this is fine.
35
+ // The app works fully offline; updates are best-effort.
36
+ console.debug('Update check skipped:', err)
37
+ }
38
+ }
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "app"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "app_lib"
8
+ crate-type = ["staticlib", "cdylib", "rlib"]
9
+
10
+ [build-dependencies]
11
+ tauri-build = { version = "2", features = [] }
12
+
13
+ [dependencies]
14
+ tauri = { version = "2", features = [] }
15
+ tauri-plugin-opener = "2"
16
+ tauri-plugin-updater = "2"
17
+ tauri-plugin-kora-sqlite = { version = "0.1", path = "../node_modules/@korajs/tauri/plugin" }
18
+ serde = { version = "1", features = ["derive"] }
19
+ serde_json = "1"
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build();
3
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "../gen/schemas/desktop-schema.json",
3
+ "identifier": "default",
4
+ "description": "Capability for the main window",
5
+ "windows": ["main"],
6
+ "permissions": [
7
+ "core:default",
8
+ "opener:default",
9
+ "kora-sqlite:default",
10
+ "updater:default"
11
+ ]
12
+ }
@@ -0,0 +1,8 @@
1
+ pub fn run() {
2
+ tauri::Builder::default()
3
+ .plugin(tauri_plugin_opener::init())
4
+ .plugin(tauri_plugin_updater::Builder::new().build())
5
+ .plugin(tauri_plugin_kora_sqlite::init())
6
+ .run(tauri::generate_context!())
7
+ .expect("error while running tauri application");
8
+ }
@@ -0,0 +1,6 @@
1
+ // Prevents additional console window on Windows in release
2
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
+
4
+ fn main() {
5
+ app_lib::run();
6
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nicholasgriffintn/tauri/refs/heads/dev/crates/tauri-config-schema/schema.json",
3
+ "productName": "kora-app",
4
+ "version": "0.0.1",
5
+ "identifier": "com.kora.app",
6
+ "build": {
7
+ "frontendDist": "../dist",
8
+ "devUrl": "http://localhost:5173",
9
+ "beforeDevCommand": "vite",
10
+ "beforeBuildCommand": "vite build"
11
+ },
12
+ "app": {
13
+ "windows": [
14
+ {
15
+ "title": "Kora Desktop App",
16
+ "width": 900,
17
+ "height": 700,
18
+ "resizable": true,
19
+ "fullscreen": false
20
+ }
21
+ ],
22
+ "security": {
23
+ "csp": null
24
+ }
25
+ },
26
+ "bundle": {
27
+ "active": true,
28
+ "targets": "all",
29
+ "icon": [
30
+ "icons/32x32.png",
31
+ "icons/128x128.png",
32
+ "icons/128x128@2x.png",
33
+ "icons/icon.icns",
34
+ "icons/icon.ico"
35
+ ],
36
+ "createUpdaterArtifacts": false
37
+ },
38
+ "plugins": {
39
+ "updater": {
40
+ "pubkey": "",
41
+ "endpoints": []
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "outDir": "dist",
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,15 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import { defineConfig } from 'vite'
3
+
4
+ // Tauri apps use native SQLite — no WASM, no OPFS, no cross-origin isolation needed.
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ // Prevent Vite from obscuring Rust compilation errors
8
+ clearScreen: false,
9
+ server: {
10
+ // Tauri expects a fixed port; fail if it's not available
11
+ strictPort: true,
12
+ },
13
+ // Environment variables with this prefix are exposed to the app
14
+ envPrefix: ['VITE_', 'TAURI_'],
15
+ })