craft-native 0.0.55 → 0.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/android/src/index.d.ts +73 -0
  2. package/dist/android/src/index.js +177 -37
  3. package/dist/android/templates/AndroidManifest.xml.template +10 -36
  4. package/dist/android/templates/CraftBridge.kt.template +192 -60
  5. package/dist/android/templates/CraftHealthConnect.kt.template +200 -0
  6. package/dist/android/templates/CraftHealthConnectStub.kt.template +28 -0
  7. package/dist/android/templates/LocationRecordingService.kt.template +160 -0
  8. package/dist/android/templates/MainActivity.kt.template +140 -19
  9. package/dist/android/templates/build.gradle.kts.app.template +23 -1
  10. package/dist/android/templates/build.gradle.kts.project.template +3 -2
  11. package/dist/android/templates/proguard-rules.pro.template +9 -0
  12. package/dist/api/android-advanced.d.ts +5 -6
  13. package/dist/api/app.d.ts +0 -1
  14. package/dist/api/clipboard.d.ts +0 -1
  15. package/dist/api/crypto.d.ts +1 -2
  16. package/dist/api/db.d.ts +1 -2
  17. package/dist/api/dialog.d.ts +0 -1
  18. package/dist/api/fs.d.ts +1 -2
  19. package/dist/api/gestures.d.ts +66 -0
  20. package/dist/api/http.d.ts +1 -2
  21. package/dist/api/index.d.ts +37 -34
  22. package/dist/api/ios-advanced.d.ts +6 -7
  23. package/dist/api/macos-advanced.d.ts +2 -3
  24. package/dist/api/media.d.ts +0 -1
  25. package/dist/api/mobile.d.ts +109 -1
  26. package/dist/api/process.d.ts +0 -1
  27. package/dist/api/sidebar.d.ts +3 -20
  28. package/dist/api/spaces.d.ts +83 -0
  29. package/dist/api/tray.d.ts +2 -3
  30. package/dist/api/window.d.ts +1 -2
  31. package/dist/api/windows-advanced.d.ts +6 -7
  32. package/dist/assets/index.d.ts +0 -1
  33. package/dist/binary-resolver.d.ts +23 -1
  34. package/dist/bridge/android.d.ts +0 -1
  35. package/dist/bridge/core.d.ts +0 -38
  36. package/dist/bridge/ids.d.ts +0 -1
  37. package/dist/bridge/ios.d.ts +0 -1
  38. package/dist/bridge/webkit-pending.d.ts +0 -1
  39. package/dist/build/bundle-optimizer.d.ts +0 -1
  40. package/dist/cli.js +666 -90
  41. package/dist/components/index.d.ts +6 -7
  42. package/dist/components/native.d.ts +0 -1
  43. package/dist/components/sidebar.d.ts +2 -81
  44. package/dist/dev/devtools.d.ts +0 -1
  45. package/dist/dev/error-overlay.d.ts +0 -1
  46. package/dist/dev/hot-reload.d.ts +0 -1
  47. package/dist/index.cjs +113 -3
  48. package/dist/index.d.cts +16 -16
  49. package/dist/index.d.ts +16 -16
  50. package/dist/index.js +112 -4
  51. package/dist/ios/src/index.d.ts +172 -0
  52. package/dist/ios/src/index.js +440 -42
  53. package/dist/ios/templates/CraftActivityAttributes.swift +14 -0
  54. package/dist/ios/templates/CraftApp.swift +1001 -73
  55. package/dist/ios/templates/CraftLiveActivityWidget.swift.template +62 -0
  56. package/dist/ios/templates/CraftWatchApp.swift.template +155 -0
  57. package/dist/ios/templates/Info.plist.template +10 -9
  58. package/dist/ios/templates/WatchApp.Info.plist.template +26 -0
  59. package/dist/ios/templates/WidgetExtension.Info.plist +31 -0
  60. package/dist/ios/templates/project.yml.template +12 -1
  61. package/dist/logging/index.d.ts +0 -1
  62. package/dist/mobile.d.ts +8 -0
  63. package/dist/mobile.js +755 -0
  64. package/dist/optimizations/index.d.ts +3 -4
  65. package/dist/package.d.ts +11 -1
  66. package/dist/performance/runtime.d.ts +0 -1
  67. package/dist/performance/startup.d.ts +0 -1
  68. package/dist/plugins/index.d.ts +0 -1
  69. package/dist/security/index.d.ts +0 -1
  70. package/dist/sidebar-types.d.ts +102 -0
  71. package/dist/signing/index.d.ts +0 -1
  72. package/dist/styles/headwind.d.ts +0 -1
  73. package/dist/styles/index.d.ts +9 -10
  74. package/dist/styles/sidebar-templates.d.ts +0 -1
  75. package/dist/styles/sidebars.d.ts +0 -1
  76. package/dist/types.d.ts +131 -1
  77. package/dist/updater/index.d.ts +0 -1
  78. package/dist/utils/audio.d.ts +0 -1
  79. package/dist/utils/index.d.ts +4 -5
  80. package/dist/utils/native.d.ts +1 -2
  81. package/dist/utils/react.d.ts +0 -1
  82. package/dist/utils/storage.d.ts +0 -1
  83. package/dist/utils/svelte.d.ts +0 -1
  84. package/dist/utils/timer.d.ts +0 -1
  85. package/dist/utils/vue.d.ts +0 -1
  86. package/package.json +32 -4
  87. package/dist/android/src/index.js.map +0 -10
  88. package/dist/api/android-advanced.d.ts.map +0 -1
  89. package/dist/api/app.d.ts.map +0 -1
  90. package/dist/api/clipboard.d.ts.map +0 -1
  91. package/dist/api/crypto.d.ts.map +0 -1
  92. package/dist/api/db.d.ts.map +0 -1
  93. package/dist/api/dialog.d.ts.map +0 -1
  94. package/dist/api/fs.d.ts.map +0 -1
  95. package/dist/api/http.d.ts.map +0 -1
  96. package/dist/api/index.d.ts.map +0 -1
  97. package/dist/api/ios-advanced.d.ts.map +0 -1
  98. package/dist/api/macos-advanced.d.ts.map +0 -1
  99. package/dist/api/media.d.ts.map +0 -1
  100. package/dist/api/mobile.d.ts.map +0 -1
  101. package/dist/api/process.d.ts.map +0 -1
  102. package/dist/api/sidebar.d.ts.map +0 -1
  103. package/dist/api/tray.d.ts.map +0 -1
  104. package/dist/api/window.d.ts.map +0 -1
  105. package/dist/api/windows-advanced.d.ts.map +0 -1
  106. package/dist/assets/index.d.ts.map +0 -1
  107. package/dist/binary-resolver.d.ts.map +0 -1
  108. package/dist/bridge/android.d.ts.map +0 -1
  109. package/dist/bridge/core.d.ts.map +0 -1
  110. package/dist/bridge/ids.d.ts.map +0 -1
  111. package/dist/bridge/ios.d.ts.map +0 -1
  112. package/dist/bridge/webkit-pending.d.ts.map +0 -1
  113. package/dist/build/bundle-optimizer.d.ts.map +0 -1
  114. package/dist/components/index.d.ts.map +0 -1
  115. package/dist/components/native.d.ts.map +0 -1
  116. package/dist/components/sidebar.d.ts.map +0 -1
  117. package/dist/dev/devtools.d.ts.map +0 -1
  118. package/dist/dev/error-overlay.d.ts.map +0 -1
  119. package/dist/dev/hot-reload.d.ts.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/dist/index.js.map +0 -45
  122. package/dist/ios/src/index.js.map +0 -10
  123. package/dist/logging/index.d.ts.map +0 -1
  124. package/dist/optimizations/index.d.ts.map +0 -1
  125. package/dist/package.d.ts.map +0 -1
  126. package/dist/package.test.d.ts +0 -2
  127. package/dist/package.test.d.ts.map +0 -1
  128. package/dist/performance/runtime.d.ts.map +0 -1
  129. package/dist/performance/startup.d.ts.map +0 -1
  130. package/dist/plugins/index.d.ts.map +0 -1
  131. package/dist/security/index.d.ts.map +0 -1
  132. package/dist/signing/index.d.ts.map +0 -1
  133. package/dist/styles/headwind.d.ts.map +0 -1
  134. package/dist/styles/index.d.ts.map +0 -1
  135. package/dist/styles/sidebar-templates.d.ts.map +0 -1
  136. package/dist/styles/sidebars.d.ts.map +0 -1
  137. package/dist/types.d.ts.map +0 -1
  138. package/dist/updater/index.d.ts.map +0 -1
  139. package/dist/utils/audio.d.ts.map +0 -1
  140. package/dist/utils/index.d.ts.map +0 -1
  141. package/dist/utils/native.d.ts.map +0 -1
  142. package/dist/utils/react.d.ts.map +0 -1
  143. package/dist/utils/storage.d.ts.map +0 -1
  144. package/dist/utils/svelte.d.ts.map +0 -1
  145. package/dist/utils/timer.d.ts.map +0 -1
  146. package/dist/utils/vue.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -32,7 +32,8 @@ __export(exports_package, {
32
32
  formatPackagingCommandError: () => formatPackagingCommandError,
33
33
  dmgCreateArguments: () => dmgCreateArguments,
34
34
  dmgCapacityMegabytes: () => dmgCapacityMegabytes,
35
- codesignArguments: () => codesignArguments
35
+ codesignArguments: () => codesignArguments,
36
+ candleArguments: () => candleArguments
36
37
  });
