craft-native 0.0.37

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 (117) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +424 -0
  3. package/bin/cli.ts +937 -0
  4. package/dist/api/android-advanced.d.ts +737 -0
  5. package/dist/api/android-advanced.d.ts.map +1 -0
  6. package/dist/api/app.d.ts +432 -0
  7. package/dist/api/app.d.ts.map +1 -0
  8. package/dist/api/clipboard.d.ts +85 -0
  9. package/dist/api/clipboard.d.ts.map +1 -0
  10. package/dist/api/crypto.d.ts +79 -0
  11. package/dist/api/crypto.d.ts.map +1 -0
  12. package/dist/api/db.d.ts +427 -0
  13. package/dist/api/db.d.ts.map +1 -0
  14. package/dist/api/dialog.d.ts +141 -0
  15. package/dist/api/dialog.d.ts.map +1 -0
  16. package/dist/api/fs.d.ts +131 -0
  17. package/dist/api/fs.d.ts.map +1 -0
  18. package/dist/api/http.d.ts +196 -0
  19. package/dist/api/http.d.ts.map +1 -0
  20. package/dist/api/index.d.ts +38 -0
  21. package/dist/api/index.d.ts.map +1 -0
  22. package/dist/api/ios-advanced.d.ts +833 -0
  23. package/dist/api/ios-advanced.d.ts.map +1 -0
  24. package/dist/api/macos-advanced.d.ts +721 -0
  25. package/dist/api/macos-advanced.d.ts.map +1 -0
  26. package/dist/api/media.d.ts +233 -0
  27. package/dist/api/media.d.ts.map +1 -0
  28. package/dist/api/mobile.d.ts +658 -0
  29. package/dist/api/mobile.d.ts.map +1 -0
  30. package/dist/api/process.d.ts +168 -0
  31. package/dist/api/process.d.ts.map +1 -0
  32. package/dist/api/sidebar.d.ts +125 -0
  33. package/dist/api/sidebar.d.ts.map +1 -0
  34. package/dist/api/tray.d.ts +410 -0
  35. package/dist/api/tray.d.ts.map +1 -0
  36. package/dist/api/window.d.ts +462 -0
  37. package/dist/api/window.d.ts.map +1 -0
  38. package/dist/api/windows-advanced.d.ts +719 -0
  39. package/dist/api/windows-advanced.d.ts.map +1 -0
  40. package/dist/assets/index.d.ts +69 -0
  41. package/dist/assets/index.d.ts.map +1 -0
  42. package/dist/binary-resolver.d.ts +39 -0
  43. package/dist/binary-resolver.d.ts.map +1 -0
  44. package/dist/bridge/android.d.ts +574 -0
  45. package/dist/bridge/android.d.ts.map +1 -0
  46. package/dist/bridge/core.d.ts +474 -0
  47. package/dist/bridge/core.d.ts.map +1 -0
  48. package/dist/bridge/ids.d.ts +18 -0
  49. package/dist/bridge/ids.d.ts.map +1 -0
  50. package/dist/bridge/ios.d.ts +528 -0
  51. package/dist/bridge/ios.d.ts.map +1 -0
  52. package/dist/bridge/webkit-pending.d.ts +50 -0
  53. package/dist/bridge/webkit-pending.d.ts.map +1 -0
  54. package/dist/build/bundle-optimizer.d.ts +69 -0
  55. package/dist/build/bundle-optimizer.d.ts.map +1 -0
  56. package/dist/components/index.d.ts +719 -0
  57. package/dist/components/index.d.ts.map +1 -0
  58. package/dist/components/native.d.ts +728 -0
  59. package/dist/components/native.d.ts.map +1 -0
  60. package/dist/components/sidebar.d.ts +458 -0
  61. package/dist/components/sidebar.d.ts.map +1 -0
  62. package/dist/dev/devtools.d.ts +118 -0
  63. package/dist/dev/devtools.d.ts.map +1 -0
  64. package/dist/dev/error-overlay.d.ts +73 -0
  65. package/dist/dev/error-overlay.d.ts.map +1 -0
  66. package/dist/dev/hot-reload.d.ts +127 -0
  67. package/dist/dev/hot-reload.d.ts.map +1 -0
  68. package/dist/index.cjs +10253 -0
  69. package/dist/index.d.cts +59 -0
  70. package/dist/index.d.ts +59 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +10240 -0
  73. package/dist/logging/index.d.ts +152 -0
  74. package/dist/logging/index.d.ts.map +1 -0
  75. package/dist/optimizations/index.d.ts +232 -0
  76. package/dist/optimizations/index.d.ts.map +1 -0
  77. package/dist/package.d.ts +90 -0
  78. package/dist/package.d.ts.map +1 -0
  79. package/dist/performance/runtime.d.ts +230 -0
  80. package/dist/performance/runtime.d.ts.map +1 -0
  81. package/dist/performance/startup.d.ts +223 -0
  82. package/dist/performance/startup.d.ts.map +1 -0
  83. package/dist/plugins/index.d.ts +114 -0
  84. package/dist/plugins/index.d.ts.map +1 -0
  85. package/dist/security/index.d.ts +268 -0
  86. package/dist/security/index.d.ts.map +1 -0
  87. package/dist/signing/index.d.ts +104 -0
  88. package/dist/signing/index.d.ts.map +1 -0
  89. package/dist/styles/headwind.d.ts +222 -0
  90. package/dist/styles/headwind.d.ts.map +1 -0
  91. package/dist/styles/index.d.ts +31 -0
  92. package/dist/styles/index.d.ts.map +1 -0
  93. package/dist/styles/sidebar-templates.d.ts +77 -0
  94. package/dist/styles/sidebar-templates.d.ts.map +1 -0
  95. package/dist/styles/sidebars.d.ts +194 -0
  96. package/dist/styles/sidebars.d.ts.map +1 -0
  97. package/dist/types.d.ts +1555 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/updater/index.d.ts +164 -0
  100. package/dist/updater/index.d.ts.map +1 -0
  101. package/dist/utils/audio.d.ts +40 -0
  102. package/dist/utils/audio.d.ts.map +1 -0
  103. package/dist/utils/index.d.ts +9 -0
  104. package/dist/utils/index.d.ts.map +1 -0
  105. package/dist/utils/native.d.ts +27 -0
  106. package/dist/utils/native.d.ts.map +1 -0
  107. package/dist/utils/react.d.ts +157 -0
  108. package/dist/utils/react.d.ts.map +1 -0
  109. package/dist/utils/storage.d.ts +35 -0
  110. package/dist/utils/storage.d.ts.map +1 -0
  111. package/dist/utils/svelte.d.ts +187 -0
  112. package/dist/utils/svelte.d.ts.map +1 -0
  113. package/dist/utils/timer.d.ts +57 -0
  114. package/dist/utils/timer.d.ts.map +1 -0
  115. package/dist/utils/vue.d.ts +157 -0
  116. package/dist/utils/vue.d.ts.map +1 -0
  117. package/package.json +78 -0
