craft-native 0.0.319 → 0.0.329
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/android/src/index.d.ts +3 -5
- package/dist/android/src/index.js +50 -5
- package/dist/android/templates/CraftBridge.kt.template +0 -90
- package/dist/android/templates/CraftNative.kt.template +3 -0
- package/dist/android/templates/CraftNativeBackground.kt.template +8 -2
- package/dist/android/templates/CraftNativeServices.kt.template +100 -1
- package/dist/android/templates/CraftPermissionPolicy.kt.template +96 -0
- package/dist/android/templates/MainActivityNative.kt.template +1 -0
- package/dist/android/templates/build.gradle.kts.app.template +1 -5
- package/dist/api/mobile.d.ts +9 -0
- package/dist/cli.js +37 -19
- package/dist/index.cjs +40 -19
- package/dist/index.js +40 -19
- package/dist/ios/src/index.d.ts +2 -5
- package/dist/ios/src/index.js +20 -0
- package/dist/ios/templates/CraftActivityAttributes.swift +29 -2
- package/dist/ios/templates/CraftApp.swift +1 -1
- package/dist/ios/templates/CraftHealth.swift +14 -0
- package/dist/ios/templates/CraftLiveActivities.swift +106 -2
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +9 -1
- package/dist/ios/templates/CraftNativeScreen.swift +51 -1
- package/dist/ios/templates/CraftVoiceRecording.swift +75 -0
- package/dist/ios/templates/project.yml.template +6 -0
- package/dist/types.d.ts +3 -2
- package/package.json +1 -1
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Generates Android apps with native Craft components and optional WebView content.
|
|
5
|
-
*/
|
|
1
|
+
/** Store large native source assets compressed; the host decodes each once on demand. */
|
|
2
|
+
export declare function compressNativeSources(target: string): void;
|
|
6
3
|
import { type AndroidMapProvider } from './native-map.js';
|
|
7
4
|
export type { AndroidMapProvider } from './native-map.js';
|
|
8
5
|
/**
|
|
@@ -55,6 +52,7 @@ export interface CraftAndroidConfig {
|
|
|
55
52
|
backgroundColor?: string;
|
|
56
53
|
backgroundColorDark?: string;
|
|
57
54
|
enableSpeechRecognition?: boolean;
|
|
55
|
+
enableAudioRecording?: boolean;
|
|
58
56
|
enableHaptics?: boolean;
|
|
59
57
|
enableShare?: boolean;
|
|
60
58
|
enableCamera?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../android/src/index.ts
|
|
3
|
-
import {
|
|
3
|
+
import { gzipSync } from "zlib";
|
|
4
|
+
import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, rmSync as rmSync2, statSync, writeFileSync } from "fs";
|
|
4
5
|
import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
5
6
|
var {$ } = globalThis.Bun;
|
|
6
7
|
|
|
@@ -342,6 +343,8 @@ async function createAndroidEmulator(options) {
|
|
|
342
343
|
const playStore = requiresPlayStore ? await run([adb, "-s", serial, "shell", "pm", "path", "com.android.vending"], undefined, probeTimeout(), true) : undefined;
|
|
343
344
|
const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
|
|
344
345
|
const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-v", "threadtime"], undefined, probeTimeout(), true);
|
|
346
|
+
if (events.code === 0 && /^\S+\s+\S+\s+\d+\s+\d+\s+[A-Z]\s+am_(?:anr|crash)\s*:/m.test(events.stdout))
|
|
347
|
+
throw new Error(`Android emulator reported a platform ANR or crash during boot; inspect ${artifacts}`);
|
|
345
348
|
const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
|
|
346
349
|
const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
|
|
347
350
|
const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || homeActivity.startsWith("com.google.android.googlesdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
|
|
@@ -357,6 +360,8 @@ async function createAndroidEmulator(options) {
|
|
|
357
360
|
}
|
|
358
361
|
await Bun.sleep(Math.min(2000, Math.max(0, deadline - Date.now())));
|
|
359
362
|
}
|
|
363
|
+
if (child.exitCode !== null)
|
|
364
|
+
throw new Error(`The owned Android emulator exited with code ${child.exitCode} before boot; inspect ${join2(artifacts, "emulator.log")}`);
|
|
360
365
|
throw new Error(`The owned Android emulator did not boot within ${bootTimeoutMs}ms; inspect ${artifacts}`);
|
|
361
366
|
} catch (error) {
|
|
362
367
|
if (child) {
|
|
@@ -381,6 +386,21 @@ async function createAndroidEmulator(options) {
|
|
|
381
386
|
}
|
|
382
387
|
|
|
383
388
|
// ../android/src/index.ts
|
|
389
|
+
function compressNativeSources(target) {
|
|
390
|
+
if (statSync(target).isDirectory()) {
|
|
391
|
+
for (const entry of readdirSync(target))
|
|
392
|
+
compressNativeSources(join3(target, entry));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!/\.(?:js|json)$/.test(target))
|
|
396
|
+
return;
|
|
397
|
+
const bytes = readFileSync2(target);
|
|
398
|
+
if (bytes.length < 1024 || bytes[0] === 31 && bytes[1] === 139)
|
|
399
|
+
return;
|
|
400
|
+
const packed = gzipSync(bytes, { level: 9 });
|
|
401
|
+
if (packed.length < bytes.length)
|
|
402
|
+
writeFileSync(target, packed);
|
|
403
|
+
}
|
|
384
404
|
var nativeRouteBundles = true;
|
|
385
405
|
var nativeRouteRuntime = true;
|
|
386
406
|
var nativeMessages = true;
|
|
@@ -538,6 +558,7 @@ var DEFAULT_CONFIG = {
|
|
|
538
558
|
darkMode: true,
|
|
539
559
|
backgroundColor: "#0b1712",
|
|
540
560
|
enableSpeechRecognition: false,
|
|
561
|
+
enableAudioRecording: false,
|
|
541
562
|
enableHaptics: false,
|
|
542
563
|
enableShare: false,
|
|
543
564
|
enableCamera: false,
|
|
@@ -825,7 +846,7 @@ function syncAndroidWebAssets(source, output) {
|
|
|
825
846
|
}
|
|
826
847
|
function renderAndroidPermissions(config) {
|
|
827
848
|
const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
|
|
828
|
-
if (config.enableSpeechRecognition)
|
|
849
|
+
if (config.enableSpeechRecognition || config.enableAudioRecording)
|
|
829
850
|
permissions.add("android.permission.RECORD_AUDIO");
|
|
830
851
|
if (config.enableCamera)
|
|
831
852
|
permissions.add("android.permission.CAMERA");
|
|
@@ -1138,6 +1159,8 @@ async function init(options) {
|
|
|
1138
1159
|
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1139
1160
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
1140
1161
|
}
|
|
1162
|
+
const permissionPolicy = readFileSync2(join3(TEMPLATES_DIR, "CraftPermissionPolicy.kt.template"), "utf8");
|
|
1163
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftPermissionPolicy.kt"), permissionPolicy.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1141
1164
|
const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
1142
1165
|
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(/\{\{ENABLE_HEART_RATE\}\}/g, String(Boolean(config.enableHeartRate))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
1143
1166
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
@@ -1158,8 +1181,21 @@ async function init(options) {
|
|
|
1158
1181
|
}` : `evaluatePromiseJavascript(
|
|
1159
1182
|
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
1160
1183
|
)`);
|
|
1161
|
-
|
|
1162
|
-
|
|
1184
|
+
const bridgeFile = join3(output, "app/src/main/java", packagePath, "CraftBridge.kt");
|
|
1185
|
+
if (config.renderer === "native")
|
|
1186
|
+
rmSync2(bridgeFile, { force: true });
|
|
1187
|
+
else
|
|
1188
|
+
writeFileSync(bridgeFile, craftBridge);
|
|
1189
|
+
let craftNative = readFileSync2(join3(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
|
|
1190
|
+
if (config.renderer === "native") {
|
|
1191
|
+
craftNative = craftNative.replace(/^import com\.google\.mlkit\..*\n/gm, "").replace(/ \/\/ CRAFT_WEB_ML_START[\s\S]*? \/\/ CRAFT_WEB_ML_END/, () => ["runImageClassification", "runObjectDetection", "runTextRecognition"].map((name) => ` @JvmStatic
|
|
1192
|
+
fun ${name}(imageBase64: String) {
|
|
1193
|
+
val (_, reject) = createMlSettlement()
|
|
1194
|
+
reject("Machine learning is unavailable in the native renderer")
|
|
1195
|
+
}`).join(`
|
|
1196
|
+
|
|
1197
|
+
`));
|
|
1198
|
+
}
|
|
1163
1199
|
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
1164
1200
|
mkdirSync2(nativeDir, { recursive: true });
|
|
1165
1201
|
writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
@@ -1184,7 +1220,13 @@ async function init(options) {
|
|
|
1184
1220
|
const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
1185
1221
|
writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
1186
1222
|
const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
1187
|
-
const appGradle = appGradleTemplate.replace(/\{\{
|
|
1223
|
+
const appGradle = appGradleTemplate.replace(/\{\{MACHINE_LEARNING_DEPENDENCIES\}\}/g, config.renderer === "native" ? "" : [
|
|
1224
|
+
"barcode-scanning:17.3.0",
|
|
1225
|
+
"image-labeling:17.0.9",
|
|
1226
|
+
"object-detection:17.0.2",
|
|
1227
|
+
"text-recognition:16.0.1"
|
|
1228
|
+
].map((module) => ` implementation("com.google.mlkit:${module}")`).join(`
|
|
1229
|
+
`)).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(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{NATIVE_MAP_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
|
|
1188
1230
|
implementation("${androidMapHost(config.mapProvider).dependency}")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
1189
1231
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{HEALTH_SERVICES_DEPENDENCIES\}\}/g, config.enableHealthServices ? ' implementation("androidx.health:health-services-client:1.1.0")' : "").replace(/\{\{WALLET_DEPENDENCY\}\}/g, config.enableWallet ? ` // Native Google Wallet save flow for signed pass JWTs
|
|
1190
1232
|
implementation("com.google.android.gms:play-services-pay:16.5.0")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
|
|
@@ -1381,11 +1423,13 @@ async function build(options) {
|
|
|
1381
1423
|
if (resolve2(nativeBundlePath) !== resolve2(routeDestination)) {
|
|
1382
1424
|
rmSync2(routeDestination, { recursive: true, force: true });
|
|
1383
1425
|
cpSync(nativeBundlePath, routeDestination, { recursive: true });
|
|
1426
|
+
compressNativeSources(routeDestination);
|
|
1384
1427
|
}
|
|
1385
1428
|
rmSync2(destination, { force: true });
|
|
1386
1429
|
} else {
|
|
1387
1430
|
if (resolve2(nativeBundlePath) !== resolve2(destination))
|
|
1388
1431
|
cpSync(nativeBundlePath, destination);
|
|
1432
|
+
compressNativeSources(destination);
|
|
1389
1433
|
rmSync2(routeDestination, { recursive: true, force: true });
|
|
1390
1434
|
}
|
|
1391
1435
|
}
|
|
@@ -1502,6 +1546,7 @@ export {
|
|
|
1502
1546
|
ANDROID_MIN_SDK,
|
|
1503
1547
|
HEALTH_CONNECT_PERMISSIONS,
|
|
1504
1548
|
build,
|
|
1549
|
+
compressNativeSources,
|
|
1505
1550
|
createAndroidEmulator,
|
|
1506
1551
|
healthConnectPermissions,
|
|
1507
1552
|
init,
|
|
@@ -112,97 +112,7 @@ import java.io.File
|
|
|
112
112
|
import java.util.concurrent.TimeUnit
|
|
113
113
|
import java.util.UUID
|
|
114
114
|
|
|
115
|
-
internal object CraftPermissionPolicy {
|
|
116
|
-
data class Request(
|
|
117
|
-
val groupIndex: Int,
|
|
118
|
-
val permissions: Array<String>
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
private fun permissionGroups(name: String, sdkInt: Int): List<List<String>>? = when (name) {
|
|
122
|
-
"camera" -> listOf(listOf(Manifest.permission.CAMERA))
|
|
123
|
-
"microphone" -> listOf(listOf(Manifest.permission.RECORD_AUDIO))
|
|
124
|
-
"photos" -> listOf(listOf(
|
|
125
|
-
if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
126
|
-
Manifest.permission.READ_MEDIA_IMAGES
|
|
127
|
-
} else {
|
|
128
|
-
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
129
|
-
}
|
|
130
|
-
))
|
|
131
|
-
// Android presents precise and approximate location as one choice. A
|
|
132
|
-
// coarse grant is usable foreground location access even when the
|
|
133
|
-
// corresponding fine result is denied.
|
|
134
|
-
"location" -> listOf(listOf(
|
|
135
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
136
|
-
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
137
|
-
))
|
|
138
|
-
"locationAlways" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
139
|
-
listOf(
|
|
140
|
-
listOf(
|
|
141
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
142
|
-
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
143
|
-
),
|
|
144
|
-
listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
145
|
-
)
|
|
146
|
-
} else {
|
|
147
|
-
listOf(listOf(
|
|
148
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
149
|
-
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
150
|
-
))
|
|
151
|
-
}
|
|
152
|
-
"notifications" -> if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
153
|
-
listOf(listOf(Manifest.permission.POST_NOTIFICATIONS))
|
|
154
|
-
} else {
|
|
155
|
-
emptyList()
|
|
156
|
-
}
|
|
157
|
-
"contacts" -> listOf(listOf(Manifest.permission.READ_CONTACTS))
|
|
158
|
-
"calendar" -> listOf(listOf(Manifest.permission.READ_CALENDAR))
|
|
159
|
-
"bluetooth" -> if (sdkInt >= Build.VERSION_CODES.S) {
|
|
160
|
-
listOf(listOf(Manifest.permission.BLUETOOTH_SCAN))
|
|
161
|
-
} else {
|
|
162
|
-
emptyList()
|
|
163
|
-
}
|
|
164
|
-
"motion" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
165
|
-
listOf(listOf(Manifest.permission.ACTIVITY_RECOGNITION))
|
|
166
|
-
} else {
|
|
167
|
-
emptyList()
|
|
168
|
-
}
|
|
169
|
-
else -> null
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
fun requiredPermissions(name: String, sdkInt: Int): Array<String>? =
|
|
173
|
-
permissionGroups(name, sdkInt)?.flatten()?.distinct()?.toTypedArray()
|
|
174
|
-
|
|
175
|
-
fun status(name: String, sdkInt: Int, isGranted: (String) -> Boolean): String {
|
|
176
|
-
val groups = permissionGroups(name, sdkInt) ?: return "undetermined"
|
|
177
|
-
return if (groups.all { group -> group.any(isGranted) }) "granted" else "denied"
|
|
178
|
-
}
|
|
179
115
|
|
|
180
|
-
fun nextRequest(
|
|
181
|
-
name: String,
|
|
182
|
-
sdkInt: Int,
|
|
183
|
-
startingAt: Int,
|
|
184
|
-
isGranted: (String) -> Boolean
|
|
185
|
-
): Request? {
|
|
186
|
-
val groups = permissionGroups(name, sdkInt) ?: return null
|
|
187
|
-
val groupIndex = (startingAt until groups.size).firstOrNull { index ->
|
|
188
|
-
groups[index].none(isGranted)
|
|
189
|
-
} ?: return null
|
|
190
|
-
return Request(groupIndex, groups[groupIndex].toTypedArray())
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
fun groupIsGranted(
|
|
194
|
-
name: String,
|
|
195
|
-
sdkInt: Int,
|
|
196
|
-
groupIndex: Int,
|
|
197
|
-
isGranted: (String) -> Boolean
|
|
198
|
-
): Boolean = permissionGroups(name, sdkInt)
|
|
199
|
-
?.getOrNull(groupIndex)
|
|
200
|
-
?.any(isGranted) == true
|
|
201
|
-
|
|
202
|
-
fun foregroundLocationIsGranted(isGranted: (String) -> Boolean): Boolean =
|
|
203
|
-
isGranted(Manifest.permission.ACCESS_FINE_LOCATION) ||
|
|
204
|
-
isGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
205
|
-
}
|
|
206
116
|
|
|
207
117
|
private data class SpeechRequest(
|
|
208
118
|
val text: String,
|
|
@@ -1245,6 +1245,7 @@ object CraftNative {
|
|
|
1245
1245
|
return resolve to reject
|
|
1246
1246
|
}
|
|
1247
1247
|
|
|
1248
|
+
// CRAFT_WEB_ML_START
|
|
1248
1249
|
@JvmStatic
|
|
1249
1250
|
fun runImageClassification(imageBase64: String) {
|
|
1250
1251
|
val (resolve, reject) = createMlSettlement()
|
|
@@ -1366,6 +1367,8 @@ object CraftNative {
|
|
|
1366
1367
|
}
|
|
1367
1368
|
}
|
|
1368
1369
|
|
|
1370
|
+
// CRAFT_WEB_ML_END
|
|
1371
|
+
|
|
1369
1372
|
fun classifyImage(imageBase64: String): Boolean {
|
|
1370
1373
|
if (!isAvailable) return false
|
|
1371
1374
|
return try { nativeClassifyImage(imageBase64) } catch (e: UnsatisfiedLinkError) { false }
|
|
@@ -65,11 +65,17 @@ internal object CraftNativeSandbox {
|
|
|
65
65
|
*/
|
|
66
66
|
internal object CraftNativeBundle {
|
|
67
67
|
private val sources = object : android.util.LruCache<String, String>(24 shl 20) {
|
|
68
|
-
override fun sizeOf(key: String, value: String) = value.length
|
|
68
|
+
override fun sizeOf(key: String, value: String) = value.length * 2
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
private fun read(context: Context, asset: String): String? = sources.get(asset)
|
|
72
|
-
?: runCatching { context.assets.open(asset).
|
|
72
|
+
?: runCatching { context.assets.open(asset).buffered().use { input ->
|
|
73
|
+
input.mark(2)
|
|
74
|
+
val packed = input.read() == 0x1f && input.read() == 0x8b
|
|
75
|
+
input.reset()
|
|
76
|
+
val decoded = if (packed) java.util.zip.GZIPInputStream(input) else input
|
|
77
|
+
decoded.bufferedReader().use { it.readText() }
|
|
78
|
+
} }.getOrNull()?.also { sources.put(asset, it) }
|
|
73
79
|
|
|
74
80
|
/**
|
|
75
81
|
* A route's file runs on the runtime stx writes beside it
|
|
@@ -9,6 +9,8 @@ import android.content.Context
|
|
|
9
9
|
import android.content.Intent
|
|
10
10
|
import android.content.pm.PackageManager
|
|
11
11
|
import android.net.Uri
|
|
12
|
+
import android.media.MediaRecorder
|
|
13
|
+
import android.media.MediaMetadataRetriever
|
|
12
14
|
import android.os.Build
|
|
13
15
|
import android.os.Handler
|
|
14
16
|
import android.os.Looper
|
|
@@ -113,6 +115,7 @@ internal class CraftNativeServices(
|
|
|
113
115
|
add("brightness")
|
|
114
116
|
add("statusBar")
|
|
115
117
|
add("speech")
|
|
118
|
+
if (config.optBoolean("enableAudioRecording")) add("audioRecording")
|
|
116
119
|
// Videos kept on the phone for offline (CraftNativeMedia).
|
|
117
120
|
add("media")
|
|
118
121
|
if (config.optBoolean("enableShare")) add("share")
|
|
@@ -142,7 +145,7 @@ internal class CraftNativeServices(
|
|
|
142
145
|
|
|
143
146
|
fun handles(module: String): Boolean = module in setOf(
|
|
144
147
|
"Dialog", "Browser", "Linking", "Share", "Push", "Health", "KeepAwake", "Brightness", "StatusBar", "Speech", "Background",
|
|
145
|
-
"Location", "Media", "CrashReporter", "HeartRate",
|
|
148
|
+
"Location", "Media", "CrashReporter", "HeartRate", "AudioRecording",
|
|
146
149
|
"Wallet",
|
|
147
150
|
)
|
|
148
151
|
|
|
@@ -165,6 +168,7 @@ internal class CraftNativeServices(
|
|
|
165
168
|
"Brightness" -> brightness(method, args, done)
|
|
166
169
|
"StatusBar" -> statusBar(args.optString(0, "default"), done)
|
|
167
170
|
"Speech" -> speech(method, args, done)
|
|
171
|
+
"AudioRecording" -> main.post { voiceRecording(method, args, done) }
|
|
168
172
|
"Location" -> location(method, args, done)
|
|
169
173
|
"HeartRate" -> heartRate(method, args, done)
|
|
170
174
|
"Media" -> when (method) {
|
|
@@ -422,6 +426,12 @@ internal class CraftNativeServices(
|
|
|
422
426
|
}
|
|
423
427
|
|
|
424
428
|
fun onRequestPermissionsResult(requestCode: Int): Boolean {
|
|
429
|
+
if (requestCode == 7134) {
|
|
430
|
+
val pending = pendingVoicePermission
|
|
431
|
+
pendingVoicePermission = null
|
|
432
|
+
pending?.invoke(ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
|
|
433
|
+
return true
|
|
434
|
+
}
|
|
425
435
|
if (heartRateEnabled && CraftHeartRate.get(context).onPermissions(requestCode)) return true
|
|
426
436
|
if (requestCode == LOCATION_PERMISSION_REQUEST) {
|
|
427
437
|
val pending = pendingLocationPermission
|
|
@@ -612,6 +622,94 @@ internal class CraftNativeServices(
|
|
|
612
622
|
done(success(true))
|
|
613
623
|
}
|
|
614
624
|
|
|
625
|
+
// A microphone belongs to its requesting screen/account, including the
|
|
626
|
+
// permission prompt. Files use the same private upload root as Files.
|
|
627
|
+
private var voiceOwner: String? = null
|
|
628
|
+
private var voiceRecorder: MediaRecorder? = null
|
|
629
|
+
private var voiceFile: File? = null
|
|
630
|
+
private var pendingVoicePermission: ((Boolean) -> Unit)? = null
|
|
631
|
+
|
|
632
|
+
fun pauseVoice() { if (voiceRecorder != null) cancelVoice() }
|
|
633
|
+
private fun cancelVoice() {
|
|
634
|
+
voiceOwner = null
|
|
635
|
+
val recorder = voiceRecorder
|
|
636
|
+
voiceRecorder = null
|
|
637
|
+
try { recorder?.stop() } catch (_: Exception) {}
|
|
638
|
+
recorder?.release()
|
|
639
|
+
voiceFile?.parentFile?.deleteRecursively()
|
|
640
|
+
voiceFile = null
|
|
641
|
+
val pending = pendingVoicePermission
|
|
642
|
+
pendingVoicePermission = null
|
|
643
|
+
pending?.invoke(false)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private fun voiceRecording(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
647
|
+
if (!config.optBoolean("enableAudioRecording")) { done(failure("CAPABILITY_DISABLED", "Audio recording is disabled")); return }
|
|
648
|
+
val key = args.optString(0)
|
|
649
|
+
if (!key.matches(Regex("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"))) { done(failure("INVALID_ARGUMENT", "A recording owner UUID is required")); return }
|
|
650
|
+
if (method == "cancel") {
|
|
651
|
+
if (voiceOwner == key) cancelVoice()
|
|
652
|
+
done(success(true)); return
|
|
653
|
+
}
|
|
654
|
+
if (method == "stop") {
|
|
655
|
+
val file = voiceFile
|
|
656
|
+
if (voiceOwner != key || file == null) { done(failure("RECORDING_NOT_FOUND", "This recording is no longer available")); return }
|
|
657
|
+
try {
|
|
658
|
+
voiceRecorder?.stop()
|
|
659
|
+
voiceRecorder?.release()
|
|
660
|
+
voiceRecorder = null
|
|
661
|
+
val metadata = MediaMetadataRetriever()
|
|
662
|
+
val duration = try { metadata.setDataSource(file.absolutePath); (metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toDoubleOrNull() ?: 0.0) / 1000 } finally { metadata.release() }
|
|
663
|
+
if (!file.exists() || file.length() == 0L || duration <= 0) throw IllegalStateException("Empty recording")
|
|
664
|
+
done(success(JSONObject().put("path", file.absolutePath).put("name", file.name).put("type", "audio/mp4").put("size", file.length()).put("durationSeconds", duration)))
|
|
665
|
+
} catch (_: Exception) { cancelVoice(); done(failure("RECORDING_FAILED", "The recording could not be read")) }
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
if (method != "start") { done(failure("UNKNOWN_ACTION", "Unknown recording action")); return }
|
|
669
|
+
if (voiceOwner != null) { done(failure("RECORDING_BUSY", "Another microphone session is active")); return }
|
|
670
|
+
val host = activity()
|
|
671
|
+
if (host == null) { done(failure("NEEDS_ACTIVITY", "Recording needs the foreground app")); return }
|
|
672
|
+
val seconds = args.optJSONObject(1)?.optInt("maxSeconds", 300) ?: 300
|
|
673
|
+
if (seconds !in 1..300) { done(failure("INVALID_ARGUMENT", "Recordings are limited to five minutes")); return }
|
|
674
|
+
voiceOwner = key
|
|
675
|
+
val start: (Boolean) -> Unit = start@ { allowed ->
|
|
676
|
+
if (voiceOwner != key) { done(failure("RECORDING_CANCELLED", "Recording was cancelled")); return@start }
|
|
677
|
+
if (!allowed) { cancelVoice(); done(failure("PERMISSION_DENIED", "Microphone permission was refused")); return@start }
|
|
678
|
+
try {
|
|
679
|
+
val folder = File(context.cacheDir, "craft-files/" + UUID.randomUUID()).apply { mkdirs() }
|
|
680
|
+
val file = File(folder, "voice-feedback.m4a")
|
|
681
|
+
voiceFile = file
|
|
682
|
+
@Suppress("DEPRECATION")
|
|
683
|
+
val recorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(context) else MediaRecorder()
|
|
684
|
+
voiceRecorder = recorder
|
|
685
|
+
recorder.setAudioSource(MediaRecorder.AudioSource.MIC)
|
|
686
|
+
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
|
687
|
+
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
|
688
|
+
recorder.setAudioChannels(1)
|
|
689
|
+
recorder.setAudioSamplingRate(16000)
|
|
690
|
+
recorder.setAudioEncodingBitRate(32000)
|
|
691
|
+
recorder.setMaxDuration(seconds * 1000 - 250)
|
|
692
|
+
recorder.setMaxFileSize(12L * 1024 * 1024)
|
|
693
|
+
recorder.setOutputFile(file.absolutePath)
|
|
694
|
+
recorder.setOnInfoListener { current, what, _ ->
|
|
695
|
+
if (voiceOwner == key && what in setOf(MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED, MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED)) {
|
|
696
|
+
try { current.stop() } catch (_: Exception) {}
|
|
697
|
+
current.release()
|
|
698
|
+
voiceRecorder = null
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
recorder.prepare()
|
|
702
|
+
recorder.start()
|
|
703
|
+
done(success(true))
|
|
704
|
+
} catch (_: Exception) { cancelVoice(); done(failure("RECORDING_FAILED", "The microphone could not start")) }
|
|
705
|
+
}
|
|
706
|
+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) start(true)
|
|
707
|
+
else {
|
|
708
|
+
pendingVoicePermission = start
|
|
709
|
+
ActivityCompat.requestPermissions(host, arrayOf(Manifest.permission.RECORD_AUDIO), 7134)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
615
713
|
// MARK: Speech
|
|
616
714
|
|
|
617
715
|
private fun speech(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
@@ -703,6 +801,7 @@ internal class CraftNativeServices(
|
|
|
703
801
|
}
|
|
704
802
|
|
|
705
803
|
fun close() {
|
|
804
|
+
cancelVoice()
|
|
706
805
|
watchCallback?.let(locationClient::removeLocationUpdates)
|
|
707
806
|
watchCallback = null
|
|
708
807
|
pendingLocationPermission = null
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.os.Build
|
|
5
|
+
|
|
6
|
+
internal object CraftPermissionPolicy {
|
|
7
|
+
data class Request(
|
|
8
|
+
val groupIndex: Int,
|
|
9
|
+
val permissions: Array<String>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
private fun permissionGroups(name: String, sdkInt: Int): List<List<String>>? = when (name) {
|
|
13
|
+
"camera" -> listOf(listOf(Manifest.permission.CAMERA))
|
|
14
|
+
"microphone" -> listOf(listOf(Manifest.permission.RECORD_AUDIO))
|
|
15
|
+
"photos" -> listOf(listOf(
|
|
16
|
+
if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
17
|
+
Manifest.permission.READ_MEDIA_IMAGES
|
|
18
|
+
} else {
|
|
19
|
+
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
20
|
+
}
|
|
21
|
+
))
|
|
22
|
+
// Android presents precise and approximate location as one choice. A
|
|
23
|
+
// coarse grant is usable foreground location access even when the
|
|
24
|
+
// corresponding fine result is denied.
|
|
25
|
+
"location" -> listOf(listOf(
|
|
26
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
27
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
28
|
+
))
|
|
29
|
+
"locationAlways" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
30
|
+
listOf(
|
|
31
|
+
listOf(
|
|
32
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
33
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
34
|
+
),
|
|
35
|
+
listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
|
36
|
+
)
|
|
37
|
+
} else {
|
|
38
|
+
listOf(listOf(
|
|
39
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
40
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
"notifications" -> if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
|
44
|
+
listOf(listOf(Manifest.permission.POST_NOTIFICATIONS))
|
|
45
|
+
} else {
|
|
46
|
+
emptyList()
|
|
47
|
+
}
|
|
48
|
+
"contacts" -> listOf(listOf(Manifest.permission.READ_CONTACTS))
|
|
49
|
+
"calendar" -> listOf(listOf(Manifest.permission.READ_CALENDAR))
|
|
50
|
+
"bluetooth" -> if (sdkInt >= Build.VERSION_CODES.S) {
|
|
51
|
+
listOf(listOf(Manifest.permission.BLUETOOTH_SCAN))
|
|
52
|
+
} else {
|
|
53
|
+
emptyList()
|
|
54
|
+
}
|
|
55
|
+
"motion" -> if (sdkInt >= Build.VERSION_CODES.Q) {
|
|
56
|
+
listOf(listOf(Manifest.permission.ACTIVITY_RECOGNITION))
|
|
57
|
+
} else {
|
|
58
|
+
emptyList()
|
|
59
|
+
}
|
|
60
|
+
else -> null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun requiredPermissions(name: String, sdkInt: Int): Array<String>? =
|
|
64
|
+
permissionGroups(name, sdkInt)?.flatten()?.distinct()?.toTypedArray()
|
|
65
|
+
|
|
66
|
+
fun status(name: String, sdkInt: Int, isGranted: (String) -> Boolean): String {
|
|
67
|
+
val groups = permissionGroups(name, sdkInt) ?: return "undetermined"
|
|
68
|
+
return if (groups.all { group -> group.any(isGranted) }) "granted" else "denied"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun nextRequest(
|
|
72
|
+
name: String,
|
|
73
|
+
sdkInt: Int,
|
|
74
|
+
startingAt: Int,
|
|
75
|
+
isGranted: (String) -> Boolean
|
|
76
|
+
): Request? {
|
|
77
|
+
val groups = permissionGroups(name, sdkInt) ?: return null
|
|
78
|
+
val groupIndex = (startingAt until groups.size).firstOrNull { index ->
|
|
79
|
+
groups[index].none(isGranted)
|
|
80
|
+
} ?: return null
|
|
81
|
+
return Request(groupIndex, groups[groupIndex].toTypedArray())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun groupIsGranted(
|
|
85
|
+
name: String,
|
|
86
|
+
sdkInt: Int,
|
|
87
|
+
groupIndex: Int,
|
|
88
|
+
isGranted: (String) -> Boolean
|
|
89
|
+
): Boolean = permissionGroups(name, sdkInt)
|
|
90
|
+
?.getOrNull(groupIndex)
|
|
91
|
+
?.any(isGranted) == true
|
|
92
|
+
|
|
93
|
+
fun foregroundLocationIsGranted(isGranted: (String) -> Boolean): Boolean =
|
|
94
|
+
isGranted(Manifest.permission.ACCESS_FINE_LOCATION) ||
|
|
95
|
+
isGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
96
|
+
}
|
|
@@ -7668,6 +7668,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7668
7668
|
}
|
|
7669
7669
|
|
|
7670
7670
|
override fun onPause() {
|
|
7671
|
+
if (::services.isInitialized) services.pauseVoice()
|
|
7671
7672
|
nativeAppState = "inactive"
|
|
7672
7673
|
publish("APP_STATE", JSONObject().put("state", nativeAppState))
|
|
7673
7674
|
super.onPause()
|
|
@@ -116,11 +116,7 @@ dependencies {
|
|
|
116
116
|
|
|
117
117
|
{{WALLET_DEPENDENCY}}
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
|
121
|
-
implementation("com.google.mlkit:image-labeling:17.0.9")
|
|
122
|
-
implementation("com.google.mlkit:object-detection:17.0.2")
|
|
123
|
-
implementation("com.google.mlkit:text-recognition:16.0.1")
|
|
119
|
+
{{MACHINE_LEARNING_DEPENDENCIES}}
|
|
124
120
|
|
|
125
121
|
// Google Play Billing (In-App Purchase)
|
|
126
122
|
implementation("com.android.billingclient:billing:6.1.0")
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1844,6 +1844,15 @@ export declare const health: {
|
|
|
1844
1844
|
onHeartRate(callback: (samples: HealthHeartRateSample[]) => void): () => void;
|
|
1845
1845
|
};
|
|
1846
1846
|
export interface LiveActivityState {
|
|
1847
|
+
/** Native step boundaries retained on device; at most 1000, with detail limited to 96 characters. Only the current state is sent to the widget. */
|
|
1848
|
+
transitions?: Array<{
|
|
1849
|
+
at: number;
|
|
1850
|
+
detail: string;
|
|
1851
|
+
duration?: number | null;
|
|
1852
|
+
}>;
|
|
1853
|
+
/** An actual HealthKit workout (iOS 26+, already authorised) owns background updates. */
|
|
1854
|
+
workoutType?: string;
|
|
1855
|
+
sessionStartedAt?: number;
|
|
1847
1856
|
/** What to show once staleAt passes without fresh app state. */
|
|
1848
1857
|
staleStatus?: string;
|
|
1849
1858
|
/** Current step, interval or session detail. */
|
package/dist/cli.js
CHANGED
|
@@ -311,6 +311,12 @@ async function packageMacOS(config, outDir) {
|
|
|
311
311
|
} catch (error) {
|
|
312
312
|
return [{ success: false, platform: "macos", format: "app", error: error.message }];
|
|
313
313
|
}
|
|
314
|
+
if (opts.notarize && !opts.appStore) {
|
|
315
|
+
const notarized = await notarizeIfRequested(opts, appBundlePath, "app");
|
|
316
|
+
results.push(...notarized);
|
|
317
|
+
if (notarized.some((result) => !result.success))
|
|
318
|
+
return results;
|
|
319
|
+
}
|
|
314
320
|
results.push({ success: true, platform: "macos", format: "app", outputPath: appBundlePath });
|
|
315
321
|
if (opts.dmg !== false) {
|
|
316
322
|
const outputPath = join2(outDir, `${name}-${version}.dmg`);
|
|
@@ -359,24 +365,36 @@ async function notarizeIfRequested(opts, artifactPath, format) {
|
|
|
359
365
|
error: "Notarization requires a keychain profile, API key, or Apple ID/password/team ID"
|
|
360
366
|
}];
|
|
361
367
|
}
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
368
|
+
const scratch = format === "app" ? createTempDir("craft-notarize-") : undefined;
|
|
369
|
+
try {
|
|
370
|
+
const submission = scratch ? join2(scratch, "app.zip") : artifactPath;
|
|
371
|
+
if (scratch) {
|
|
372
|
+
const zipped = await runTool("ditto", ["-c", "-k", "--keepParent", artifactPath, submission]);
|
|
373
|
+
if (!zipped.success)
|
|
374
|
+
return [{ success: false, platform: "macos", format: `${format} (notarize)`, error: zipped.error }];
|
|
375
|
+
}
|
|
376
|
+
const submitted = await runTool("xcrun", notarytoolArguments({
|
|
377
|
+
artifactPath: submission,
|
|
378
|
+
appleId: opts.appleId,
|
|
379
|
+
applePassword: opts.applePassword,
|
|
380
|
+
teamId: opts.teamId,
|
|
381
|
+
keychainProfile: opts.notaryKeychainProfile,
|
|
382
|
+
apiKey: opts.notaryApiKey
|
|
383
|
+
}));
|
|
384
|
+
if (!submitted.success)
|
|
385
|
+
return [{ success: false, platform: "macos", format: `${format} (notarize)`, error: submitted.error }];
|
|
386
|
+
const stapled = await runTool("xcrun", ["stapler", "staple", artifactPath]);
|
|
387
|
+
return [{
|
|
388
|
+
success: stapled.success,
|
|
389
|
+
platform: "macos",
|
|
390
|
+
format: `${format} (notarize)`,
|
|
391
|
+
outputPath: stapled.success ? artifactPath : undefined,
|
|
392
|
+
error: stapled.success ? undefined : stapled.error
|
|
393
|
+
}];
|
|
394
|
+
} finally {
|
|
395
|
+
if (scratch)
|
|
396
|
+
removeTempDir(scratch);
|
|
397
|
+
}
|
|
380
398
|
}
|
|
381
399
|
async function packageWindows(config, outDir) {
|
|
382
400
|
const results = [];
|
|
@@ -2970,7 +2988,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
2970
2988
|
`);
|
|
2971
2989
|
}
|
|
2972
2990
|
// package.json
|
|
2973
|
-
var version = "0.0.
|
|
2991
|
+
var version = "0.0.329";
|
|
2974
2992
|
|
|
2975
2993
|
// bin/cli.ts
|
|
2976
2994
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|