37
38
  import { spawn } from "child_process";
38
39
  import { createHash } from "crypto";
@@ -622,6 +623,9 @@ function windowsExecutableName(name) {
622
623
  throw new Error(`Reserved Windows application name: ${JSON.stringify(name)}`);
623
624
  return `${name}.exe`;
624
625
  }
626
+ function candleArguments(architecture, wixobjPath, wxsPath) {
627
+ return ["-nologo", "-arch", architecture, "-out", wixobjPath, wxsPath];
628
+ }
625
629
  function runCommand(command, args, cwd) {
626
630
  return new Promise((resolve, reject) => {
627
631
  const child = spawn(command, args, { cwd, stdio: "inherit", windowsHide: true });
@@ -638,7 +642,7 @@ async function createMSI(opts) {
638
642
  const wixobjPath = join(tempDir, "installer.wixobj");
639
643
  copyFileSync(opts.binaryPath, sourcePath);
640
644
  writeFileSync(wxsPath, renderWixSource(opts, binaryName));
641
- await runCommand("candle.exe", ["-nologo", "-out", wixobjPath, wxsPath], tempDir);
645
+ await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
642
646
  await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
643
647
  if (opts.certificatePath) {
644
648
  const signArgs = ["sign", "/fd", "sha256", "/tr", "https://timestamp.digicert.com", "/td", "sha256", "/f", opts.certificatePath];
@@ -970,20 +974,261 @@ var init_package = __esm(() => {
970
974
  // dist/ios/src/index.js
971
975
  var exports_src = {};
972
976
  __export(exports_src, {
977
+ syncWebAssets: () => syncWebAssets,
978
+ showSimulator: () => showSimulator,
973
979
  run: () => run,
980
+ renderWatchEntitlements: () => renderWatchEntitlements,
981
+ renderUsageDescriptions: () => renderUsageDescriptions,
982
+ renderUrlTypes: () => renderUrlTypes,
983
+ renderPrivacyManifest: () => renderPrivacyManifest,
984
+ renderOrientations: () => renderOrientations,
985
+ renderEntitlements: () => renderEntitlements,
986
+ renderDeviceFamilies: () => renderDeviceFamilies,
987
+ renderBackgroundModes: () => renderBackgroundModes,
988
+ pickSimulator: () => pickSimulator,
989
+ orderSimulators: () => orderSimulators,
974
990
  open: () => open,
975
991
  init: () => init,
976
- build: () => build
992
+ build: () => build,
993
+ bootSimulator: () => bootSimulator
977
994
  });
978
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
979
- import { join as join2, dirname } from "path";
995
+ import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
996
+ import { dirname, join as join2, resolve } from "path";
997
+ function renderDeviceFamilies(config) {
998
+ const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
999
+ return [...values].join(",");
1000
+ }
1001
+ function xmlEscape(value) {
1002
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll(/\u0027/g, "&apos;");
1003
+ }
1004
+ function plistString(key, value) {
1005
+ return ` <key>${key}</key>
1006
+ <string>${xmlEscape(value)}</string>`;
1007
+ }
1008
+ function renderUsageDescriptions(config) {
1009
+ const entries = [
1010
+ [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
1011
+ [config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
1012
+ [config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
1013
+ [config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
1014
+ [config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
1015
+ [config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
1016
+ [config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
1017
+ [config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
1018
+ [config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
1019
+ [config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
1020
+ [config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
1021
+ [config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
1022
+ [config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
1023
+ [config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
1024
+ ];
1025
+ return entries.filter(([enabled2]) => enabled2).map(([, key, value]) => plistString(key, value)).join(`
1026
+ `);
1027
+ }
1028
+ function renderOrientations(config) {
1029
+ const names = {
1030
+ portrait: "UIInterfaceOrientationPortrait",
1031
+ "landscape-left": "UIInterfaceOrientationLandscapeLeft",
1032
+ "landscape-right": "UIInterfaceOrientationLandscapeRight",
1033
+ "portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
1034
+ };
1035
+ const values = config.orientations?.length ? config.orientations : ["portrait"];
1036
+ return values.map((value) => ` <string>${names[value]}</string>`).join(`
1037
+ `);
1038
+ }
1039
+ function renderUrlTypes(config) {
1040
+ const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
1041
+ if (!schemes.length)
1042
+ return "";
1043
+ return ` <key>CFBundleURLTypes</key>
1044
+ <array>
1045
+ <dict>
1046
+ <key>CFBundleURLSchemes</key>
1047
+ <array>
1048
+ ${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
1049
+ `)}
1050
+ </array>
1051
+ </dict>
1052
+ </array>`;
1053
+ }
1054
+ function plistArray(values, indent = 2) {
1055
+ const padding = " ".repeat(indent);
1056
+ return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
1057
+ `);
1058
+ }
1059
+ function renderBackgroundModes(config) {
1060
+ const modes = new Set;
1061
+ if (config.enableBackgroundLocation)
1062
+ modes.add("location");
1063
+ if (config.enableBackgroundTasks)
1064
+ modes.add("processing");
1065
+ if (config.enablePushNotifications)
1066
+ modes.add("remote-notification");
1067
+ if (!modes.size)
1068
+ return "";
1069
+ return ` <key>UIBackgroundModes</key>
1070
+ <array>
1071
+ ${plistArray([...modes])}
1072
+ </array>`;
1073
+ }
1074
+ function renderEntitlements(config) {
1075
+ const entries = [];
1076
+ if (config.associatedDomains?.length) {
1077
+ entries.push(` <key>com.apple.developer.associated-domains</key>
1078
+ <array>
1079
+ ${plistArray(config.associatedDomains)}
1080
+ </array>`);
1081
+ }
1082
+ if (config.appGroups?.length) {
1083
+ entries.push(` <key>com.apple.security.application-groups</key>
1084
+ <array>
1085
+ ${plistArray(config.appGroups)}
1086
+ </array>`);
1087
+ }
1088
+ if (config.enableHealthKit) {
1089
+ entries.push(` <key>com.apple.developer.healthkit</key>
1090
+ <true/>`);
1091
+ }
1092
+ if (config.enablePushNotifications) {
1093
+ entries.push(` <key>aps-environment</key>
1094
+ <string>development</string>`);
1095
+ }
1096
+ return `<?xml version="1.0" encoding="UTF-8"?>
1097
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1098
+ <plist version="1.0">
1099
+ <dict>
1100
+ ${entries.join(`
1101
+ `)}
1102
+ </dict>
1103
+ </plist>
1104
+ `;
1105
+ }
1106
+ function renderWatchEntitlements(config) {
1107
+ const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
1108
+ <array>
1109
+ ${plistArray(config.appGroups)}
1110
+ </array>
1111
+ ` : "";
1112
+ return `<?xml version="1.0" encoding="UTF-8"?>
1113
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1114
+ <plist version="1.0">
1115
+ <dict>
1116
+ ${appGroups}</dict>
1117
+ </plist>
1118
+ `;
1119
+ }
1120
+ function renderPrivacyManifest(config) {
1121
+ const privacy = config.privacy ?? {};
1122
+ const collected = privacy.collectedDataTypes ?? [];
1123
+ const accessed = privacy.accessedApiTypes ?? [];
1124
+ const collectedXml = collected.map((item) => ` <dict>
1125
+ <key>NSPrivacyCollectedDataType</key>
1126
+ <string>${xmlEscape(item.type)}</string>
1127
+ <key>NSPrivacyCollectedDataTypeLinked</key>
1128
+ <${item.linked ? "true" : "false"}/>
1129
+ <key>NSPrivacyCollectedDataTypeTracking</key>
1130
+ <${item.tracking ? "true" : "false"}/>
1131
+ <key>NSPrivacyCollectedDataTypePurposes</key>
1132
+ <array>
1133
+ ${plistArray(item.purposes, 4)}
1134
+ </array>
1135
+ </dict>`).join(`
1136
+ `);
1137
+ const accessedXml = accessed.map((item) => ` <dict>
1138
+ <key>NSPrivacyAccessedAPIType</key>
1139
+ <string>${xmlEscape(item.type)}</string>
1140
+ <key>NSPrivacyAccessedAPITypeReasons</key>
1141
+ <array>
1142
+ ${plistArray(item.reasons, 4)}
1143
+ </array>
1144
+ </dict>`).join(`
1145
+ `);
1146
+ return `<?xml version="1.0" encoding="UTF-8"?>
1147
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1148
+ <plist version="1.0">
1149
+ <dict>
1150
+ <key>NSPrivacyTracking</key>
1151
+ <${privacy.tracking ? "true" : "false"}/>
1152
+ <key>NSPrivacyTrackingDomains</key>
1153
+ <array>
1154
+ ${plistArray(privacy.trackingDomains ?? [])}
1155
+ </array>
1156
+ <key>NSPrivacyCollectedDataTypes</key>
1157
+ <array>
1158
+ ${collectedXml}
1159
+ </array>
1160
+ <key>NSPrivacyAccessedAPITypes</key>
1161
+ <array>
1162
+ ${accessedXml}
1163
+ </array>
1164
+ </dict>
1165
+ </plist>
1166
+ `;
1167
+ }
1168
+ function renderAssetCatalog(output, config) {
1169
+ const catalog = join2(output, "Assets.xcassets");
1170
+ const appIcon = join2(catalog, "AppIcon.appiconset");
1171
+ const launchBackground = join2(catalog, "LaunchBackground.colorset");
1172
+ mkdirSync2(appIcon, { recursive: true });
1173
+ mkdirSync2(launchBackground, { recursive: true });
1174
+ writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
1175
+ `);
1176
+ const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
1177
+ if (config.appIconPath) {
1178
+ if (!existsSync4(config.appIconPath))
1179
+ throw new Error(`App icon not found: ${config.appIconPath}`);
1180
+ cpSync2(config.appIconPath, join2(appIcon, iconFilename));
1181
+ }
1182
+ writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
1183
+ images: iconFilename ? [{
1184
+ filename: iconFilename,
1185
+ idiom: "universal",
1186
+ platform: "ios",
1187
+ size: "1024x1024"
1188
+ }] : [],
1189
+ info: { author: "xcode", version: 1 }
1190
+ }, null, 2)}
1191
+ `);
1192
+ const color = config.backgroundColor?.replace(/^#/, "") || "000000";
1193
+ const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
1194
+ const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
1195
+ writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
1196
+ colors: [{
1197
+ color: {
1198
+ "color-space": "srgb",
1199
+ components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
1200
+ },
1201
+ idiom: "universal"
1202
+ }],
1203
+ info: { author: "xcode", version: 1 }
1204
+ }, null, 2)}
1205
+ `);
1206
+ }
1207
+ function syncWebAssets(source, output) {
1208
+ const sourcePath = resolve(source);
1209
+ if (!existsSync4(sourcePath))
1210
+ throw new Error(`Web asset path not found: ${source}`);
1211
+ const distDir = join2(output, "dist");
1212
+ rmSync2(distDir, { recursive: true, force: true });
1213
+ mkdirSync2(distDir, { recursive: true });
1214
+ if (statSync(sourcePath).isDirectory()) {
1215
+ cpSync2(sourcePath, distDir, { recursive: true });
1216
+ } else {
1217
+ cpSync2(sourcePath, join2(distDir, "index.html"));
1218
+ }
1219
+ if (!existsSync4(join2(distDir, "index.html"))) {
1220
+ throw new Error(`Web asset directory must contain index.html: ${source}`);
1221
+ }
1222
+ }
980
1223
  async function init(options) {
981
1224
  const { name, bundleId, teamId, output } = options;
982
1225
  console.log(`
983
1226
  \u26A1 Initializing Craft iOS project: ${name}`);
984
1227
  console.log(` Output: ${output}
985
1228
  `);
986
- const dirs = [output, join2(output, "Sources"), join2(output, "dist")];
1229
+ const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
1230
+ if (options.config?.enableWatchApp)
1231
+ dirs.push(join2(output, "WatchApp"));
987
1232
  for (const dir of dirs) {
988
1233
  if (!existsSync4(dir)) {
989
1234
  mkdirSync2(dir, { recursive: true });
@@ -992,27 +1237,84 @@ async function init(options) {
992
1237
  const finalBundleId = bundleId || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
993
1238
  const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
994
1239
  const config = {
1240
+ ...DEFAULT_CONFIG,
995
1241
  appName: name,
996
1242
  bundleId: finalBundleId,
997
- version: "1.0.0",
998
- buildNumber: "1",
999
- darkMode: true,
1000
- backgroundColor: "#1a1a2e",
1001
- enableSpeechRecognition: true,
1002
- enableHaptics: true,
1003
- enableShare: true,
1004
- iosVersion: "15.0",
1005
- teamId: teamId || ""
1243
+ teamId: teamId || "",
1244
+ ...options.config
1006
1245
  };
1246
+ if (config.enableBackgroundLocation)
1247
+ config.enableGeolocation = true;
1007
1248
  writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1008
1249
  const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1009
- writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftTemplate.replace(/CraftApp/g, `${name}App`));
1250
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1251
+ writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
1010
1252
  const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1011
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light");
1253
+ const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light").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>
1254
+ <true/>
1255
+ <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1256
+ <true/>` : "");
1012
1257
  writeFileSync2(join2(output, "Info.plist"), infoPlist);
1013
1258
  const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1014
- const projectYml = projectYmlTemplate.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(/\{\{TEAM_ID\}\}/g, teamId || "");
1259
+ const nativeDependencies = [];
1260
+ if (config.enableLiveActivities)
1261
+ nativeDependencies.push(` - target: ${name}LiveActivity`);
1262
+ if (config.enableWatchApp)
1263
+ nativeDependencies.push(` - target: ${name}Watch
1264
+ embed: true`);
1265
+ const nativeTargets = [];
1266
+ if (config.enableLiveActivities) {
1267
+ nativeTargets.push(` ${name}LiveActivity:
1268
+ type: app-extension
1269
+ platform: iOS
1270
+ deploymentTarget: "16.2"
1271
+ sources:
1272
+ - WidgetExtension
1273
+ - Shared
1274
+ settings:
1275
+ INFOPLIST_FILE: WidgetExtension/Info.plist
1276
+ PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
1277
+ SWIFT_VERSION: "5.0"
1278
+ APPLICATION_EXTENSION_API_ONLY: YES
1279
+ SKIP_INSTALL: YES`);
1280
+ }
1281
+ if (config.enableWatchApp) {
1282
+ nativeTargets.push(` ${name}Watch:
1283
+ type: application
1284
+ platform: watchOS
1285
+ deploymentTarget: "${config.watchosVersion || "9.0"}"
1286
+ sources:
1287
+ - WatchApp
1288
+ settings:
1289
+ INFOPLIST_FILE: WatchApp/Info.plist
1290
+ CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
1291
+ PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
1292
+ SWIFT_VERSION: "5.0"
1293
+ SKIP_INSTALL: YES`);
1294
+ }
1295
+ const projectYml = projectYmlTemplate.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:
1296
+ ${nativeDependencies.join(`
1297
+ `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1298
+ `));
1015
1299
  writeFileSync2(join2(output, "project.yml"), projectYml);
1300
+ writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
1301
+ writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1302
+ renderAssetCatalog(output, config);
1303
+ cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
1304
+ if (config.enableLiveActivities) {
1305
+ mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1306
+ const widgetSource = readFileSync2(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1307
+ writeFileSync2(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
1308
+ const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1309
+ writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1310
+ }
1311
+ if (config.enableWatchApp) {
1312
+ const watchSource = readFileSync2(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1313
+ writeFileSync2(join2(output, "WatchApp", `${name}WatchApp.swift`), watchSource);
1314
+ const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1315
+ writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
1316
+ writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1317
+ }
1016
1318
  const placeholderHtml = `<!DOCTYPE html>
1017
1319
  <html>
1018
1320
  <head>
@@ -1062,7 +1364,7 @@ async function init(options) {
1062
1364
  console.log("");
1063
1365
  }
1064
1366
  async function build(options) {
1065
- const { htmlPath, devServer, output } = options;
1367
+ const { htmlPath, devServer, output, generateProject = true } = options;
1066
1368
  console.log(`
1067
1369
  \uD83D\uDCE6 Building Craft iOS project...`);
1068
1370
  const configPath = join2(output, "craft.config.json");
@@ -1072,25 +1374,17 @@ async function build(options) {
1072
1374
  const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1073
1375
  if (devServer) {
1074
1376
  config.devServerURL = devServer;
1377
+ const origin = new URL(devServer).origin;
1378
+ config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
1075
1379
  writeFileSync2(configPath, JSON.stringify(config, null, 2));
1076
1380
  console.log(` Dev server: ${devServer}`);
1077
1381
  }
1078
1382
  if (htmlPath) {
1079
- const distDir = join2(output, "dist");
1080
- if (!existsSync4(distDir)) {
1081
- mkdirSync2(distDir, { recursive: true });
1082
- }
1083
- if (existsSync4(htmlPath)) {
1084
- const stat = await Bun.file(htmlPath).exists();
1085
- if (stat) {
1086
- const html = readFileSync2(htmlPath, "utf-8");
1087
- writeFileSync2(join2(distDir, "index.html"), html);
1088
- console.log(` Copied: ${htmlPath} \u2192 dist/index.html`);
1089
- }
1090
- } else {
1091
- throw new Error(`HTML path not found: ${htmlPath}`);
1092
- }
1383
+ syncWebAssets(htmlPath, output);
1384
+ console.log(` Synced: ${htmlPath} \u2192 dist/`);
1093
1385
  }
1386
+ if (!generateProject)
1387
+ return;
1094
1388
  try {
1095
1389
  const result = await $`which xcodegen`.quiet();
1096
1390
  if (result.exitCode === 0) {
@@ -1100,10 +1394,10 @@ async function build(options) {
1100
1394
  } else {
1101
1395
  throw new Error("xcodegen not found");
1102
1396
  }
1103
- } catch {
1104
- console.log("\u26A0\uFE0F xcodegen not found. Install with: brew install xcodegen");
1105
- console.log(" Then run: craft ios build");
1106
- return;
1397
+ } catch (error) {
1398
+ throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
1399
+ cause: error
1400
+ });
1107
1401
  }
1108
1402
  console.log("");
1109
1403
  }
@@ -1118,6 +1412,51 @@ async function open(options) {
1118
1412
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1119
1413
  await $`open ${projectPath}`;
1120
1414
  }
1415
+ function orderSimulators(devices) {
1416
+ return [...devices].sort((left, right) => {
1417
+ const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
1418
+ if (booted !== 0)
1419
+ return booted;
1420
+ const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
1421
+ if (phone !== 0)
1422
+ return phone;
1423
+ return right.runtime.localeCompare(left.runtime);
1424
+ });
1425
+ }
1426
+ async function pickSimulator() {
1427
+ const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
1428
+ if (listed.exitCode !== 0)
1429
+ return null;
1430
+ let parsed;
1431
+ try {
1432
+ parsed = JSON.parse(listed.stdout.toString());
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ const devices = [];
1437
+ for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
1438
+ for (const device of list)
1439
+ devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
1440
+ }
1441
+ return orderSimulators(devices)[0] ?? null;
1442
+ }
1443
+ async function bootSimulator(device) {
1444
+ if (device.state !== "Booted")
1445
+ await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
1446
+ await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
1447
+ }
1448
+ async function showSimulator() {
1449
+ const selected = await $`xcode-select -p`.quiet().nothrow();
1450
+ const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1451
+ if (developer) {
1452
+ const app = join2(developer, "Applications", "Simulator.app");
1453
+ if (existsSync4(app)) {
1454
+ await $`open ${app}`.quiet().nothrow();
1455
+ return;
1456
+ }
1457
+ }
1458
+ await $`open -a Simulator`.quiet().nothrow();
1459
+ }
1121
1460
  async function run(options) {
1122
1461
  const { simulator, output } = options;
1123
1462
  await build({ output });
@@ -1128,15 +1467,31 @@ async function run(options) {
1128
1467
  }
1129
1468
  const projectPath = join2(output, xcodeproj);
1130
1469
  const appName = xcodeproj.replace(".xcodeproj", "");
1470
+ const configPath = join2(output, "craft.config.json");
1471
+ if (!existsSync4(configPath))
1472
+ throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1473
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1131
1474
  if (simulator) {
1132
1475
  console.log("\uD83D\uDCF1 Building and running on simulator...");
1476
+ const device = await pickSimulator();
1477
+ if (!device) {
1478
+ throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
1479
+ }
1480
+ console.log(` Device: ${device.name} (${device.runtime})`);
1133
1481
  try {
1134
- await $`xcodebuild -project ${projectPath} -scheme ${appName} -destination 'platform=iOS Simulator,name=iPhone 15' build`;
1135
- await $`xcrun simctl boot "iPhone 15"`.quiet().nothrow();
1136
- await $`open -a Simulator`;
1137
- console.log("\u2705 App deployed to simulator");
1482
+ await bootSimulator(device);
1483
+ const derivedData = join2(output, "build");
1484
+ const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1485
+ await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1486
+ if (!existsSync4(product)) {
1487
+ throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
1488
+ }
1489
+ await $`xcrun simctl install ${device.udid} ${product}`;
1490
+ await showSimulator();
1491
+ await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
1492
+ console.log(`\u2705 ${config.appName} is running on ${device.name}`);
1138
1493
  } catch (error) {
1139
- console.error("Build failed. Open in Xcode for details:", projectPath);
1494
+ throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
1140
1495
  }
1141
1496
  } else {
1142
1497
  console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
@@ -1149,22 +1504,140 @@ async function run(options) {
1149
1504
  console.log(" 4. Click Run (\u25B6\uFE0F)");
1150
1505
  }
1151
1506
  }
1152
- var $, TEMPLATES_DIR;
1507
+ var $, TEMPLATES_DIR, DEFAULT_CONFIG;
1153
1508
  var init_src = __esm(() => {
1154
1509
  ({ $ } = globalThis.Bun);
1155
1510
  TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
1511
+ DEFAULT_CONFIG = {
1512
+ version: "1.0.0",
1513
+ buildNumber: "1",
1514
+ darkMode: true,
1515
+ backgroundColor: "#0b1712",
1516
+ enableSpeechRecognition: false,
1517
+ enableHaptics: false,
1518
+ enableShare: false,
1519
+ enableCamera: false,
1520
+ enableBiometric: false,
1521
+ enablePushNotifications: false,
1522
+ enableSecureStorage: false,
1523
+ enableGeolocation: false,
1524
+ enableClipboard: false,
1525
+ enableContacts: false,
1526
+ enableCalendar: false,
1527
+ enableLocalNotifications: false,
1528
+ enableInAppPurchase: false,
1529
+ enableKeepAwake: false,
1530
+ enableOrientationLock: false,
1531
+ enableDeepLinks: false,
1532
+ enableQRScanner: false,
1533
+ enableFilePicker: false,
1534
+ enableFileDownload: false,
1535
+ enableSocialAuth: false,
1536
+ enableAudioRecording: false,
1537
+ enableVideoRecording: false,
1538
+ enableMotionSensors: false,
1539
+ enableLocalDatabase: false,
1540
+ enableBluetooth: false,
1541
+ enableNFC: false,
1542
+ enableHealthKit: false,
1543
+ enableLiveActivities: false,
1544
+ enableWatchApp: false,
1545
+ enableBackgroundLocation: false,
1546
+ enableBackgroundTasks: false,
1547
+ enableScreenCapture: false,
1548
+ enablePDFViewer: false,
1549
+ enableAR: false,
1550
+ enableMLKit: false,
1551
+ iosVersion: "16.0",
1552
+ watchosVersion: "9.0",
1553
+ teamId: "",
1554
+ trustedOrigins: [],
1555
+ associatedDomains: [],
1556
+ appGroups: [],
1557
+ orientations: ["portrait"],
1558
+ deviceFamilies: ["iphone", "ipad"]
1559
+ };
1156
1560
  });
1157
1561
 
1158
1562
  // dist/android/src/index.js
1159
1563
  var exports_src2 = {};
1160
1564
  __export(exports_src2, {
1565
+ syncAndroidWebAssets: () => syncAndroidWebAssets,
1161
1566
  run: () => run2,
1567
+ renderAndroidPermissions: () => renderAndroidPermissions,
1568
+ renderAndroidDeepLinks: () => renderAndroidDeepLinks,
1162
1569
  open: () => open2,
1163
1570
  init: () => init2,
1164
1571
  build: () => build2
1165
1572
  });
1166
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync5, statSync } from "fs";
1167
- import { join as join3, dirname as dirname2 } from "path";
1573
+ import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
1574
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1575
+ function syncAndroidWebAssets(source, output) {
1576
+ const sourcePath = resolve2(source);
1577
+ if (!existsSync5(sourcePath))
1578
+ throw new Error(`Web asset path not found: ${source}`);
1579
+ const assetsDir = join3(output, "app/src/main/assets");
1580
+ const configPath = join3(assetsDir, "craft.config.json");
1581
+ const config = existsSync5(configPath) ? readFileSync3(configPath) : undefined;
1582
+ rmSync3(assetsDir, { recursive: true, force: true });
1583
+ mkdirSync3(assetsDir, { recursive: true });
1584
+ if (statSync2(sourcePath).isDirectory())
1585
+ cpSync3(sourcePath, assetsDir, { recursive: true });
1586
+ else
1587
+ cpSync3(sourcePath, join3(assetsDir, "index.html"));
1588
+ if (!existsSync5(join3(assetsDir, "index.html")))
1589
+ throw new Error(`Web asset directory must contain index.html: ${source}`);
1590
+ if (config)
1591
+ writeFileSync3(configPath, config);
1592
+ }
1593
+ function renderAndroidPermissions(config) {
1594
+ const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
1595
+ if (config.enableSpeechRecognition)
1596
+ permissions.add("android.permission.RECORD_AUDIO");
1597
+ if (config.enableCamera)
1598
+ permissions.add("android.permission.CAMERA");
1599
+ if (config.enableHaptics)
1600
+ permissions.add("android.permission.VIBRATE");
1601
+ if (config.enableBiometric)
1602
+ permissions.add("android.permission.USE_BIOMETRIC");
1603
+ if (config.enableGeolocation || config.enableBackgroundLocation) {
1604
+ permissions.add("android.permission.ACCESS_FINE_LOCATION");
1605
+ permissions.add("android.permission.ACCESS_COARSE_LOCATION");
1606
+ }
1607
+ if (config.enableBackgroundLocation) {
1608
+ permissions.add("android.permission.ACCESS_BACKGROUND_LOCATION");
1609
+ permissions.add("android.permission.FOREGROUND_SERVICE");
1610
+ permissions.add("android.permission.FOREGROUND_SERVICE_LOCATION");
1611
+ }
1612
+ if (config.enablePushNotifications)
1613
+ permissions.add("android.permission.POST_NOTIFICATIONS");
1614
+ if (config.enableKeepAwake)
1615
+ permissions.add("android.permission.WAKE_LOCK");
1616
+ if (config.enableHealthConnect) {
1617
+ permissions.add("android.permission.health.READ_STEPS");
1618
+ permissions.add("android.permission.health.READ_HEART_RATE");
1619
+ permissions.add("android.permission.health.READ_ACTIVE_CALORIES_BURNED");
1620
+ permissions.add("android.permission.health.READ_DISTANCE");
1621
+ permissions.add("android.permission.health.READ_EXERCISE");
1622
+ permissions.add("android.permission.health.WRITE_EXERCISE");
1623
+ permissions.add("android.permission.health.WRITE_ACTIVE_CALORIES_BURNED");
1624
+ permissions.add("android.permission.health.WRITE_DISTANCE");
1625
+ }
1626
+ return [...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`).join(`
1627
+ `);
1628
+ }
1629
+ function renderAndroidDeepLinks(config) {
1630
+ if (!config.enableDeepLinks)
1631
+ return "";
1632
+ const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
1633
+ return schemes.map((scheme) => ` <intent-filter>
1634
+ <action android:name="android.intent.action.VIEW" />
1635
+ <category android:name="android.intent.category.DEFAULT" />
1636
+ <category android:name="android.intent.category.BROWSABLE" />
1637
+ <data android:scheme="${scheme}" />
1638
+ </intent-filter>`).join(`
1639
+ `);
1640
+ }
1168
1641
  async function init2(options) {
1169
1642
  const { name, packageName, output } = options;
1170
1643
  console.log(`
@@ -1188,36 +1661,73 @@ async function init2(options) {
1188
1661
  }
1189
1662
  }
1190
1663
  const config = {
1664
+ ...DEFAULT_CONFIG2,
1191
1665
  appName: name,
1192
1666
  packageName: finalPackageName,
1193
- version: "1.0.0",
1194
- versionCode: 1,
1195
- darkMode: true,
1196
- backgroundColor: "#1a1a2e",
1197
- enableSpeechRecognition: true,
1198
- enableHaptics: true,
1199
- enableShare: true,
1200
- enableCamera: true,
1201
- enableBiometric: true,
1202
- enablePushNotifications: false,
1203
- minSdk: 24,
1204
- targetSdk: 34
1667
+ ...options.config
1205
1668
  };
1669
+ if (config.enableBackgroundLocation)
1670
+ config.enableGeolocation = true;
1206
1671
  writeFileSync3(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1672
+ writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
1673
+ const hasGoogleServices = Boolean(config.googleServicesFile);
1674
+ if (config.googleServicesFile) {
1675
+ if (!existsSync5(config.googleServicesFile))
1676
+ throw new Error(`Google services file not found: ${config.googleServicesFile}`);
1677
+ cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
1678
+ }
1207
1679
  const mainActivityTemplate = readFileSync3(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
1208
1680
  const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
1209
1681
  writeFileSync3(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
1210
1682
  const craftBridgeTemplate = readFileSync3(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
1211
- const craftBridge = craftBridgeTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName);
1683
+ const craftBridge = craftBridgeTemplate.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 {
1684
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
1685
+ && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
1686
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
1687
+ }
1688
+ try {
1689
+ FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
1690
+ val token = if (task.isSuccessful) task.result else null
1691
+ val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
1692
+ val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
1693
+ webView.evaluateJavascript("$callback && $callback($payload)", null)
1694
+ }
1695
+ } catch (error: Exception) {
1696
+ val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
1697
+ webView.evaluateJavascript("window._craftPushReject && window._craftPushReject($payload)", null)
1698
+ }
1699
+ }` : `activity.runOnUiThread {
1700
+ webView.evaluateJavascript(
1701
+ "window._craftPushReject && window._craftPushReject('Push notifications are disabled')",
1702
+ null
1703
+ )
1704
+ }`);
1212
1705
  writeFileSync3(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
1706
+ const healthTemplate = readFileSync3(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
1707
+ writeFileSync3(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1708
+ if (config.enableBackgroundLocation) {
1709
+ const serviceTemplate = readFileSync3(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
1710
+ writeFileSync3(join3(output, "app/src/main/java", packagePath, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1711
+ }
1213
1712
  const manifestTemplate = readFileSync3(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
1214
- const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
1713
+ 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=".LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
1714
+ <package android:name="com.google.android.apps.healthdata" />
1715
+ </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
1716
+ <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
1717
+ </intent-filter>
1718
+ <intent-filter>
1719
+ <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
1720
+ <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
1721
+ </intent-filter>` : "");
1215
1722
  writeFileSync3(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
1216
1723
  const projectGradleTemplate = readFileSync3(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
1217
- writeFileSync3(join3(output, "build.gradle.kts"), projectGradleTemplate);
1724
+ writeFileSync3(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
1218
1725
  const appGradleTemplate = readFileSync3(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
1219
- const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, config.version || "1.0.0").replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 34));
1726
+ const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, config.version || "1.0.0").replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{COMPILE_SDK\}\}/g, String(Math.max(config.compileSdk || 36, config.enableHealthConnect ? 36 : 1))).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")
1727
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
1220
1728
  writeFileSync3(join3(output, "app/build.gradle.kts"), appGradle);
1729
+ const proguardTemplate = readFileSync3(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
1730
+ writeFileSync3(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1221
1731
  const settingsTemplate = readFileSync3(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
1222
1732
  const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, name);
1223
1733
  writeFileSync3(join3(output, "settings.gradle.kts"), settings2);
@@ -1231,7 +1741,7 @@ android.nonTransitiveRClass=true
1231
1741
  `);
1232
1742
  const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
1233
1743
  distributionPath=wrapper/dists
1234
- distributionUrl=https\\://services.gradle.org/distributions/gradle-8.4-bin.zip
1744
+ distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
1235
1745
  networkTimeout=10000
1236
1746
  validateDistributionUrl=true
1237
1747
  zipStoreBase=GRADLE_USER_HOME
@@ -1253,6 +1763,23 @@ zipStorePath=wrapper/dists
1253
1763
  </resources>
1254
1764
  `;
1255
1765
  writeFileSync3(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
1766
+ const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
1767
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
1768
+ android:width="108dp"
1769
+ android:height="108dp"
1770
+ android:viewportWidth="108"
1771
+ android:viewportHeight="108">
1772
+ <path android:fillColor="${config.backgroundColor}" android:pathData="M0,0h108v108h-108z" />
1773
+ <path android:fillColor="#10B981" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
1774
+ </vector>
1775
+ `;
1776
+ if (config.appIconPath) {
1777
+ if (!existsSync5(config.appIconPath))
1778
+ throw new Error(`App icon not found: ${config.appIconPath}`);
1779
+ cpSync3(config.appIconPath, join3(output, "app/src/main/res/drawable/craft_app_icon.png"));
1780
+ } else {
1781
+ writeFileSync3(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
1782
+ }
1256
1783
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1257
1784
  <resources>
1258
1785
  <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
@@ -1326,7 +1853,7 @@ zipStorePath=wrapper/dists
1326
1853
  console.log("");
1327
1854
  }
1328
1855
  async function build2(options) {
1329
- const { htmlPath, devServer, output, release } = options;
1856
+ const { htmlPath, devServer, output, release, compile = true } = options;
1330
1857
  console.log(`
1331
1858
  \uD83D\uDCE6 Building Craft Android project...`);
1332
1859
  const configPath = join3(output, "craft.config.json");
@@ -1336,31 +1863,27 @@ async function build2(options) {
1336
1863
  const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1337
1864
  if (devServer) {
1338
1865
  config.devServerURL = devServer;
1866
+ config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], new URL(devServer).origin])];
1339
1867
  writeFileSync3(configPath, JSON.stringify(config, null, 2));
1868
+ writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
1340
1869
  console.log(` Dev server: ${devServer}`);
1341
1870
  }
1342
1871
  if (htmlPath) {
1343
- const assetsDir = join3(output, "app/src/main/assets");
1344
- if (!existsSync5(assetsDir)) {
1345
- mkdirSync3(assetsDir, { recursive: true });
1346
- }
1347
- if (existsSync5(htmlPath) && statSync(htmlPath).isFile()) {
1348
- const html = readFileSync3(htmlPath, "utf-8");
1349
- writeFileSync3(join3(assetsDir, "index.html"), html);
1350
- console.log(` Copied: ${htmlPath} \u2192 assets/index.html`);
1351
- } else {
1352
- throw new Error(`HTML file not found or is a directory: ${htmlPath}`);
1353
- }
1872
+ syncAndroidWebAssets(htmlPath, output);
1873
+ config.hasBundledFallback = Boolean(devServer);
1874
+ writeFileSync3(configPath, JSON.stringify(config, null, 2));
1875
+ writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
1876
+ console.log(` Synced: ${htmlPath} \u2192 assets/`);
1354
1877
  }
1878
+ if (!compile)
1879
+ return;
1355
1880
  const gradlewPath = join3(output, "gradlew");
1356
1881
  if (!existsSync5(gradlewPath)) {
1357
1882
  console.log(" Generating Gradle wrapper...");
1358
1883
  try {
1359
1884
  await $2`cd ${output} && gradle wrapper`.quiet();
1360
- } catch {
1361
- console.log("\u26A0\uFE0F Gradle not found. Please install Android Studio or Gradle.");
1362
- console.log(" Download: https://developer.android.com/studio");
1363
- return;
1885
+ } catch (error) {
1886
+ throw new Error("Gradle is unavailable. Install Android Studio or Gradle before building Android.", { cause: error });
1364
1887
  }
1365
1888
  }
1366
1889
  const buildType = release ? "assembleRelease" : "assembleDebug";
@@ -1419,10 +1942,33 @@ async function run2(options) {
1419
1942
  throw error;
1420
1943
  }
1421
1944
  }
1422
- var $2, TEMPLATES_DIR2;
1945
+ var $2, TEMPLATES_DIR2, DEFAULT_CONFIG2;
1423
1946
  var init_src2 = __esm(() => {
1424
1947
  ({ $: $2 } = globalThis.Bun);
1425
1948
  TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
1949
+ DEFAULT_CONFIG2 = {
1950
+ version: "1.0.0",
1951
+ versionCode: 1,
1952
+ darkMode: true,
1953
+ backgroundColor: "#0b1712",
1954
+ enableSpeechRecognition: false,
1955
+ enableHaptics: false,
1956
+ enableShare: false,
1957
+ enableCamera: false,
1958
+ enableBiometric: false,
1959
+ enablePushNotifications: false,
1960
+ enableSecureStorage: false,
1961
+ enableGeolocation: false,
1962
+ enableBackgroundLocation: false,
1963
+ enableHealthConnect: false,
1964
+ enableKeepAwake: false,
1965
+ enableDeepLinks: false,
1966
+ urlSchemes: [],
1967
+ trustedOrigins: [],
1968
+ minSdk: 26,
1969
+ compileSdk: 36,
1970
+ targetSdk: 35
1971
+ };
1426
1972
  });
1427
1973
 
1428
1974
  // ../../node_modules/@stacksjs/clapp/dist/index.js
@@ -3487,6 +4033,28 @@ function resolveCraftBinary(explicit) {
3487
4033
  }
3488
4034
  return "craft";
3489
4035
  }
4036
+ var CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
4037
+ function craftBinaryIsCliShimMessage(spawnedFrom) {
4038
+ return [
4039
+ `"${spawnedFrom}" on PATH is the Craft CLI, not the native binary.`,
4040
+ "",
4041
+ "The CLI spawned it expecting the native build, and re-entered itself \u2014",
4042
+ "which is why an option you never typed (--url) came back as unknown.",
4043
+ "",
4044
+ "Fix it in one of two ways:",
4045
+ "",
4046
+ " 1. Install the native binary and make sure it comes first on PATH:",
4047
+ "",
4048
+ " pantry install craft",
4049
+ "",
4050
+ " 2. Point CRAFT_BIN at a build directly:",
4051
+ "",
4052
+ " CRAFT_BIN=/path/to/craft craft <url>",
4053
+ "",
4054
+ "For monorepo development, CRAFT_BIN is the supported way in."
4055
+ ].join(`
4056
+ `);
4057
+ }
3490
4058
  function craftBinaryNotFoundMessage(triedPath) {
3491
4059
  return [
3492
4060
  `Craft native binary not found (tried "${triedPath}").`,
@@ -3504,17 +4072,25 @@ function craftBinaryNotFoundMessage(triedPath) {
3504
4072
  `);
3505
4073
  }
3506
4074
  // package.json
3507
- var version = "0.0.55";
4075
+ var version = "0.0.72";
3508
4076
 
3509
4077
  // bin/cli.ts
4078
+ var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
4079
+ if (spawnedFrom) {
4080
+ console.error(craftBinaryIsCliShimMessage(spawnedFrom));
4081
+ process6.exit(1);
4082
+ }
3510
4083
  var cli = new CLI("craft");
3511
4084
  async function runCraftBinary(args) {
3512
4085
  const craftPath = resolveCraftBinary();
3513
- return new Promise((resolve, reject) => {
3514
- const proc = spawn2(craftPath, args, { stdio: "inherit" });
4086
+ return new Promise((resolve3, reject) => {
4087
+ const proc = spawn2(craftPath, args, {
4088
+ stdio: "inherit",
4089
+ env: { ...process6.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
4090
+ });
3515
4091
  proc.on("exit", (code) => {
3516
4092
  if (code === 0 || code === null)
3517
- resolve();
4093
+ resolve3();
3518
4094
  else
3519
4095
  reject(new Error(`Craft exited with code ${code}`));
3520
4096
  });
@@ -3621,7 +4197,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
3621
4197
  const iosDir = "./ios";
3622
4198
  if (existsSync7(iosDir)) {
3623
4199
  const buildType = options?.release ? "Release" : "Debug";
3624
- await new Promise((resolve, reject) => {
4200
+ await new Promise((resolve3, reject) => {
3625
4201
  const proc = spawn3("xcodebuild", [
3626
4202
  "-project",
3627
4203
  `${iosDir}/App.xcodeproj`,
@@ -3633,7 +4209,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
3633
4209
  "iphoneos",
3634
4210
  "build"
3635
4211
  ], { stdio: "inherit" });
3636
- proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4212
+ proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3637
4213
  proc.on("error", reject);
3638
4214
  });
3639
4215
  console.log(` \u2705 iOS ${buildType} build complete`);
@@ -3644,12 +4220,12 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
3644
4220
  const androidDir = "./android";
3645
4221
  if (existsSync7(androidDir)) {
3646
4222
  const task = options?.release ? "assembleRelease" : "assembleDebug";
3647
- await new Promise((resolve, reject) => {
4223
+ await new Promise((resolve3, reject) => {
3648
4224
  const proc = spawn3("./gradlew", [task], {
3649
4225
  cwd: androidDir,
3650
4226
  stdio: "inherit"
3651
4227
  });
3652
- proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4228
+ proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3653
4229
  proc.on("error", reject);
3654
4230
  });
3655
4231
  console.log(` \u2705 Android ${options?.release ? "Release" : "Debug"} build complete`);
@@ -3659,7 +4235,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
3659
4235
  } else if (platform === "macos" || platform === "windows" || platform === "linux") {
3660
4236
  const zigDir = existsSync7("./packages/zig") ? "./packages/zig" : ".";
3661
4237
  const optimizeFlag = options?.release ? "-Doptimize=ReleaseSafe" : "";
3662
- await new Promise((resolve, reject) => {
4238
+ await new Promise((resolve3, reject) => {
3663
4239
  const args = ["build"];
3664
4240
  if (optimizeFlag)
3665
4241
  args.push(optimizeFlag);
@@ -3667,7 +4243,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
3667
4243
  cwd: zigDir,
3668
4244
  stdio: "inherit"
3669
4245
  });
3670
- proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4246
+ proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3671
4247
  proc.on("error", reject);
3672
4248
  });
3673
4249
  console.log(` \u2705 ${platform} build complete`);
@@ -3990,7 +4566,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
3990
4566
  const template = options?.template || "blank";
3991
4567
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
3992
4568
  const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
3993
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync4, cpSync: cpSync2 } = await import("fs");
4569
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync4, cpSync: cpSync4 } = await import("fs");
3994
4570
  const { join: join4, dirname: dirname3 } = await import("path");
3995
4571
  const replaceVars = (content) => {
3996
4572
  return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");