package/bin/cli.ts ADDED
@@ -0,0 +1,937 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Craft CLI - Build desktop apps with web languages
5
+ */
6
+
7
+ import { CLI } from '@stacksjs/clapp'
8
+ import { spawn } from 'node:child_process'
9
+ import { existsSync } from 'node:fs'
10
+ import process from 'node:process'
11
+ import { craftBinaryNotFoundMessage, resolveCraftBinary } from '../src/binary-resolver'
12
+ import { version } from '../package.json'
13
+
14
+ const cli = new CLI('craft')
15
+
16
+ /**
17
+ * Run the `craft` native binary with the given argv. Craft ships through
18
+ * the pantry registry, so the binary is expected to be on PATH after
19
+ * `pantry install craft`. The shared resolver respects an explicit
20
+ * `CRAFT_BIN` for tests and the monorepo dev loop; everything else
21
+ * delegates to PATH. ENOENT surfaces a single, pantry-pointing error.
22
+ */
23
+ async function runCraftBinary(args: string[]): Promise<void> {
24
+ const craftPath = resolveCraftBinary()
25
+
26
+ return new Promise((resolve, reject) => {
27
+ const proc = spawn(craftPath, args, { stdio: 'inherit' })
28
+
29
+ proc.on('exit', (code) => {
30
+ if (code === 0 || code === null) resolve()
31
+ else reject(new Error(`Craft exited with code ${code}`))
32
+ })
33
+
34
+ proc.on('error', (error: Error & { code?: string }) => {
35
+ if (error.code === 'ENOENT') {
36
+ reject(new Error(craftBinaryNotFoundMessage(craftPath)))
37
+ return
38
+ }
39
+ reject(error)
40
+ })
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Coerce a CLI value to an integer in [min, max], throwing a friendly
46
+ * error on invalid input. Used so `--width abc` and `--width 9999999999`
47
+ * produce a clear message instead of a downstream crash.
48
+ */
49
+ function parseIntInRange(name: string, value: unknown, min: number, max: number): number {
50
+ const n = typeof value === 'number' ? value : Number.parseInt(String(value), 10)
51
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
52
+ throw new Error(`--${name} must be an integer (got: ${String(value)})`)
53
+ }
54
+ if (n < min || n > max) {
55
+ throw new Error(`--${name} must be between ${min} and ${max} (got: ${n})`)
56
+ }
57
+ return n
58
+ }
59
+
60
+ /**
61
+ * Validate `--url` against URL parsing. Allows `http`, `https`, `file`, and
62
+ * `data:` schemes; everything else (e.g. `javascript:`) is rejected so a
63
+ * malicious shortcut can't bypass the SDK's own URL handling.
64
+ */
65
+ function validateUrl(value: string): string {
66
+ let parsed: URL
67
+ try {
68
+ parsed = new URL(value)
69
+ }
70
+ catch {
71
+ throw new Error(`--url must be a valid URL (got: ${value})`)
72
+ }
73
+ const allowed = new Set(['http:', 'https:', 'file:', 'data:'])
74
+ if (!allowed.has(parsed.protocol)) {
75
+ throw new Error(`--url uses unsupported scheme "${parsed.protocol}"; allowed: ${[...allowed].join(', ')}`)
76
+ }
77
+ return parsed.toString()
78
+ }
79
+
80
+ // Default command - launch app with URL
81
+ cli
82
+ .command('[url]', 'Launch a Craft desktop app')
83
+ .option('--title <title>', 'Window title')
84
+ .option('--width <width>', 'Window width', { default: 800 })
85
+ .option('--height <height>', 'Window height', { default: 600 })
86
+ .option('--x <x>', 'Window x position')
87
+ .option('--y <y>', 'Window y position')
88
+ .option('--frameless', 'Frameless window')
89
+ .option('--transparent', 'Transparent window')
90
+ .option('--always-on-top', 'Always on top')
91
+ .option('--fullscreen', 'Start in fullscreen')
92
+ .option('--dark-mode', 'Enable dark mode')
93
+ .option('--hot-reload', 'Enable hot reload')
94
+ .option('--dev-tools', 'Enable developer tools')
95
+ .option('--no-resize', 'Disable window resizing')
96
+ .example('craft http://localhost:3000')
97
+ .example('craft http://localhost:3000 --title "My App" --width 1200 --height 800')
98
+ .example('craft http://localhost:3000 --frameless --transparent --always-on-top')
99
+ .action(async (url?: string, options?: any) => {
100
+ const args: string[] = []
101
+
102
+ try {
103
+ if (url) {
104
+ args.push('--url', validateUrl(url))
105
+ }
106
+
107
+ if (options?.title) {
108
+ const title = String(options.title)
109
+ if (title.length > 256) throw new Error('--title must be 256 chars or fewer')
110
+ args.push('--title', title)
111
+ }
112
+ if (options?.width !== undefined)
113
+ args.push('--width', String(parseIntInRange('width', options.width, 100, 10_000)))
114
+ if (options?.height !== undefined)
115
+ args.push('--height', String(parseIntInRange('height', options.height, 100, 10_000)))
116
+ if (options?.x !== undefined)
117
+ args.push('--x', String(parseIntInRange('x', options.x, -100_000, 100_000)))
118
+ if (options?.y !== undefined)
119
+ args.push('--y', String(parseIntInRange('y', options.y, -100_000, 100_000)))
120
+
121
+ if (options?.frameless) args.push('--frameless')
122
+ if (options?.transparent) args.push('--transparent')
123
+ if (options?.alwaysOnTop) args.push('--always-on-top')
124
+ if (options?.fullscreen) args.push('--fullscreen')
125
+ if (options?.darkMode) args.push('--dark-mode')
126
+ if (options?.hotReload) args.push('--hot-reload')
127
+ if (options?.devTools) args.push('--dev-tools')
128
+ if (options?.noResize) args.push('--no-resize')
129
+ }
130
+ catch (validationError) {
131
+ console.error(`Error: ${(validationError as Error).message}`)
132
+ process.exit(2)
133
+ }
134
+
135
+ try {
136
+ await runCraftBinary(args)
137
+ }
138
+ catch (error: any) {
139
+ console.error('Error:', error.message)
140
+ process.exit(1)
141
+ }
142
+ })
143
+
144
+ // Version command
145
+ cli
146
+ .command('version', 'Show the version')
147
+ .action(() => {
148
+ console.log(`v${version}`)
149
+ })
150
+
151
+ // Build command
152
+ cli
153
+ .command('build', 'Build the Craft application')
154
+ .option('--release', 'Build in release mode', { default: false })
155
+ .option('--platform <platforms>', 'Target platforms (ios,android,macos,windows,linux)', { default: 'current' })
156
+ .option('--config <path>', 'Path to craft.config.ts')
157
+ .example('craft build')
158
+ .example('craft build --release')
159
+ .example('craft build --platform ios,android')
160
+ .example('craft build --platform macos,windows,linux')
161
+ .action(async (options?: any) => {
162
+ const platforms = options?.platform === 'current'
163
+ ? [process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : 'linux']
164
+ : options?.platform.split(',').map((p: string) => p.trim())
165
+
166
+ console.log('\n⚡ Craft Build\n')
167
+ console.log(`Platforms: ${platforms.join(', ')}`)
168
+ console.log(`Mode: ${options?.release ? 'Release' : 'Debug'}\n`)
169
+
170
+ const { existsSync } = await import('node:fs')
171
+ const { spawn } = await import('node:child_process')
172
+
173
+ for (const platform of platforms) {
174
+ console.log(`📦 Building for ${platform}...`)
175
+
176
+ try {
177
+ if (platform === 'ios') {
178
+ if (process.platform !== 'darwin') {
179
+ console.log(` ⚠️ iOS builds require macOS`)
180
+ continue
181
+ }
182
+ const iosDir = './ios'
183
+ if (existsSync(iosDir)) {
184
+ const buildType = options?.release ? 'Release' : 'Debug'
185
+ await new Promise<void>((resolve, reject) => {
186
+ const proc = spawn('xcodebuild', [
187
+ '-project', `${iosDir}/App.xcodeproj`,
188
+ '-scheme', 'App',
189
+ '-configuration', buildType,
190
+ '-sdk', 'iphoneos',
191
+ 'build'
192
+ ], { stdio: 'inherit' })
193
+ proc.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)))
194
+ proc.on('error', reject)
195
+ })
196
+ console.log(` ✅ iOS ${buildType} build complete`)
197
+ }
198
+ else {
199
+ console.log(` ⚠️ No iOS project found. Run: craft ios init`)
200
+ }
201
+ }
202
+
203
+ else if (platform === 'android') {
204
+ const androidDir = './android'
205
+ if (existsSync(androidDir)) {
206
+ const task = options?.release ? 'assembleRelease' : 'assembleDebug'
207
+ await new Promise<void>((resolve, reject) => {
208
+ const proc = spawn('./gradlew', [task], {
209
+ cwd: androidDir,
210
+ stdio: 'inherit'
211
+ })
212
+ proc.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)))
213
+ proc.on('error', reject)
214
+ })
215
+ console.log(` ✅ Android ${options?.release ? 'Release' : 'Debug'} build complete`)
216
+ }
217
+ else {
218
+ console.log(` ⚠️ No Android project found. Run: craft android init`)
219
+ }
220
+ }
221
+
222
+ else if (platform === 'macos' || platform === 'windows' || platform === 'linux') {
223
+ const zigDir = existsSync('./packages/zig') ? './packages/zig' : '.'
224
+ const optimizeFlag = options?.release ? '-Doptimize=ReleaseSafe' : ''
225
+ await new Promise<void>((resolve, reject) => {
226
+ const args = ['build']
227
+ if (optimizeFlag) args.push(optimizeFlag)
228
+ const proc = spawn('zig', args, {
229
+ cwd: zigDir,
230
+ stdio: 'inherit'
231
+ })
232
+ proc.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)))
233
+ proc.on('error', reject)
234
+ })
235
+ console.log(` ✅ ${platform} build complete`)
236
+ }
237
+
238
+ }
239
+ catch (error: any) {
240
+ console.error(` ❌ ${platform} build failed: ${error.message}`)
241
+ }
242
+ }
243
+
244
+ console.log('\n✨ Build complete\n')
245
+ })
246
+
247
+ // Dev command - launch with hot reload and dev tools enabled
248
+ cli
249
+ .command('dev [url]', 'Launch in development mode with hot reload')
250
+ .option('--title <title>', 'Window title', { default: 'Craft Dev' })
251
+ .option('--width <width>', 'Window width', { default: 1200 })
252
+ .option('--height <height>', 'Window height', { default: 800 })
253
+ .example('craft dev http://localhost:3000')
254
+ .action(async (url?: string, options?: any) => {
255
+ const args = [
256
+ '--url',
257
+ url || 'http://localhost:3000',
258
+ '--title',
259
+ options?.title || 'Craft Dev',
260
+ '--width',
261
+ String(options?.width || 1200),
262
+ '--height',
263
+ String(options?.height || 800),
264
+ '--hot-reload',
265
+ '--dev-tools',
266
+ '--dark-mode',
267
+ ]
268
+
269
+ try {
270
+ await runCraftBinary(args)
271
+ }
272
+ catch (error: any) {
273
+ console.error('Error:', error.message)
274
+ process.exit(1)
275
+ }
276
+ })
277
+
278
+ // Package command - create installers
279
+ cli
280
+ .command('package', 'Create installers for your Craft application')
281
+ .option('--name <name>', 'Application name')
282
+ .option('--version <version>', 'Application version')
283
+ .option('--binary <path>', 'Path to application binary')
284
+ .option('--description <text>', 'Application description')
285
+ .option('--author <name>', 'Author/Maintainer name')
286
+ .option('--bundle-id <id>', 'Bundle identifier (macOS/iOS)')
287
+ .option('--out <dir>', 'Output directory (default: ./dist)')
288
+ .option('--icon <path>', 'Application icon path')
289
+ .option('--platforms <list>', 'Comma-separated platforms (macos,windows,linux)')
290
+ .option('--config <path>', 'Load config from JSON file')
291
+ .option('--dmg', 'Create DMG installer (macOS)')
292
+ .option('--pkg', 'Create PKG installer (macOS)')
293
+ .option('--msi', 'Create MSI installer (Windows)')
294
+ .option('--zip', 'Create ZIP archive (Windows)')
295
+ .option('--deb', 'Create DEB package (Linux)')
296
+ .option('--rpm', 'Create RPM package (Linux)')
297
+ .option('--appimage', 'Create AppImage (Linux)')
298
+ .example('craft package --name "My App" --version "1.0.0" --binary ./build/myapp')
299
+ .example('craft package --config package.json')
300
+ .example('craft package --name "My App" --version "1.0.0" --binary ./build/myapp --platforms macos,windows,linux')
301
+ .action(async (options?: any) => {
302
+ const packageModule = await import('../src/package.js')
303
+ const { packageApp } = packageModule
304
+
305
+ let config: any
306
+
307
+ // Load config from file or CLI args
308
+ if (options?.config) {
309
+ if (!existsSync(options.config)) {
310
+ console.error(`❌ Config file not found: ${options.config}`)
311
+ process.exit(1)
312
+ }
313
+
314
+ const configContent = await Bun.file(options.config).text()
315
+ config = JSON.parse(configContent)
316
+ }
317
+ else {
318
+ // Build config from CLI options
319
+ if (!options?.name) {
320
+ console.error('❌ --name is required')
321
+ process.exit(1)
322
+ }
323
+ if (!options?.version) {
324
+ console.error('❌ --version is required')
325
+ process.exit(1)
326
+ }
327
+ if (!options?.binary) {
328
+ console.error('❌ --binary is required')
329
+ process.exit(1)
330
+ }
331
+
332
+ config = {
333
+ name: options.name,
334
+ version: options.version,
335
+ binaryPath: options.binary,
336
+ description: options.description,
337
+ author: options.author,
338
+ bundleId: options.bundleId,
339
+ outDir: options.out,
340
+ iconPath: options.icon,
341
+ platforms: options.platforms?.split(','),
342
+ }
343
+
344
+ // Platform-specific options
345
+ if (options.dmg || options.pkg) {
346
+ config.macos = {
347
+ dmg: options.dmg,
348
+ pkg: options.pkg,
349
+ }
350
+ }
351
+
352
+ if (options.msi || options.zip) {
353
+ config.windows = {
354
+ msi: options.msi,
355
+ zip: options.zip,
356
+ }
357
+ }
358
+
359
+ if (options.deb || options.rpm || options.appimage) {
360
+ config.linux = {
361
+ deb: options.deb,
362
+ rpm: options.rpm,
363
+ appImage: options.appimage,
364
+ }
365
+ }
366
+ }
367
+
368
+ console.log('📦 Craft Packaging Tool\n')
369
+ console.log(`Application: ${config.name} v${config.version}`)
370
+ console.log(`Platforms: ${(config.platforms || ['current']).join(', ')}\n`)
371
+
372
+ try {
373
+ const results = await packageApp(config)
374
+
375
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
376
+ console.log('📊 Packaging Results\n')
377
+
378
+ for (const result of results) {
379
+ const status = result.success ? '✅' : '❌'
380
+ const format = result.format.toUpperCase()
381
+
382
+ console.log(`${status} ${result.platform}/${format}`)
383
+
384
+ if (result.success && result.outputPath) {
385
+ console.log(` 📁 ${result.outputPath}`)
386
+ }
387
+ else if (result.error) {
388
+ console.log(` ⚠️ ${result.error}`)
389
+ }
390
+ }
391
+
392
+ const successCount = results.filter(r => r.success).length
393
+ const totalCount = results.length
394
+
395
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
396
+ console.log(`\n✨ Complete: ${successCount}/${totalCount} packages created\n`)
397
+
398
+ process.exit(successCount === totalCount ? 0 : 1)
399
+ }
400
+ catch (error: any) {
401
+ console.error(`\n❌ Error: ${error.message}\n`)
402
+ process.exit(1)
403
+ }
404
+ })
405
+
406
+ // iOS commands
407
+ cli
408
+ .command('ios init <name>', 'Initialize a new iOS project')
409
+ .option('--bundle-id <id>', 'Bundle identifier (e.g., com.example.app)')
410
+ .option('--team-id <id>', 'Apple Developer Team ID')
411
+ .option('-o, --output <dir>', 'Output directory', { default: './ios' })
412
+ .example('craft ios init MyApp')
413
+ .example('craft ios init MyApp --bundle-id com.example.myapp')
414
+ .action(async (name: string, options?: any) => {
415
+ // @ts-ignore -- sibling package may not exist at typecheck time
416
+ const iosModule = await import('../../ios/dist/index.js')
417
+ await iosModule.init({
418
+ name,
419
+ bundleId: options?.bundleId,
420
+ teamId: options?.teamId,
421
+ output: options?.output || './ios',
422
+ })
423
+ })
424
+
425
+ cli
426
+ .command('ios build', 'Build iOS project')
427
+ .option('--html-path <path>', 'Path to HTML file')
428
+ .option('-d, --dev-server <url>', 'Development server URL')
429
+ .option('-o, --output <dir>', 'iOS project directory', { default: './ios' })
430
+ .option('-w, --watch', 'Watch for file changes and rebuild')
431
+ .example('craft ios build')
432
+ .example('craft ios build --html-path ./dist/index.html')
433
+ .example('craft ios build --dev-server http://localhost:3456')
434
+ .example('craft ios build --watch')
435
+ .action(async (options?: any) => {
436
+ // @ts-ignore -- sibling package may not exist at typecheck time
437
+ const iosModule = await import('../../ios/dist/index.js')
438
+
439
+ const doBuild = async () => {
440
+ await iosModule.build({
441
+ htmlPath: options?.htmlPath,
442
+ devServer: options?.devServer,
443
+ output: options?.output || './ios',
444
+ })
445
+ }
446
+
447
+ await doBuild()
448
+
449
+ if (options?.watch) {
450
+ console.log('\n👀 Watching for changes...\n')
451
+ const { watch } = await import('node:fs')
452
+ const { dirname } = await import('node:path')
453
+
454
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : '.'
455
+ watch(watchPath, { recursive: true }, async (event, filename) => {
456
+ if (filename && (filename.endsWith('.html') || filename.endsWith('.js') || filename.endsWith('.css'))) {
457
+ console.log(`\n📝 ${filename} changed, rebuilding...`)
458
+ await doBuild()
459
+ }
460
+ })
461
+
462
+ // Keep process running
463
+ await new Promise(() => {})
464
+ }
465
+ })
466
+
467
+ cli
468
+ .command('ios open', 'Open iOS project in Xcode')
469
+ .option('-o, --output <dir>', 'iOS project directory', { default: './ios' })
470
+ .action(async (options?: any) => {
471
+ // @ts-ignore -- sibling package may not exist at typecheck time
472
+ const iosModule = await import('../../ios/dist/index.js')
473
+ await iosModule.open({
474
+ output: options?.output || './ios',
475
+ })
476
+ })
477
+
478
+ cli
479
+ .command('ios run', 'Build and run on iOS device or simulator')
480
+ .option('-s, --simulator', 'Run on simulator instead of device')
481
+ .option('-o, --output <dir>', 'iOS project directory', { default: './ios' })
482
+ .example('craft ios run')
483
+ .example('craft ios run --simulator')
484
+ .action(async (options?: any) => {
485
+ // @ts-ignore -- sibling package may not exist at typecheck time
486
+ const iosModule = await import('../../ios/dist/index.js')
487
+ await iosModule.run({
488
+ simulator: options?.simulator || false,
489
+ output: options?.output || './ios',
490
+ })
491
+ })
492
+
493
+ // Android commands
494
+ cli
495
+ .command('android init <name>', 'Initialize a new Android project')
496
+ .option('--package <name>', 'Package name (e.g., com.example.app)')
497
+ .option('-o, --output <dir>', 'Output directory', { default: './android' })
498
+ .example('craft android init MyApp')
499
+ .example('craft android init MyApp --package com.example.myapp')
500
+ .action(async (name: string, options?: any) => {
501
+ // @ts-ignore -- sibling package may not exist at typecheck time
502
+ const androidModule = await import('../../android/dist/index.js')
503
+ await androidModule.init({
504
+ name,
505
+ packageName: options?.package,
506
+ output: options?.output || './android',
507
+ })
508
+ })
509
+
510
+ cli
511
+ .command('android build', 'Build Android project')
512
+ .option('--html-path <path>', 'Path to HTML file')
513
+ .option('-d, --dev-server <url>', 'Development server URL')
514
+ .option('-o, --output <dir>', 'Android project directory', { default: './android' })
515
+ .option('--release', 'Build release APK')
516
+ .option('-w, --watch', 'Watch for file changes and rebuild')
517
+ .example('craft android build')
518
+ .example('craft android build --release')
519
+ .example('craft android build --watch')
520
+ .action(async (options?: any) => {
521
+ // @ts-ignore -- sibling package may not exist at typecheck time
522
+ const androidModule = await import('../../android/dist/index.js')
523
+
524
+ const doBuild = async () => {
525
+ await androidModule.build({
526
+ htmlPath: options?.htmlPath,
527
+ devServer: options?.devServer,
528
+ output: options?.output || './android',
529
+ release: options?.release || false,
530
+ })
531
+ }
532
+
533
+ await doBuild()
534
+
535
+ if (options?.watch) {
536
+ console.log('\n👀 Watching for changes...\n')
537
+ const { watch } = await import('node:fs')
538
+ const { dirname } = await import('node:path')
539
+
540
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : '.'
541
+ watch(watchPath, { recursive: true }, async (event, filename) => {
542
+ if (filename && (filename.endsWith('.html') || filename.endsWith('.js') || filename.endsWith('.css'))) {
543
+ console.log(`\n📝 ${filename} changed, rebuilding...`)
544
+ await doBuild()
545
+ }
546
+ })
547
+
548
+ // Keep process running
549
+ await new Promise(() => {})
550
+ }
551
+ })
552
+
553
+ cli
554
+ .command('android open', 'Open Android project in Android Studio')
555
+ .option('-o, --output <dir>', 'Android project directory', { default: './android' })
556
+ .action(async (options?: any) => {
557
+ // @ts-ignore -- sibling package may not exist at typecheck time
558
+ const androidModule = await import('../../android/dist/index.js')
559
+ await androidModule.open({
560
+ output: options?.output || './android',
561
+ })
562
+ })
563
+
564
+ cli
565
+ .command('android run', 'Build and run on Android device or emulator')
566
+ .option('-d, --device <id>', 'Target device ID')
567
+ .option('-o, --output <dir>', 'Android project directory', { default: './android' })
568
+ .example('craft android run')
569
+ .example('craft android run --device emulator-5554')
570
+ .action(async (options?: any) => {
571
+ // @ts-ignore -- sibling package may not exist at typecheck time
572
+ const androidModule = await import('../../android/dist/index.js')
573
+ await androidModule.run({
574
+ device: options?.device,
575
+ output: options?.output || './android',
576
+ })
577
+ })
578
+
579
+ // Preview command - preview app in browser
580
+ cli
581
+ .command('preview [path]', 'Preview app in browser before building native')
582
+ .option('-p, --port <port>', 'Port to serve on', { default: 3456 })
583
+ .option('--host <host>', 'Host to bind to', { default: 'localhost' })
584
+ .example('craft preview ./dist')
585
+ .example('craft preview ./index.html --port 8080')
586
+ .action(async (path?: string, options?: any) => {
587
+ const servePath = path || '.'
588
+ const port = options?.port || 3456
589
+ const host = options?.host || 'localhost'
590
+
591
+ console.log(`\n🌐 Starting preview server...`)
592
+ console.log(` Path: ${servePath}`)
593
+ console.log(` URL: http://${host}:${port}\n`)
594
+
595
+ const { spawn } = await import('node:child_process')
596
+
597
+ // Use bunx serve for simple static file serving
598
+ const proc = spawn('bunx', ['--bun', 'serve', '-p', String(port), servePath], {
599
+ stdio: 'inherit',
600
+ })
601
+
602
+ proc.on('error', () => {
603
+ console.log('Falling back to python http.server...')
604
+ spawn('python3', ['-m', 'http.server', String(port), '--directory', servePath], {
605
+ stdio: 'inherit',
606
+ })
607
+ })
608
+ })
609
+
610
+ // Publish command - publish to App Store / Play Store
611
+ cli
612
+ .command('publish', 'Publish app to App Store or Play Store')
613
+ .option('--ios', 'Publish to App Store (TestFlight)')
614
+ .option('--android', 'Publish to Play Store')
615
+ .option('--api-key <path>', 'App Store Connect API key path')
616
+ .option('--service-account <path>', 'Google Play service account JSON')
617
+ .option('-o, --output <dir>', 'Project directory', { default: '.' })
618
+ .example('craft publish --ios')
619
+ .example('craft publish --android')
620
+ .action(async (options?: any) => {
621
+ const { existsSync } = await import('node:fs')
622
+ const { join } = await import('node:path')
623
+ const { $ } = await import('bun')
624
+
625
+ if (options?.ios) {
626
+ console.log('\n📱 Publishing to App Store (TestFlight)...\n')
627
+
628
+ const iosDir = join(options?.output || '.', 'ios')
629
+ if (!existsSync(iosDir)) {
630
+ console.error('❌ No iOS project found. Run: craft ios init')
631
+ process.exit(1)
632
+ }
633
+
634
+ // Find xcarchive or build it
635
+ console.log('1. Building archive...')
636
+ try {
637
+ await $`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`
638
+ console.log('✅ Archive created')
639
+
640
+ console.log('2. Exporting IPA...')
641
+ await $`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`
642
+ console.log('✅ IPA exported')
643
+
644
+ console.log('3. Uploading to TestFlight...')
645
+ if (options?.apiKey) {
646
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`
647
+ }
648
+ else {
649
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`
650
+ }
651
+ console.log('✅ Uploaded to TestFlight!')
652
+ }
653
+ catch (error) {
654
+ console.error('❌ Publish failed. Make sure you have:')
655
+ console.error(' - Valid signing certificates')
656
+ console.error(' - App Store Connect API key (--api-key)')
657
+ console.error(' - ExportOptions.plist in your ios directory')
658
+ }
659
+ }
660
+
661
+ if (options?.android) {
662
+ console.log('\n🤖 Publishing to Play Store...\n')
663
+
664
+ const androidDir = join(options?.output || '.', 'android')
665
+ if (!existsSync(androidDir)) {
666
+ console.error('❌ No Android project found. Run: craft android init')
667
+ process.exit(1)
668
+ }
669
+
670
+ console.log('1. Building release AAB...')
671
+ try {
672
+ await $`cd ${androidDir} && ./gradlew bundleRelease`
673
+ console.log('✅ AAB created')
674
+
675
+ const aabPath = join(androidDir, 'app/build/outputs/bundle/release/app-release.aab')
676
+
677
+ if (options?.serviceAccount) {
678
+ console.log('2. Uploading to Play Store...')
679
+ // Would use Google Play Developer API here
680
+ console.log('⚠️ Automatic Play Store upload requires fastlane or Google Play API integration.')
681
+ console.log(` AAB file: ${aabPath}`)
682
+ console.log(' Upload manually via Play Console or use:')
683
+ console.log(' fastlane supply --aab ' + aabPath)
684
+ }
685
+ else {
686
+ console.log('✅ Release AAB ready:')
687
+ console.log(` ${aabPath}`)
688
+ console.log('')
689
+ console.log('Upload manually via Play Console, or use fastlane:')
690
+ console.log(' fastlane supply --aab ' + aabPath)
691
+ }
692
+ }
693
+ catch (error) {
694
+ console.error('❌ Build failed. Make sure you have:')
695
+ console.error(' - Valid signing key (keystore)')
696
+ console.error(' - Release signing config in build.gradle.kts')
697
+ }
698
+ }
699
+
700
+ if (!options?.ios && !options?.android) {
701
+ console.log('Please specify a platform:')
702
+ console.log(' craft publish --ios # Publish to App Store')
703
+ console.log(' craft publish --android # Publish to Play Store')
704
+ }
705
+ })
706
+
707
+ // Init command - initialize a new Craft project
708
+ cli
709
+ .command('init <name>', 'Initialize a new Craft project')
710
+ .option('--template <type>', 'Project template (blank, tabs, drawer, dashboard, desktop, ios, android, all)', { default: 'blank' })
711
+ .option('--bundle-id <id>', 'Bundle identifier for mobile')
712
+ .example('craft init MyApp')
713
+ .example('craft init MyApp --template tabs')
714
+ .example('craft init MyApp --template dashboard')
715
+ .example('craft init MyApp --template all')
716
+ .action(async (name: string, options?: any) => {
717
+ console.log(`\n⚡ Creating new Craft project: ${name}\n`)
718
+
719
+ const template = options?.template || 'blank'
720
+ const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, '')}`
721
+ const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, '-')
722
+
723
+ const { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, cpSync } = await import('node:fs')
724
+ const { join, dirname } = await import('node:path')
725
+
726
+ // Helper to replace template variables
727
+ const replaceVars = (content: string): string => {
728
+ return content
729
+ .replace(/\{\{APP_NAME\}\}/g, name)
730
+ .replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug)
731
+ .replace(/\{\{BUNDLE_ID\}\}/g, bundleId)
732
+ .replace(/\{\{AUTHOR\}\}/g, 'Developer')
733
+ }
734
+
735
+ // Helper to copy template directory
736
+ const copyTemplate = async (templateName: string, destDir: string) => {
737
+ const templateDir = join(import.meta.dir, '../../../templates/projects', templateName)
738
+
739
+ if (!existsSync(templateDir)) {
740
+ console.log(` ⚠️ Template '${templateName}' not found, using blank template`)
741
+ return false
742
+ }
743
+
744
+ const copyRecursive = (src: string, dest: string) => {
745
+ if (!existsSync(dest)) {
746
+ mkdirSync(dest, { recursive: true })
747
+ }
748
+
749
+ const entries = readdirSync(src, { withFileTypes: true })
750
+
751
+ for (const entry of entries) {
752
+ const srcPath = join(src, entry.name)
753
+ const destPath = join(dest, entry.name)
754
+
755
+ if (entry.isDirectory()) {
756
+ copyRecursive(srcPath, destPath)
757
+ }
758
+ else {
759
+ const content = readFileSync(srcPath, 'utf-8')
760
+ const processedContent = replaceVars(content)
761
+ writeFileSync(destPath, processedContent)
762
+ }
763
+ }
764
+ }
765
+
766
+ copyRecursive(templateDir, destDir)
767
+ return true
768
+ }
769
+
770
+ // Create project from template
771
+ if (['blank', 'tabs', 'drawer', 'dashboard'].includes(template)) {
772
+ console.log(`📁 Creating ${template} project from template...`)
773
+
774
+ if (!existsSync(name)) {
775
+ mkdirSync(name, { recursive: true })
776
+ }
777
+
778
+ const copied = await copyTemplate(template, name)
779
+
780
+ if (!copied) {
781
+ // Fallback: create basic project inline
782
+ mkdirSync(join(name, 'src'), { recursive: true })
783
+
784
+ writeFileSync(join(name, 'index.html'), replaceVars(`<!DOCTYPE html>
785
+ <html lang="en">
786
+ <head>
787
+ <meta charset="UTF-8">
788
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
789
+ <title>{{APP_NAME}}</title>
790
+ </head>
791
+ <body>
792
+ <div id="app">
793
+ <h1>{{APP_NAME}}</h1>
794
+ <p>Built with Craft</p>
795
+ </div>
796
+ </body>
797
+ </html>`))
798
+
799
+ writeFileSync(join(name, 'package.json'), replaceVars(JSON.stringify({
800
+ name: '{{APP_NAME_SLUG}}',
801
+ version: '1.0.0',
802
+ private: true,
803
+ type: 'module',
804
+ scripts: {
805
+ dev: 'craft dev',
806
+ build: 'craft build'
807
+ },
808
+ dependencies: {
809
+ 'craft-native': 'workspace:*'
810
+ }
811
+ }, null, 2)))
812
+ }
813
+
814
+ console.log(`✅ ${template} project created`)
815
+ }
816
+
817
+ if (template === 'desktop' || template === 'all') {
818
+ console.log('📁 Creating desktop project structure...')
819
+
820
+ if (!existsSync(name)) {
821
+ mkdirSync(name, { recursive: true })
822
+ }
823
+
824
+ // Create craft.config.ts
825
+ const configContent = `import type { CraftConfig } from 'craft-native'
826
+
827
+ export default {
828
+ name: '${name}',
829
+ window: {
830
+ title: '${name}',
831
+ width: 1200,
832
+ height: 800,
833
+ darkMode: true,
834
+ },
835
+ } satisfies CraftConfig
836
+ `
837
+ writeFileSync(`${name}/craft.config.ts`, configContent)
838
+
839
+ // Create index.html
840
+ const htmlContent = `<!DOCTYPE html>
841
+ <html lang="en">
842
+ <head>
843
+ <meta charset="UTF-8">
844
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
845
+ <title>${name}</title>
846
+ <style>
847
+ * { margin: 0; padding: 0; box-sizing: border-box; }
848
+ body {
849
+ font-family: -apple-system, system-ui, sans-serif;
850
+ background: #1a1a2e;
851
+ color: white;
852
+ min-height: 100vh;
853
+ display: flex;
854
+ justify-content: center;
855
+ align-items: center;
856
+ }
857
+ .container { text-align: center; }
858
+ h1 { font-size: 3rem; margin-bottom: 1rem; }
859
+ p { opacity: 0.7; }
860
+ </style>
861
+ </head>
862
+ <body>
863
+ <div class="container">
864
+ <h1>⚡ ${name}</h1>
865
+ <p>Built with Craft</p>
866
+ </div>
867
+ </body>
868
+ </html>
869
+ `
870
+ writeFileSync(`${name}/index.html`, htmlContent)
871
+
872
+ // Create package.json
873
+ const packageJson = {
874
+ name: appNameSlug,
875
+ version: '0.1.0',
876
+ private: true,
877
+ scripts: {
878
+ dev: 'craft dev http://localhost:3000',
879
+ build: 'craft build',
880
+ 'ios:init': 'craft ios init ' + name,
881
+ 'ios:build': 'craft ios build',
882
+ 'ios:open': 'craft ios open',
883
+ },
884
+ devDependencies: {
885
+ 'craft-native': '*',
886
+ },
887
+ }
888
+ writeFileSync(`${name}/package.json`, JSON.stringify(packageJson, null, 2))
889
+
890
+ console.log('✅ Desktop project created')
891
+ }
892
+
893
+ if (template === 'ios' || template === 'all') {
894
+ console.log('📱 Creating iOS project...')
895
+ // @ts-ignore -- sibling package may not exist at typecheck time
896
+ const iosModule = await import('../../ios/dist/index.js')
897
+ await iosModule.init({
898
+ name,
899
+ bundleId: options?.bundleId,
900
+ output: template === 'all' ? `${name}/ios` : './ios',
901
+ })
902
+ console.log('✅ iOS project created')
903
+ }
904
+
905
+ if (template === 'android' || template === 'all') {
906
+ console.log('🤖 Creating Android project...')
907
+ // @ts-ignore -- sibling package may not exist at typecheck time
908
+ const androidModule = await import('../../android/dist/index.js')
909
+ await androidModule.init({
910
+ name,
911
+ packageName: options?.bundleId,
912
+ output: template === 'all' ? `${name}/android` : './android',
913
+ })
914
+ console.log('✅ Android project created')
915
+ }
916
+
917
+ console.log('')
918
+ console.log('Next steps:')
919
+ console.log(` cd ${name}`)
920
+ console.log(' bun install')
921
+ if (template === 'desktop' || template === 'all') {
922
+ console.log(' craft dev http://localhost:3000')
923
+ }
924
+ if (template === 'ios' || template === 'all') {
925
+ console.log(' craft ios build')
926
+ console.log(' craft ios open')
927
+ }
928
+ if (template === 'android' || template === 'all') {
929
+ console.log(' craft android build')
930
+ console.log(' craft android open')
931
+ }
932
+ console.log('')
933
+ })
934
+
935
+ cli.version(version)
936
+ cli.help()
937
+ cli.parse()