craft-native 0.0.105 → 0.0.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1139,1416 +1139,6 @@ var init_package = __esm(() => {
1139
1139
  MEBIBYTE = 1024 * 1024;
1140
1140
  });
1141
1141
 
1142
- // dist/ios/src/index.js
1143
- var exports_src = {};
1144
- __export(exports_src, {
1145
- syncWebAssets: () => syncWebAssets,
1146
- showSimulator: () => showSimulator,
1147
- run: () => run,
1148
- resolveRuntimeDir: () => resolveRuntimeDir,
1149
- resolveAppearance: () => resolveAppearance,
1150
- renderWatchEntitlements: () => renderWatchEntitlements,
1151
- renderUsageDescriptions: () => renderUsageDescriptions,
1152
- renderUrlTypes: () => renderUrlTypes,
1153
- renderRuntimeSettings: () => renderRuntimeSettings,
1154
- renderPrivacyManifest: () => renderPrivacyManifest,
1155
- renderOrientations: () => renderOrientations,
1156
- renderEntitlements: () => renderEntitlements,
1157
- renderDeviceFamilies: () => renderDeviceFamilies,
1158
- renderBackgroundModes: () => renderBackgroundModes,
1159
- renderAppearance: () => renderAppearance,
1160
- productName: () => productName,
1161
- pickSimulator: () => pickSimulator,
1162
- orderSimulators: () => orderSimulators,
1163
- open: () => open,
1164
- installRuntime: () => installRuntime,
1165
- init: () => init,
1166
- build: () => build,
1167
- bootSimulator: () => bootSimulator
1168
- });
1169
- import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
1170
- import { dirname, join as join2, resolve } from "path";
1171
- function renderDeviceFamilies(config) {
1172
- const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
1173
- return [...values].join(",");
1174
- }
1175
- function xmlEscape(value) {
1176
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll(/\u0027/g, "&apos;");
1177
- }
1178
- function plistString(key, value) {
1179
- return ` <key>${key}</key>
1180
- <string>${xmlEscape(value)}</string>`;
1181
- }
1182
- function resolveAppearance(config) {
1183
- if (config.appearance)
1184
- return config.appearance;
1185
- return config.darkMode === false ? "light" : "dark";
1186
- }
1187
- function renderAppearance(config) {
1188
- const appearance = resolveAppearance(config);
1189
- if (appearance === "system")
1190
- return { interfaceStyle: "Automatic", statusBarStyle: "UIStatusBarStyleDefault" };
1191
- if (appearance === "light")
1192
- return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
1193
- return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
1194
- }
1195
- function renderUsageDescriptions(config) {
1196
- const entries = [
1197
- [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
1198
- [config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
1199
- [config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
1200
- [config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
1201
- [config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
1202
- [config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
1203
- [config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
1204
- [config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
1205
- [config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
1206
- [config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
1207
- [config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
1208
- [config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
1209
- [config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
1210
- [config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
1211
- ];
1212
- return entries.filter(([enabled2]) => enabled2).map(([, key, value]) => plistString(key, value)).join(`
1213
- `);
1214
- }
1215
- function renderOrientations(config) {
1216
- const names = {
1217
- portrait: "UIInterfaceOrientationPortrait",
1218
- "landscape-left": "UIInterfaceOrientationLandscapeLeft",
1219
- "landscape-right": "UIInterfaceOrientationLandscapeRight",
1220
- "portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
1221
- };
1222
- const values = config.orientations?.length ? config.orientations : ["portrait"];
1223
- return values.map((value) => ` <string>${names[value]}</string>`).join(`
1224
- `);
1225
- }
1226
- function renderUrlTypes(config) {
1227
- const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
1228
- if (!schemes.length)
1229
- return "";
1230
- return ` <key>CFBundleURLTypes</key>
1231
- <array>
1232
- <dict>
1233
- <key>CFBundleURLSchemes</key>
1234
- <array>
1235
- ${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
1236
- `)}
1237
- </array>
1238
- </dict>
1239
- </array>`;
1240
- }
1241
- function plistArray(values, indent = 2) {
1242
- const padding = " ".repeat(indent);
1243
- return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
1244
- `);
1245
- }
1246
- function renderBackgroundModes(config) {
1247
- const modes = new Set;
1248
- if (config.enableBackgroundLocation)
1249
- modes.add("location");
1250
- if (config.enableBackgroundTasks)
1251
- modes.add("processing");
1252
- if (config.enablePushNotifications)
1253
- modes.add("remote-notification");
1254
- if (!modes.size)
1255
- return "";
1256
- return ` <key>UIBackgroundModes</key>
1257
- <array>
1258
- ${plistArray([...modes])}
1259
- </array>`;
1260
- }
1261
- function renderEntitlements(config) {
1262
- const entries = [];
1263
- if (config.associatedDomains?.length) {
1264
- entries.push(` <key>com.apple.developer.associated-domains</key>
1265
- <array>
1266
- ${plistArray(config.associatedDomains)}
1267
- </array>`);
1268
- }
1269
- if (config.appGroups?.length) {
1270
- entries.push(` <key>com.apple.security.application-groups</key>
1271
- <array>
1272
- ${plistArray(config.appGroups)}
1273
- </array>`);
1274
- }
1275
- if (config.enableHealthKit) {
1276
- entries.push(` <key>com.apple.developer.healthkit</key>
1277
- <true/>`);
1278
- }
1279
- if (config.enablePushNotifications) {
1280
- entries.push(` <key>aps-environment</key>
1281
- <string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
1282
- }
1283
- return `<?xml version="1.0" encoding="UTF-8"?>
1284
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1285
- <plist version="1.0">
1286
- <dict>
1287
- ${entries.join(`
1288
- `)}
1289
- </dict>
1290
- </plist>
1291
- `;
1292
- }
1293
- function renderWatchEntitlements(config) {
1294
- const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
1295
- <array>
1296
- ${plistArray(config.appGroups)}
1297
- </array>
1298
- ` : "";
1299
- return `<?xml version="1.0" encoding="UTF-8"?>
1300
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1301
- <plist version="1.0">
1302
- <dict>
1303
- ${appGroups}</dict>
1304
- </plist>
1305
- `;
1306
- }
1307
- function renderRuntimeSettings() {
1308
- return [
1309
- ' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
1310
- ' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
1311
- ' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
1312
- ].join(`
1313
- `);
1314
- }
1315
- function renderPrivacyManifest(config) {
1316
- const privacy = config.privacy ?? {};
1317
- const collected = privacy.collectedDataTypes ?? [];
1318
- const accessed = privacy.accessedApiTypes ?? [];
1319
- const collectedXml = collected.map((item) => ` <dict>
1320
- <key>NSPrivacyCollectedDataType</key>
1321
- <string>${xmlEscape(item.type)}</string>
1322
- <key>NSPrivacyCollectedDataTypeLinked</key>
1323
- <${item.linked ? "true" : "false"}/>
1324
- <key>NSPrivacyCollectedDataTypeTracking</key>
1325
- <${item.tracking ? "true" : "false"}/>
1326
- <key>NSPrivacyCollectedDataTypePurposes</key>
1327
- <array>
1328
- ${plistArray(item.purposes, 4)}
1329
- </array>
1330
- </dict>`).join(`
1331
- `);
1332
- const accessedXml = accessed.map((item) => ` <dict>
1333
- <key>NSPrivacyAccessedAPIType</key>
1334
- <string>${xmlEscape(item.type)}</string>
1335
- <key>NSPrivacyAccessedAPITypeReasons</key>
1336
- <array>
1337
- ${plistArray(item.reasons, 4)}
1338
- </array>
1339
- </dict>`).join(`
1340
- `);
1341
- return `<?xml version="1.0" encoding="UTF-8"?>
1342
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1343
- <plist version="1.0">
1344
- <dict>
1345
- <key>NSPrivacyTracking</key>
1346
- <${privacy.tracking ? "true" : "false"}/>
1347
- <key>NSPrivacyTrackingDomains</key>
1348
- <array>
1349
- ${plistArray(privacy.trackingDomains ?? [])}
1350
- </array>
1351
- <key>NSPrivacyCollectedDataTypes</key>
1352
- <array>
1353
- ${collectedXml}
1354
- </array>
1355
- <key>NSPrivacyAccessedAPITypes</key>
1356
- <array>
1357
- ${accessedXml}
1358
- </array>
1359
- </dict>
1360
- </plist>
1361
- `;
1362
- }
1363
- function renderAssetCatalog(output, config) {
1364
- const catalog = join2(output, "Assets.xcassets");
1365
- const appIcon = join2(catalog, "AppIcon.appiconset");
1366
- const launchBackground = join2(catalog, "LaunchBackground.colorset");
1367
- mkdirSync2(appIcon, { recursive: true });
1368
- mkdirSync2(launchBackground, { recursive: true });
1369
- writeFileSync3(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
1370
- `);
1371
- const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
1372
- if (config.appIconPath) {
1373
- if (!existsSync4(config.appIconPath))
1374
- throw new Error(`App icon not found: ${config.appIconPath}`);
1375
- cpSync2(config.appIconPath, join2(appIcon, iconFilename));
1376
- }
1377
- writeFileSync3(join2(appIcon, "Contents.json"), `${JSON.stringify({
1378
- images: iconFilename ? [{
1379
- filename: iconFilename,
1380
- idiom: "universal",
1381
- platform: "ios",
1382
- size: "1024x1024"
1383
- }] : [],
1384
- info: { author: "xcode", version: 1 }
1385
- }, null, 2)}
1386
- `);
1387
- const colorEntry = (hex) => {
1388
- const color = hex?.replace(/^#/, "") || "000000";
1389
- const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
1390
- const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
1391
- return {
1392
- "color-space": "srgb",
1393
- components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
1394
- };
1395
- };
1396
- const colors2 = [{ color: colorEntry(config.backgroundColor), idiom: "universal" }];
1397
- if (config.backgroundColorDark) {
1398
- colors2.push({
1399
- appearances: [{ appearance: "luminosity", value: "dark" }],
1400
- color: colorEntry(config.backgroundColorDark),
1401
- idiom: "universal"
1402
- });
1403
- }
1404
- writeFileSync3(join2(launchBackground, "Contents.json"), `${JSON.stringify({
1405
- colors: colors2,
1406
- info: { author: "xcode", version: 1 }
1407
- }, null, 2)}
1408
- `);
1409
- }
1410
- function syncWebAssets(source, output) {
1411
- const sourcePath = resolve(source);
1412
- if (!existsSync4(sourcePath))
1413
- throw new Error(`Web asset path not found: ${source}`);
1414
- const distDir = join2(output, "dist");
1415
- rmSync2(distDir, { recursive: true, force: true });
1416
- mkdirSync2(distDir, { recursive: true });
1417
- if (statSync(sourcePath).isDirectory()) {
1418
- cpSync2(sourcePath, distDir, { recursive: true });
1419
- } else {
1420
- cpSync2(sourcePath, join2(distDir, "index.html"));
1421
- }
1422
- if (!existsSync4(join2(distDir, "index.html"))) {
1423
- throw new Error(`Web asset directory must contain index.html: ${source}`);
1424
- }
1425
- }
1426
- function resolveRuntimeDir(override) {
1427
- if (override === null)
1428
- return null;
1429
- const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
1430
- if (!dir)
1431
- return null;
1432
- if (!existsSync4(dir)) {
1433
- const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
1434
- throw new Error(`${source} ${dir}, which does not exist.`);
1435
- }
1436
- return dir;
1437
- }
1438
- async function installRuntime(output, runtimeDir) {
1439
- const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
1440
- const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
1441
- if (present.length === 0) {
1442
- throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
1443
- }
1444
- return { sdk, archives, present };
1445
- });
1446
- const dest = join2(output, "Runtime");
1447
- rmSync2(dest, { recursive: true, force: true });
1448
- for (const { sdk, archives, present } of resolved) {
1449
- const sdkDir = join2(dest, sdk);
1450
- mkdirSync2(sdkDir, { recursive: true });
1451
- const target = join2(sdkDir, "libcraft-ios.a");
1452
- if (present.length === 1) {
1453
- const missing = archives.filter((a) => !present.includes(a));
1454
- if (missing.length > 0) {
1455
- console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1456
- }
1457
- cpSync2(join2(runtimeDir, present[0]), target);
1458
- } else {
1459
- await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
1460
- }
1461
- }
1462
- return true;
1463
- }
1464
- async function refreshRuntime(output, override) {
1465
- if (!existsSync4(join2(output, "Runtime")))
1466
- return;
1467
- const dir = resolveRuntimeDir(override);
1468
- if (!dir) {
1469
- console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
1470
- return;
1471
- }
1472
- await installRuntime(output, dir);
1473
- console.log(" Refreshed the Zig runtime from", dir);
1474
- }
1475
- function productName(name) {
1476
- const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
1477
- const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
1478
- if (!joined)
1479
- return "Craft";
1480
- return /^\d/.test(joined) ? `App${joined}` : joined;
1481
- }
1482
- async function init(options) {
1483
- const { name: displayName, bundleId, teamId, output } = options;
1484
- const name = productName(displayName);
1485
- console.log(`
1486
- \u26A1 Initializing Craft iOS project: ${displayName}`);
1487
- console.log(` Output: ${output}
1488
- `);
1489
- const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
1490
- if (options.config?.enableWatchApp)
1491
- dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
1492
- for (const dir of dirs) {
1493
- if (!existsSync4(dir)) {
1494
- mkdirSync2(dir, { recursive: true });
1495
- }
1496
- }
1497
- const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
1498
- const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
1499
- const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
1500
- const config = {
1501
- ...DEFAULT_CONFIG,
1502
- appName: displayName,
1503
- bundleId: finalBundleId,
1504
- teamId: teamId || "",
1505
- ...given
1506
- };
1507
- if (config.enableBackgroundLocation)
1508
- config.enableGeolocation = true;
1509
- writeFileSync3(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1510
- const swiftTemplate = readFileSync3(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1511
- const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1512
- writeFileSync3(join2(output, "Sources", `${name}App.swift`), swiftSource);
1513
- const infoPlistTemplate = readFileSync3(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1514
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
1515
- <true/>
1516
- <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1517
- <true/>` : "");
1518
- writeFileSync3(join2(output, "Info.plist"), infoPlist);
1519
- const projectYmlTemplate = readFileSync3(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1520
- const nativeDependencies = [];
1521
- if (config.enableLiveActivities)
1522
- nativeDependencies.push(` - target: ${name}LiveActivity`);
1523
- if (config.enableWatchApp)
1524
- nativeDependencies.push(` - target: ${name}Watch
1525
- embed: true`);
1526
- const nativeTargets = [];
1527
- if (config.enableLiveActivities) {
1528
- nativeTargets.push(` ${name}LiveActivity:
1529
- type: app-extension
1530
- platform: iOS
1531
- deploymentTarget: "16.2"
1532
- sources:
1533
- - WidgetExtension
1534
- - Shared
1535
- settings:
1536
- INFOPLIST_FILE: WidgetExtension/Info.plist
1537
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
1538
- SWIFT_VERSION: "5.0"
1539
- APPLICATION_EXTENSION_API_ONLY: YES
1540
- SKIP_INSTALL: YES`);
1541
- }
1542
- if (config.enableWatchApp) {
1543
- nativeTargets.push(` ${name}Watch:
1544
- type: application.watchapp2
1545
- platform: watchOS
1546
- deploymentTarget: "${config.watchosVersion || "9.0"}"
1547
- sources:
1548
- - WatchApp
1549
- settings:
1550
- INFOPLIST_FILE: WatchApp/Info.plist
1551
- CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
1552
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
1553
- SWIFT_VERSION: "5.0"
1554
- SKIP_INSTALL: YES
1555
- dependencies:
1556
- - target: ${name}WatchExtension
1557
- ${name}WatchExtension:
1558
- type: watchkit2-extension
1559
- platform: watchOS
1560
- deploymentTarget: "${config.watchosVersion || "9.0"}"
1561
- sources:
1562
- - WatchExtension
1563
- settings:
1564
- INFOPLIST_FILE: WatchExtension/Info.plist
1565
- CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
1566
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
1567
- SWIFT_VERSION: "5.0"
1568
- APPLICATION_EXTENSION_API_ONLY: YES
1569
- SKIP_INSTALL: YES`);
1570
- }
1571
- const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1572
- const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
1573
- if (hasRuntime) {
1574
- console.log(" Linked the Zig runtime from", runtimeDir);
1575
- } else {
1576
- rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
1577
- }
1578
- const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1579
- ${nativeDependencies.join(`
1580
- `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1581
- `));
1582
- writeFileSync3(join2(output, "project.yml"), projectYml);
1583
- writeFileSync3(join2(output, "Craft.entitlements"), renderEntitlements(config));
1584
- writeFileSync3(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1585
- renderAssetCatalog(output, config);
1586
- cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
1587
- if (config.enableLiveActivities) {
1588
- mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1589
- const widgetSource = readFileSync3(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1590
- writeFileSync3(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
1591
- const widgetInfo = readFileSync3(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1592
- writeFileSync3(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1593
- }
1594
- if (config.enableWatchApp) {
1595
- const watchSource = readFileSync3(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1596
- writeFileSync3(join2(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
1597
- const watchInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1598
- writeFileSync3(join2(output, "WatchApp", "Info.plist"), watchInfo);
1599
- writeFileSync3(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1600
- const watchExtensionInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1601
- writeFileSync3(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1602
- writeFileSync3(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1603
- }
1604
- const placeholderHtml = `<!DOCTYPE html>
1605
- <html>
1606
- <head>
1607
- <meta charset="UTF-8">
1608
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1609
- <title>${xmlEscape(displayName)}</title>
1610
- <style>
1611
- * { margin: 0; padding: 0; box-sizing: border-box; }
1612
- body {
1613
- font-family: -apple-system, system-ui, sans-serif;
1614
- background: ${config.backgroundColor};
1615
- color: white;
1616
- min-height: 100vh;
1617
- display: flex;
1618
- justify-content: center;
1619
- align-items: center;
1620
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
1621
- }
1622
- .container { text-align: center; padding: 2rem; }
1623
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
1624
- p { opacity: 0.7; }
1625
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
1626
- </style>
1627
- </head>
1628
- <body>
1629
- <div class="container">
1630
- <h1>\u26A1 ${xmlEscape(displayName)}</h1>
1631
- <p>Built with Craft iOS</p>
1632
- <p class="ready" id="status">Waiting for Craft bridge...</p>
1633
- </div>
1634
- <script>
1635
- window.addEventListener('craftReady', (e) => {
1636
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
1637
- console.log('Craft capabilities:', e.detail.capabilities);
1638
- });
1639
- </script>
1640
- </body>
1641
- </html>`;
1642
- writeFileSync3(join2(output, "dist", "index.html"), placeholderHtml);
1643
- console.log("\u2705 Project initialized");
1644
- console.log("");
1645
- console.log("Next steps:");
1646
- console.log(` 1. cd ${output}`);
1647
- console.log(" 2. Add your web content to dist/index.html");
1648
- console.log(" 3. Run: craft ios build");
1649
- console.log(" 4. Run: craft ios open");
1650
- console.log("");
1651
- }
1652
- async function build(options) {
1653
- const { htmlPath, devServer, output, generateProject = true } = options;
1654
- console.log(`
1655
- \uD83D\uDCE6 Building Craft iOS project...`);
1656
- const configPath = join2(output, "craft.config.json");
1657
- if (!existsSync4(configPath)) {
1658
- throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1659
- }
1660
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1661
- if (devServer) {
1662
- config.devServerURL = devServer;
1663
- const origin = new URL(devServer).origin;
1664
- config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
1665
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
1666
- console.log(` Dev server: ${devServer}`);
1667
- }
1668
- if (htmlPath) {
1669
- syncWebAssets(htmlPath, output);
1670
- console.log(` Synced: ${htmlPath} \u2192 dist/`);
1671
- }
1672
- await refreshRuntime(output, options.runtimeDir);
1673
- if (!generateProject)
1674
- return;
1675
- try {
1676
- const result = await $`which xcodegen`.quiet();
1677
- if (result.exitCode === 0) {
1678
- console.log(" Running xcodegen...");
1679
- await $`cd ${output} && xcodegen generate`.quiet();
1680
- console.log(`\u2705 Xcode project created: ${productName(config.appName)}.xcodeproj`);
1681
- } else {
1682
- throw new Error("xcodegen not found");
1683
- }
1684
- } catch (error) {
1685
- throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
1686
- cause: error
1687
- });
1688
- }
1689
- console.log("");
1690
- }
1691
- async function open(options) {
1692
- const { output } = options;
1693
- const files = readdirSync2(output);
1694
- const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1695
- if (!xcodeproj) {
1696
- throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
1697
- }
1698
- const projectPath = join2(output, xcodeproj);
1699
- console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1700
- await $`open ${projectPath}`;
1701
- }
1702
- function orderSimulators(devices) {
1703
- return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
1704
- const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
1705
- if (booted !== 0)
1706
- return booted;
1707
- const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
1708
- if (phone !== 0)
1709
- return phone;
1710
- return right.runtime.localeCompare(left.runtime);
1711
- });
1712
- }
1713
- async function pickSimulator() {
1714
- const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
1715
- if (listed.exitCode !== 0)
1716
- return null;
1717
- let parsed;
1718
- try {
1719
- parsed = JSON.parse(listed.stdout.toString());
1720
- } catch {
1721
- return null;
1722
- }
1723
- const devices = [];
1724
- for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
1725
- for (const device of list)
1726
- devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
1727
- }
1728
- return orderSimulators(devices)[0] ?? null;
1729
- }
1730
- async function bootSimulator(device) {
1731
- if (device.state !== "Booted")
1732
- await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
1733
- await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
1734
- }
1735
- async function showSimulator() {
1736
- const selected = await $`xcode-select -p`.quiet().nothrow();
1737
- const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1738
- if (developer) {
1739
- const app = join2(developer, "Applications", "Simulator.app");
1740
- if (existsSync4(app)) {
1741
- await $`open ${app}`.quiet().nothrow();
1742
- return;
1743
- }
1744
- }
1745
- await $`open -a Simulator`.quiet().nothrow();
1746
- }
1747
- async function run(options) {
1748
- const { simulator, output } = options;
1749
- await build({ output });
1750
- const files = readdirSync2(output);
1751
- const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1752
- if (!xcodeproj) {
1753
- throw new Error(`No Xcode project found in ${output}`);
1754
- }
1755
- const projectPath = join2(output, xcodeproj);
1756
- const appName = xcodeproj.replace(".xcodeproj", "");
1757
- const configPath = join2(output, "craft.config.json");
1758
- if (!existsSync4(configPath))
1759
- throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1760
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1761
- if (simulator) {
1762
- console.log("\uD83D\uDCF1 Building and running on simulator...");
1763
- const device = await pickSimulator();
1764
- if (!device) {
1765
- throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
1766
- }
1767
- console.log(` Device: ${device.name} (${device.runtime})`);
1768
- try {
1769
- await bootSimulator(device);
1770
- const derivedData = join2(output, "build");
1771
- const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1772
- await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1773
- if (!existsSync4(product)) {
1774
- throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
1775
- }
1776
- await $`xcrun simctl install ${device.udid} ${product}`;
1777
- await showSimulator();
1778
- await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
1779
- console.log(`\u2705 ${config.appName} is running on ${device.name}`);
1780
- } catch (error) {
1781
- throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
1782
- }
1783
- } else {
1784
- console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
1785
- await $`open ${projectPath}`;
1786
- console.log("");
1787
- console.log("In Xcode:");
1788
- console.log(" 1. Select your Team in Signing & Capabilities");
1789
- console.log(" 2. Connect your iPhone");
1790
- console.log(" 3. Select your device");
1791
- console.log(" 4. Click Run (\u25B6\uFE0F)");
1792
- }
1793
- }
1794
- var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
1795
- var init_src = __esm(() => {
1796
- ({ $ } = globalThis.Bun);
1797
- TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
1798
- DEFAULT_CONFIG = {
1799
- version: "1.0.0",
1800
- buildNumber: "1",
1801
- darkMode: true,
1802
- backgroundColor: "#0b1712",
1803
- enableSpeechRecognition: false,
1804
- enableHaptics: false,
1805
- enableShare: false,
1806
- enableCamera: false,
1807
- enableBiometric: false,
1808
- enablePushNotifications: false,
1809
- enableSecureStorage: false,
1810
- enableGeolocation: false,
1811
- enableClipboard: false,
1812
- enableContacts: false,
1813
- enableCalendar: false,
1814
- enableLocalNotifications: false,
1815
- enableInAppPurchase: false,
1816
- enableKeepAwake: false,
1817
- enableOrientationLock: false,
1818
- enableDeepLinks: false,
1819
- enableQRScanner: false,
1820
- enableFilePicker: false,
1821
- enableFileDownload: false,
1822
- enableSocialAuth: false,
1823
- enableAudioRecording: false,
1824
- enableVideoRecording: false,
1825
- enableMotionSensors: false,
1826
- enableLocalDatabase: false,
1827
- enableBluetooth: false,
1828
- enableNFC: false,
1829
- enableHealthKit: false,
1830
- enableLiveActivities: false,
1831
- enableWatchApp: false,
1832
- enableBackgroundLocation: false,
1833
- enableBackgroundTasks: false,
1834
- enableScreenCapture: false,
1835
- enablePDFViewer: false,
1836
- enableAR: false,
1837
- enableMLKit: false,
1838
- iosVersion: "16.0",
1839
- watchosVersion: "9.0",
1840
- teamId: "",
1841
- trustedOrigins: [],
1842
- associatedDomains: [],
1843
- appGroups: [],
1844
- orientations: ["portrait"],
1845
- deviceFamilies: ["iphone", "ipad"]
1846
- };
1847
- RUNTIME_ARCHIVES = {
1848
- device: ["libcraft-ios.a"],
1849
- simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
1850
- };
1851
- });
1852
-
1853
- // dist/android/src/index.js
1854
- var exports_src2 = {};
1855
- __export(exports_src2, {
1856
- syncAndroidWebAssets: () => syncAndroidWebAssets,
1857
- run: () => run2,
1858
- resolveRuntimeDir: () => resolveRuntimeDir2,
1859
- renderAndroidPermissions: () => renderAndroidPermissions,
1860
- renderAndroidDeepLinks: () => renderAndroidDeepLinks,
1861
- open: () => open2,
1862
- installRuntime: () => installRuntime2,
1863
- init: () => init2,
1864
- build: () => build2,
1865
- ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
1866
- });
1867
- import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1868
- import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
1869
- function renderAndroidPromiseRuntime(indent = "") {
1870
- return ANDROID_PROMISE_RUNTIME.trim().split(`
1871
- `).map((line) => `${indent}${line}`).join(`
1872
- `);
1873
- }
1874
- function escapeXml(value) {
1875
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1876
- }
1877
- function escapeKotlinString(value) {
1878
- return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
1879
- `, "\\n");
1880
- }
1881
- function generatedPackageSegment(name) {
1882
- const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
1883
- if (!normalized)
1884
- return "app";
1885
- return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
1886
- }
1887
- function generatedGradleProjectName(name) {
1888
- const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
1889
- const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
1890
- return normalized || "craft-app";
1891
- }
1892
- function requireRegularFile(path2, label) {
1893
- if (!existsSync5(path2))
1894
- throw new Error(`${label} not found: ${path2}`);
1895
- if (!statSync2(path2).isFile())
1896
- throw new Error(`${label} must be a file: ${path2}`);
1897
- }
1898
- function containsPath(parent, candidate) {
1899
- const relativePath = relative(parent, candidate);
1900
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
1901
- }
1902
- function validateGoogleServicesFile(path2, packageName) {
1903
- requireRegularFile(path2, "Google services file");
1904
- let document;
1905
- try {
1906
- document = JSON.parse(readFileSync4(path2, "utf8"));
1907
- } catch (error) {
1908
- throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
1909
- }
1910
- const matchingClient = document.client?.some((client) => {
1911
- return client.client_info?.android_client_info?.package_name === packageName;
1912
- });
1913
- if (!matchingClient) {
1914
- throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
1915
- }
1916
- }
1917
- function androidWebUrl(value, field) {
1918
- let url;
1919
- try {
1920
- url = new URL(value);
1921
- } catch {
1922
- throw new Error(`Invalid ${field}: ${value}`);
1923
- }
1924
- const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
1925
- if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
1926
- throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
1927
- }
1928
- return url;
1929
- }
1930
- function normalizeAndroidNetworkConfig(config) {
1931
- const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
1932
- if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
1933
- throw new Error("Android deep-link schemes must be valid URI schemes");
1934
- }
1935
- config.urlSchemes = [...new Set(schemes)];
1936
- if (config.enableDeepLinks && config.urlSchemes.length === 0) {
1937
- throw new Error("Android deep links require at least one URL scheme");
1938
- }
1939
- const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
1940
- return androidWebUrl(value, "Android trusted origin").origin;
1941
- });
1942
- if (config.devServerURL) {
1943
- const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
1944
- config.devServerURL = devServer.toString();
1945
- trustedOrigins.push(devServer.origin);
1946
- }
1947
- config.trustedOrigins = [...new Set(trustedOrigins)];
1948
- }
1949
- function validateAndroidConfig(config) {
1950
- if (!config.appName.trim())
1951
- throw new Error("Android app name must not be empty");
1952
- const packageSegments = config.packageName.split(".");
1953
- if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
1954
- throw new Error(`Invalid Android package name: ${config.packageName}`);
1955
- }
1956
- if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
1957
- throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
1958
- }
1959
- for (const [name, value] of [
1960
- ["versionCode", config.versionCode],
1961
- ["minSdk", config.minSdk],
1962
- ["compileSdk", config.compileSdk],
1963
- ["targetSdk", config.targetSdk]
1964
- ]) {
1965
- if (!Number.isInteger(value) || Number(value) < 1) {
1966
- throw new Error(`Android ${name} must be a positive integer`);
1967
- }
1968
- }
1969
- if (Number(config.minSdk) < ANDROID_MIN_SDK) {
1970
- throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
1971
- }
1972
- if (Number(config.minSdk) > Number(config.targetSdk)) {
1973
- throw new Error("Android minSdk must not exceed targetSdk");
1974
- }
1975
- if (Number(config.targetSdk) > Number(config.compileSdk)) {
1976
- throw new Error("Android targetSdk must not exceed compileSdk");
1977
- }
1978
- }
1979
- function writeAndroidConfig(output, config) {
1980
- writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1981
- const runtimeConfig = { ...config };
1982
- delete runtimeConfig.appIconPath;
1983
- delete runtimeConfig.googleServicesFile;
1984
- writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
1985
- }
1986
- function syncAndroidWebAssets(source, output) {
1987
- const sourcePath = resolve2(source);
1988
- if (!existsSync5(sourcePath))
1989
- throw new Error(`Web asset path not found: ${source}`);
1990
- const sourceStat = statSync2(sourcePath);
1991
- if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
1992
- throw new Error(`Web asset path must be a file or directory: ${source}`);
1993
- }
1994
- const assetsDir = resolve2(output, "app/src/main/assets");
1995
- if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
1996
- throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
1997
- }
1998
- if (sourceStat.isDirectory()) {
1999
- requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
2000
- }
2001
- const configPath = join3(assetsDir, "craft.config.json");
2002
- const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
2003
- rmSync3(assetsDir, { recursive: true, force: true });
2004
- mkdirSync3(assetsDir, { recursive: true });
2005
- if (sourceStat.isDirectory())
2006
- cpSync3(sourcePath, assetsDir, { recursive: true });
2007
- else
2008
- cpSync3(sourcePath, join3(assetsDir, "index.html"));
2009
- if (!existsSync5(join3(assetsDir, "index.html")))
2010
- throw new Error(`Web asset directory must contain index.html: ${source}`);
2011
- if (config)
2012
- writeFileSync4(configPath, config);
2013
- }
2014
- function renderAndroidPermissions(config) {
2015
- const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
2016
- if (config.enableSpeechRecognition)
2017
- permissions.add("android.permission.RECORD_AUDIO");
2018
- if (config.enableCamera)
2019
- permissions.add("android.permission.CAMERA");
2020
- if (config.enableHaptics)
2021
- permissions.add("android.permission.VIBRATE");
2022
- if (config.enableBiometric)
2023
- permissions.add("android.permission.USE_BIOMETRIC");
2024
- if (config.enableGeolocation || config.enableBackgroundLocation) {
2025
- permissions.add("android.permission.ACCESS_FINE_LOCATION");
2026
- permissions.add("android.permission.ACCESS_COARSE_LOCATION");
2027
- }
2028
- if (config.enableBackgroundLocation) {
2029
- permissions.add("android.permission.ACCESS_BACKGROUND_LOCATION");
2030
- permissions.add("android.permission.FOREGROUND_SERVICE");
2031
- permissions.add("android.permission.FOREGROUND_SERVICE_LOCATION");
2032
- }
2033
- if (config.enablePushNotifications)
2034
- permissions.add("android.permission.POST_NOTIFICATIONS");
2035
- if (config.enableKeepAwake)
2036
- permissions.add("android.permission.WAKE_LOCK");
2037
- if (config.enableHealthConnect) {
2038
- permissions.add("android.permission.health.READ_STEPS");
2039
- permissions.add("android.permission.health.READ_HEART_RATE");
2040
- permissions.add("android.permission.health.READ_ACTIVE_CALORIES_BURNED");
2041
- permissions.add("android.permission.health.READ_DISTANCE");
2042
- permissions.add("android.permission.health.READ_EXERCISE");
2043
- permissions.add("android.permission.health.WRITE_EXERCISE");
2044
- permissions.add("android.permission.health.WRITE_ACTIVE_CALORIES_BURNED");
2045
- permissions.add("android.permission.health.WRITE_DISTANCE");
2046
- }
2047
- return [...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`).join(`
2048
- `);
2049
- }
2050
- function renderAndroidDeepLinks(config) {
2051
- if (!config.enableDeepLinks)
2052
- return "";
2053
- const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
2054
- return schemes.map((scheme) => ` <intent-filter>
2055
- <action android:name="android.intent.action.VIEW" />
2056
- <category android:name="android.intent.category.DEFAULT" />
2057
- <category android:name="android.intent.category.BROWSABLE" />
2058
- <data android:scheme="${scheme}" />
2059
- </intent-filter>`).join(`
2060
- `);
2061
- }
2062
- function resolveRuntimeDir2(override) {
2063
- if (override === null)
2064
- return null;
2065
- const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
2066
- if (!dir)
2067
- return null;
2068
- if (!existsSync5(dir)) {
2069
- const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
2070
- throw new Error(`${source} ${dir}, which does not exist.`);
2071
- }
2072
- return dir;
2073
- }
2074
- function installRuntime2(output, runtimeDir) {
2075
- const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
2076
- if (found.length === 0) {
2077
- throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
2078
- }
2079
- if (found.length < RUNTIME_ABIS.length) {
2080
- const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
2081
- console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
2082
- }
2083
- const dest = join3(output, "app/src/main/jniLibs");
2084
- rmSync3(dest, { force: true, recursive: true });
2085
- for (const { abi, source } of found) {
2086
- const abiDir = join3(dest, abi);
2087
- mkdirSync3(abiDir, { recursive: true });
2088
- cpSync3(source, join3(abiDir, "libcraft.so"));
2089
- }
2090
- return true;
2091
- }
2092
- async function init2(options) {
2093
- const { name, packageName, output } = options;
2094
- console.log(`
2095
- \u26A1 Initializing Craft Android project: ${name}`);
2096
- console.log(` Output: ${output}
2097
- `);
2098
- const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
2099
- const packagePath = finalPackageName.replace(/\./g, "/");
2100
- const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
2101
- const config = {
2102
- ...DEFAULT_CONFIG2,
2103
- ...given,
2104
- appName: name,
2105
- packageName: finalPackageName
2106
- };
2107
- if (config.enableBackgroundLocation)
2108
- config.enableGeolocation = true;
2109
- if (config.enableHealthConnect) {
2110
- config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
2111
- }
2112
- normalizeAndroidNetworkConfig(config);
2113
- validateAndroidConfig(config);
2114
- if (config.enablePushNotifications && !config.googleServicesFile) {
2115
- throw new Error("Android push notifications require a googleServicesFile");
2116
- }
2117
- if (config.googleServicesFile)
2118
- validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
2119
- if (config.appIconPath)
2120
- requireRegularFile(config.appIconPath, "App icon");
2121
- const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
2122
- if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
2123
- throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
2124
- }
2125
- const dirs = [
2126
- output,
2127
- join3(output, "app/src/main/java", packagePath),
2128
- join3(output, "app/src/main/res/layout"),
2129
- join3(output, "app/src/main/res/values"),
2130
- join3(output, "app/src/main/res/drawable"),
2131
- join3(output, "app/src/main/assets"),
2132
- join3(output, "gradle/wrapper")
2133
- ];
2134
- if (existsSync5(output) && !statSync2(output).isDirectory()) {
2135
- throw new Error(`Android project output must be a directory: ${output}`);
2136
- }
2137
- for (const dir of dirs) {
2138
- if (!existsSync5(dir)) {
2139
- mkdirSync3(dir, { recursive: true });
2140
- }
2141
- }
2142
- writeAndroidConfig(output, config);
2143
- const hasGoogleServices = Boolean(config.googleServicesFile);
2144
- if (config.googleServicesFile) {
2145
- cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
2146
- }
2147
- const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
2148
- const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
2149
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
2150
- const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
2151
- const craftBridge = craftBridgeTemplate.replace(/\{\{PROMISE_RUNTIME\}\}/g, () => renderAndroidPromiseRuntime(" ")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
2152
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
2153
- && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
2154
- ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
2155
- }
2156
- try {
2157
- FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
2158
- val token = if (task.isSuccessful) task.result else null
2159
- val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
2160
- val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
2161
- evaluatePromiseJavascript("$callback && $callback($payload)")
2162
- }
2163
- } catch (error: Exception) {
2164
- val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
2165
- evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
2166
- }
2167
- }` : `evaluatePromiseJavascript(
2168
- "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
2169
- )`);
2170
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
2171
- const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
2172
- const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
2173
- mkdirSync3(nativeDir, { recursive: true });
2174
- writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
2175
- const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
2176
- writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
2177
- const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
2178
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2179
- const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
2180
- const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
2181
- <package android:name="com.google.android.apps.healthdata" />
2182
- </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
2183
- <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
2184
- </intent-filter>
2185
- <intent-filter>
2186
- <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
2187
- <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
2188
- </intent-filter>` : "");
2189
- writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
2190
- const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
2191
- writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
2192
- const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
2193
- const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
2194
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
2195
- writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
2196
- const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
2197
- writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2198
- const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
2199
- const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
2200
- writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
2201
- const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2202
- android.useAndroidX=true
2203
- kotlin.code.style=official
2204
- android.nonTransitiveRClass=true
2205
- `;
2206
- writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
2207
- writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
2208
- `);
2209
- const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
2210
- distributionPath=wrapper/dists
2211
- distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
2212
- networkTimeout=10000
2213
- validateDistributionUrl=true
2214
- zipStoreBase=GRADLE_USER_HOME
2215
- zipStorePath=wrapper/dists
2216
- `;
2217
- writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
2218
- const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
2219
- <resources>
2220
- <string name="app_name">${escapeXml(name)}</string>
2221
- </resources>
2222
- `;
2223
- writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
2224
- const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
2225
- <resources>
2226
- <color name="primary">#1a1a2e</color>
2227
- <color name="primary_dark">#16162b</color>
2228
- <color name="accent">#4ade80</color>
2229
- <color name="background">${config.backgroundColor}</color>
2230
- </resources>
2231
- `;
2232
- writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
2233
- const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
2234
- <vector xmlns:android="http://schemas.android.com/apk/res/android"
2235
- android:width="108dp"
2236
- android:height="108dp"
2237
- android:viewportWidth="108"
2238
- android:viewportHeight="108">
2239
- <path android:fillColor="${config.backgroundColor}" android:pathData="M0,0h108v108h-108z" />
2240
- <path android:fillColor="#10B981" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
2241
- </vector>
2242
- `;
2243
- if (config.appIconPath) {
2244
- cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
2245
- } else {
2246
- writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
2247
- }
2248
- const themesXml = `<?xml version="1.0" encoding="utf-8"?>
2249
- <resources>
2250
- <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
2251
- <item name="android:statusBarColor">@color/primary_dark</item>
2252
- <item name="android:navigationBarColor">@color/primary</item>
2253
- <item name="android:windowBackground">@color/background</item>
2254
- </style>
2255
- </resources>
2256
- `;
2257
- writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
2258
- const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
2259
- <androidx.coordinatorlayout.widget.CoordinatorLayout
2260
- xmlns:android="http://schemas.android.com/apk/res/android"
2261
- android:layout_width="match_parent"
2262
- android:layout_height="match_parent"
2263
- android:fitsSystemWindows="true">
2264
-
2265
- <WebView
2266
- android:id="@+id/webview"
2267
- android:layout_width="match_parent"
2268
- android:layout_height="match_parent" />
2269
-
2270
- </androidx.coordinatorlayout.widget.CoordinatorLayout>
2271
- `;
2272
- writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
2273
- const placeholderHtml = `<!DOCTYPE html>
2274
- <html>
2275
- <head>
2276
- <meta charset="UTF-8">
2277
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
2278
- <title>${escapeXml(name)}</title>
2279
- <style>
2280
- * { margin: 0; padding: 0; box-sizing: border-box; }
2281
- body {
2282
- font-family: 'Roboto', sans-serif;
2283
- background: ${config.backgroundColor};
2284
- color: white;
2285
- min-height: 100vh;
2286
- display: flex;
2287
- justify-content: center;
2288
- align-items: center;
2289
- }
2290
- .container { text-align: center; padding: 2rem; }
2291
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
2292
- p { opacity: 0.7; }
2293
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
2294
- </style>
2295
- </head>
2296
- <body>
2297
- <div class="container">
2298
- <h1>\u26A1 ${escapeXml(name)}</h1>
2299
- <p>Built with Craft Android</p>
2300
- <p class="ready" id="status">Waiting for Craft bridge...</p>
2301
- </div>
2302
- <script>
2303
- window.addEventListener('craftReady', (e) => {
2304
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
2305
- console.log('Craft capabilities:', e.detail.capabilities);
2306
- });
2307
- </script>
2308
- </body>
2309
- </html>`;
2310
- writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
2311
- const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2312
- if (runtimeDir) {
2313
- installRuntime2(output, runtimeDir);
2314
- console.log(" Installed the Zig runtime from", runtimeDir);
2315
- }
2316
- console.log("\u2705 Project initialized");
2317
- console.log("");
2318
- console.log("Next steps:");
2319
- console.log(` 1. cd ${output}`);
2320
- console.log(" 2. Add your web content to app/src/main/assets/index.html");
2321
- console.log(" 3. Run: craft android build");
2322
- console.log(" 4. Run: craft android open");
2323
- console.log("");
2324
- }
2325
- async function build2(options) {
2326
- const { htmlPath, devServer, output, release, compile = true } = options;
2327
- console.log(`
2328
- \uD83D\uDCE6 Building Craft Android project...`);
2329
- const configPath = join3(output, "craft.config.json");
2330
- if (!existsSync5(configPath)) {
2331
- throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
2332
- }
2333
- const config = JSON.parse(readFileSync4(configPath, "utf-8"));
2334
- if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
2335
- const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2336
- if (runtimeDir) {
2337
- installRuntime2(output, runtimeDir);
2338
- console.log(" Refreshed the Zig runtime from", runtimeDir);
2339
- } else {
2340
- console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
2341
- }
2342
- }
2343
- if (devServer) {
2344
- const url = androidWebUrl(devServer, "Android dev server URL");
2345
- config.devServerURL = url.toString();
2346
- config.trustedOrigins = [...new Set([
2347
- ...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
2348
- url.origin
2349
- ])];
2350
- writeAndroidConfig(output, config);
2351
- console.log(` Dev server: ${devServer}`);
2352
- }
2353
- if (htmlPath) {
2354
- syncAndroidWebAssets(htmlPath, output);
2355
- config.hasBundledFallback = Boolean(devServer);
2356
- writeAndroidConfig(output, config);
2357
- console.log(` Synced: ${htmlPath} \u2192 assets/`);
2358
- }
2359
- if (!compile)
2360
- return;
2361
- const gradlewPath = join3(output, "gradlew");
2362
- if (!existsSync5(gradlewPath)) {
2363
- console.log(" Generating Gradle wrapper...");
2364
- try {
2365
- await $2`cd ${output} && gradle wrapper`.quiet();
2366
- } catch (error) {
2367
- throw new Error("Gradle is unavailable. Install Android Studio or Gradle before building Android.", { cause: error });
2368
- }
2369
- }
2370
- const buildType = release ? "assembleRelease" : "assembleDebug";
2371
- console.log(` Building ${release ? "release" : "debug"} APK...`);
2372
- try {
2373
- await $2`cd ${output} && ./gradlew ${buildType}`;
2374
- const apkPath = release ? "app/build/outputs/apk/release/app-release.apk" : "app/build/outputs/apk/debug/app-debug.apk";
2375
- console.log(`\u2705 APK built: ${apkPath}`);
2376
- } catch (error) {
2377
- console.error("Build failed. Open in Android Studio for details.");
2378
- throw error;
2379
- }
2380
- console.log("");
2381
- }
2382
- async function open2(options) {
2383
- const { output } = options;
2384
- if (!existsSync5(output)) {
2385
- throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`);
2386
- }
2387
- console.log(`\uD83D\uDE80 Opening Android project in Android Studio...`);
2388
- try {
2389
- await $2`open -a "Android Studio" ${output}`.quiet();
2390
- } catch {
2391
- try {
2392
- await $2`studio ${output}`.quiet();
2393
- } catch {
2394
- console.log("\u26A0\uFE0F Android Studio not found.");
2395
- console.log(" Please open the project manually in Android Studio:");
2396
- console.log(` ${output}`);
2397
- }
2398
- }
2399
- }
2400
- async function run2(options) {
2401
- const { device, output } = options;
2402
- await build2({ output });
2403
- console.log("\uD83D\uDCF1 Installing and running on device...");
2404
- try {
2405
- const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
2406
- if (device) {
2407
- await $2`adb -s ${device} install -r ${apkPath}`;
2408
- } else {
2409
- await $2`adb install -r ${apkPath}`;
2410
- }
2411
- const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
2412
- const launchCmd = `${config.packageName}/.MainActivity`;
2413
- if (device) {
2414
- await $2`adb -s ${device} shell am start -n ${launchCmd}`;
2415
- } else {
2416
- await $2`adb shell am start -n ${launchCmd}`;
2417
- }
2418
- console.log("\u2705 App launched");
2419
- } catch (error) {
2420
- console.error("Failed to install/run. Make sure:");
2421
- console.error(" 1. ADB is installed and in PATH");
2422
- console.error(" 2. A device/emulator is connected (run: adb devices)");
2423
- throw error;
2424
- }
2425
- }
2426
- var $2, ANDROID_PROMISE_RUNTIME = `
2427
- if (window.__craftRejectPendingPromises) {
2428
- window.__craftRejectPendingPromises('Android bridge reinitialized');
2429
- }
2430
- if (window.__craftRejectPermissionRequests) {
2431
- window.__craftRejectPermissionRequests('Android bridge reinitialized');
2432
- }
2433
- window.__craftPromiseRuntimeClosed = false;
2434
- window.__craftPendingPromises = Object.create(null);
2435
- window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
2436
- if (window.__craftPromiseRuntimeClosed) {
2437
- return Promise.reject(new Error('Android bridge is closed'));
2438
- }
2439
- if (window.__craftPendingPromises[channel]) {
2440
- return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
2441
- }
2442
-
2443
- return new Promise(function(resolve, reject) {
2444
- var entry = {settled: false, timer: null, settle: null};
2445
- var resolveCallback;
2446
- var rejectCallback;
2447
-
2448
- entry.settle = function(succeeded, value) {
2449
- if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
2450
- entry.settled = true;
2451
- if (entry.timer !== null) clearTimeout(entry.timer);
2452
- if (window[resolveName] === resolveCallback) window[resolveName] = null;
2453
- if (window[rejectName] === rejectCallback) window[rejectName] = null;
2454
- delete window.__craftPendingPromises[channel];
2455
- if (succeeded) resolve(value);
2456
- else reject(value);
2457
- };
2458
-
2459
- resolveCallback = function(value) {
2460
- entry.settle(true, value);
2461
- };
2462
- rejectCallback = function(error) {
2463
- entry.settle(false, error);
2464
- };
2465
- window[resolveName] = resolveCallback;
2466
- window[rejectName] = rejectCallback;
2467
- window.__craftPendingPromises[channel] = entry;
2468
-
2469
- if (timeoutMs > 0) {
2470
- entry.timer = setTimeout(function() {
2471
- entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
2472
- }, timeoutMs);
2473
- }
2474
-
2475
- try {
2476
- invoke();
2477
- }
2478
- catch (error) {
2479
- entry.settle(false, error);
2480
- }
2481
- });
2482
- };
2483
-
2484
- window.__craftRejectPendingPromises = function(message) {
2485
- window.__craftPromiseRuntimeClosed = true;
2486
- Object.keys(window.__craftPendingPromises).forEach(function(channel) {
2487
- var entry = window.__craftPendingPromises[channel];
2488
- if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
2489
- });
2490
- };
2491
- `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
2492
- var init_src2 = __esm(() => {
2493
- ({ $: $2 } = globalThis.Bun);
2494
- TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
2495
- LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
2496
- KOTLIN_KEYWORDS = new Set([
2497
- "as",
2498
- "break",
2499
- "class",
2500
- "continue",
2501
- "do",
2502
- "else",
2503
- "false",
2504
- "for",
2505
- "fun",
2506
- "if",
2507
- "in",
2508
- "interface",
2509
- "is",
2510
- "null",
2511
- "object",
2512
- "package",
2513
- "return",
2514
- "super",
2515
- "this",
2516
- "throw",
2517
- "true",
2518
- "try",
2519
- "typealias",
2520
- "typeof",
2521
- "val",
2522
- "var",
2523
- "when",
2524
- "while"
2525
- ]);
2526
- DEFAULT_CONFIG2 = {
2527
- version: "1.0.0",
2528
- versionCode: 1,
2529
- darkMode: true,
2530
- backgroundColor: "#0b1712",
2531
- enableSpeechRecognition: false,
2532
- enableHaptics: false,
2533
- enableShare: false,
2534
- enableCamera: false,
2535
- enableBiometric: false,
2536
- enablePushNotifications: false,
2537
- enableSecureStorage: false,
2538
- enableGeolocation: false,
2539
- enableBackgroundLocation: false,
2540
- enableHealthConnect: false,
2541
- enableKeepAwake: false,
2542
- enableDeepLinks: false,
2543
- urlSchemes: [],
2544
- trustedOrigins: [],
2545
- minSdk: ANDROID_MIN_SDK,
2546
- compileSdk: 36,
2547
- targetSdk: 35
2548
- };
2549
- RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
2550
- });
2551
-
2552
1142
  // ../../node_modules/@stacksjs/clapp/dist/index.js
2553
1143
  import { existsSync } from "fs";
2554
1144
  import fs from "fs/promises";
@@ -4633,7 +3223,7 @@ var prefix = getPrefix();
4633
3223
 
4634
3224
  // bin/cli.ts
4635
3225
  import { spawn as spawn2 } from "child_process";
4636
- import { existsSync as existsSync6 } from "fs";
3226
+ import { existsSync as existsSync4 } from "fs";
4637
3227
  import process6 from "process";
4638
3228
 
4639
3229
  // src/binary-resolver.ts
@@ -4713,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
4713
3303
  `);
4714
3304
  }
4715
3305
  // package.json
4716
- var version = "0.0.105";
3306
+ var version = "0.0.109";
4717
3307
 
4718
3308
  // bin/cli.ts
4719
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -4722,16 +3312,18 @@ if (spawnedFrom) {
4722
3312
  process6.exit(1);
4723
3313
  }
4724
3314
  var cli = new CLI("craft");
3315
+ var iosModuleURL = new URL("../dist/ios/src/index.js", import.meta.url).href;
3316
+ var androidModuleURL = new URL("../dist/android/src/index.js", import.meta.url).href;
4725
3317
  async function runCraftBinary(args) {
4726
3318
  const craftPath = resolveCraftBinary();
4727
- return new Promise((resolve3, reject) => {
3319
+ return new Promise((resolve, reject) => {
4728
3320
  const proc = spawn2(craftPath, args, {
4729
3321
  stdio: "inherit",
4730
3322
  env: { ...process6.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
4731
3323
  });
4732
3324
  proc.on("exit", (code) => {
4733
3325
  if (code === 0 || code === null)
4734
- resolve3();
3326
+ resolve();
4735
3327
  else
4736
3328
  reject(new Error(`Craft exited with code ${code}`));
4737
3329
  });
@@ -4825,7 +3417,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4825
3417
  console.log(`Platforms: ${platforms.join(", ")}`);
4826
3418
  console.log(`Mode: ${options?.release ? "Release" : "Debug"}
4827
3419
  `);
4828
- const { existsSync: existsSync7 } = await import("fs");
3420
+ const { existsSync: existsSync5 } = await import("fs");
4829
3421
  const { spawn: spawn3 } = await import("child_process");
4830
3422
  for (const platform of platforms) {
4831
3423
  console.log(`\uD83D\uDCE6 Building for ${platform}...`);
@@ -4836,9 +3428,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4836
3428
  continue;
4837
3429
  }
4838
3430
  const iosDir = "./ios";
4839
- if (existsSync7(iosDir)) {
3431
+ if (existsSync5(iosDir)) {
4840
3432
  const buildType = options?.release ? "Release" : "Debug";
4841
- await new Promise((resolve3, reject) => {
3433
+ await new Promise((resolve, reject) => {
4842
3434
  const proc = spawn3("xcodebuild", [
4843
3435
  "-project",
4844
3436
  `${iosDir}/App.xcodeproj`,
@@ -4850,7 +3442,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4850
3442
  "iphoneos",
4851
3443
  "build"
4852
3444
  ], { stdio: "inherit" });
4853
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3445
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4854
3446
  proc.on("error", reject);
4855
3447
  });
4856
3448
  console.log(` \u2705 iOS ${buildType} build complete`);
@@ -4859,14 +3451,14 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4859
3451
  }
4860
3452
  } else if (platform === "android") {
4861
3453
  const androidDir = "./android";
4862
- if (existsSync7(androidDir)) {
3454
+ if (existsSync5(androidDir)) {
4863
3455
  const task = options?.release ? "assembleRelease" : "assembleDebug";
4864
- await new Promise((resolve3, reject) => {
3456
+ await new Promise((resolve, reject) => {
4865
3457
  const proc = spawn3("./gradlew", [task], {
4866
3458
  cwd: androidDir,
4867
3459
  stdio: "inherit"
4868
3460
  });
4869
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3461
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4870
3462
  proc.on("error", reject);
4871
3463
  });
4872
3464
  console.log(` \u2705 Android ${options?.release ? "Release" : "Debug"} build complete`);
@@ -4874,9 +3466,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4874
3466
  console.log(` \u26A0\uFE0F No Android project found. Run: craft android init`);
4875
3467
  }
4876
3468
  } else if (platform === "macos" || platform === "windows" || platform === "linux") {
4877
- const zigDir = existsSync7("./packages/zig") ? "./packages/zig" : ".";
3469
+ const zigDir = existsSync5("./packages/zig") ? "./packages/zig" : ".";
4878
3470
  const optimizeFlag = options?.release ? "-Doptimize=ReleaseSafe" : "";
4879
- await new Promise((resolve3, reject) => {
3471
+ await new Promise((resolve, reject) => {
4880
3472
  const args = ["build"];
4881
3473
  if (optimizeFlag)
4882
3474
  args.push(optimizeFlag);
@@ -4884,7 +3476,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4884
3476
  cwd: zigDir,
4885
3477
  stdio: "inherit"
4886
3478
  });
4887
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3479
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4888
3480
  proc.on("error", reject);
4889
3481
  });
4890
3482
  console.log(` \u2705 ${platform} build complete`);
@@ -4923,7 +3515,7 @@ cli.command("package", "Create installers for your Craft application").option("-
4923
3515
  const { packageApp: packageApp2 } = packageModule;
4924
3516
  let config;
4925
3517
  if (options?.config) {
4926
- if (!existsSync6(options.config)) {
3518
+ if (!existsSync4(options.config)) {
4927
3519
  console.error(`\u274C Config file not found: ${options.config}`);
4928
3520
  process6.exit(1);
4929
3521
  }
@@ -5009,20 +3601,22 @@ cli.command("package", "Create installers for your Craft application").option("-
5009
3601
  process6.exit(1);
5010
3602
  }
5011
3603
  });
5012
- cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
5013
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3604
+ cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("--renderer <kind>", "iOS renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
3605
+ const iosModule = await import(iosModuleURL);
5014
3606
  await iosModule.init({
5015
3607
  name,
5016
3608
  bundleId: options?.bundleId,
5017
3609
  teamId: options?.teamId,
5018
- output: options?.output || "./ios"
3610
+ output: options?.output || "./ios",
3611
+ config: options?.renderer ? { renderer: options.renderer } : undefined
5019
3612
  });
5020
3613
  });
5021
- cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
5022
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3614
+ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --native-bundle ./screen.js").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
3615
+ const iosModule = await import(iosModuleURL);
5023
3616
  const doBuild = async () => {
5024
3617
  await iosModule.build({
5025
3618
  htmlPath: options?.htmlPath,
3619
+ nativeBundlePath: options?.nativeBundle,
5026
3620
  devServer: options?.devServer,
5027
3621
  output: options?.output || "./ios"
5028
3622
  });
@@ -5033,8 +3627,8 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
5033
3627
  \uD83D\uDC40 Watching for changes...
5034
3628
  `);
5035
3629
  const { watch } = await import("fs");
5036
- const { dirname: dirname3 } = await import("path");
5037
- const watchPath = options?.htmlPath ? dirname3(options.htmlPath) : ".";
3630
+ const { dirname } = await import("path");
3631
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
5038
3632
  watch(watchPath, { recursive: true }, async (event, filename) => {
5039
3633
  if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
5040
3634
  console.log(`
@@ -5046,31 +3640,33 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
5046
3640
  }
5047
3641
  });
5048
3642
  cli.command("ios open", "Open iOS project in Xcode").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).action(async (options) => {
5049
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3643
+ const iosModule = await import(iosModuleURL);
5050
3644
  await iosModule.open({
5051
3645
  output: options?.output || "./ios"
5052
3646
  });
5053
3647
  });
5054
3648
  cli.command("ios run", "Build and run on iOS device or simulator").option("-s, --simulator", "Run on simulator instead of device").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).example("craft ios run").example("craft ios run --simulator").action(async (options) => {
5055
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3649
+ const iosModule = await import(iosModuleURL);
5056
3650
  await iosModule.run({
5057
3651
  simulator: options?.simulator || false,
5058
3652
  output: options?.output || "./ios"
5059
3653
  });
5060
3654
  });
5061
- cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
5062
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3655
+ cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("--renderer <kind>", "Android renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
3656
+ const androidModule = await import(androidModuleURL);
5063
3657
  await androidModule.init({
5064
3658
  name,
5065
3659
  packageName: options?.package,
5066
- output: options?.output || "./android"
3660
+ output: options?.output || "./android",
3661
+ config: options?.renderer ? { renderer: options.renderer } : undefined
5067
3662
  });
5068
3663
  });
5069
- cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --watch").action(async (options) => {
5070
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3664
+ cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --native-bundle ./screen.js").example("craft android build --watch").action(async (options) => {
3665
+ const androidModule = await import(androidModuleURL);
5071
3666
  const doBuild = async () => {
5072
3667
  await androidModule.build({
5073
3668
  htmlPath: options?.htmlPath,
3669
+ nativeBundlePath: options?.nativeBundle,
5074
3670
  devServer: options?.devServer,
5075
3671
  output: options?.output || "./android",
5076
3672
  release: options?.release || false
@@ -5082,8 +3678,8 @@ cli.command("android build", "Build Android project").option("--html-path <path>
5082
3678
  \uD83D\uDC40 Watching for changes...
5083
3679
  `);
5084
3680
  const { watch } = await import("fs");
5085
- const { dirname: dirname3 } = await import("path");
5086
- const watchPath = options?.htmlPath ? dirname3(options.htmlPath) : ".";
3681
+ const { dirname } = await import("path");
3682
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
5087
3683
  watch(watchPath, { recursive: true }, async (event, filename) => {
5088
3684
  if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
5089
3685
  console.log(`
@@ -5095,13 +3691,13 @@ cli.command("android build", "Build Android project").option("--html-path <path>
5095
3691
  }
5096
3692
  });
5097
3693
  cli.command("android open", "Open Android project in Android Studio").option("-o, --output <dir>", "Android project directory", { default: "./android" }).action(async (options) => {
5098
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3694
+ const androidModule = await import(androidModuleURL);
5099
3695
  await androidModule.open({
5100
3696
  output: options?.output || "./android"
5101
3697
  });
5102
3698
  });
5103
3699
  cli.command("android run", "Build and run on Android device or emulator").option("-d, --device <id>", "Target device ID").option("-o, --output <dir>", "Android project directory", { default: "./android" }).example("craft android run").example("craft android run --device emulator-5554").action(async (options) => {
5104
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3700
+ const androidModule = await import(androidModuleURL);
5105
3701
  await androidModule.run({
5106
3702
  device: options?.device,
5107
3703
  output: options?.output || "./android"
@@ -5128,30 +3724,30 @@ cli.command("preview [path]", "Preview app in browser before building native").o
5128
3724
  });
5129
3725
  });
5130
3726
  cli.command("publish", "Publish app to App Store or Play Store").option("--ios", "Publish to App Store (TestFlight)").option("--android", "Publish to Play Store").option("--api-key <path>", "App Store Connect API key path").option("--service-account <path>", "Google Play service account JSON").option("-o, --output <dir>", "Project directory", { default: "." }).example("craft publish --ios").example("craft publish --android").action(async (options) => {
5131
- const { existsSync: existsSync7 } = await import("fs");
5132
- const { join: join4 } = await import("path");
5133
- const { $: $3 } = await Promise.resolve(globalThis.Bun);
3727
+ const { existsSync: existsSync5 } = await import("fs");
3728
+ const { join: join2 } = await import("path");
3729
+ const { $ } = await Promise.resolve(globalThis.Bun);
5134
3730
  if (options?.ios) {
5135
3731
  console.log(`
5136
3732
  \uD83D\uDCF1 Publishing to App Store (TestFlight)...
5137
3733
  `);
5138
- const iosDir = join4(options?.output || ".", "ios");
5139
- if (!existsSync7(iosDir)) {
3734
+ const iosDir = join2(options?.output || ".", "ios");
3735
+ if (!existsSync5(iosDir)) {
5140
3736
  console.error("\u274C No iOS project found. Run: craft ios init");
5141
3737
  process6.exit(1);
5142
3738
  }
5143
3739
  console.log("1. Building archive...");
5144
3740
  try {
5145
- await $3`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
3741
+ await $`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
5146
3742
  console.log("\u2705 Archive created");
5147
3743
  console.log("2. Exporting IPA...");
5148
- await $3`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
3744
+ await $`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
5149
3745
  console.log("\u2705 IPA exported");
5150
3746
  console.log("3. Uploading to TestFlight...");
5151
3747
  if (options?.apiKey) {
5152
- await $3`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
3748
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
5153
3749
  } else {
5154
- await $3`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
3750
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
5155
3751
  }
5156
3752
  console.log("\u2705 Uploaded to TestFlight!");
5157
3753
  } catch (error) {
@@ -5165,16 +3761,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
5165
3761
  console.log(`
5166
3762
  \uD83E\uDD16 Publishing to Play Store...
5167
3763
  `);
5168
- const androidDir = join4(options?.output || ".", "android");
5169
- if (!existsSync7(androidDir)) {
3764
+ const androidDir = join2(options?.output || ".", "android");
3765
+ if (!existsSync5(androidDir)) {
5170
3766
  console.error("\u274C No Android project found. Run: craft android init");
5171
3767
  process6.exit(1);
5172
3768
  }
5173
3769
  console.log("1. Building release AAB...");
5174
3770
  try {
5175
- await $3`cd ${androidDir} && ./gradlew bundleRelease`;
3771
+ await $`cd ${androidDir} && ./gradlew bundleRelease`;
5176
3772
  console.log("\u2705 AAB created");
5177
- const aabPath = join4(androidDir, "app/build/outputs/bundle/release/app-release.aab");
3773
+ const aabPath = join2(androidDir, "app/build/outputs/bundle/release/app-release.aab");
5178
3774
  if (options?.serviceAccount) {
5179
3775
  console.log("2. Uploading to Play Store...");
5180
3776
  console.log("\u26A0\uFE0F Automatic Play Store upload requires fastlane or Google Play API integration.");
@@ -5213,31 +3809,31 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5213
3809
  `);
5214
3810
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
5215
3811
  const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
5216
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
5217
- const { join: join4, dirname: dirname3 } = await import("path");
3812
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3, existsSync: existsSync5, readdirSync: readdirSync2, readFileSync: readFileSync3, cpSync: cpSync2 } = await import("fs");
3813
+ const { join: join2, dirname } = await import("path");
5218
3814
  const replaceVars = (content) => {
5219
3815
  return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
5220
3816
  };
5221
3817
  const copyTemplate = async (templateName, destDir) => {
5222
- const templateDir = join4(import.meta.dir, "templates/projects", templateName);
5223
- if (!existsSync7(templateDir)) {
3818
+ const templateDir = join2(import.meta.dir, "templates/projects", templateName);
3819
+ if (!existsSync5(templateDir)) {
5224
3820
  console.log(` \u26A0\uFE0F Template '${templateName}' not found, using blank template`);
5225
3821
  return false;
5226
3822
  }
5227
3823
  const copyRecursive = (src, dest) => {
5228
- if (!existsSync7(dest)) {
5229
- mkdirSync4(dest, { recursive: true });
3824
+ if (!existsSync5(dest)) {
3825
+ mkdirSync2(dest, { recursive: true });
5230
3826
  }
5231
- const entries = readdirSync3(src, { withFileTypes: true });
3827
+ const entries = readdirSync2(src, { withFileTypes: true });
5232
3828
  for (const entry of entries) {
5233
- const srcPath = join4(src, entry.name);
5234
- const destPath = join4(dest, entry.name);
3829
+ const srcPath = join2(src, entry.name);
3830
+ const destPath = join2(dest, entry.name);
5235
3831
  if (entry.isDirectory()) {
5236
3832
  copyRecursive(srcPath, destPath);
5237
3833
  } else {
5238
- const content = readFileSync5(srcPath, "utf-8");
3834
+ const content = readFileSync3(srcPath, "utf-8");
5239
3835
  const processedContent = replaceVars(content);
5240
- writeFileSync5(destPath, processedContent);
3836
+ writeFileSync3(destPath, processedContent);
5241
3837
  }
5242
3838
  }
5243
3839
  };
@@ -5246,13 +3842,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5246
3842
  };
5247
3843
  if (["blank", "tabs", "drawer", "dashboard"].includes(template)) {
5248
3844
  console.log(`\uD83D\uDCC1 Creating ${template} project from template...`);
5249
- if (!existsSync7(name)) {
5250
- mkdirSync4(name, { recursive: true });
3845
+ if (!existsSync5(name)) {
3846
+ mkdirSync2(name, { recursive: true });
5251
3847
  }
5252
3848
  const copied = await copyTemplate(template, name);
5253
3849
  if (!copied) {
5254
- mkdirSync4(join4(name, "src"), { recursive: true });
5255
- writeFileSync5(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
3850
+ mkdirSync2(join2(name, "src"), { recursive: true });
3851
+ writeFileSync3(join2(name, "index.html"), replaceVars(`<!DOCTYPE html>
5256
3852
  <html lang="en">
5257
3853
  <head>
5258
3854
  <meta charset="UTF-8">
@@ -5266,7 +3862,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5266
3862
  </div>
5267
3863
  </body>
5268
3864
  </html>`));
5269
- writeFileSync5(join4(name, "package.json"), replaceVars(JSON.stringify({
3865
+ writeFileSync3(join2(name, "package.json"), replaceVars(JSON.stringify({
5270
3866
  name: "{{APP_NAME_SLUG}}",
5271
3867
  version: "1.0.0",
5272
3868
  private: true,
@@ -5280,13 +3876,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5280
3876
  }
5281
3877
  }, null, 2)));
5282
3878
  }
5283
- pinCraftNativeDependency(join4(name, "package.json"), version);
3879
+ pinCraftNativeDependency(join2(name, "package.json"), version);
5284
3880
  console.log(`\u2705 ${template} project created`);
5285
3881
  }
5286
3882
  if (template === "desktop" || template === "all") {
5287
3883
  console.log("\uD83D\uDCC1 Creating desktop project structure...");
5288
- if (!existsSync7(name)) {
5289
- mkdirSync4(name, { recursive: true });
3884
+ if (!existsSync5(name)) {
3885
+ mkdirSync2(name, { recursive: true });
5290
3886
  }
5291
3887
  const configContent = `import type { CraftConfig } from 'craft-native'
5292
3888
 
@@ -5300,7 +3896,7 @@ export default {
5300
3896
  },
5301
3897
  } satisfies CraftConfig
5302
3898
  `;
5303
- writeFileSync5(`${name}/craft.config.ts`, configContent);
3899
+ writeFileSync3(`${name}/craft.config.ts`, configContent);
5304
3900
  const htmlContent = `<!DOCTYPE html>
5305
3901
  <html lang="en">
5306
3902
  <head>
@@ -5331,7 +3927,7 @@ export default {
5331
3927
  </body>
5332
3928
  </html>
5333
3929
  `;
5334
- writeFileSync5(`${name}/index.html`, htmlContent);
3930
+ writeFileSync3(`${name}/index.html`, htmlContent);
5335
3931
  const packageJson = {
5336
3932
  name: appNameSlug,
5337
3933
  version: "0.1.0",
@@ -5347,13 +3943,13 @@ export default {
5347
3943
  "craft-native": `^${version}`
5348
3944
  }
5349
3945
  };
5350
- writeFileSync5(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
3946
+ writeFileSync3(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5351
3947
  pinCraftNativeDependency(`${name}/package.json`, version);
5352
3948
  console.log("\u2705 Desktop project created");
5353
3949
  }
5354
3950
  if (template === "ios" || template === "all") {
5355
3951
  console.log("\uD83D\uDCF1 Creating iOS project...");
5356
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3952
+ const iosModule = await import(iosModuleURL);
5357
3953
  await iosModule.init({
5358
3954
  name,
5359
3955
  bundleId: options?.bundleId,
@@ -5363,7 +3959,7 @@ export default {
5363
3959
  }
5364
3960
  if (template === "android" || template === "all") {
5365
3961
  console.log("\uD83E\uDD16 Creating Android project...");
5366
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3962
+ const androidModule = await import(androidModuleURL);
5367
3963
  await androidModule.init({
5368
3964
  name,
5369
3965
  packageName: options?.bundleId,
@@ -5390,4 +3986,7 @@ export default {
5390
3986
  });
5391
3987
  cli.version(version);
5392
3988
  cli.help();
3989
+ if ((process6.argv[2] === "ios" || process6.argv[2] === "android") && ["init", "build", "open", "run"].includes(process6.argv[3])) {
3990
+ process6.argv.splice(2, 2, `${process6.argv[2]} ${process6.argv[3]}`);
3991
+ }
5393
3992
  cli.parse();