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