diff --git a/client/android/.gitignore b/client/android/.gitignore new file mode 100644 index 000000000..48354a3df --- /dev/null +++ b/client/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/client/android/app/.gitignore b/client/android/app/.gitignore new file mode 100644 index 000000000..043df802a --- /dev/null +++ b/client/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/client/android/app/build.gradle b/client/android/app/build.gradle new file mode 100644 index 000000000..7ceb0b871 --- /dev/null +++ b/client/android/app/build.gradle @@ -0,0 +1,61 @@ +apply plugin: 'com.android.application' + +android { + namespace = "de.localcrag.app" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "de.localcrag.app" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + // Keep in sync with client/package.json version (single version story, D-04). + versionName "1.51.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + buildFeatures { + // Needed for the BuildConfig.DEBUG guard around MainActivity's debug-only mixed-content + // relaxation (D-09/D-10) — AGP 8 no longer generates BuildConfig by default. + buildConfig true + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + implementation 'com.google.android.gms:play-services-location:21.4.0' + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/client/android/app/capacitor.build.gradle b/client/android/app/capacitor.build.gradle new file mode 100644 index 000000000..adfe712e5 --- /dev/null +++ b/client/android/app/capacitor.build.gradle @@ -0,0 +1,22 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-app') + implementation project(':capacitor-preferences') + implementation project(':capacitor-splash-screen') + implementation project(':capacitor-status-bar') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/client/android/app/proguard-rules.pro b/client/android/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/client/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 000000000..f2c2217ef --- /dev/null +++ b/client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/client/android/app/src/debug/AndroidManifest.xml b/client/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..68f484a43 --- /dev/null +++ b/client/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/client/android/app/src/debug/res/xml/network_security_config.xml b/client/android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 000000000..d00f0f77f --- /dev/null +++ b/client/android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9d503cd00 --- /dev/null +++ b/client/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/android/app/src/main/java/de/localcrag/app/MainActivity.java b/client/android/app/src/main/java/de/localcrag/app/MainActivity.java new file mode 100644 index 000000000..16624270c --- /dev/null +++ b/client/android/app/src/main/java/de/localcrag/app/MainActivity.java @@ -0,0 +1,22 @@ +package de.localcrag.app; + +import android.os.Bundle; +import android.webkit.WebSettings; +import com.getcapacitor.BridgeActivity; +import de.localcrag.app.gps.GpsBridgePlugin; + +public class MainActivity extends BridgeActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(GpsBridgePlugin.class); + super.onCreate(savedInstanceState); + if (BuildConfig.DEBUG) { + // Belt-and-suspenders for cleartext API XHR to http://10.0.2.2:5000 (D-09/D-10). + // Primary fix for media is capacitor.config server.androidScheme = 'http' (avoids + // https://localhost mixed-content blocks on ). BuildConfig.DEBUG is false in + // release, matching the debug-only network security config in plan 02. + getBridge().getWebView().getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + } +} diff --git a/client/android/app/src/main/java/de/localcrag/app/gps/GpsBridgePlugin.java b/client/android/app/src/main/java/de/localcrag/app/gps/GpsBridgePlugin.java new file mode 100644 index 000000000..6a81093f8 --- /dev/null +++ b/client/android/app/src/main/java/de/localcrag/app/gps/GpsBridgePlugin.java @@ -0,0 +1,270 @@ +package de.localcrag.app.gps; + +import android.Manifest; +import android.content.Intent; +import android.location.Location; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import com.getcapacitor.JSObject; +import com.getcapacitor.PermissionState; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.annotation.Permission; +import com.getcapacitor.annotation.PermissionCallback; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.Priority; +import com.google.android.gms.tasks.CancellationTokenSource; + +/** + * Capacitor GpsBridge: starts/stops {@link GpsForegroundService} for Rock Explorer GPS. + * + *

Location updates are owned by the FGS (not Activity-only FLP) so tracking survives + * screen-off / backgrounding (Phase 18 / GPS-01). Pause must not call {@link #stop} — JS + * keeps the service running and gates path appends with {@code isRecording} (D-07). + * + *

Do not auto-start the FGS from {@link #load()} (D-05 / T-18-06). No native HTTP (D-15). + */ +@CapacitorPlugin( + name = "GpsBridge", + permissions = { + @Permission( + alias = "location", + strings = { + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + } + ), + @Permission( + alias = "background", + strings = { Manifest.permission.ACCESS_BACKGROUND_LOCATION } + ), + @Permission( + alias = "notifications", + strings = { Manifest.permission.POST_NOTIFICATIONS } + ) + } +) +public class GpsBridgePlugin extends Plugin { + + private static final long DEFAULT_INTERVAL_MS = 1000L; + + private FusedLocationProviderClient fusedClient; + private boolean serviceStarted = false; + + private final GpsForegroundService.FixListener fixListener = + new GpsForegroundService.FixListener() { + @Override + public void onFix(@NonNull Location location) { + notifyListeners("locationUpdate", toFixPayload(location)); + } + }; + + @Override + public void load() { + fusedClient = LocationServices.getFusedLocationProviderClient(getContext()); + GpsForegroundService.setFixListener(fixListener); + // Intentionally do not start FGS here — JS calls start() after staged perms (D-05). + } + + /** + * Starts the location FGS while the Activity is foreground. Does not request permissions — + * the JS orchestrator owns staging (D-08). Pause must not invoke stop (D-07). + */ + @PluginMethod + public void start(PluginCall call) { + if (!hasLocationPermission()) { + call.reject("Location permission not granted", "PERMISSION_DENIED"); + return; + } + + long intervalMs = DEFAULT_INTERVAL_MS; + if (call.getData() != null && call.getData().has("intervalMs")) { + Integer fromCall = call.getInt("intervalMs"); + if (fromCall != null && fromCall > 0) { + intervalMs = fromCall.longValue(); + } + } + + GpsForegroundService.setFixListener(fixListener); + + Intent intent = new Intent(getContext(), GpsForegroundService.class); + intent.putExtra(GpsForegroundService.EXTRA_INTERVAL_MS, intervalMs); + ContextCompat.startForegroundService(getContext(), intent); + serviceStarted = true; + call.resolve(); + } + + /** Stops the FGS and clears updates. Idempotent (D-06). */ + @PluginMethod + public void stop(PluginCall call) { + Intent intent = new Intent(getContext(), GpsForegroundService.class); + getContext().stopService(intent); + serviceStarted = false; + call.resolve(); + } + + @PluginMethod + public void getCurrentPosition(PluginCall call) { + if (!hasLocationPermission()) { + call.reject("Location permission not granted", "PERMISSION_DENIED"); + return; + } + + CancellationTokenSource cts = new CancellationTokenSource(); + try { + fusedClient + .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, cts.getToken()) + .addOnSuccessListener( + location -> { + if (location != null) { + call.resolve(toFixPayload(location)); + return; + } + fusedClient + .getLastLocation() + .addOnSuccessListener( + last -> { + if (last != null) { + call.resolve(toFixPayload(last)); + } else { + call.reject("Unable to determine location", "POSITION_UNAVAILABLE"); + } + } + ) + .addOnFailureListener( + e -> call.reject("Unable to determine location", "POSITION_UNAVAILABLE", e) + ); + } + ) + .addOnFailureListener( + e -> call.reject("Unable to determine location", "POSITION_UNAVAILABLE", e) + ); + } catch (SecurityException e) { + call.reject("Location permission not granted", "PERMISSION_DENIED", e); + } + } + + @PluginMethod + public void checkPermissions(PluginCall call) { + JSObject result = new JSObject(); + result.put("location", getPermissionState("location").toString()); + result.put("background", getPermissionState("background").toString()); + result.put("notifications", notificationPermissionState().toString()); + call.resolve(result); + } + + @PluginMethod + public void requestPermissions(PluginCall call) { + if (getPermissionState("location") == PermissionState.GRANTED) { + JSObject result = new JSObject(); + result.put("location", PermissionState.GRANTED.toString()); + call.resolve(result); + return; + } + requestPermissionForAlias("location", call, "locationPermissionCallback"); + } + + @PluginMethod + public void requestBackgroundPermission(PluginCall call) { + if (getPermissionState("background") == PermissionState.GRANTED) { + JSObject result = new JSObject(); + result.put("background", PermissionState.GRANTED.toString()); + call.resolve(result); + return; + } + requestPermissionForAlias("background", call, "backgroundPermissionCallback"); + } + + @PluginMethod + public void requestNotificationPermission(PluginCall call) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + JSObject result = new JSObject(); + result.put("notifications", PermissionState.GRANTED.toString()); + call.resolve(result); + return; + } + if (getPermissionState("notifications") == PermissionState.GRANTED) { + JSObject result = new JSObject(); + result.put("notifications", PermissionState.GRANTED.toString()); + call.resolve(result); + return; + } + requestPermissionForAlias("notifications", call, "notificationsPermissionCallback"); + } + + @PermissionCallback + private void locationPermissionCallback(PluginCall call) { + JSObject result = new JSObject(); + result.put("location", getPermissionState("location").toString()); + call.resolve(result); + } + + @PermissionCallback + private void backgroundPermissionCallback(PluginCall call) { + JSObject result = new JSObject(); + result.put("background", getPermissionState("background").toString()); + call.resolve(result); + } + + @PermissionCallback + private void notificationsPermissionCallback(PluginCall call) { + JSObject result = new JSObject(); + result.put("notifications", notificationPermissionState().toString()); + call.resolve(result); + } + + private PermissionState notificationPermissionState() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return PermissionState.GRANTED; + } + return getPermissionState("notifications"); + } + + private boolean hasLocationPermission() { + return ( + ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) == + android.content.pm.PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == + android.content.pm.PackageManager.PERMISSION_GRANTED + ); + } + + private static JSObject toFixPayload(@NonNull Location location) { + JSObject ret = new JSObject(); + ret.put("latitude", location.getLatitude()); + ret.put("longitude", location.getLongitude()); + ret.put("accuracy", (double) location.getAccuracy()); + if (location.hasAltitude()) { + ret.put("altitude", location.getAltitude()); + } else { + ret.put("altitude", JSObject.NULL); + } + if (location.hasBearing()) { + ret.put("heading", (double) location.getBearing()); + } else { + ret.put("heading", JSObject.NULL); + } + if (location.hasSpeed()) { + ret.put("speed", (double) location.getSpeed()); + } else { + ret.put("speed", JSObject.NULL); + } + ret.put("timestamp", location.getTime()); + return ret; + } + + @Override + protected void handleOnDestroy() { + if (serviceStarted) { + Intent intent = new Intent(getContext(), GpsForegroundService.class); + getContext().stopService(intent); + serviceStarted = false; + } + GpsForegroundService.setFixListener(null); + super.handleOnDestroy(); + } +} diff --git a/client/android/app/src/main/java/de/localcrag/app/gps/GpsForegroundService.java b/client/android/app/src/main/java/de/localcrag/app/gps/GpsForegroundService.java new file mode 100644 index 000000000..f8f9eec14 --- /dev/null +++ b/client/android/app/src/main/java/de/localcrag/app/gps/GpsForegroundService.java @@ -0,0 +1,196 @@ +package de.localcrag.app.gps; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.location.Location; +import android.os.Build; +import android.os.IBinder; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.app.ServiceCompat; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.Priority; +import de.localcrag.app.MainActivity; +import de.localcrag.app.R; + +/** + * Location-typed foreground service owning Fused Location updates + ongoing + * tracking notification (Phase 18 / GPS-01, GPS-03). + * + *

No pause/finish notification actions (D-11). No HTTP from this service (D-15). + * FLP lives here (not only in the Activity plugin) so updates survive backgrounding. + */ +public class GpsForegroundService extends Service { + + public static final String EXTRA_INTERVAL_MS = "intervalMs"; + public static final String ACTION_RECREATE_NOTIFICATION = + "de.localcrag.app.gps.RECREATE_NOTIFICATION"; + + private static final String CHANNEL_ID = "gps_tracking"; + private static final int NOTIFICATION_ID = 1818; + private static final long DEFAULT_INTERVAL_MS = 1000L; + private static final long MIN_UPDATE_INTERVAL_MS = 500L; + + /** Forwards fixes to {@link GpsBridgePlugin} so {@code locationUpdate} keeps working (D-02). */ + public interface FixListener { + void onFix(@NonNull Location location); + } + + private static volatile FixListener fixListener; + + public static void setFixListener(@Nullable FixListener listener) { + fixListener = listener; + } + + private FusedLocationProviderClient fusedClient; + private LocationCallback locationCallback; + private boolean updatesActive = false; + private long intervalMs = DEFAULT_INTERVAL_MS; + + @Override + public void onCreate() { + super.onCreate(); + fusedClient = LocationServices.getFusedLocationProviderClient(this); + locationCallback = + new LocationCallback() { + @Override + public void onLocationResult(@NonNull LocationResult result) { + Location location = result.getLastLocation(); + if (location == null) { + return; + } + FixListener listener = fixListener; + if (listener != null) { + listener.onFix(location); + } + } + }; + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) { + // D-12: Android 14+ may allow dismissing ongoing FGS notifications — recreate. + promoteToForeground(); + return START_STICKY; + } + + if (intent != null && intent.hasExtra(EXTRA_INTERVAL_MS)) { + long fromIntent = intent.getLongExtra(EXTRA_INTERVAL_MS, DEFAULT_INTERVAL_MS); + if (fromIntent > 0) { + intervalMs = fromIntent; + } + } + + promoteToForeground(); + startLocationUpdates(); + return START_STICKY; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + stopLocationUpdates(); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); + super.onDestroy(); + } + + private void promoteToForeground() { + Notification notification = buildNotification(); + ServiceCompat.startForeground( + this, + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ); + } + + private void startLocationUpdates() { + LocationRequest request = + new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, intervalMs) + .setMinUpdateIntervalMillis(MIN_UPDATE_INTERVAL_MS) + .setWaitForAccurateLocation(false) + .build(); + + try { + if (updatesActive) { + fusedClient.removeLocationUpdates(locationCallback); + updatesActive = false; + } + fusedClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper()); + updatesActive = true; + } catch (SecurityException e) { + stopSelf(); + } + } + + private void stopLocationUpdates() { + if (updatesActive && locationCallback != null && fusedClient != null) { + fusedClient.removeLocationUpdates(locationCallback); + updatesActive = false; + } + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + NotificationChannel channel = + new NotificationChannel( + CHANNEL_ID, + getString(R.string.gps_tracking_channel_name), + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription(getString(R.string.gps_tracking_channel_description)); + channel.setShowBadge(false); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + + private Notification buildNotification() { + Intent launchIntent = new Intent(this, MainActivity.class); + launchIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + int pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingFlags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, launchIntent, pendingFlags); + + // D-12: recreate startForeground if the user dismisses the ongoing notification on API 34+. + Intent deleteIntent = new Intent(this, GpsForegroundService.class); + deleteIntent.setAction(ACTION_RECREATE_NOTIFICATION); + PendingIntent deletePending = PendingIntent.getService(this, 1, deleteIntent, pendingFlags); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.gps_tracking_notification_title)) + .setContentText(getString(R.string.gps_tracking_notification_text)) + .setSmallIcon(R.mipmap.ic_launcher) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentIntent(contentIntent) + .setDeleteIntent(deletePending) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .build(); + } +} diff --git a/client/android/app/src/main/res/drawable-land-hdpi/splash.png b/client/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 000000000..30b633cfa Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-ldpi/splash.png b/client/android/app/src/main/res/drawable-land-ldpi/splash.png new file mode 100644 index 000000000..e7d971d36 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-ldpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-mdpi/splash.png b/client/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 000000000..c6943fc46 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-hdpi/splash.png b/client/android/app/src/main/res/drawable-land-night-hdpi/splash.png new file mode 100644 index 000000000..6bfdeb5e8 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-hdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-ldpi/splash.png b/client/android/app/src/main/res/drawable-land-night-ldpi/splash.png new file mode 100644 index 000000000..de60f2482 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-ldpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-mdpi/splash.png b/client/android/app/src/main/res/drawable-land-night-mdpi/splash.png new file mode 100644 index 000000000..8cdf60464 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-mdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-xhdpi/splash.png b/client/android/app/src/main/res/drawable-land-night-xhdpi/splash.png new file mode 100644 index 000000000..316811d73 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-xhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png b/client/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png new file mode 100644 index 000000000..6c74ac780 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png b/client/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png new file mode 100644 index 000000000..d0a920470 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-xhdpi/splash.png b/client/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 000000000..b0659d00c Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/client/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 000000000..c12402c68 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/client/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 000000000..669a71906 Binary files /dev/null and b/client/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-night/splash.png b/client/android/app/src/main/res/drawable-night/splash.png new file mode 100644 index 000000000..de60f2482 Binary files /dev/null and b/client/android/app/src/main/res/drawable-night/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-hdpi/splash.png b/client/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 000000000..33cbe0b3f Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-ldpi/splash.png b/client/android/app/src/main/res/drawable-port-ldpi/splash.png new file mode 100644 index 000000000..184b25207 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-ldpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-mdpi/splash.png b/client/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 000000000..0be2ab108 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-hdpi/splash.png b/client/android/app/src/main/res/drawable-port-night-hdpi/splash.png new file mode 100644 index 000000000..517dc5729 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-hdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-ldpi/splash.png b/client/android/app/src/main/res/drawable-port-night-ldpi/splash.png new file mode 100644 index 000000000..9ba28781c Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-ldpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-mdpi/splash.png b/client/android/app/src/main/res/drawable-port-night-mdpi/splash.png new file mode 100644 index 000000000..90281fbf7 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-mdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-xhdpi/splash.png b/client/android/app/src/main/res/drawable-port-night-xhdpi/splash.png new file mode 100644 index 000000000..b2fcd0430 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-xhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png b/client/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png new file mode 100644 index 000000000..7c225ad20 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png b/client/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png new file mode 100644 index 000000000..c38d5f1d6 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-xhdpi/splash.png b/client/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 000000000..612475962 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/client/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 000000000..44d833ab7 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/client/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 000000000..0527102c2 Binary files /dev/null and b/client/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/client/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/client/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..c7bd21dbd --- /dev/null +++ b/client/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/client/android/app/src/main/res/drawable/ic_launcher_background.xml b/client/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..d5fccc538 --- /dev/null +++ b/client/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/android/app/src/main/res/drawable/splash.png b/client/android/app/src/main/res/drawable/splash.png new file mode 100644 index 000000000..0be2ab108 Binary files /dev/null and b/client/android/app/src/main/res/drawable/splash.png differ diff --git a/client/android/app/src/main/res/layout/activity_main.xml b/client/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..b5ad13870 --- /dev/null +++ b/client/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..0aa82aa1b --- /dev/null +++ b/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..0aa82aa1b --- /dev/null +++ b/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..8097b4875 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 000000000..2ee9e21db Binary files /dev/null and b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..13266cea2 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..4f17f2bc9 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 000000000..a40cbe944 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png new file mode 100644 index 000000000..94aa6b1fd Binary files /dev/null and b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png new file mode 100644 index 000000000..29a132f4a Binary files /dev/null and b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png new file mode 100644 index 000000000..7d5da2c82 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..41d3e9780 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 000000000..045e6cbfd Binary files /dev/null and b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..4c5b57edb Binary files /dev/null and b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..fce6f4371 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8fcae6d17 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 000000000..97cdc325f Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..8fa0575ff Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..9cdd989a3 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0c76d19cb Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..018acd7cd Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..199192777 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..4fa1f5ce4 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..a5e3a304c Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..583ee6325 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..e511f4ee5 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..4d04ba3d6 Binary files /dev/null and b/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/client/android/app/src/main/res/values/ic_launcher_background.xml b/client/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/client/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/client/android/app/src/main/res/values/strings.xml b/client/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..2435b8176 --- /dev/null +++ b/client/android/app/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + LocalCrag + LocalCrag + de.localcrag.app + de.localcrag.app + Path recording + Shows while Rock Explorer is recording your path + Recording path + Rock Explorer GPS tracking is active + diff --git a/client/android/app/src/main/res/values/styles.xml b/client/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..be874e54a --- /dev/null +++ b/client/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/client/android/app/src/main/res/xml/file_paths.xml b/client/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..bd0c4d80d --- /dev/null +++ b/client/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/client/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/client/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 000000000..029732784 --- /dev/null +++ b/client/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/client/android/build.gradle b/client/android/build.gradle new file mode 100644 index 000000000..f8f0e43b6 --- /dev/null +++ b/client/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/client/android/capacitor.settings.gradle b/client/android/capacitor.settings.gradle new file mode 100644 index 000000000..9847b1a0d --- /dev/null +++ b/client/android/capacitor.settings.gradle @@ -0,0 +1,15 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') + +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') + +include ':capacitor-splash-screen' +project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android') + +include ':capacitor-status-bar' +project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') diff --git a/client/android/gradle.properties b/client/android/gradle.properties new file mode 100644 index 000000000..2e87c52f8 --- /dev/null +++ b/client/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/client/android/gradle/wrapper/gradle-wrapper.jar b/client/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1b33c55ba Binary files /dev/null and b/client/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..de413606b --- /dev/null +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/client/android/gradlew b/client/android/gradlew new file mode 100755 index 000000000..23d15a936 --- /dev/null +++ b/client/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/client/android/gradlew.bat b/client/android/gradlew.bat new file mode 100644 index 000000000..db3a6ac20 --- /dev/null +++ b/client/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/client/android/settings.gradle b/client/android/settings.gradle new file mode 100644 index 000000000..3b4431d77 --- /dev/null +++ b/client/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/client/android/variables.gradle b/client/android/variables.gradle new file mode 100644 index 000000000..ee4ba41c4 --- /dev/null +++ b/client/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/client/angular.json b/client/angular.json index 48f0ec6ba..e7b9a47db 100644 --- a/client/angular.json +++ b/client/angular.json @@ -108,6 +108,18 @@ "with": "src/app/modules/rock-explorer/rock-explorer-mock-gps.loader.prod.ts" } ] + }, + "android": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.android.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/client/assets/icon-background.png b/client/assets/icon-background.png new file mode 100644 index 000000000..48aaf7e6d Binary files /dev/null and b/client/assets/icon-background.png differ diff --git a/client/assets/icon-foreground.png b/client/assets/icon-foreground.png new file mode 100644 index 000000000..fd99763d6 Binary files /dev/null and b/client/assets/icon-foreground.png differ diff --git a/client/assets/icon-only.png b/client/assets/icon-only.png new file mode 100644 index 000000000..19a296a5a Binary files /dev/null and b/client/assets/icon-only.png differ diff --git a/client/assets/splash-dark.png b/client/assets/splash-dark.png new file mode 100644 index 000000000..fe58d12b0 Binary files /dev/null and b/client/assets/splash-dark.png differ diff --git a/client/assets/splash.png b/client/assets/splash.png new file mode 100644 index 000000000..204102cd2 Binary files /dev/null and b/client/assets/splash.png differ diff --git a/client/capacitor.config.ts b/client/capacitor.config.ts new file mode 100644 index 000000000..ca0e7aa15 --- /dev/null +++ b/client/capacitor.config.ts @@ -0,0 +1,30 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'de.localcrag.app', + appName: 'LocalCrag', + webDir: 'dist/client/browser', + // Default androidScheme is https, which treats the WebView as a secure context and blocks + // cleartext loads to the local MinIO endpoint (http://10.0.2.2:9000) even when + // MIXED_CONTENT_ALWAYS_ALLOW is set. http keeps debug media working with the emulator + // loopback rewrite; release still ships the same bundled assets without remote cleartext + // requirements once production media is HTTPS. + server: { + androidScheme: 'http', + }, + plugins: { + SplashScreen: { + launchAutoHide: false, + androidSplashResourceName: 'splash', + splashFullScreen: false, + splashImmersive: false, + }, + // D-05 / SHELL-03: patch XHR/fetch through native HttpURLConnection so + // Angular HttpClient bypasses WebView CORS against arbitrary operator APIs. + CapacitorHttp: { + enabled: true, + }, + }, +}; + +export default config; diff --git a/client/package-lock.json b/client/package-lock.json index 32f20fe70..0ae7f0e22 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "localcrag-client", - "version": "1.49.0", + "version": "1.51.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "localcrag-client", - "version": "1.49.0", + "version": "1.51.0", "dependencies": { "@angular/animations": "^21.1.3", "@angular/cdk": "^21.1.3", @@ -17,6 +17,12 @@ "@angular/platform-browser": "^21.1.3", "@angular/platform-browser-dynamic": "^21.1.3", "@angular/router": "^21.1.3", + "@capacitor/android": "^8.5.0", + "@capacitor/app": "^8.1.1", + "@capacitor/core": "^8.5.0", + "@capacitor/preferences": "^8.0.1", + "@capacitor/splash-screen": "^8.0.2", + "@capacitor/status-bar": "^8.0.3", "@jsverse/transloco": "^8.1.0", "@jsverse/transloco-messageformat": "^8.1.0", "@ngrx/effects": "^21.0.1", @@ -57,6 +63,8 @@ "@angular/build": "^21.1.3", "@angular/cli": "~21.2.0", "@angular/compiler-cli": "^21.1.3", + "@capacitor/assets": "^3.0.5", + "@capacitor/cli": "^8.5.0", "@jsverse/transloco-keys-manager": "^7.0.2", "@ngrx/store-devtools": "^21.0.1", "@sentry/cli": "^2.42.2", @@ -1113,1108 +1121,1843 @@ "node": ">=6.9.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, + "node_modules/@capacitor/android": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.5.0.tgz", + "integrity": "sha512-Rb3prJeQiTp0pQhSSOReJuG9VgibOGMG4ECs+UhMwr6TCCHZ3NCZO811bDA0HxD0xrT/gCrJAsY5yfEATu84JQ==", "license": "MIT", - "engines": { - "node": ">=0.1.90" + "peerDependencies": { + "@capacitor/core": "^8.5.0" } }, - "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" + "node_modules/@capacitor/app": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.1.tgz", + "integrity": "sha512-xM2ZTX5jK60tFtmjsmJv+Cdj1Qsb6NcavkqmdEnCPQBeya9oKhamZJxYOnQNj1ZvcJM7sWfG6BEtSLx42pisbA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/assets": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/assets/-/assets-3.0.5.tgz", + "integrity": "sha512-ohz/OUq61Y1Fc6aVSt0uDrUdeOA7oTH4pkWDbv/8I3UrPjH7oPkzYhShuDRUjekNp9RBi198VSFdt0CetpEOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@capacitor/cli": "^5.3.0", + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@trapezedev/project": "^7.0.10", + "commander": "8.3.0", + "debug": "4.3.4", + "fs-extra": "10.1.0", + "node-fetch": "2.7.0", + "node-html-parser": "5.4.2", + "sharp": "0.32.6", + "tslib": "2.6.2", + "yargs": "17.7.2" + }, + "bin": { + "capacitor-assets": "bin/capacitor-assets" }, "engines": { - "node": ">= 6" + "node": ">=10.3.0" } }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "node_modules/@capacitor/assets/node_modules/@capacitor/cli": { + "version": "5.7.8", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-5.7.8.tgz", + "integrity": "sha512-qN8LDlREMhrYhOvVXahoJVNkP8LP55/YPRJrzTAFrMqlNJC18L3CzgWYIblFPnuwfbH/RxbfoZT/ydkwgVpMrw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" + "@ionic/cli-framework-output": "^2.2.5", + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-subprocess": "^2.1.11", + "@ionic/utils-terminal": "^2.3.3", + "commander": "^9.3.0", + "debug": "^4.3.4", + "env-paths": "^2.2.0", + "kleur": "^4.1.4", + "native-run": "^2.0.0", + "open": "^8.4.0", + "plist": "^3.0.5", + "prompts": "^2.4.2", + "rimraf": "^4.4.1", + "semver": "^7.3.7", + "tar": "^6.1.11", + "tslib": "^2.4.0", + "xml2js": "^0.5.0" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/@capacitor/assets/node_modules/@capacitor/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": "^12.20.0 || >=14" } }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "node_modules/@capacitor/assets/node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "node_modules/@capacitor/assets/node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "tslib": "^2.4.0" + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "node_modules/@capacitor/assets/node_modules/@ionic/utils-subprocess": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.14.tgz", + "integrity": "sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "tslib": "^2.4.0" + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@capacitor/assets/node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=16.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], + "node_modules/@capacitor/assets/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@capacitor/assets/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">= 12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "ms": "2.1.2" + }, "engines": { - "node": ">=18" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], + "node_modules/@capacitor/assets/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], + "node_modules/@capacitor/assets/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], + "node_modules/@capacitor/assets/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], + "node_modules/@capacitor/assets/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "ISC" }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@capacitor/assets/node_modules/minimatch": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@capacitor/assets/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], + "node_modules/@capacitor/assets/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "bin": { + "mkdirp": "bin/cmd.js" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT" + }, + "node_modules/@capacitor/assets/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/rimraf": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "ISC", + "dependencies": { + "glob": "^9.2.0" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } + "license": "ISC" }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], + "node_modules/@capacitor/assets/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], + "node_modules/@capacitor/assets/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], + "node_modules/@capacitor/assets/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } + "license": "0BSD" }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "node_modules/@capacitor/assets/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@capacitor/assets/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=4.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "node_modules/@capacitor/assets/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=4.0" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@capacitor/assets/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/@capacitor/assets/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@capacitor/assets/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@capacitor/cli": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.0.tgz", + "integrity": "sha512-rLdzMUM5QV4WITcqoWv04p32i14BXgUH2diqEH6MWQlWaJfiyNrvOyt/+d5vHAfOxOm1klBu637VDEobctlwBA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.17.0" + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xcode": "^3.0.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.0.0" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "node_modules/@capacitor/cli/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@capacitor/cli/node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, + "node_modules/@capacitor/core": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.0.tgz", + "integrity": "sha512-Ca4krtqH1hothjtBIwf2J2TW7IhYq1ujp8QeItTiJohNsqij8ja2DYYH3DU0l8RmxCWaBAFTGA2TgOgOMCSNsQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "tslib": "^2.1.0" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "node_modules/@capacitor/preferences": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz", + "integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==", "license": "MIT", - "engines": { - "node": ">= 4" + "peerDependencies": { + "@capacitor/core": ">=8.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "node_modules/@capacitor/splash-screen": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-8.0.2.tgz", + "integrity": "sha512-0C6rYScr13WfAE9nPl4ScOQynmjFv9NT3Th+RZkD4ezYHmER6mcl1/lDYsv7jg3Rj3FOvLrlTlmNkUrr83kZzQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@capacitor/status-bar": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz", + "integrity": "sha512-csSpfNeN49Hx9JaQBSJEIiEbOLtXg3kcc2IpScq2fu5L520h3AWEvsxoH8Srk1jxfRKepaJ4S4sqSC3foI4AgA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=0.1.90" } }, - "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" }, - "funding": { - "url": "https://eslint.org/donate" + "engines": { + "node": ">=12" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 6" } }, - "node_modules/@gar/promise-retry": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", - "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, "license": "MIT", - "engines": { - "node": "^20.17.0 || >=22.9.0" + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" } }, - "node_modules/@harperfast/extended-iterable": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", - "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "Apache-2.0", - "optional": true + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } }, - "node_modules/@hono/node-server": { - "version": "1.19.17", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", - "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" + "tslib": "^2.4.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" + "tslib": "^2.4.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=18" } }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" } }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@hutson/parse-repository-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", + "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", @@ -2267,64 +3010,288 @@ "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=8" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "node_modules/@ionic/utils-terminal/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-terminal/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "peerDependencies": { - "@types/node": ">=18" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@isaacs/fs-minipass": { @@ -4106,6 +5073,17 @@ "typescript": "^3 || ^4 || ^5" } }, + "node_modules/@prettier/plugin-xml": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@prettier/plugin-xml/-/plugin-xml-2.2.0.tgz", + "integrity": "sha512-UWRmygBsyj4bVXvDiqSccwT1kmsorcwQwaIy30yVh8T+Gspx4OlC0shX1y+ZuwXZvgnafmpRYKks0bAu9urJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xml-tools/parser": "^1.0.11", + "prettier": ">=2.4.0" + } + }, "node_modules/@primeuix/motion": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@primeuix/motion/-/motion-0.0.10.tgz", @@ -5446,49 +6424,314 @@ "license": "0BSD", "optional": true }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@trapezedev/gradle-parse": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@trapezedev/gradle-parse/-/gradle-parse-7.1.3.tgz", + "integrity": "sha512-WQVF5pEJ5o/mUyvfGTG9nBKx9Te/ilKM3r2IT69GlbaooItT5ao7RyF1MUTBNjHLPk/xpGUY3c6PyVnjDlz0Vw==", + "dev": true, + "license": "SEE LICENSE" + }, + "node_modules/@trapezedev/project": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@trapezedev/project/-/project-7.1.4.tgz", + "integrity": "sha512-b5rszBgT5XiRp/m4V2S2Ara2fdFXhPiduxhvCIVTSHq51PgLBjiTEStL6NbUz3V0K5bebF971O+SLRtyBxfCNA==", + "dev": true, + "license": "SEE LICENSE", + "dependencies": { + "@ionic/utils-fs": "^3.1.5", + "@ionic/utils-subprocess": "^2.1.8", + "@prettier/plugin-xml": "^2.2.0", + "@trapezedev/gradle-parse": "7.1.3", + "@xmldom/xmldom": "^0.9.9", + "conventional-changelog": "^3.1.4", + "cross-spawn": "^7.0.3", + "diff": "^5.1.0", + "env-paths": "^3.0.0", + "gradle-to-js": "^2.0.0", + "ini": "^2.0.0", + "kleur": "^4.1.5", + "lodash": "^4.17.21", + "plist": "^3.0.4", + "prettier": "^2.7.1", + "prompts": "^2.4.2", + "replace": "^1.1.0", + "tempy": "^3.1.0", + "tmp": "^0.2.1", + "ts-node": "^10.2.1", + "xcode": "^3.0.1", + "xml-js": "^1.6.11", + "xpath": "^0.0.32", + "yargs": "^17.2.1" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-subprocess": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.14.tgz", + "integrity": "sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@trapezedev/project/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@trapezedev/project/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@trapezedev/project/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@trapezedev/project/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@trapezedev/project/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@trapezedev/project/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@trapezedev/project/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@trapezedev/project/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">= 20" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", - "cpu": [ - "x64" - ], + "node_modules/@trapezedev/project/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">= 20" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", - "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "node_modules/@trapezedev/project/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, "license": "MIT", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "postcss": "^8.5.16", - "tailwindcss": "4.3.3" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@trapezedev/project/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/@ts-morph/common": { @@ -5553,6 +6796,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -5622,6 +6893,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -5663,6 +6944,13 @@ "@types/geojson": "*" } }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", @@ -5684,6 +6972,13 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -5698,6 +6993,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/through": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", @@ -6230,6 +7532,26 @@ "vite": "^6.0.0 || ^7.0.0" } }, + "node_modules/@xml-tools/parser": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", + "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chevrotain": "7.1.1" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -6324,6 +7646,26 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -6519,6 +7861,13 @@ ], "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6545,6 +7894,23 @@ "node": ">=12.17" } }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -6626,6 +7992,21 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -6636,6 +8017,91 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.1.tgz", + "integrity": "sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6711,6 +8177,16 @@ "node": ">=18.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6828,6 +8304,29 @@ "dev": true, "license": "ISC" }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -7038,6 +8537,34 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001806", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", @@ -7279,6 +8806,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/chevrotain": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", + "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -7549,6 +9086,20 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7569,6 +9120,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -7649,6 +9211,17 @@ "node": ">=4.0.0" } }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7898,6 +9471,263 @@ "node": ">= 0.6" } }, + "node_modules/conventional-changelog": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", + "integrity": "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^5.0.12", + "conventional-changelog-atom": "^2.0.8", + "conventional-changelog-codemirror": "^2.0.8", + "conventional-changelog-conventionalcommits": "^4.5.0", + "conventional-changelog-core": "^4.2.1", + "conventional-changelog-ember": "^2.0.9", + "conventional-changelog-eslint": "^3.0.9", + "conventional-changelog-express": "^2.0.6", + "conventional-changelog-jquery": "^3.0.11", + "conventional-changelog-jshint": "^2.0.9", + "conventional-changelog-preset-loader": "^2.3.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", + "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", + "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", + "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", + "deprecated": "Deprecated and no longer maintained. Please use conventional-changelog instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^5.0.0", + "conventional-commits-parser": "^3.2.0", + "dateformat": "^3.0.0", + "get-pkg-repo": "^4.0.0", + "git-raw-commits": "^2.0.8", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^4.1.1", + "lodash": "^4.17.15", + "normalize-package-data": "^3.0.0", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", + "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", + "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-express": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", + "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", + "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", + "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", + "deprecated": "This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", + "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", + "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-filter": "^2.0.7", + "dateformat": "^3.0.0", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-writer/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/conventional-commits-filter": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", + "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -7977,6 +9807,13 @@ } } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7992,6 +9829,35 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/css-select": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", @@ -8314,6 +10180,16 @@ "node": ">=8" } }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -8347,6 +10223,16 @@ "node": ">=4.0" } }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", @@ -8372,6 +10258,59 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-diff": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", @@ -8400,6 +10339,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8456,6 +10405,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -8530,6 +10489,16 @@ "dev": true, "license": "MIT" }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -8594,12 +10563,25 @@ "dev": true, "license": "BSD-2-Clause", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "engines": { + "node": ">=8" } }, "node_modules/dunder-proto": { @@ -8647,6 +10629,19 @@ "dev": true, "license": "ISC" }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -9340,6 +11335,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -9407,6 +11412,16 @@ "node": ">=4" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -9555,6 +11570,13 @@ "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9886,6 +11908,13 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", @@ -10011,6 +12040,177 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-pkg-repo": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", + "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hutson/parse-repository-url": "^3.0.0", + "hosted-git-info": "^4.0.0", + "through2": "^2.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "get-pkg-repo": "src/cli.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-pkg-repo/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/get-pkg-repo/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/get-pkg-repo/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-pkg-repo/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-pkg-repo/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-pkg-repo/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/get-pkg-repo/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/get-pkg-repo/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/get-pkg-repo/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-pkg-repo/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-pkg-repo/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -10076,6 +12276,93 @@ "node": ">=18" } }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "deprecated": "Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/git-semver-tags": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", + "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", + "deprecated": "Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^8.0.0", + "semver": "^6.0.0" + }, + "bin": { + "git-semver-tags": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-semver-tags/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "dev": true, + "license": "BSD", + "dependencies": { + "ini": "^1.3.2" + } + }, + "node_modules/gitconfiglocal/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/gl-matrix": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", @@ -10177,6 +12464,61 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gradle-to-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gradle-to-js/-/gradle-to-js-2.0.1.tgz", + "integrity": "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.merge": "^4.6.2" + }, + "bin": { + "gradle-to-js": "cli.js" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -10238,6 +12580,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hono": { "version": "4.12.34", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.34.tgz", @@ -10748,6 +13100,22 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", @@ -10764,6 +13132,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10843,6 +13227,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -10853,6 +13247,16 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -10891,6 +13295,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -10911,6 +13328,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -11113,6 +13550,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", @@ -11207,6 +13651,23 @@ ], "license": "MIT" }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -11735,6 +14196,26 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/konva": { "version": "9.3.22", "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", @@ -12503,6 +14984,46 @@ "license": "MIT", "optional": true }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -12533,6 +15054,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12820,6 +15348,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "15.0.6", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", @@ -12850,6 +15385,19 @@ "integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==", "license": "Unicode-DFS-2016" }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mapbox-gl": { "version": "3.27.0", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.27.0.tgz", @@ -12921,6 +15469,195 @@ "url": "https://opencollective.com/express" } }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -13037,6 +15774,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -13062,6 +15822,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -13208,6 +15983,23 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/moo": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", @@ -13309,6 +16101,59 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/native-run/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/native-run/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -13326,6 +16171,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/ngx-colors": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/ngx-colors/-/ngx-colors-3.6.0.tgz", @@ -13383,6 +16235,19 @@ "@angular/core": "^21.0.0" } }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -13520,6 +16385,103 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/node-html-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^4.2.1", + "he": "1.2.0" + } + }, + "node_modules/node-html-parser/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/node-html-parser/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/node-html-parser/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/node-html-parser/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/node-html-parser/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/node-html-parser/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/node-releases": { "version": "2.0.51", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", @@ -13546,6 +16508,55 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -13771,6 +16782,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13997,6 +17026,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pacote": { "version": "21.5.1", "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", @@ -14262,6 +17308,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -14400,6 +17453,21 @@ "node": ">=16.20.0" } }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/postcss": { "version": "8.5.25", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", @@ -14455,18 +17523,108 @@ } ], "license": "MIT", - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, - "node_modules/potpack": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", - "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", - "license": "ISC" + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -14593,33 +17751,271 @@ "license": "MIT", "optional": true, "os": [ - "android" + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/primeclt/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/darwin-arm64": { + "node_modules/primeclt/node_modules/@esbuild/openbsd-x64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "openbsd" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/darwin-x64": { + "node_modules/primeclt/node_modules/@esbuild/sunos-x64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "cpu": [ "x64" ], @@ -14627,16 +18023,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "sunos" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/freebsd-arm64": { + "node_modules/primeclt/node_modules/@esbuild/win32-arm64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "cpu": [ "arm64" ], @@ -14644,930 +18040,1194 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/freebsd-x64": { + "node_modules/primeclt/node_modules/@esbuild/win32-ia32": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-arm": { + "node_modules/primeclt/node_modules/@esbuild/win32-x64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-arm64": { + "node_modules/primeclt/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/primeclt/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/primeclt/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/primeclt/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/primeclt/node_modules/esbuild": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/primeclt/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primeclt/node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/primeclt/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], + "node_modules/primeclt/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], + "node_modules/primeclt/node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], + "node_modules/primeclt/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], + "node_modules/primeclt/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], + "node_modules/primeclt/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/primeclt/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primelocale": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/primelocale/-/primelocale-2.4.0.tgz", + "integrity": "sha512-+FxfN+NjImzSoFTE8n5TLidMucC+7pl1DZKrNokgcMPbN7iVWYtb9+zC+y9ZSkn81vDa328PiCPh966gSjW6zQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18.0.0", + "npm": ">=8.6.0" } }, - "node_modules/primeclt/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/primeng": { + "version": "21.1.9", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.9.tgz", + "integrity": "sha512-Z76PtF08X0PNSTCNMobao8Qm71vC56mtZSdUtX/gsn4+q4x0NbUUtkeK1pc33a+kxVm2zkmOwzUIEGmb9VdoIA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@primeuix/motion": "^0.0.10", + "@primeuix/styled": "^0.7.4", + "@primeuix/styles": "^2.0.3", + "@primeuix/utils": "^0.7.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.7", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "rxjs": "^6.0.0 || ^7.8.1" + } + }, + "node_modules/primeng/node_modules/@primeuix/utils": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.7.2.tgz", + "integrity": "sha512-pmEbSfP0Phf9W9RweiM66zXnkn73ZeKyYINElbX3uZ2+stzzaba2svLAl3B1pHVcRw5t43O0VciaGe4ye2EXKw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=12.11.0" } }, - "node_modules/primeclt/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "ISC", "engines": { - "node": ">=12" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/primeclt/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=12" + "node": ">= 0.6.0" } }, - "node_modules/primeclt/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.4.0" } }, - "node_modules/primeclt/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/primeclt/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/primeclt/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, "engines": { - "node": ">=12" + "node": ">= 0.10" } }, - "node_modules/primeclt/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/primeclt/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=0.6.0", + "teleport": ">=0.2.0" } }, - "node_modules/primeclt/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true, "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.9" } }, - "node_modules/primeclt/node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { - "node": ">=6" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/primeclt/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=8" } }, - "node_modules/primeclt/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" }, - "node_modules/primeclt/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "hasInstallScript": true, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-blot-formatter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz", + "integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^2.0.0" + }, + "peerDependencies": { + "quill": "^1.3.4" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "node": ">=0.10" } }, - "node_modules/primeclt/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/quill-image-uploader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/quill-image-uploader/-/quill-image-uploader-1.3.0.tgz", + "integrity": "sha512-vO43GEn93rGThje/MlotkQE9OV5nOKBZ4oKhn71L/EjrM/J2P/8iDDVd9GEwlsGsbskeJqPLopsSQ4HlVVIn6A==", + "license": "MIT", + "peerDependencies": { + "quill": "^1.3.7" + } + }, + "node_modules/quill/node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/primeclt/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "rc": "cli.js" } }, - "node_modules/primeclt/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/primeclt/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/primeclt/node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } + "engines": { + "node": ">=4" } }, - "node_modules/primeclt/node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "locate-path": "^2.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/primeclt/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/primeclt/node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "p-try": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=4" } }, - "node_modules/primeclt/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "p-limit": "^1.1.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/primeclt/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=4" } }, - "node_modules/primeclt/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "node_modules/primeclt/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "node_modules/primeicons": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", - "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", - "license": "MIT" + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/primelocale": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/primelocale/-/primelocale-2.4.0.tgz", - "integrity": "sha512-+FxfN+NjImzSoFTE8n5TLidMucC+7pl1DZKrNokgcMPbN7iVWYtb9+zC+y9ZSkn81vDa328PiCPh966gSjW6zQ==", + "node_modules/read-pkg/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18.0.0", - "npm": ">=8.6.0" + "node": ">=4" } }, - "node_modules/primeng": { - "version": "21.1.9", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.9.tgz", - "integrity": "sha512-Z76PtF08X0PNSTCNMobao8Qm71vC56mtZSdUtX/gsn4+q4x0NbUUtkeK1pc33a+kxVm2zkmOwzUIEGmb9VdoIA==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", "dependencies": { - "@primeuix/motion": "^0.0.10", - "@primeuix/styled": "^0.7.4", - "@primeuix/styles": "^2.0.3", - "@primeuix/utils": "^0.7.2", - "tslib": "^2.3.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, - "peerDependencies": { - "@angular/cdk": "^21.0.0", - "@angular/common": "^21.0.0", - "@angular/core": "^21.0.7", - "@angular/forms": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/router": "^21.0.0", - "rxjs": "^6.0.0 || ^7.8.1" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/primeng/node_modules/@primeuix/utils": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.7.2.tgz", - "integrity": "sha512-pmEbSfP0Phf9W9RweiM66zXnkn73ZeKyYINElbX3uZ2+stzzaba2svLAl3B1pHVcRw5t43O0VciaGe4ye2EXKw==", + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12.11.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=8" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", "dev": true, + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, "engines": { - "node": ">= 0.6.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "node_modules/replace": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", + "integrity": "sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "2.4.2", + "minimatch": "3.0.5", + "yargs": "^15.3.1" + }, + "bin": { + "replace": "bin/replace.js", + "search": "bin/search.js" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 6" } }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", - "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/replace/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">= 0.10" + "node": ">=4" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "node_modules/replace/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "node_modules/replace/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "node_modules/replace/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, "engines": { - "node": ">=0.9" + "node": ">=4" } }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "node_modules/replace/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/replace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" - }, - "node_modules/quill": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", - "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "deep-equal": "^1.0.1", - "eventemitter3": "^2.0.3", - "extend": "^3.0.2", - "parchment": "^1.1.4", - "quill-delta": "^3.6.2" + "color-name": "1.1.3" } }, - "node_modules/quill-blot-formatter": { + "node_modules/replace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/replace/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz", - "integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==", - "license": "Apache-2.0", - "dependencies": { - "deepmerge": "^2.0.0" - }, - "peerDependencies": { - "quill": "^1.3.4" + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" } }, - "node_modules/quill-delta": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", - "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "node_modules/replace/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { - "deep-equal": "^1.0.1", - "extend": "^3.0.2", - "fast-diff": "1.1.2" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, - "node_modules/quill-image-uploader": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/quill-image-uploader/-/quill-image-uploader-1.3.0.tgz", - "integrity": "sha512-vO43GEn93rGThje/MlotkQE9OV5nOKBZ4oKhn71L/EjrM/J2P/8iDDVd9GEwlsGsbskeJqPLopsSQ4HlVVIn6A==", + "node_modules/replace/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "license": "MIT", - "peerDependencies": { - "quill": "^1.3.7" + "engines": { + "node": ">=4" } }, - "node_modules/quill/node_modules/eventemitter3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "node_modules/replace/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "p-locate": "^4.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">=8" } }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "node_modules/replace/node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.10" + "node": "*" } }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "node_modules/replace/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "p-try": "^2.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/readable-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/replace/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "node_modules/replace/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "dependencies": { + "has-flag": "^3.0.0" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=4" } }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "node_modules/replace/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true, - "license": "Apache-2.0" + "license": "ISC" }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "node_modules/replace/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/replace/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, "node_modules/request-progress": { @@ -15600,6 +19260,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15607,6 +19274,28 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -15975,6 +19664,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -16062,6 +19758,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -16101,6 +19804,47 @@ "dev": true, "license": "ISC" }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16244,6 +19988,102 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -16410,6 +20250,28 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -16435,6 +20297,44 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/split2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -16497,6 +20397,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -16547,6 +20457,18 @@ "node": ">= 4.0.0" } }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -16602,7 +20524,17 @@ "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/strip-final-newline": { @@ -16615,6 +20547,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -16644,6 +20589,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/table-layout": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", @@ -16703,6 +20661,34 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -16713,6 +20699,91 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", + "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -16730,6 +20801,31 @@ "dev": true, "license": "MIT" }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -16836,6 +20932,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -16860,6 +20966,60 @@ "code-block-writer": "^12.0.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -17057,6 +21217,20 @@ "node": "*" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -17074,6 +21248,22 @@ "dev": true, "license": "MIT" }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -17183,6 +21373,35 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", @@ -17416,6 +21635,13 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17426,6 +21652,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wordwrapjs": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", @@ -17496,6 +21729,108 @@ } } }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xcode/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xml-js/node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17622,6 +21957,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index 1814addaa..4377c5e83 100644 --- a/client/package.json +++ b/client/package.json @@ -8,6 +8,9 @@ "start": "npm run tw:build && ng serve", "dev": "npm run tw:build && concurrently -k -n tw,ng \"npm run tw:watch\" \"ng serve\"", "build": "npm run tw:build && ng build", + "verify:webdir": "test -f dist/client/browser/index.html", + "cap:build": "npm run tw:build && ng build --configuration android", + "cap:sync": "npm run cap:build && npx cap sync android", "watch": "npm run tw:build && concurrently -k -n tw,ng \"npm run tw:watch\" \"ng build --watch --configuration development\"", "test": "npm run tw:build && ng test", "test:ci": "npm run tw:build && ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless", @@ -31,6 +34,12 @@ "@angular/platform-browser": "^21.1.3", "@angular/platform-browser-dynamic": "^21.1.3", "@angular/router": "^21.1.3", + "@capacitor/android": "^8.5.0", + "@capacitor/app": "^8.1.1", + "@capacitor/core": "^8.5.0", + "@capacitor/preferences": "^8.0.1", + "@capacitor/splash-screen": "^8.0.2", + "@capacitor/status-bar": "^8.0.3", "@jsverse/transloco": "^8.1.0", "@jsverse/transloco-messageformat": "^8.1.0", "@ngrx/effects": "^21.0.1", @@ -71,6 +80,8 @@ "@angular/build": "^21.1.3", "@angular/cli": "~21.2.0", "@angular/compiler-cli": "^21.1.3", + "@capacitor/assets": "^3.0.5", + "@capacitor/cli": "^8.5.0", "@jsverse/transloco-keys-manager": "^7.0.2", "@ngrx/store-devtools": "^21.0.1", "@sentry/cli": "^2.42.2", diff --git a/client/scripts/generate-placeholder-assets.mjs b/client/scripts/generate-placeholder-assets.mjs new file mode 100644 index 000000000..990dc85dc --- /dev/null +++ b/client/scripts/generate-placeholder-assets.mjs @@ -0,0 +1,78 @@ +// Generates placeholder launcher icon and light/dark splash source images from the +// existing LocalCrag logo (client/src/assets/lc_logo.svg), written into client/assets/. +// +// These are explicit placeholders (D-03) — final brand assets land before a Play +// release. Re-run this script any time the source logo changes: +// cd client && node scripts/generate-placeholder-assets.mjs +// +// Consumed by `npx @capacitor/assets generate --android`, which turns these five +// source images into the full Android density matrix (mipmap-*, drawable-*, +// drawable-night-*) under client/android/app/src/main/res. + +import { mkdir, readFile } from "node:fs/promises"; +import sharp from "sharp"; + +const LIGHT_BACKGROUND = "#ffffff"; +const DARK_BACKGROUND = "#18181b"; + +const logoSvg = await readFile("src/assets/lc_logo.svg"); +await mkdir("assets", { recursive: true }); + +async function compose({ out, canvas, background, logoSize }) { + const base = sharp({ + create: { width: canvas, height: canvas, channels: 4, background }, + }); + if (!logoSize) { + await base.png().toFile(`assets/${out}`); + return; + } + const logo = await sharp(logoSvg, { density: 512 }) + .resize(logoSize, logoSize, { + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .png() + .toBuffer(); + await base + .composite([{ input: logo, gravity: "centre" }]) + .png() + .toFile(`assets/${out}`); +} + +await compose({ + out: "icon-only.png", + canvas: 1024, + background: LIGHT_BACKGROUND, + logoSize: 640, +}); + +await compose({ + out: "icon-foreground.png", + canvas: 1024, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + logoSize: 560, +}); + +await compose({ + out: "icon-background.png", + canvas: 1024, + background: LIGHT_BACKGROUND, +}); + +await compose({ + out: "splash.png", + canvas: 2732, + background: LIGHT_BACKGROUND, + logoSize: 800, +}); + +await compose({ + out: "splash-dark.png", + canvas: 2732, + // Matches the Aura dark surface painted behind .site-header, so the handoff + // from splash to first paint has no color jump. + background: DARK_BACKGROUND, + logoSize: 800, +}); + +console.log("Generated placeholder assets in client/assets/"); diff --git a/client/src/app/models/file.ts b/client/src/app/models/file.ts index 3581af4e1..75fe57092 100644 --- a/client/src/app/models/file.ts +++ b/client/src/app/models/file.ts @@ -1,6 +1,7 @@ import { AbstractModel } from './abstract-model'; import { ThumbnailWidths } from '../enums/thumbnail-widths'; import { normalizeImageFocusY } from '../utility/image-focus'; +import { rewriteLoopbackMediaUrlForAndroid } from '../utility/rewrite-loopback-media-url'; /** * Model of a file object. @@ -30,11 +31,14 @@ export class File extends AbstractModel { public static deserialize(payload: any): File { const media = new File(); AbstractModel.deserializeAbstractAttributes(media, payload); - media.filename = payload.filename; + // On Android, rewrite loopback MinIO/S3 access hosts to the same machine as + // the runtime API host (emulator 10.0.2.2, LAN IP, or adb-reversed 127.0.0.1) + // so URLs the API serializes as http://127.0.0.1:9000/... are reachable. + media.filename = rewriteLoopbackMediaUrlForAndroid(payload.filename); media.originalFilename = payload.originalFilename; media.width = payload.width; media.height = payload.height; - media.path = payload.filename; + media.path = media.filename; media.thumbnailXS = payload.thumbnailXS ? media.path.replace(/.([^.]*)$/, '_xs.' + '$1') : null; diff --git a/client/src/app/modules/core/app.config.ts b/client/src/app/modules/core/app.config.ts index bc2377c32..678dd1000 100644 --- a/client/src/app/modules/core/app.config.ts +++ b/client/src/app/modules/core/app.config.ts @@ -29,6 +29,7 @@ import * as Sentry from '@sentry/angular'; import { providePrimeNG } from 'primeng/config'; import { LocalCragTheme } from './theme/theme'; import { ThemeService } from '../../services/core/theme.service'; +import { HardwareBackButtonService } from '../../services/core/hardware-back-button.service'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideTransloco, @@ -38,10 +39,13 @@ import { import { provideStore, Store } from '@ngrx/store'; import { InstanceSettingsService } from '../../services/crud/instance-settings.service'; import { MenuItemsService } from '../../services/crud/menu-items.service'; -import { concatMap } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { concatMap, from, of, timeout } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { Capacitor } from '@capacitor/core'; +import { hasCompletedInstanceOnboarding } from '../../services/core/instance-registry'; import { updateInstanceSettings } from '../../ngrx/actions/instance-settings.actions'; import { MessageService } from 'primeng/api'; +import { DialogService } from 'primeng/dynamicdialog'; import { metaReducers, reducers } from '../../ngrx/reducers'; import { provideStoreDevtools } from '@ngrx/store-devtools'; import { Actions, ofType, provideEffects } from '@ngrx/effects'; @@ -90,20 +94,52 @@ const initInstanceSettingsAndLanguage = ( store: Store, ) => { return () => { - return instanceSettingsService.getInstanceSettings().pipe( - map((instanceSettings) => { - store.dispatch(updateInstanceSettings({ settings: instanceSettings })); - initSentryFromSettings(instanceSettings); - return instanceSettings.language; + return from(hasCompletedInstanceOnboarding()).pipe( + switchMap((onboardingDone) => { + // Native onboarding: skip the API round-trip until an instance is saved. + // Otherwise a seeded emulator host (10.0.2.2) hangs CapacitorHttp on phones. + if (Capacitor.isNativePlatform() && !onboardingDone) { + return languageService.initApp(undefined); + } + return instanceSettingsService.getInstanceSettings().pipe( + timeout({ first: 8000 }), + map((instanceSettings) => { + store.dispatch( + updateInstanceSettings({ settings: instanceSettings }), + ); + initSentryFromSettings(instanceSettings); + return instanceSettings.language; + }), + concatMap(languageService.initApp.bind(languageService)), + // A failed API call here (offline, unreachable backend) must not block app bootstrap + // forever — the native shell still needs to render and hide its splash screen (D-07) + // even with no connectivity, falling back to the browser/default language. + catchError((err) => { + console.error('Failed to load instance settings on startup', err); + return languageService.initApp(undefined); + }), + ); }), - concatMap(languageService.initApp.bind(languageService)), ); }; }; const preloadMenus = (menuItemsService: MenuItemsService) => { return () => { - return menuItemsService.getMenuItems(); + return from(hasCompletedInstanceOnboarding()).pipe( + switchMap((onboardingDone) => { + if (Capacitor.isNativePlatform() && !onboardingDone) { + return of(null); + } + return menuItemsService.getMenuItems().pipe( + timeout({ first: 8000 }), + catchError((err) => { + console.error('Failed to preload menu items on startup', err); + return of(null); + }), + ); + }), + ); }; }; @@ -175,6 +211,10 @@ export const appConfig: ApplicationConfig = { inject(ThemeService).init(); return Promise.resolve(); }), + provideAppInitializer(() => { + inject(HardwareBackButtonService).register(); + return Promise.resolve(); + }), provideRouter( appRoutes, withNavigationErrorHandler((navigationError) => { @@ -223,6 +263,9 @@ export const appConfig: ApplicationConfig = { }, }), MessageService, + // Root DialogService so RockExplorerLiveSessionGuard can open Finish/Discard/Cancel + // from menu + picker even when local component DialogService trees are out of scope. + DialogService, provideAnimationsAsync(), provideTransloco({ config: { diff --git a/client/src/app/modules/core/core.component.html b/client/src/app/modules/core/core.component.html index 3b2ef0f45..905a04eb2 100644 --- a/client/src/app/modules/core/core.component.html +++ b/client/src/app/modules/core/core.component.html @@ -1,23 +1,31 @@ - -

-
-
- @if (!fullscreenMap) { + @if (!fullscreenMap && !hideAppChrome) { } -
- @if (!fullscreenMap) { +
+ @if (!fullscreenMap && !hideAppChrome) { } @@ -25,7 +33,7 @@
-@if (!fullscreenMap) { +@if (!fullscreenMap && !hideAppChrome) { } diff --git a/client/src/app/modules/core/core.component.ts b/client/src/app/modules/core/core.component.ts index 29e6f539a..316f7e55a 100644 --- a/client/src/app/modules/core/core.component.ts +++ b/client/src/app/modules/core/core.component.ts @@ -37,6 +37,8 @@ import { FooterComponent } from './footer/footer.component'; import { RefreshLoginModalComponent } from './refresh-login-modal/refresh-login-modal.component'; import { Toast } from 'primeng/toast'; import { Subscription } from 'rxjs'; +import { Capacitor } from '@capacitor/core'; +import { hasCompletedInstanceOnboarding } from '../../services/core/instance-registry'; /** * Application shell: fixed site header, main layout, and global chrome. @@ -77,6 +79,9 @@ export class CoreComponent implements OnInit, AfterViewInit, OnDestroy { /** Full-bleed map pages (e.g. Rock Explorer): header only, no chrome. */ fullscreenMap = false; + /** Instance picker: no site header/footer/sidebar (native gate / switch). */ + hideAppChrome = false; + private readonly hostEl = inject(ElementRef); private title = inject(Title); private router = inject(Router); @@ -163,21 +168,34 @@ export class CoreComponent implements OnInit, AfterViewInit, OnDestroy { this.store.dispatch(tryAutoLogin()); this.store.dispatch(checkShowCookieAlert()); this.store.dispatch(checkIsMobile()); - this.syncFullscreenMapFromRoute(); + this.syncShellFlagsFromRoute(); + void this.enforceNativeInstanceGate(); this.routerSub = this.router.events .pipe(filter((event) => event instanceof NavigationEnd)) - .subscribe(() => this.syncFullscreenMapFromRoute()); + .subscribe(() => { + this.syncShellFlagsFromRoute(); + void this.enforceNativeInstanceGate(); + }); } - ngAfterViewInit() { - const headerElement = this.siteHeader?.nativeElement; - if (!headerElement || typeof ResizeObserver === 'undefined') { + /** + * Phase 16 D-01/D-02: native apps without a saved instance stay on /instances. + */ + private async enforceNativeInstanceGate(): Promise { + if (!Capacitor.isNativePlatform()) { return; } + if (this.router.url.startsWith('/instances')) { + return; + } + if (await hasCompletedInstanceOnboarding()) { + return; + } + await this.router.navigateByUrl('/instances'); + } - // Re-measure when alerts or menu content change the header height. - this.resizeObserver = new ResizeObserver(() => this.updateHeaderHeight()); - this.resizeObserver.observe(headerElement); + ngAfterViewInit() { + this.observeSiteHeader(); } ngOnDestroy() { @@ -185,16 +203,37 @@ export class CoreComponent implements OnInit, AfterViewInit, OnDestroy { this.routerSub?.unsubscribe(); } - private syncFullscreenMapFromRoute() { + private syncShellFlagsFromRoute() { let route = this.activatedRoute; while (route.firstChild) { route = route.firstChild; } this.fullscreenMap = !!route.snapshot.data?.['fullscreenMap']; + this.hideAppChrome = !!route.snapshot.data?.['hideAppChrome']; this.hostEl.nativeElement.classList.toggle( 'lc-fullscreen-map', this.fullscreenMap, ); + this.hostEl.nativeElement.classList.toggle( + 'lc-hide-app-chrome', + this.hideAppChrome, + ); + // Header may appear/disappear when leaving/entering the instance picker. + queueMicrotask(() => { + this.observeSiteHeader(); + this.updateHeaderHeight(); + }); + } + + private observeSiteHeader() { + this.resizeObserver?.disconnect(); + this.resizeObserver = undefined; + const headerElement = this.siteHeader?.nativeElement; + if (!headerElement || typeof ResizeObserver === 'undefined') { + return; + } + this.resizeObserver = new ResizeObserver(() => this.updateHeaderHeight()); + this.resizeObserver.observe(headerElement); } /** @@ -202,7 +241,9 @@ export class CoreComponent implements OnInit, AfterViewInit, OnDestroy { * layout (e.g. full-page views that subtract menu height from viewport). */ private updateHeaderHeight() { - this.headerHeight = this.siteHeader?.nativeElement.offsetHeight ?? 0; + this.headerHeight = this.hideAppChrome + ? 0 + : (this.siteHeader?.nativeElement.offsetHeight ?? 0); const heightValue = `${this.headerHeight}px`; document.documentElement.style.setProperty('--lc-menu-height', heightValue); this.hostEl.nativeElement.style.setProperty( diff --git a/client/src/app/modules/core/instance-picker/instance-picker.component.html b/client/src/app/modules/core/instance-picker/instance-picker.component.html new file mode 100644 index 000000000..5df98b9ad --- /dev/null +++ b/client/src/app/modules/core/instance-picker/instance-picker.component.html @@ -0,0 +1,66 @@ + + +
+

{{ t("title") }}

+

{{ t("description") }}

+ + @if (!isNative) { + + } @else { +
+ + +
+ + @if (errorKey === "invalidUrl") { + + } + @if (errorKey === "notLocalCrag") { + + } + @if (errorKey === "unreachable") { + + } + +

{{ t("savedHeading") }}

+ @if (instances.length === 0) { +

{{ t("emptyList") }}

+ } @else { +
    + @for (instance of instances; track instance.url) { +
  • + + +
  • + } +
+ } + } +
+
diff --git a/client/src/app/modules/core/instance-picker/instance-picker.component.scss b/client/src/app/modules/core/instance-picker/instance-picker.component.scss new file mode 100644 index 000000000..00d84e365 --- /dev/null +++ b/client/src/app/modules/core/instance-picker/instance-picker.component.scss @@ -0,0 +1,56 @@ +.instance-picker { + max-width: 32rem; + margin: 0 auto; + padding: 2rem 1.5rem; + min-height: 100dvh; + box-sizing: border-box; +} + +.hint, +.empty { + opacity: 0.85; + margin-bottom: 1rem; +} + +.add-row { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.add-row input { + flex: 1; +} + +.instance-list { + list-style: none; + padding: 0; + margin: 0; +} + +.instance-list li { + display: flex; + align-items: center; + gap: 0.5rem; + border-bottom: 1px solid var(--p-content-border-color, #ddd); + padding: 0.5rem 0; +} + +.instance-list li.active .name { + font-weight: 700; +} + +.pick { + flex: 1; + text-align: left; + background: none; + border: 0; + cursor: pointer; + padding: 0.25rem 0; +} + +.pick .url { + display: block; + font-size: 0.85rem; + opacity: 0.75; +} diff --git a/client/src/app/modules/core/instance-picker/instance-picker.component.spec.ts b/client/src/app/modules/core/instance-picker/instance-picker.component.spec.ts new file mode 100644 index 000000000..7f2e1ee0b --- /dev/null +++ b/client/src/app/modules/core/instance-picker/instance-picker.component.spec.ts @@ -0,0 +1,96 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideTransloco } from '@jsverse/transloco'; +import { ConfirmationService } from 'primeng/api'; +import { DialogService } from 'primeng/dynamicdialog'; +import { InstancePickerComponent } from './instance-picker.component'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; +import { + LiveSessionEndHandlers, + RockExplorerLiveSessionGuard, +} from '../../../services/core/rock-explorer-live-session.guard'; + +describe('InstancePickerComponent', () => { + let fixture: ComponentFixture; + let component: InstancePickerComponent; + let liveGuard: RockExplorerLiveSessionGuard; + let confirmation: ConfirmationService; + + const activeHost = 'http://10.0.2.2:5000'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstancePickerComponent], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + RockExplorerLiveSessionGuard, + DialogService, + { provide: RUNTIME_API_HOST, useValue: activeHost }, + provideTransloco({ + config: { + availableLangs: ['en'], + defaultLang: 'en', + reRenderOnLangChange: true, + }, + }), + ], + }).compileComponents(); + fixture = TestBed.createComponent(InstancePickerComponent); + component = fixture.componentInstance; + liveGuard = TestBed.inject(RockExplorerLiveSessionGuard); + confirmation = fixture.debugElement.injector.get(ConfirmationService); + fixture.detectChanges(); + }); + + it('creates', () => { + expect(component).toBeTruthy(); + }); + + it('same-host switchTo leaves home without runGuardedAction or Finish/Discard (D-08)', () => { + const enterHome = spyOn( + component as unknown as { enterInstanceHome: () => void }, + 'enterInstanceHome', + ); + const runGuarded = spyOn(liveGuard, 'runGuardedAction').and.resolveTo(); + const confirmSpy = spyOn(confirmation, 'confirm'); + liveGuard.setLiveSession(true, { + finish: async () => undefined, + discard: async () => undefined, + } satisfies LiveSessionEndHandlers); + + component.switchTo({ + url: activeHost, + instanceName: 'Current', + }); + + expect(enterHome).toHaveBeenCalled(); + expect(runGuarded).not.toHaveBeenCalled(); + expect(confirmSpy).not.toHaveBeenCalled(); + }); + + it('different-host switchTo invokes runGuardedAction before setActiveHost when live (D-06)', () => { + const runGuarded = spyOn(liveGuard, 'runGuardedAction').and.callFake( + async (action) => { + await action(); + }, + ); + liveGuard.setLiveSession(true, { + finish: async () => { + liveGuard.setLiveSession(false); + }, + discard: async () => { + liveGuard.setLiveSession(false); + }, + }); + + component.switchTo({ + url: 'https://other.example', + instanceName: 'Other', + }); + + // RED until plan 02 wraps host-change in runGuardedAction before setActiveHost. + expect(runGuarded).toHaveBeenCalled(); + }); +}); diff --git a/client/src/app/modules/core/instance-picker/instance-picker.component.ts b/client/src/app/modules/core/instance-picker/instance-picker.component.ts new file mode 100644 index 000000000..5638aba32 --- /dev/null +++ b/client/src/app/modules/core/instance-picker/instance-picker.component.ts @@ -0,0 +1,143 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Capacitor } from '@capacitor/core'; +import { FormsModule } from '@angular/forms'; +import { TranslocoDirective, TranslocoPipe } from '@jsverse/transloco'; +import { Button } from 'primeng/button'; +import { InputText } from 'primeng/inputtext'; +import { MessageModule } from 'primeng/message'; +import { ConfirmationService } from 'primeng/api'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { firstValueFrom } from 'rxjs'; +import { + addInstance, + listInstances, + removeInstance, + setActiveHost, + type SavedInstance, +} from '../../../services/core/instance-registry'; +import { isLocalCragHealthResponse } from '../../../services/core/localcrag-health'; +import { + isAllowedApiHostUrl, + normalizeApiHostUrl, + RUNTIME_API_HOST, +} from '../../../services/core/runtime-api-host'; +import { RockExplorerLiveSessionGuard } from '../../../services/core/rock-explorer-live-session.guard'; + +@Component({ + selector: 'lc-instance-picker', + templateUrl: './instance-picker.component.html', + styleUrls: ['./instance-picker.component.scss'], + providers: [ConfirmationService], + imports: [ + FormsModule, + TranslocoDirective, + TranslocoPipe, + Button, + InputText, + MessageModule, + ConfirmDialogModule, + ], +}) +export class InstancePickerComponent implements OnInit { + public urlInput = ''; + public instances: SavedInstance[] = []; + public busy = false; + public errorKey: string | null = null; + public readonly isNative = Capacitor.isNativePlatform(); + public readonly activeHost = inject(RUNTIME_API_HOST); + + private http = inject(HttpClient); + private confirmation = inject(ConfirmationService); + private liveSessionGuard = inject(RockExplorerLiveSessionGuard); + + async ngOnInit(): Promise { + this.instances = await listInstances(); + } + + async validateAndSave(): Promise { + this.errorKey = null; + const url = normalizeApiHostUrl(this.urlInput); + if (!isAllowedApiHostUrl(url)) { + this.errorKey = 'invalidUrl'; + return; + } + this.busy = true; + try { + const health = await firstValueFrom( + this.http.get(`${url}/api/health`), + ); + if (!isLocalCragHealthResponse(health)) { + this.errorKey = 'notLocalCrag'; + return; + } + let instanceName: string | null = null; + try { + const settings = await firstValueFrom( + this.http.get<{ instanceName?: string }>( + `${url}/api/instance-settings`, + ), + ); + instanceName = settings?.instanceName ?? null; + } catch { + instanceName = null; + } + const wasEmpty = this.instances.length === 0; + const apiVersion = + health && + typeof health === 'object' && + typeof (health as { version?: unknown }).version === 'string' + ? ((health as { version: string }).version as string) + : null; + await addInstance({ + url, + instanceName, + apiVersion, + }); + this.instances = await listInstances(); + this.urlInput = ''; + if (wasEmpty) { + await setActiveHost(url); + this.enterInstanceHome(); + } + } catch { + this.errorKey = 'unreachable'; + } finally { + this.busy = false; + } + } + + async deleteInstance(instance: SavedInstance): Promise { + this.instances = await removeInstance(instance.url); + } + + async switchTo(instance: SavedInstance): Promise { + if ( + normalizeApiHostUrl(instance.url) === normalizeApiHostUrl(this.activeHost) + ) { + // Already on this host — leave the picker for instance home (D-08). + this.enterInstanceHome(); + return; + } + // Resolve live Record first (D-06), then existing Switch confirm + setActiveHost. + await this.liveSessionGuard.runGuardedAction(() => { + this.confirmation.confirm({ + message: instance.instanceName + ? `${instance.instanceName} (${instance.url})` + : instance.url, + header: 'Switch instance?', + acceptLabel: 'Switch', + rejectLabel: 'Cancel', + accept: async () => { + await setActiveHost(instance.url); + this.enterInstanceHome(); + }, + }); + }); + } + + /** Full navigation so bootstrap picks up the new host and leaves /instances. */ + private enterInstanceHome(): void { + window.location.assign('/'); + } +} diff --git a/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.html b/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.html new file mode 100644 index 000000000..39d2f8f41 --- /dev/null +++ b/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.html @@ -0,0 +1,21 @@ + +

{{ t("rockExplorer.liveSessionGuardMessage") }}

+ +
+ + + +
+
diff --git a/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.ts b/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.ts new file mode 100644 index 000000000..b8b2099b3 --- /dev/null +++ b/client/src/app/modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component.ts @@ -0,0 +1,32 @@ +import { Component, inject } from '@angular/core'; +import { TranslocoDirective } from '@jsverse/transloco'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { Button } from 'primeng/button'; + +export type LiveSessionResolveResult = 'finish' | 'discard' | 'cancel'; + +/** + * Finish / Discard / Cancel resolve UI for INST-07 (D-01). + * Closing without Finish/Discard (mask, Escape, Cancel) ≡ Cancel (D-02). + */ +@Component({ + selector: 'lc-live-session-resolve-dialog', + templateUrl: './live-session-resolve-dialog.component.html', + standalone: true, + imports: [TranslocoDirective, Button], +}) +export class LiveSessionResolveDialogComponent { + private ref = inject(DynamicDialogRef); + + finish(): void { + this.ref.close('finish' satisfies LiveSessionResolveResult); + } + + discard(): void { + this.ref.close('discard' satisfies LiveSessionResolveResult); + } + + cancel(): void { + this.ref.close('cancel' satisfies LiveSessionResolveResult); + } +} diff --git a/client/src/app/modules/core/login/login.component.scss b/client/src/app/modules/core/login/login.component.scss index e69de29bb..693e56f36 100644 --- a/client/src/app/modules/core/login/login.component.scss +++ b/client/src/app/modules/core/login/login.component.scss @@ -0,0 +1 @@ +/* Login styles reserved for future use. */ diff --git a/client/src/app/modules/core/menu/menu.component.html b/client/src/app/modules/core/menu/menu.component.html index 30c367ec5..33abc10db 100644 --- a/client/src/app/modules/core/menu/menu.component.html +++ b/client/src/app/modules/core/menu/menu.component.html @@ -72,15 +72,27 @@ /> } - + + @if (isNativePlatform) { + + } + +
diff --git a/client/src/app/modules/core/menu/menu.component.spec.ts b/client/src/app/modules/core/menu/menu.component.spec.ts new file mode 100644 index 000000000..c6be316e0 --- /dev/null +++ b/client/src/app/modules/core/menu/menu.component.spec.ts @@ -0,0 +1,151 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; +import { Actions } from '@ngrx/effects'; +import { of, Subject } from 'rxjs'; +import { DialogService } from 'primeng/dynamicdialog'; +import { TranslocoService } from '@jsverse/transloco'; +import { MenuComponent } from './menu.component'; +import { MenuItemsService } from '../../../services/crud/menu-items.service'; +import { LanguageService } from '../../../services/core/language.service'; +import { ThemeService } from '../../../services/core/theme.service'; +import { RockExplorerLiveSessionGuard } from '../../../services/core/rock-explorer-live-session.guard'; +import { logout } from '../../../ngrx/actions/auth.actions'; +import { + selectAuthState, + selectCurrentUser, +} from '../../../ngrx/selectors/auth.selectors'; +import { selectIsMobile } from '../../../ngrx/selectors/device.selectors'; +import { + selectDarkLogoImage, + selectGymMode, + selectInstanceName, + selectLogoImage, + selectSkippedHierarchyLayers, +} from '../../../ngrx/selectors/instance-settings.selectors'; + +/** + * Wave 0 RED: D-06 Switch instance + D-07 user-initiated logout must call + * RockExplorerLiveSessionGuard.runGuardedAction before navigate/dispatch. + * Plan 02 adds switchInstance() and wraps logout — keep these expect names. + */ +describe('MenuComponent live-session guard hooks (D-06, D-07)', () => { + let component: MenuComponent; + let liveGuard: jasmine.SpyObj; + let router: jasmine.SpyObj; + let store: MockStore; + + beforeEach(async () => { + liveGuard = jasmine.createSpyObj( + 'RockExplorerLiveSessionGuard', + ['runGuardedAction', 'setLiveSession', 'isLive'], + ); + liveGuard.runGuardedAction.and.callFake(async (action) => { + await action(); + }); + liveGuard.isLive.and.returnValue(false); + + router = jasmine.createSpyObj('Router', ['navigateByUrl']); + router.navigateByUrl.and.resolveTo(true); + + await TestBed.configureTestingModule({ + imports: [MenuComponent], + providers: [ + provideMockStore({ + selectors: [ + { selector: selectAuthState, value: { user: null } }, + { selector: selectCurrentUser, value: null }, + { selector: selectIsMobile, value: false }, + { selector: selectInstanceName, value: 'Test' }, + { selector: selectLogoImage, value: null }, + { selector: selectDarkLogoImage, value: null }, + { selector: selectSkippedHierarchyLayers, value: 0 }, + { selector: selectGymMode, value: false }, + ], + }), + { + provide: MenuItemsService, + useValue: { + getMenuItems: () => of([]), + getCragMenuStructure: () => of([]), + }, + }, + { + provide: TranslocoService, + useValue: { translate: (key: string) => key }, + }, + { + provide: DialogService, + useValue: jasmine.createSpyObj('DialogService', ['open']), + }, + { provide: Actions, useValue: new Subject() }, + { + provide: LanguageService, + useValue: { + calculatedLanguage: 'en', + renderedLanguage$: of('en'), + setPreferredLanguage: jasmine.createSpy('setPreferredLanguage'), + }, + }, + { + provide: ThemeService, + useValue: { + isDarkMode: signal(false), + toggleGuestColorScheme: jasmine.createSpy('toggleGuestColorScheme'), + }, + }, + { provide: RockExplorerLiveSessionGuard, useValue: liveGuard }, + { provide: Router, useValue: router }, + ], + }) + .overrideComponent(MenuComponent, { + set: { template: '', imports: [] }, + }) + .compileComponents(); + + store = TestBed.inject(MockStore); + spyOn(store, 'dispatch'); + const fixture = TestBed.createComponent(MenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('switchInstance invokes runGuardedAction before navigateByUrl("/instances") (D-06)', async () => { + const switchInstance = ( + component as unknown as { + switchInstance?: () => Promise; + } + ).switchInstance; + + expect(switchInstance).toEqual(jasmine.any(Function)); + if (typeof switchInstance !== 'function') { + return; + } + + await switchInstance.call(component); + + expect(liveGuard.runGuardedAction).toHaveBeenCalled(); + const call = liveGuard.runGuardedAction.calls.mostRecent(); + expect(call).toBeDefined(); + if (!call) { + return; + } + await (call.args[0] as () => void | Promise)(); + expect(router.navigateByUrl).toHaveBeenCalledWith('/instances'); + }); + + it('logout invokes runGuardedAction before store.dispatch(logout) when user-initiated (D-07)', async () => { + await Promise.resolve(component.logout()); + + expect(liveGuard.runGuardedAction).toHaveBeenCalled(); + if (!liveGuard.runGuardedAction.calls.any()) { + return; + } + const call = liveGuard.runGuardedAction.calls.mostRecent(); + await (call.args[0] as () => void | Promise)(); + expect(store.dispatch).toHaveBeenCalledWith( + logout({ isAutoLogout: false, silent: false }), + ); + }); +}); diff --git a/client/src/app/modules/core/menu/menu.component.ts b/client/src/app/modules/core/menu/menu.component.ts index 9715b1194..1704612e7 100644 --- a/client/src/app/modules/core/menu/menu.component.ts +++ b/client/src/app/modules/core/menu/menu.component.ts @@ -48,7 +48,7 @@ import { SearchDialogComponent } from '../search-dialog/search-dialog.component' import { environment } from '../../../../environments/environment'; import { HeaderMenuComponent } from '../../shared/components/header-menu/header-menu.component'; import { AsyncPipe } from '@angular/common'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { Menu } from 'primeng/menu'; import { UserAvatarComponent } from '../../shared/components/user-avatar/user-avatar.component'; import { HasPermissionDirective } from '../../shared/directives/has-permission.directive'; @@ -63,6 +63,8 @@ import { MAX_NAVBAR_COLLAPSE_LEVEL, NAVBAR_COLLAPSE_LEVELS, } from './navbar-collapse'; +import { Capacitor } from '@capacitor/core'; +import { RockExplorerLiveSessionGuard } from '../../../services/core/rock-explorer-live-session.guard'; @Component({ selector: 'lc-menu', @@ -116,7 +118,11 @@ export class MenuComponent implements OnInit, AfterViewInit { private languageService = inject(LanguageService); private destroyRef = inject(DestroyRef); private cdr = inject(ChangeDetectorRef); + private router = inject(Router); + private liveSessionGuard = inject(RockExplorerLiveSessionGuard); readonly themeService = inject(ThemeService); + /** Native-only: Switch instance entry for guests (logged-in uses account menu). */ + readonly isNativePlatform = Capacitor.isNativePlatform(); private readonly isDarkMode$ = toObservable(this.themeService.isDarkMode); private navbarResizeObserver?: ResizeObserver; private isReconcilingNavbarOverflow = false; @@ -340,13 +346,22 @@ export class MenuComponent implements OnInit, AfterViewInit { ), routerLink: '/change-password', }, - { - id: 'auth-menu-logout', - label: this.translocoService.translate(marker('menu.logout')), - icon: 'pi pi-fw pi-sign-out', - command: this.logout.bind(this), - }, ]; + if (Capacitor.isNativePlatform()) { + accountItems.push({ + icon: 'pi pi-fw pi-server', + label: this.translocoService.translate( + marker('menu.switchInstance'), + ), + command: () => void this.switchInstance(), + }); + } + accountItems.push({ + id: 'auth-menu-logout', + label: this.translocoService.translate(marker('menu.logout')), + icon: 'pi pi-fw pi-sign-out', + command: () => void this.logout(), + }); items.push({ label: this.translocoService.translate( marker('menu.accountCategory'), @@ -497,10 +512,23 @@ export class MenuComponent implements OnInit, AfterViewInit { } /** - * Logs out the user. + * Navigates to the instance picker after resolving any live Record session (D-06). + * Intercepts before router navigation so Rock Explorer destroy cannot race the guard. */ - logout() { - this.store.dispatch(logout({ isAutoLogout: false, silent: false })); + async switchInstance(): Promise { + await this.liveSessionGuard.runGuardedAction(async () => { + await this.router.navigateByUrl('/instances'); + }); + } + + /** + * Logs out the user after resolving any live Record session (D-07). + * User-initiated only — do not wrap AuthEffects auto-logout. + */ + async logout(): Promise { + await this.liveSessionGuard.runGuardedAction(() => { + this.store.dispatch(logout({ isAutoLogout: false, silent: false })); + }); } openSearch() { diff --git a/client/src/app/modules/core/routes/misc.routes.ts b/client/src/app/modules/core/routes/misc.routes.ts index 67f29064e..275a38e61 100644 --- a/client/src/app/modules/core/routes/misc.routes.ts +++ b/client/src/app/modules/core/routes/misc.routes.ts @@ -13,6 +13,14 @@ export const rootRedirectRoutes: Routes = [ ]; export const miscRoutes: Routes = [ + { + path: 'instances', + loadComponent: () => + import('../instance-picker/instance-picker.component').then( + (m) => m.InstancePickerComponent, + ), + data: { hideAppChrome: true }, + }, { path: 'sentry-test', loadComponent: () => diff --git a/client/src/app/modules/rock-explorer/native-gps/capacitor-app.token.ts b/client/src/app/modules/rock-explorer/native-gps/capacitor-app.token.ts new file mode 100644 index 000000000..016dfade1 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/capacitor-app.token.ts @@ -0,0 +1,11 @@ +import { InjectionToken } from '@angular/core'; +import { App, type AppPlugin } from '@capacitor/app'; + +/** + * Injectable Capacitor App seam so unit tests never touch the real plugin proxy + * (mirror EXIT_APP / GPS_BRIDGE). Facade registers appStateChange → flush in plan 03. + */ +export const CAPACITOR_APP = new InjectionToken('CAPACITOR_APP', { + providedIn: 'root', + factory: () => App, +}); diff --git a/client/src/app/modules/rock-explorer/native-gps/gps-bridge.ts b/client/src/app/modules/rock-explorer/native-gps/gps-bridge.ts new file mode 100644 index 000000000..71fff7022 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/gps-bridge.ts @@ -0,0 +1,57 @@ +import { InjectionToken } from '@angular/core'; +import { registerPlugin, type PluginListenerHandle } from '@capacitor/core'; + +/** Native fix payload emitted by the GpsBridge Capacitor plugin. */ +export interface GpsFixPayload { + latitude: number; + longitude: number; + accuracy?: number | null; + altitude?: number | null; + heading?: number | null; + speed?: number | null; + timestamp?: number; +} + +export type LocationPermissionState = + 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'; + +/** + * Thin Capacitor plugin wrapping Android Fused Location (Phase 17+) / + * location foreground service (Phase 18). Consumed by the navigator.geolocation shim. + */ +export interface GpsBridgePlugin { + start(options?: { intervalMs?: number }): Promise; + stop(): Promise; + getCurrentPosition(): Promise; + /** FG location alias — Phase 17; optional BG/notifications state for Phase 18 staging. */ + checkPermissions(): Promise<{ + location: LocationPermissionState; + background?: LocationPermissionState; + notifications?: LocationPermissionState; + }>; + requestPermissions(): Promise<{ location: LocationPermissionState }>; + /** Capacitor alias "background" → ACCESS_BACKGROUND_LOCATION (D-03, D-08). */ + requestBackgroundPermission(): Promise<{ + background: LocationPermissionState; + }>; + /** Capacitor alias "notifications" → POST_NOTIFICATIONS on API 33+ (D-08). */ + requestNotificationPermission(): Promise<{ + notifications: LocationPermissionState; + }>; + addListener( + eventName: 'locationUpdate', + listenerFunc: (fix: GpsFixPayload) => void, + ): Promise; +} + +/** Production default = Capacitor registerPlugin('GpsBridge'). */ +export const GpsBridge = registerPlugin('GpsBridge'); + +/** + * Injectable seam so unit tests never call the real Capacitor bridge + * (mirror EXIT_APP in hardware-back-button.service.ts). + */ +export const GPS_BRIDGE = new InjectionToken('GPS_BRIDGE', { + providedIn: 'root', + factory: () => GpsBridge, +}); diff --git a/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.spec.ts b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.spec.ts new file mode 100644 index 000000000..ca2ada3e1 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.spec.ts @@ -0,0 +1,217 @@ +import type { GpsBridgePlugin, LocationPermissionState } from './gps-bridge'; +import { ensureRockExplorerTrackingPermissions } from './rock-explorer-gps-permissions'; + +type PermSnapshot = { + location: LocationPermissionState; + background?: LocationPermissionState; + notifications?: LocationPermissionState; +}; + +function createFakeBridge( + initial: PermSnapshot, +): jasmine.SpyObj & { state: PermSnapshot } { + const bridge = jasmine.createSpyObj('GpsBridge', [ + 'start', + 'stop', + 'getCurrentPosition', + 'checkPermissions', + 'requestPermissions', + 'requestBackgroundPermission', + 'requestNotificationPermission', + 'addListener', + ]) as jasmine.SpyObj & { state: PermSnapshot }; + bridge.state = { ...initial }; + + bridge.checkPermissions.and.callFake(async () => ({ ...bridge.state })); + bridge.requestPermissions.and.callFake(async () => { + bridge.state.location = 'granted'; + return { location: bridge.state.location }; + }); + bridge.requestBackgroundPermission.and.callFake(async () => { + bridge.state.background = 'granted'; + return { background: bridge.state.background }; + }); + bridge.requestNotificationPermission.and.callFake(async () => { + bridge.state.notifications = 'granted'; + return { notifications: bridge.state.notifications }; + }); + bridge.start.and.resolveTo(); + bridge.stop.and.resolveTo(); + + return bridge; +} + +describe('ensureRockExplorerTrackingPermissions (Wave 0 RED / GPS-04)', () => { + let disclosure: jasmine.Spy<() => Promise>; + + beforeEach(() => { + disclosure = jasmine + .createSpy('showBackgroundDisclosure') + .and.resolveTo(true); + }); + + it('requests FG first when location is not granted, then disclosure before BG', async () => { + const bridge = createFakeBridge({ + location: 'prompt', + background: 'prompt', + notifications: 'prompt', + }); + const callOrder: string[] = []; + bridge.requestPermissions.and.callFake(async () => { + callOrder.push('requestPermissions'); + bridge.state.location = 'granted'; + return { location: 'granted' }; + }); + disclosure.and.callFake(async () => { + callOrder.push('disclosure'); + return true; + }); + bridge.requestBackgroundPermission.and.callFake(async () => { + callOrder.push('requestBackgroundPermission'); + bridge.state.background = 'granted'; + return { background: 'granted' }; + }); + bridge.requestNotificationPermission.and.callFake(async () => { + callOrder.push('requestNotificationPermission'); + bridge.state.notifications = 'granted'; + return { notifications: 'granted' }; + }); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => true, + }); + + expect(ok).toBeTrue(); + expect(bridge.requestPermissions).toHaveBeenCalled(); + expect(disclosure).toHaveBeenCalled(); + expect(bridge.requestBackgroundPermission).toHaveBeenCalled(); + expect(bridge.requestNotificationPermission).toHaveBeenCalled(); + expect(callOrder).toEqual([ + 'requestPermissions', + 'disclosure', + 'requestBackgroundPermission', + 'requestNotificationPermission', + ]); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('does not request BG when disclosure is rejected', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'prompt', + notifications: 'granted', + }); + disclosure.and.resolveTo(false); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => false, + }); + + expect(ok).toBeFalse(); + expect(disclosure).toHaveBeenCalled(); + expect(bridge.requestBackgroundPermission).not.toHaveBeenCalled(); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('returns false when BG permission is denied', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'prompt', + notifications: 'granted', + }); + bridge.requestBackgroundPermission.and.resolveTo({ + background: 'denied', + }); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => false, + }); + + expect(ok).toBeFalse(); + expect(bridge.requestBackgroundPermission).toHaveBeenCalled(); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('returns false when notifications are denied on API 33+ (hard stop)', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'granted', + notifications: 'prompt', + }); + bridge.requestNotificationPermission.and.resolveTo({ + notifications: 'denied', + }); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => true, + }); + + expect(ok).toBeFalse(); + expect(bridge.requestNotificationPermission).toHaveBeenCalled(); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('skips POST_NOTIFICATIONS when needsPostNotifications is false', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'granted', + notifications: 'prompt', + }); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => false, + }); + + expect(ok).toBeTrue(); + expect(bridge.requestNotificationPermission).not.toHaveBeenCalled(); + expect(disclosure).not.toHaveBeenCalled(); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('returns true without disclosure when FG+BG+notifications already granted', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'granted', + notifications: 'granted', + }); + + const ok = await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => true, + }); + + expect(ok).toBeTrue(); + expect(disclosure).not.toHaveBeenCalled(); + expect(bridge.requestPermissions).not.toHaveBeenCalled(); + expect(bridge.requestBackgroundPermission).not.toHaveBeenCalled(); + expect(bridge.requestNotificationPermission).not.toHaveBeenCalled(); + expect(bridge.start).not.toHaveBeenCalled(); + }); + + it('never calls bridge.start — orchestrator only gates permissions', async () => { + const bridge = createFakeBridge({ + location: 'granted', + background: 'granted', + notifications: 'granted', + }); + + await ensureRockExplorerTrackingPermissions({ + bridge, + showBackgroundDisclosure: disclosure, + needsPostNotifications: () => true, + }); + + expect(bridge.start).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.ts b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.ts new file mode 100644 index 000000000..f4c57f207 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-gps-permissions.ts @@ -0,0 +1,58 @@ +import type { GpsBridgePlugin } from './gps-bridge'; + +/** true = user accepted Play-compliant prominent background-location disclosure. */ +export type TrackingPermissionDisclosure = () => Promise; + +export type EnsureTrackingPermissionsDeps = { + bridge: GpsBridgePlugin; + showBackgroundDisclosure: TrackingPermissionDisclosure; + /** + * When false (API < 33), skip POST_NOTIFICATIONS step. + * Default (plan 03): detect via Capacitor platform + device info or injected flag. + */ + needsPostNotifications?: () => boolean; +}; + +/** + * Staged GPS-04 order (D-08): FG location → disclosure → BG → POST_NOTIFICATIONS (13+) → true. + * Any required deny / disclosure reject → false (caller runs onGeoPermissionDenied — D-09). + * POST_NOTIFICATIONS deny = hard stop. + * Never calls `bridge.start()` — FGS starts after the gate via shim watch (D-05). + */ +export async function ensureRockExplorerTrackingPermissions( + deps: EnsureTrackingPermissionsDeps, +): Promise { + const { bridge, showBackgroundDisclosure } = deps; + const needsPostNotifications = deps.needsPostNotifications ?? (() => false); + + let state = await bridge.checkPermissions(); + + if (state.location !== 'granted') { + const fg = await bridge.requestPermissions(); + if (fg.location !== 'granted') { + return false; + } + state = await bridge.checkPermissions(); + } + + if (state.background !== 'granted') { + const accepted = await showBackgroundDisclosure(); + if (!accepted) { + return false; + } + const bg = await bridge.requestBackgroundPermission(); + if (bg.background !== 'granted') { + return false; + } + state = await bridge.checkPermissions(); + } + + if (needsPostNotifications() && state.notifications !== 'granted') { + const notif = await bridge.requestNotificationPermission(); + if (notif.notifications !== 'granted') { + return false; + } + } + + return true; +} diff --git a/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.spec.ts b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.spec.ts new file mode 100644 index 000000000..243450ad6 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.spec.ts @@ -0,0 +1,187 @@ +import { Capacitor, type PluginListenerHandle } from '@capacitor/core'; +import type { GpsBridgePlugin, GpsFixPayload } from './gps-bridge'; +import { + installNativeGpsShim, + uninstallNativeGpsShim, +} from './rock-explorer-native-gps.shim'; + +describe('RockExplorerNativeGpsShim (Wave 0)', () => { + let isNativeSpy: jasmine.Spy; + let fakeBridge: jasmine.SpyObj; + let locationUpdateListener: ((fix: GpsFixPayload) => void) | null; + let listenerRemove: jasmine.Spy<() => Promise>; + let nativeGetCurrentPosition: jasmine.Spy; + let nativeWatchPosition: jasmine.Spy; + let nativeClearWatch: jasmine.Spy; + + beforeEach(() => { + isNativeSpy = spyOn(Capacitor, 'isNativePlatform'); + locationUpdateListener = null; + listenerRemove = jasmine + .createSpy('listenerRemove') + .and.resolveTo(undefined); + + fakeBridge = jasmine.createSpyObj('GpsBridge', [ + 'start', + 'stop', + 'getCurrentPosition', + 'checkPermissions', + 'requestPermissions', + 'requestBackgroundPermission', + 'requestNotificationPermission', + 'addListener', + ]); + fakeBridge.start.and.resolveTo(); + fakeBridge.stop.and.resolveTo(); + fakeBridge.checkPermissions.and.resolveTo({ location: 'granted' }); + fakeBridge.requestPermissions.and.resolveTo({ location: 'granted' }); + fakeBridge.requestBackgroundPermission.and.resolveTo({ + background: 'granted', + }); + fakeBridge.requestNotificationPermission.and.resolveTo({ + notifications: 'granted', + }); + fakeBridge.getCurrentPosition.and.resolveTo({ + latitude: 48.1, + longitude: 11.5, + accuracy: 5, + timestamp: Date.now(), + }); + fakeBridge.addListener.and.callFake( + async ( + eventName: 'locationUpdate', + listenerFunc: (fix: GpsFixPayload) => void, + ): Promise => { + if (eventName === 'locationUpdate') { + locationUpdateListener = listenerFunc; + } + return { remove: listenerRemove }; + }, + ); + + nativeGetCurrentPosition = jasmine.createSpy('getCurrentPosition'); + nativeWatchPosition = jasmine.createSpy('watchPosition').and.returnValue(1); + nativeClearWatch = jasmine.createSpy('clearWatch'); + + spyOnProperty(navigator, 'geolocation', 'get').and.returnValue({ + getCurrentPosition: nativeGetCurrentPosition, + watchPosition: nativeWatchPosition, + clearWatch: nativeClearWatch, + } as unknown as Geolocation); + }); + + afterEach(() => { + uninstallNativeGpsShim(); + }); + + it('installNativeGpsShim is a no-op when Capacitor.isNativePlatform() is false', () => { + isNativeSpy.and.returnValue(false); + const beforeGet = navigator.geolocation.getCurrentPosition; + const beforeWatch = navigator.geolocation.watchPosition; + const beforeClear = navigator.geolocation.clearWatch; + + installNativeGpsShim(fakeBridge); + + expect(navigator.geolocation.getCurrentPosition).toBe(beforeGet); + expect(navigator.geolocation.watchPosition).toBe(beforeWatch); + expect(navigator.geolocation.clearWatch).toBe(beforeClear); + expect(fakeBridge.addListener).not.toHaveBeenCalled(); + expect(fakeBridge.start).not.toHaveBeenCalled(); + }); + + it('on native, install patches getCurrentPosition, watchPosition, and clearWatch', () => { + isNativeSpy.and.returnValue(true); + const beforeGet = navigator.geolocation.getCurrentPosition; + const beforeWatch = navigator.geolocation.watchPosition; + const beforeClear = navigator.geolocation.clearWatch; + + installNativeGpsShim(fakeBridge); + + expect(navigator.geolocation.getCurrentPosition).not.toBe(beforeGet); + expect(navigator.geolocation.watchPosition).not.toBe(beforeWatch); + expect(navigator.geolocation.clearWatch).not.toBe(beforeClear); + }); + + it('maps finite locationUpdate payloads to GeolocationPosition for watchers', async () => { + isNativeSpy.and.returnValue(true); + installNativeGpsShim(fakeBridge); + + const positions: GeolocationPosition[] = []; + const watchId = navigator.geolocation.watchPosition((pos) => { + positions.push(pos); + }); + expect(watchId).toBeGreaterThan(0); + + // Allow async permission / listener / start to settle. + await Promise.resolve(); + await Promise.resolve(); + + expect(locationUpdateListener).not.toBeNull(); + locationUpdateListener?.({ + latitude: 48.137, + longitude: 11.575, + accuracy: 8, + altitude: 520, + heading: 90, + speed: 1.2, + timestamp: 1_700_000_000_000, + }); + + expect(positions.length).toBe(1); + expect(positions[0].coords.latitude).toBe(48.137); + expect(positions[0].coords.longitude).toBe(11.575); + expect(Number.isFinite(positions[0].coords.latitude)).toBeTrue(); + expect(Number.isFinite(positions[0].coords.longitude)).toBeTrue(); + }); + + it('drops locationUpdate payloads with non-finite lat or lng', async () => { + isNativeSpy.and.returnValue(true); + installNativeGpsShim(fakeBridge); + + const success = jasmine.createSpy('watchSuccess'); + navigator.geolocation.watchPosition(success); + + await Promise.resolve(); + await Promise.resolve(); + + expect(locationUpdateListener).not.toBeNull(); + locationUpdateListener?.({ + latitude: NaN, + longitude: 11.575, + timestamp: Date.now(), + }); + locationUpdateListener?.({ + latitude: 48.1, + longitude: Infinity, + timestamp: Date.now(), + }); + locationUpdateListener?.({ + latitude: -Infinity, + longitude: NaN, + timestamp: Date.now(), + }); + + expect(success).not.toHaveBeenCalled(); + }); + + it('calls plugin stop and removes listener when the last watcher is cleared', async () => { + isNativeSpy.and.returnValue(true); + installNativeGpsShim(fakeBridge); + + const watchId = navigator.geolocation.watchPosition(() => undefined); + + await Promise.resolve(); + await Promise.resolve(); + + expect(fakeBridge.start).toHaveBeenCalled(); + expect(fakeBridge.addListener).toHaveBeenCalled(); + + navigator.geolocation.clearWatch(watchId); + + await Promise.resolve(); + await Promise.resolve(); + + expect(fakeBridge.stop).toHaveBeenCalledTimes(1); + expect(listenerRemove).toHaveBeenCalledTimes(1); + }); +}); diff --git a/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.ts b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.ts new file mode 100644 index 000000000..9043a9bc8 --- /dev/null +++ b/client/src/app/modules/rock-explorer/native-gps/rock-explorer-native-gps.shim.ts @@ -0,0 +1,272 @@ +import { Capacitor, type PluginListenerHandle } from '@capacitor/core'; +import { + GpsBridge, + type GpsBridgePlugin, + type GpsFixPayload, +} from './gps-bridge'; + +type Watcher = { + success: PositionCallback; + error: PositionErrorCallback | null | undefined; +}; + +/** Module-scoped state so the shim survives Angular CD / child re-renders (D-09). */ +let shimInstalled = false; +let activeBridge: GpsBridgePlugin = GpsBridge; +let nextWatchId = 1; +const watchers = new Map(); +let locationListener: PluginListenerHandle | null = null; +let nativeStartInFlight: Promise | null = null; + +let nativeGetCurrentPosition: Geolocation['getCurrentPosition'] | null = null; +let nativeWatchPosition: Geolocation['watchPosition'] | null = null; +let nativeClearWatch: Geolocation['clearWatch'] | null = null; + +/** + * Patch `navigator.geolocation` to fan out GpsBridge `locationUpdate` events. + * No-op on web (`!Capacitor.isNativePlatform()`). Does not request permissions + * or start the plugin — that happens on first watch / get (and Record facade hook). + * + * @param bridge Optional test double; defaults to registered `GpsBridge`. + */ +export function installNativeGpsShim(bridge?: GpsBridgePlugin): void { + if (!Capacitor.isNativePlatform() || shimInstalled) { + return; + } + if (typeof navigator === 'undefined' || !navigator.geolocation) { + return; + } + + activeBridge = bridge ?? GpsBridge; + const geo = navigator.geolocation; + nativeGetCurrentPosition = geo.getCurrentPosition.bind(geo); + nativeWatchPosition = geo.watchPosition.bind(geo); + nativeClearWatch = geo.clearWatch.bind(geo); + + geo.getCurrentPosition = ( + success: PositionCallback, + error?: PositionErrorCallback | null, + _options?: PositionOptions, + ) => { + void shimGetCurrentPosition(success, error); + }; + geo.watchPosition = ( + success: PositionCallback, + error?: PositionErrorCallback | null, + _options?: PositionOptions, + ) => shimWatchPosition(success, error); + geo.clearWatch = (watchId: number) => { + void shimClearWatch(watchId); + }; + + shimInstalled = true; +} + +/** Restore real `navigator.geolocation` and force-stop the native plugin. */ +export function uninstallNativeGpsShim(): void { + if (!shimInstalled) { + return; + } + + if ( + typeof navigator !== 'undefined' && + navigator.geolocation && + nativeGetCurrentPosition && + nativeWatchPosition && + nativeClearWatch + ) { + navigator.geolocation.getCurrentPosition = nativeGetCurrentPosition; + navigator.geolocation.watchPosition = nativeWatchPosition; + navigator.geolocation.clearWatch = nativeClearWatch; + } + + nativeGetCurrentPosition = null; + nativeWatchPosition = null; + nativeClearWatch = null; + watchers.clear(); + nextWatchId = 1; + shimInstalled = false; + + void forceStopNative(); +} + +/** True while `navigator.geolocation` methods are patched. */ +export function isNativeGpsShimInstalled(): boolean { + return shimInstalled; +} + +async function shimGetCurrentPosition( + success: PositionCallback, + error?: PositionErrorCallback | null, +): Promise { + try { + const fix = await activeBridge.getCurrentPosition(); + const pos = toGeolocationPosition(fix); + if (!pos) { + error?.(makePositionError(2, 'Invalid location fix')); + return; + } + success(pos); + } catch (err) { + error?.(mapBridgeError(err)); + } +} + +function shimWatchPosition( + success: PositionCallback, + error?: PositionErrorCallback | null, +): number { + const id = nextWatchId++; + watchers.set(id, { success, error }); + void ensureNativeStarted().catch((err) => { + if (!watchers.has(id)) { + return; + } + watchers.delete(id); + error?.(mapBridgeError(err)); + void maybeStopNative(); + }); + return id; +} + +async function shimClearWatch(watchId: number): Promise { + watchers.delete(watchId); + await maybeStopNative(); +} + +async function ensureNativeStarted(): Promise { + if (watchers.size === 0) { + return; + } + if (nativeStartInFlight) { + await nativeStartInFlight; + return; + } + nativeStartInFlight = (async () => { + if (!locationListener) { + locationListener = await activeBridge.addListener( + 'locationUpdate', + (fix) => { + const pos = toGeolocationPosition(fix); + if (!pos) { + return; + } + for (const watcher of watchers.values()) { + watcher.success(pos); + } + }, + ); + } + await activeBridge.start({ intervalMs: 1000 }); + })(); + try { + await nativeStartInFlight; + } finally { + nativeStartInFlight = null; + } +} + +async function maybeStopNative(): Promise { + if (watchers.size > 0) { + return; + } + await forceStopNative(); +} + +async function forceStopNative(): Promise { + try { + await activeBridge.stop(); + } catch { + // Best-effort stop on uninstall / last clearWatch. + } + if (locationListener) { + try { + await locationListener.remove(); + } catch { + // Ignore remove failures during tear-down. + } + locationListener = null; + } +} + +/** + * Map plugin payload → GeolocationPosition. Returns null when lat/lng are + * non-finite so callers can drop the update (T-17-03). + */ +function toGeolocationPosition(fix: GpsFixPayload): GeolocationPosition | null { + const lat = fix.latitude; + const lng = fix.longitude; + if (!Number.isFinite(lat) || !Number.isFinite(lng)) { + return null; + } + + const accuracy = + fix.accuracy != null && Number.isFinite(fix.accuracy) ? fix.accuracy : null; + const altitude = + fix.altitude != null && Number.isFinite(fix.altitude) ? fix.altitude : null; + const heading = + fix.heading != null && Number.isFinite(fix.heading) ? fix.heading : null; + const speed = + fix.speed != null && Number.isFinite(fix.speed) ? fix.speed : null; + const timestamp = + fix.timestamp != null && Number.isFinite(fix.timestamp) + ? fix.timestamp + : Date.now(); + + const coords: GeolocationCoordinates = { + latitude: lat, + longitude: lng, + accuracy: accuracy ?? 0, + altitude, + altitudeAccuracy: null, + heading, + speed, + toJSON() { + return { + latitude: this.latitude, + longitude: this.longitude, + accuracy: this.accuracy, + altitude: this.altitude, + altitudeAccuracy: this.altitudeAccuracy, + heading: this.heading, + speed: this.speed, + }; + }, + }; + + return { + coords, + timestamp, + toJSON() { + return { coords: coords.toJSON(), timestamp: this.timestamp }; + }, + }; +} + +function mapBridgeError(err: unknown): GeolocationPositionError { + const message = + err && typeof err === 'object' && 'message' in err + ? String((err as { message: unknown }).message) + : String(err ?? 'Location error'); + const codeRaw = + err && typeof err === 'object' && 'code' in err + ? String((err as { code: unknown }).code) + : ''; + if (codeRaw.includes('PERMISSION') || /permission/i.test(message)) { + return makePositionError(1, message || 'Permission denied'); + } + return makePositionError(2, message || 'Position unavailable'); +} + +function makePositionError( + code: number, + message: string, +): GeolocationPositionError { + return { + code, + message, + PERMISSION_DENIED: 1, + POSITION_UNAVAILABLE: 2, + TIMEOUT: 3, + }; +} diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-reconcile.service.spec.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-reconcile.service.spec.ts index 508dda924..98f2b93bb 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-reconcile.service.spec.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-reconcile.service.spec.ts @@ -3,13 +3,17 @@ import { of } from 'rxjs'; import { RockExplorerFeature } from '../../../models/rock-explorer-feature'; import { RockExplorerService } from '../../../services/crud/rock-explorer.service'; import { RockExplorerRecordingSession } from '../rock-explorer-recording'; -import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { + rockExplorerDraftDb, + ROCK_EXPLORER_TEST_HOST, +} from './rock-explorer-draft.db'; import { RockExplorerDraftReconcileService } from './rock-explorer-draft-reconcile.service'; import { RockExplorerDraftStoreService, deleteRockExplorerDraftDb, } from './rock-explorer-draft-store.service'; import { RockExplorerDraftSyncService } from './rock-explorer-draft-sync.service'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; function remoteDraft(opts: { id: string; @@ -47,6 +51,7 @@ describe('RockExplorerDraftReconcileService', () => { RockExplorerDraftStoreService, RockExplorerDraftSyncService, RockExplorerDraftReconcileService, + { provide: RUNTIME_API_HOST, useValue: ROCK_EXPLORER_TEST_HOST }, { provide: RockExplorerService, useValue: { listDrafts }, diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.spec.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.spec.ts index f7e7bce05..d2e3b7e7a 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.spec.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.spec.ts @@ -4,7 +4,12 @@ import { RockExplorerDraftStoreService, deleteRockExplorerDraftDb, } from './rock-explorer-draft-store.service'; -import { ROCK_EXPLORER_DB_NAME } from './rock-explorer-draft.db'; +import { + LEGACY_ROCK_EXPLORER_DB_NAME, + ROCK_EXPLORER_TEST_HOST, + rockExplorerDbName, +} from './rock-explorer-draft.db'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; describe('RockExplorerDraftStoreService', () => { let store: RockExplorerDraftStoreService; @@ -12,7 +17,10 @@ describe('RockExplorerDraftStoreService', () => { beforeEach(async () => { await deleteRockExplorerDraftDb(); TestBed.configureTestingModule({ - providers: [RockExplorerDraftStoreService], + providers: [ + RockExplorerDraftStoreService, + { provide: RUNTIME_API_HOST, useValue: ROCK_EXPLORER_TEST_HOST }, + ], }); store = TestBed.inject(RockExplorerDraftStoreService); await store.probeOpen(); @@ -112,7 +120,9 @@ describe('RockExplorerDraftStoreService', () => { expect(found?.updatedAt).toBe(updatedAt); }); - it('uses DB name localcrag.rockExplorer', () => { - expect(ROCK_EXPLORER_DB_NAME).toBe('localcrag.rockExplorer'); + it('uses host-scoped DB name derived from RUNTIME_API_HOST', () => { + expect(rockExplorerDbName(ROCK_EXPLORER_TEST_HOST)).toBe( + `${LEGACY_ROCK_EXPLORER_DB_NAME}:${ROCK_EXPLORER_TEST_HOST}`, + ); }); }); diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.ts index 3c4e843c1..971180105 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-store.service.ts @@ -1,6 +1,11 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import Dexie from 'dexie'; -import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { + migrateLegacyRockExplorerDbIfNeeded, + openRockExplorerDraftDb, + ROCK_EXPLORER_TEST_HOST, +} from './rock-explorer-draft.db'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; import type { DraftSyncStatus, RockExplorerDraftRecord, @@ -22,19 +27,22 @@ export type PutSnapshotMeta = { /** * IndexedDB CRUD for Rock Explorer offline drafts. - * providedIn root so Dexie singleton is shared app-wide. + * providedIn root; DB is scoped to RUNTIME_API_HOST (INST-05). */ @Injectable({ providedIn: 'root', }) export class RockExplorerDraftStoreService { - private readonly db = rockExplorerDraftDb; + private readonly db = openRockExplorerDraftDb(inject(RUNTIME_API_HOST)); + private readonly apiHost = inject(RUNTIME_API_HOST); + private legacyMigrated = false; /** * Probe IndexedDB open; false on quota / open failures. */ async probeOpen(): Promise { try { + await this.ensureLegacyMigrated(); await this.db.open(); return true; } catch (error) { @@ -45,11 +53,20 @@ export class RockExplorerDraftStoreService { } } + private async ensureLegacyMigrated(): Promise { + if (this.legacyMigrated) { + return; + } + await migrateLegacyRockExplorerDbIfNeeded(this.apiHost); + this.legacyMigrated = true; + } + async putSnapshot( localId: string, session: RockExplorerRecordingSession, meta: PutSnapshotMeta = {}, ): Promise { + await this.ensureLegacyMigrated(); const existing = await this.db.drafts.get(localId); const snapshot = session.toSnapshot(); const deviceId = @@ -162,7 +179,10 @@ export class RockExplorerDraftStoreService { } } -/** Delete the Rock Explorer draft DB (tests / recovery). Next access reopens. */ -export async function deleteRockExplorerDraftDb(): Promise { - await rockExplorerDraftDb.delete(); +/** Delete the Rock Explorer draft DB for a host (tests / recovery). */ +export async function deleteRockExplorerDraftDb( + apiHost: string = ROCK_EXPLORER_TEST_HOST, +): Promise { + const db = openRockExplorerDraftDb(apiHost); + await db.delete(); } diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.spec.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.spec.ts index e97d4a200..5493d8cee 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.spec.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.spec.ts @@ -5,11 +5,13 @@ import { RockExplorerFeature } from '../../../models/rock-explorer-feature'; import { RockExplorerService } from '../../../services/crud/rock-explorer.service'; import { RockExplorerRecordingSession } from '../rock-explorer-recording'; import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { ROCK_EXPLORER_TEST_HOST } from './rock-explorer-draft.db'; import { RockExplorerDraftStoreService, deleteRockExplorerDraftDb, } from './rock-explorer-draft-store.service'; import { RockExplorerDraftSyncService } from './rock-explorer-draft-sync.service'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; describe('RockExplorerDraftSyncService', () => { let store: RockExplorerDraftStoreService; @@ -26,6 +28,7 @@ describe('RockExplorerDraftSyncService', () => { providers: [ RockExplorerDraftStoreService, RockExplorerDraftSyncService, + { provide: RUNTIME_API_HOST, useValue: ROCK_EXPLORER_TEST_HOST }, { provide: RockExplorerService, useValue: { createFeature, updateFeature }, diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.ts index 3338abb0e..f79294ac8 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft-sync.service.ts @@ -3,10 +3,11 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Subject, firstValueFrom } from 'rxjs'; import { RockExplorerService } from '../../../services/crud/rock-explorer.service'; import { RockExplorerRecordingSession } from '../rock-explorer-recording'; -import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { openRockExplorerDraftDb } from './rock-explorer-draft.db'; import { RockExplorerDraftStoreService } from './rock-explorer-draft-store.service'; import { RockExplorerPendingImageService } from './rock-explorer-pending-image.service'; import type { RockExplorerOpRecord } from './rock-explorer-draft.types'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; export type DeviceLockConflictEvent = { localId: string; @@ -34,7 +35,7 @@ export class RockExplorerDraftSyncService { private readonly store = inject(RockExplorerDraftStoreService); private readonly api = inject(RockExplorerService); private readonly pendingImages = inject(RockExplorerPendingImageService); - private readonly db = rockExplorerDraftDb; + private readonly db = openRockExplorerDraftDb(inject(RUNTIME_API_HOST)); private flushInFlight = false; diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.spec.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.spec.ts new file mode 100644 index 000000000..e090ae979 --- /dev/null +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.spec.ts @@ -0,0 +1,25 @@ +import { + LEGACY_ROCK_EXPLORER_DB_NAME, + openRockExplorerDraftDb, + rockExplorerDbName, +} from './rock-explorer-draft.db'; + +describe('rockExplorerDbName', () => { + it('namespaces by normalized host', () => { + expect(rockExplorerDbName('https://a.example/')).toBe( + `${LEGACY_ROCK_EXPLORER_DB_NAME}:https://a.example`, + ); + expect(rockExplorerDbName('https://a.example')).not.toBe( + rockExplorerDbName('https://b.example'), + ); + }); +}); + +describe('openRockExplorerDraftDb', () => { + it('returns distinct DB instances for distinct hosts', () => { + const a = openRockExplorerDraftDb('https://a.example'); + const b = openRockExplorerDraftDb('https://b.example'); + expect(a.name).not.toBe(b.name); + expect(openRockExplorerDraftDb('https://a.example/')).toBe(a); + }); +}); diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.ts index 7be292cf0..ba460e876 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-draft.db.ts @@ -1,11 +1,16 @@ import Dexie, { type EntityTable } from 'dexie'; +import { normalizeApiHostUrl } from '../../../services/core/runtime-api-host'; import type { RockExplorerDraftRecord, RockExplorerOpRecord, RockExplorerPendingImageRecord, } from './rock-explorer-draft.types'; -export const ROCK_EXPLORER_DB_NAME = 'localcrag.rockExplorer'; +/** Pre-Phase-16 unscoped DB name. */ +export const LEGACY_ROCK_EXPLORER_DB_NAME = 'localcrag.rockExplorer'; + +/** Stable host used by unit tests that touch IndexedDB directly. */ +export const ROCK_EXPLORER_TEST_HOST = 'http://test.localcrag'; export type RockExplorerDraftDb = Dexie & { drafts: EntityTable; @@ -13,18 +18,91 @@ export type RockExplorerDraftDb = Dexie & { pendingImages: EntityTable; }; -/** Singleton Dexie DB for Rock Explorer offline drafts + outbox. */ -export const rockExplorerDraftDb = new Dexie( - ROCK_EXPLORER_DB_NAME, -) as RockExplorerDraftDb; - -rockExplorerDraftDb.version(1).stores({ - drafts: 'localId, serverId, updatedAt, syncStatus, deviceId', - ops: '++id, localId, createdAt', -}); - -rockExplorerDraftDb.version(2).stores({ - drafts: 'localId, serverId, updatedAt, syncStatus, deviceId', - ops: '++id, localId, createdAt', - pendingImages: 'id, localId, createdAt', -}); +const dbCache = new Map(); + +export function rockExplorerDbName(apiHost: string): string { + return `${LEGACY_ROCK_EXPLORER_DB_NAME}:${normalizeApiHostUrl(apiHost)}`; +} + +function createRockExplorerDraftDb(name: string): RockExplorerDraftDb { + const db = new Dexie(name) as RockExplorerDraftDb; + db.version(1).stores({ + drafts: 'localId, serverId, updatedAt, syncStatus, deviceId', + ops: '++id, localId, createdAt', + }); + db.version(2).stores({ + drafts: 'localId, serverId, updatedAt, syncStatus, deviceId', + ops: '++id, localId, createdAt', + pendingImages: 'id, localId, createdAt', + }); + return db; +} + +/** + * Open (or reuse) the Dexie DB for an API host (INST-05 / D-09). + */ +export function openRockExplorerDraftDb(apiHost: string): RockExplorerDraftDb { + const name = rockExplorerDbName(apiHost); + let db = dbCache.get(name); + if (!db) { + db = createRockExplorerDraftDb(name); + dbCache.set(name, db); + } + return db; +} + +/** + * Copy legacy unscoped DB into the active host DB once, then delete legacy. + */ +export async function migrateLegacyRockExplorerDbIfNeeded( + apiHost: string, +): Promise { + const target = openRockExplorerDraftDb(apiHost); + await target.open(); + const targetCount = + (await target.drafts.count()) + + (await target.ops.count()) + + (await target.pendingImages.count()); + if (targetCount > 0) { + return; + } + + const legacyExists = await Dexie.exists(LEGACY_ROCK_EXPLORER_DB_NAME); + if (!legacyExists) { + return; + } + + const legacy = createRockExplorerDraftDb(LEGACY_ROCK_EXPLORER_DB_NAME); + await legacy.open(); + const legacyCount = + (await legacy.drafts.count()) + + (await legacy.ops.count()) + + (await legacy.pendingImages.count()); + if (legacyCount === 0) { + legacy.close(); + await Dexie.delete(LEGACY_ROCK_EXPLORER_DB_NAME); + return; + } + + await target.transaction( + 'rw', + target.drafts, + target.ops, + target.pendingImages, + async () => { + await target.drafts.bulkPut(await legacy.drafts.toArray()); + await target.ops.bulkPut(await legacy.ops.toArray()); + await target.pendingImages.bulkPut(await legacy.pendingImages.toArray()); + }, + ); + legacy.close(); + await Dexie.delete(LEGACY_ROCK_EXPLORER_DB_NAME); +} + +/** + * Back-compat export for specs that import the module singleton. + * Production services must call openRockExplorerDraftDb(RUNTIME_API_HOST). + */ +export const rockExplorerDraftDb = openRockExplorerDraftDb( + ROCK_EXPLORER_TEST_HOST, +); diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.spec.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.spec.ts index c286240f5..8fe2fd843 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.spec.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.spec.ts @@ -7,7 +7,11 @@ import { } from './rock-explorer-draft-store.service'; import { RockExplorerPendingImageService } from './rock-explorer-pending-image.service'; import { RockExplorerRecordingSession } from '../rock-explorer-recording'; -import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { + rockExplorerDraftDb, + ROCK_EXPLORER_TEST_HOST, +} from './rock-explorer-draft.db'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; describe('RockExplorerPendingImageService', () => { let pending: RockExplorerPendingImageService; @@ -21,6 +25,7 @@ describe('RockExplorerPendingImageService', () => { provideHttpClientTesting(), RockExplorerPendingImageService, RockExplorerDraftStoreService, + { provide: RUNTIME_API_HOST, useValue: ROCK_EXPLORER_TEST_HOST }, ], }); pending = TestBed.inject(RockExplorerPendingImageService); diff --git a/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.ts b/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.ts index 64912f83c..deff56802 100644 --- a/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.ts +++ b/client/src/app/modules/rock-explorer/offline/rock-explorer-pending-image.service.ts @@ -6,8 +6,9 @@ import { GalleryImage } from '../../../models/gallery-image'; import { RockExplorerFeature } from '../../../models/rock-explorer-feature'; import { Tag } from '../../../models/tag'; import { ObjectType } from '../../../models/object'; -import { rockExplorerDraftDb } from './rock-explorer-draft.db'; +import { openRockExplorerDraftDb } from './rock-explorer-draft.db'; import type { RockExplorerPendingImageRecord } from './rock-explorer-draft.types'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; /** * Offline geotagged image queue for Record mode. @@ -17,7 +18,7 @@ import type { RockExplorerPendingImageRecord } from './rock-explorer-draft.types providedIn: 'root', }) export class RockExplorerPendingImageService { - private readonly db = rockExplorerDraftDb; + private readonly db = openRockExplorerDraftDb(inject(RUNTIME_API_HOST)); private readonly uploadService = inject(UploadService); private readonly galleryService = inject(GalleryService); diff --git a/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.spec.ts b/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.spec.ts new file mode 100644 index 000000000..1ef7bfcfb --- /dev/null +++ b/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.spec.ts @@ -0,0 +1,249 @@ +import { ChangeDetectorRef, DestroyRef, NgZone } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Capacitor, type PluginListenerHandle } from '@capacitor/core'; +import type { AppPlugin, AppState } from '@capacitor/app'; +import { provideMockStore } from '@ngrx/store/testing'; +import { Subject } from 'rxjs'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { TranslocoService } from '@jsverse/transloco'; +import { selectShowOfflineAlert } from '../../ngrx/selectors/app-level-alerts.selectors'; +import { GalleryService } from '../../services/crud/gallery.service'; +import { RockExplorerService } from '../../services/crud/rock-explorer.service'; +import { UploadService } from '../../services/crud/upload.service'; +import { CAPACITOR_APP } from './native-gps/capacitor-app.token'; +import { + RockExplorerRecordingFacade, + type RockExplorerRecordingFacadeHost, +} from './rock-explorer-recording.facade'; +import { RockExplorerRecordingSession } from './rock-explorer-recording'; +import { RockExplorerUiService } from './rock-explorer-ui.service'; +import { RockExplorerDraftReconcileService } from './offline/rock-explorer-draft-reconcile.service'; +import { RockExplorerDraftStoreService } from './offline/rock-explorer-draft-store.service'; +import { + RockExplorerDraftSyncService, + type DeviceLockConflictEvent, +} from './offline/rock-explorer-draft-sync.service'; +import { RockExplorerPendingImageService } from './offline/rock-explorer-pending-image.service'; +import type { RockExplorerImageLocations } from './map/rock-explorer-image-locations'; + +describe('RockExplorerRecordingFacade (Wave 0 / GPS-05 + GPS-F01)', () => { + let facade: RockExplorerRecordingFacade; + let ui: RockExplorerUiService; + let capApp: jasmine.SpyObj; + let appStateListener: ((state: AppState) => void) | null; + let listenerRemove: jasmine.Spy<() => Promise>; + let draftStore: jasmine.SpyObj; + let isNativeSpy: jasmine.Spy; + + function buildHost(): RockExplorerRecordingFacadeHost { + return { + map: undefined, + layers: undefined, + ui, + images: {} as RockExplorerImageLocations, + cdr: jasmine.createSpyObj('ChangeDetectorRef', [ + 'detectChanges', + ]), + destroyRef: TestBed.inject(DestroyRef), + ngZone: TestBed.inject(NgZone), + messageService: jasmine.createSpyObj('MessageService', [ + 'add', + ]), + confirmationService: jasmine.createSpyObj( + 'ConfirmationService', + ['confirm'], + ), + reloadFeatures: jasmine.createSpy('reloadFeatures'), + applyFeatureToPanel: jasmine.createSpy('applyFeatureToPanel'), + openEditPanel: jasmine.createSpy('openEditPanel'), + closePanel: jasmine.createSpy('closePanel'), + cancelPathDraw: jasmine.createSpy('cancelPathDraw'), + setRockExplorerDrawMode: jasmine.createSpy('setRockExplorerDrawMode'), + fitMapToPositions: jasmine.createSpy('fitMapToPositions'), + triggerGeolocate: jasmine.createSpy('triggerGeolocate'), + getRecordImageInputElement: () => undefined, + }; + } + + beforeEach(() => { + appStateListener = null; + listenerRemove = jasmine + .createSpy('listenerRemove') + .and.resolveTo(undefined); + + capApp = jasmine.createSpyObj('CapacitorApp', [ + 'addListener', + 'exitApp', + 'getInfo', + 'getState', + 'getLaunchUrl', + 'minimizeApp', + 'getAppLanguage', + 'toggleBackButtonHandler', + 'removeAllListeners', + ]); + capApp.addListener.and.callFake(async (eventName, listenerFunc) => { + if (eventName === 'appStateChange') { + appStateListener = listenerFunc as (state: AppState) => void; + } + return { remove: listenerRemove } as PluginListenerHandle; + }); + + draftStore = jasmine.createSpyObj( + 'RockExplorerDraftStoreService', + ['probeOpen'], + ); + // Avoid init → probe → flush noise; Cap App / visibility tests drive flush. + draftStore.probeOpen.and.resolveTo(false); + + const draftSync = { + deviceLockConflict$: new Subject(), + } as unknown as RockExplorerDraftSyncService; + + TestBed.configureTestingModule({ + providers: [ + RockExplorerUiService, + provideMockStore({ + selectors: [{ selector: selectShowOfflineAlert, value: false }], + }), + { provide: CAPACITOR_APP, useValue: capApp }, + { provide: RockExplorerDraftStoreService, useValue: draftStore }, + { provide: RockExplorerDraftSyncService, useValue: draftSync }, + { + provide: RockExplorerDraftReconcileService, + useValue: {}, + }, + { + provide: RockExplorerPendingImageService, + useValue: {}, + }, + { provide: RockExplorerService, useValue: {} }, + { provide: GalleryService, useValue: {} }, + { provide: UploadService, useValue: {} }, + { + provide: TranslocoService, + useValue: { translate: (k: string) => k }, + }, + ], + }); + + ui = TestBed.inject(RockExplorerUiService); + isNativeSpy = spyOn(Capacitor, 'isNativePlatform').and.returnValue(true); + + facade = TestBed.runInInjectionContext( + () => new RockExplorerRecordingFacade(buildHost()), + ); + }); + + afterEach(() => { + facade.destroy(); + }); + + describe('GPS-F01 nativeGpsTrackingActive', () => { + it('defaults to false on RockExplorerUiService', () => { + expect(ui.nativeGpsTrackingActive()).toBeFalse(); + }); + + it('clears nativeGpsTrackingActive on destroy when tracking was active (plan 03)', () => { + ui.nativeGpsTrackingActive.set(true); + facade.destroy(); + expect(ui.nativeGpsTrackingActive()).toBeFalse(); + }); + + it('pauseRecording does not clear nativeGpsTrackingActive (D-07)', () => { + const session = new RockExplorerRecordingSession('test-device'); + ( + facade as unknown as { + recordingSession: RockExplorerRecordingSession | null; + } + ).recordingSession = session; + ui.recordModeActive.set(true); + ui.nativeGpsTrackingActive.set(true); + spyOn( + facade as unknown as { + persistAndSync: (force: boolean) => Promise; + }, + 'persistAndSync', + ).and.resolveTo(); + + facade.pauseRecording(); + + expect(session.recordingState).toBe('paused'); + expect(ui.nativeGpsTrackingActive()).toBeTrue(); + }); + }); + + describe('GPS-05 Cap App resume flush', () => { + it('registers appStateChange on native init and flushes when isActive', async () => { + const flushSpy = spyOn(facade, 'flushDraftQueue').and.resolveTo(); + + facade.init(); + await Promise.resolve(); + await Promise.resolve(); + + expect(capApp.addListener).toHaveBeenCalled(); + const lastCall = capApp.addListener.calls.mostRecent(); + expect(lastCall).toBeDefined(); + if (!lastCall) { + return; + } + const [eventName] = lastCall.args as unknown as [string]; + expect(eventName).toBe('appStateChange'); + expect(appStateListener).not.toBeNull(); + if (!appStateListener) { + return; + } + + flushSpy.calls.reset(); + appStateListener({ isActive: true }); + expect(flushSpy).toHaveBeenCalled(); + + flushSpy.calls.reset(); + appStateListener({ isActive: false }); + expect(flushSpy).not.toHaveBeenCalled(); + }); + + it('does not register Cap App listener when not native', async () => { + isNativeSpy.and.returnValue(false); + facade.init(); + await Promise.resolve(); + expect(capApp.addListener).not.toHaveBeenCalled(); + }); + + it('removes Cap App listener on destroy', async () => { + facade.init(); + await Promise.resolve(); + await Promise.resolve(); + expect(appStateListener).not.toBeNull(); + + facade.destroy(); + expect(listenerRemove).toHaveBeenCalled(); + }); + + it('still flushes on visibilitychange when document becomes visible', async () => { + const flushSpy = spyOn(facade, 'flushDraftQueue').and.resolveTo(); + facade.init(); + await Promise.resolve(); + flushSpy.calls.reset(); + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: () => 'visible', + }); + document.dispatchEvent(new Event('visibilitychange')); + + expect(flushSpy).toHaveBeenCalled(); + }); + }); + + it('does not introduce continuous/background HTTP sync helpers', () => { + // Contract guard for D-15 / T-18-04 — Wave 0 must not invent BG upload APIs. + expect( + (facade as unknown as { syncInBackground?: unknown }).syncInBackground, + ).toBeUndefined(); + expect( + (facade as unknown as { startBackgroundHttpSync?: unknown }) + .startBackgroundHttpSync, + ).toBeUndefined(); + }); +}); diff --git a/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.ts b/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.ts index 1e46bf22c..51cc4e9da 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.ts +++ b/client/src/app/modules/rock-explorer/rock-explorer-recording.facade.ts @@ -36,7 +36,12 @@ import { } from './rock-explorer-recording'; import type { RockExplorerMockGpsService } from './rock-explorer-mock-gps.service'; import { loadMockGps } from './rock-explorer-mock-gps.loader'; +import { Capacitor, type PluginListenerHandle } from '@capacitor/core'; import { mockGpsRecording } from '../../../environments/environment'; +import { uninstallNativeGpsShim } from './native-gps/rock-explorer-native-gps.shim'; +import { CAPACITOR_APP } from './native-gps/capacitor-app.token'; +import { GpsBridge } from './native-gps/gps-bridge'; +import { ensureRockExplorerTrackingPermissions } from './native-gps/rock-explorer-gps-permissions'; import type { RockExplorerDraftRecord } from './offline/rock-explorer-draft.types'; import { RockExplorerDraftStoreService } from './offline/rock-explorer-draft-store.service'; import { @@ -45,6 +50,7 @@ import { } from './offline/rock-explorer-draft-sync.service'; import { RockExplorerDraftReconcileService } from './offline/rock-explorer-draft-reconcile.service'; import { RockExplorerPendingImageService } from './offline/rock-explorer-pending-image.service'; +import { RockExplorerLiveSessionGuard } from '../../services/core/rock-explorer-live-session.guard'; /** * Component behaviors the facade calls back into. Kept intentionally small — @@ -113,12 +119,22 @@ export class RockExplorerRecordingFacade { private readonly galleryService = inject(GalleryService); private readonly uploadService = inject(UploadService); private readonly transloco = inject(TranslocoService); + private readonly liveSessionGuard = inject(RockExplorerLiveSessionGuard); private readonly store = inject(Store); + private readonly capacitorApp = inject(CAPACITOR_APP); - /** Lazily loaded only when `mockGpsRecording` (stripped from prod). */ + /** Cap App `appStateChange` handle — removed in {@link destroy} (D-15). */ + private appStateHandle: PluginListenerHandle | null = null; + + /** Lazily loaded only when mock GPS is allowed (web/dev; never on native). */ private mockGps: RockExplorerMockGpsService | null = null; private mockGpsLoad: Promise | null = null; + /** Web/dev mock walker only — gated off Capacitor native (D-05). */ + private isMockGpsEnabled(): boolean { + return mockGpsRecording && !Capacitor.isNativePlatform(); + } + /** Live-tracking session (survives exit Record until component destroy). */ private recordingSession: RockExplorerRecordingSession | null = null; /** IndexedDB local draft id for the active recording session. */ @@ -201,6 +217,17 @@ export class RockExplorerRecordingFacade { window.addEventListener('online', this.onWindowOnline); window.addEventListener('offline', this.onWindowOffline); document.addEventListener('visibilitychange', this.onVisibilityChange); + if (Capacitor.isNativePlatform()) { + void this.capacitorApp + .addListener('appStateChange', ({ isActive }) => { + if (isActive) { + void this.flushDraftQueue(); + } + }) + .then((handle) => { + this.appStateHandle = handle; + }); + } this.store .select(selectShowOfflineAlert) .pipe(takeUntilDestroyed(this.host.destroyRef)) @@ -216,9 +243,17 @@ export class RockExplorerRecordingFacade { window.removeEventListener('online', this.onWindowOnline); window.removeEventListener('offline', this.onWindowOffline); document.removeEventListener('visibilitychange', this.onVisibilityChange); + const appStateHandle = this.appStateHandle; + this.appStateHandle = null; + if (appStateHandle) { + void appStateHandle.remove(); + } this.releaseSyncUiHold(); this.stopGeoWatch(); + this.host.ui.nativeGpsTrackingActive.set(false); + this.liveSessionGuard.setLiveSession(false); this.uninstallMockGpsShim(); + uninstallNativeGpsShim(); } // --------------------------------------------------------------------- @@ -243,13 +278,13 @@ export class RockExplorerRecordingFacade { if (!this.recordingSession || !this.host.ui.recordModeActive()) { return; } - if (mockGpsRecording) { + if (this.isMockGpsEnabled()) { this.seedMockGpsNearActiveSession(); } this.recordingSession.resume(); this.syncRecordUiSignals(); this.zoomCloseOnNextFix = true; - this.startGeoWatch(); + void this.startGeoWatch(); this.host.cdr.detectChanges(); } @@ -282,7 +317,7 @@ export class RockExplorerRecordingFacade { this.recordingSession.newPath(); this.syncRecordUiSignals(); this.refreshRecordingPathsOnMap(); - this.startGeoWatch(); + void this.startGeoWatch(); void this.persistAndSync(true); this.host.cdr.detectChanges(); } @@ -311,6 +346,7 @@ export class RockExplorerRecordingFacade { ); } this.host.ui.recordModeActive.set(false); + this.liveSessionGuard.setLiveSession(false); this.syncRecordUiSignals(); this.host.layers?.setPaths(emptyFeatureCollection()); this.host.layers?.clearDraft(); @@ -413,13 +449,13 @@ export class RockExplorerRecordingFacade { this.host.ui.syncStatus.set('pending'); // Await reset before watchPosition — a fire-and-forget reset raced the // shim seed and could emit the default [0,0] (Gulf of Guinea) first. - if (mockGpsRecording) { + if (this.isMockGpsEnabled()) { const mock = await this.ensureMockGps(); await mock?.resetSeed(); } } else if (options.resume) { this.recordingSession.resume(); - if (mockGpsRecording) { + if (this.isMockGpsEnabled()) { this.seedMockGpsNearActiveSession(); } } else { @@ -428,11 +464,15 @@ export class RockExplorerRecordingFacade { this.host.ui.recordModeActive.set(true); this.host.ui.hasRecordingSession.set(true); + this.liveSessionGuard.setLiveSession(true, { + finish: () => this.exitRecordModeAsync(), + discard: () => this.discardActiveLiveSession(), + }); this.syncRecordUiSignals(); this.refreshRecordingPathsOnMap(); if (options.resume && this.recordingSession.isRecording) { this.zoomCloseOnNextFix = true; - this.startGeoWatch(); + void this.startGeoWatch(); try { this.host.triggerGeolocate(); } catch { @@ -490,7 +530,7 @@ export class RockExplorerRecordingFacade { this.host.ui.syncStatus.set(draft.syncStatus); this.host.ui.hasRecordingSession.set(true); - if (mockGpsRecording) { + if (this.isMockGpsEnabled()) { this.seedMockGpsNearDraft(session, draft); } @@ -587,7 +627,7 @@ export class RockExplorerRecordingFacade { * `facade.ensureMockGps().then((mock) => mock?.installNavigatorShim(fallback))`. */ ensureMockGps(): Promise { - if (!mockGpsRecording) { + if (!this.isMockGpsEnabled()) { return Promise.resolve(null); } if (this.mockGps) { @@ -856,6 +896,7 @@ export class RockExplorerRecordingFacade { if (this.activeLocalId === localId) { this.stopGeoWatch(); this.host.ui.recordModeActive.set(false); + this.liveSessionGuard.setLiveSession(false); this.recordingSession = null; this.activeLocalId = null; this.host.ui.activeLocalDraftId.set(null); @@ -1008,7 +1049,7 @@ export class RockExplorerRecordingFacade { if (!localId) { return; } - if (!mockGpsRecording && !navigator.geolocation) { + if (!this.isMockGpsEnabled() && !navigator.geolocation) { this.host.messageService.add({ severity: 'error', summary: this.transloco.translate( @@ -1138,6 +1179,23 @@ export class RockExplorerRecordingFacade { }); } + /** + * Discard the active live draft without a nested confirm (D-04). + * Used by RockExplorerLiveSessionGuard after the user picks Discard. + */ + async discardActiveLiveSession(): Promise { + if (this.activeLocalId) { + await this.deleteDraft(this.activeLocalId); + return; + } + this.stopGeoWatch(); + this.host.ui.recordModeActive.set(false); + this.host.ui.hasRecordingSession.set(false); + this.syncRecordUiSignals(); + this.liveSessionGuard.setLiveSession(false); + this.host.cdr.detectChanges(); + } + private async deleteDraft(localId: string): Promise { const draft = await this.draftStore.get(localId); const wasActive = this.activeLocalId === localId; @@ -1156,6 +1214,7 @@ export class RockExplorerRecordingFacade { this.host.ui.hasRecordingSession.set(false); this.syncRecordUiSignals(); this.host.layers?.setPaths(emptyFeatureCollection()); + this.liveSessionGuard.setLiveSession(false); } if (draft?.serverId && this.draftSync.isOnline()) { @@ -1377,7 +1436,12 @@ export class RockExplorerRecordingFacade { this.host.ui.activeLocalDraftId.set(this.activeLocalId); } - private startGeoWatch(): void { + /** + * Start the geo watch for Record. On native, run staged GPS-04 permissions + * (FG → disclosure → BG → POST_NOTIFICATIONS) before watch/FGS start (D-08). + * Call sites may fire-and-forget with `void this.startGeoWatch()`. + */ + private async startGeoWatch(): Promise { if (this.geoWatchId != null) { return; } @@ -1385,7 +1449,33 @@ export class RockExplorerRecordingFacade { this.onGeoPermissionDenied(); return; } - if (mockGpsRecording && !this.mockGps?.isSeeded) { + if (Capacitor.isNativePlatform()) { + try { + const ok = await ensureRockExplorerTrackingPermissions({ + bridge: GpsBridge, + showBackgroundDisclosure: () => + this.showBackgroundLocationDisclosure(), + needsPostNotifications: () => Capacitor.getPlatform() === 'android', + }); + if (!ok) { + this.onGeoPermissionDenied(); + return; + } + // Pitfall 2: user backgrounded mid-staging — do not start FGS silently. + if (document.visibilityState === 'hidden') { + this.onGeoPermissionDenied(); + return; + } + } catch { + this.onGeoPermissionDenied(); + return; + } + // Bail if a concurrent call already started the watch while we awaited. + if (this.geoWatchId != null) { + return; + } + } + if (this.isMockGpsEnabled() && !this.mockGps?.isSeeded) { this.host.messageService.add({ severity: 'info', summary: 'Mock GPS', @@ -1403,6 +1493,44 @@ export class RockExplorerRecordingFacade { }, { enableHighAccuracy: true, maximumAge: 1000, timeout: 20000 }, ); + if (Capacitor.isNativePlatform()) { + this.host.ui.nativeGpsTrackingActive.set(true); + } + } + + /** + * Play-prominent ConfirmDialog before ACCESS_BACKGROUND_LOCATION (D-04). + * Resolves true on accept, false on reject / dismiss. + */ + private showBackgroundLocationDisclosure(): Promise { + return new Promise((resolve) => { + let settled = false; + const settle = (value: boolean) => { + if (settled) { + return; + } + settled = true; + resolve(value); + }; + this.host.confirmationService.confirm({ + message: this.transloco.translate( + marker('rockExplorer.gpsBackgroundDisclosureMessage'), + ), + header: this.transloco.translate( + marker('rockExplorer.gpsBackgroundDisclosureHeader'), + ), + acceptLabel: this.transloco.translate( + marker('rockExplorer.gpsBackgroundDisclosureAccept'), + ), + rejectLabel: this.transloco.translate( + marker('rockExplorer.gpsBackgroundDisclosureReject'), + ), + icon: 'pi pi-map-marker', + rejectButtonStyleClass: 'p-button-secondary', + accept: () => settle(true), + reject: () => settle(false), + }); + }); } private stopGeoWatch(): void { @@ -1411,6 +1539,7 @@ export class RockExplorerRecordingFacade { } navigator.geolocation.clearWatch(this.geoWatchId); this.geoWatchId = null; + this.host.ui.nativeGpsTrackingActive.set(false); } /** One-shot fix for geotagged images (mocked via navigator shim when enabled). */ diff --git a/client/src/app/modules/rock-explorer/rock-explorer-sessions/rock-explorer-sessions.component.ts b/client/src/app/modules/rock-explorer/rock-explorer-sessions/rock-explorer-sessions.component.ts index 6e97c88f0..043615054 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer-sessions/rock-explorer-sessions.component.ts +++ b/client/src/app/modules/rock-explorer/rock-explorer-sessions/rock-explorer-sessions.component.ts @@ -7,8 +7,9 @@ import { Tag } from 'primeng/tag'; import { TranslocoDirective } from '@jsverse/transloco'; import { TimeAgoPipe } from '../../shared/pipes/time-ago.pipe'; import { RockExplorerUiService } from '../rock-explorer-ui.service'; -import { rockExplorerDraftDb } from '../offline/rock-explorer-draft.db'; +import { openRockExplorerDraftDb } from '../offline/rock-explorer-draft.db'; import type { RockExplorerDraftRecord } from '../offline/rock-explorer-draft.types'; +import { RUNTIME_API_HOST } from '../../../services/core/runtime-api-host'; @Component({ selector: 'lc-rock-explorer-sessions', @@ -18,12 +19,11 @@ import type { RockExplorerDraftRecord } from '../offline/rock-explorer-draft.typ }) export class RockExplorerSessionsComponent { readonly ui = inject(RockExplorerUiService); + private readonly db = openRockExplorerDraftDb(inject(RUNTIME_API_HOST)); readonly drafts = toSignal( from( - liveQuery(() => - rockExplorerDraftDb.drafts.orderBy('updatedAt').reverse().toArray(), - ), + liveQuery(() => this.db.drafts.orderBy('updatedAt').reverse().toArray()), ), { initialValue: [] as RockExplorerDraftRecord[] }, ); diff --git a/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.html b/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.html index e6cf205fd..55b30da1c 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.html +++ b/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.html @@ -31,6 +31,16 @@ } @if (ui.recordModeActive()) {
+ @if (ui.nativeGpsTrackingActive()) { +
+ + {{ t("rockExplorer.gpsTrackingActive") }} +
+ }
{{ t("rockExplorer.recordGroupPath") diff --git a/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.scss b/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.scss index 769621a57..8760bd38d 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.scss +++ b/client/src/app/modules/rock-explorer/rock-explorer-toolbar/rock-explorer-toolbar.component.scss @@ -27,6 +27,27 @@ max-width: min(100%, 28rem); } +.rock-explorer__gps-tracking-active { + display: inline-flex; + align-items: center; + gap: 0.35rem; + align-self: flex-start; + padding: 0.2rem 0.5rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--p-green-700, #15803d); + background: color-mix(in srgb, var(--p-green-500, #22c55e) 14%, transparent); + border: 1px solid + color-mix(in srgb, var(--p-green-500, #22c55e) 35%, transparent); + border-radius: var(--p-content-border-radius); + line-height: 1.2; + + .pi { + font-size: 0.8rem; + } +} + .rock-explorer__record-group { display: flex; flex-direction: column; diff --git a/client/src/app/modules/rock-explorer/rock-explorer-ui.service.ts b/client/src/app/modules/rock-explorer/rock-explorer-ui.service.ts index 728cfc790..545adb10d 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer-ui.service.ts +++ b/client/src/app/modules/rock-explorer/rock-explorer-ui.service.ts @@ -105,6 +105,11 @@ export class RockExplorerUiService { /** True while Record mode chrome is active (exclusive vs point/polygon). */ readonly recordModeActive = signal(false); + /** + * True while native GpsBridge / FGS tracking is live (GPS-F01). + * Wired true/false by the recording facade in plan 03. + */ + readonly nativeGpsTrackingActive = signal(false); /** Mirrors session recordingState for toolbar Pause/Resume. */ readonly recordingState = signal(null); /** Active open GPS path vertex count (for Finish path enablement). */ diff --git a/client/src/app/modules/rock-explorer/rock-explorer/rock-explorer.component.html b/client/src/app/modules/rock-explorer/rock-explorer/rock-explorer.component.html index 7f66c754b..04edea708 100644 --- a/client/src/app/modules/rock-explorer/rock-explorer/rock-explorer.component.html +++ b/client/src/app/modules/rock-explorer/rock-explorer/rock-explorer.component.html @@ -1,6 +1,7 @@
+ { mock?.installNavigatorShim(() => { const c = this.map?.getCenter(); diff --git a/client/src/app/modules/shared/pipes/sanitize-html.pipe.ts b/client/src/app/modules/shared/pipes/sanitize-html.pipe.ts index f94718b5d..070323d1b 100644 --- a/client/src/app/modules/shared/pipes/sanitize-html.pipe.ts +++ b/client/src/app/modules/shared/pipes/sanitize-html.pipe.ts @@ -1,8 +1,11 @@ import { Pipe, PipeTransform, inject } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { rewriteLoopbackMediaUrlForAndroid } from '../../../utility/rewrite-loopback-media-url'; /** * Sanitizes HTML for use in innerHTML attributes. + * On native Android, also rewrites loopback MinIO/S3 hosts inside the HTML so + * embedded URLs from the editor reach the emulator host (10.0.2.2). */ @Pipe({ name: 'sanitizeHtml', @@ -11,6 +14,7 @@ export class SanitizeHtmlPipe implements PipeTransform { private _sanitizer = inject(DomSanitizer); transform(value: string): SafeHtml { - return this._sanitizer.bypassSecurityTrustHtml(value); + const rewritten = rewriteLoopbackMediaUrlForAndroid(value) ?? ''; + return this._sanitizer.bypassSecurityTrustHtml(rewritten); } } diff --git a/client/src/app/ngrx/effects/auth.effects.ts b/client/src/app/ngrx/effects/auth.effects.ts index edd2f1144..af8ecd704 100644 --- a/client/src/app/ngrx/effects/auth.effects.ts +++ b/client/src/app/ngrx/effects/auth.effects.ts @@ -38,6 +38,12 @@ import { unixToDate } from '../../utility/operators/unix-to-date'; import { toastNotification } from '../actions/notifications.actions'; import { LoginResponse } from '../../models/login-response'; import { differenceInMilliseconds, isAfter, subMilliseconds } from 'date-fns'; +import { + clearAuthStorage, + readAuthStorage, + writeAuthStorage, +} from '../../services/core/auth-storage'; +import { RUNTIME_API_HOST } from '../../services/core/runtime-api-host'; /** * Time before expiry before an access token gets refreshed. Accounts for an approximate server response delay of the refresh request @@ -59,6 +65,7 @@ export class AuthEffects { private actions$ = inject(Actions); private router = inject(Router); private store = inject>(Store); + private apiHost = inject(RUNTIME_API_HOST); /** * Calls the password forgotten route to send a reset password mail and notifies the app about success or failure. @@ -202,10 +209,7 @@ export class AuthEffects { user: action.user, message: '', }; - localStorage.setItem( - 'LocalCragAuth', - JSON.stringify(autoLoginObject), - ); + writeAuthStorage(this.apiHost, JSON.stringify(autoLoginObject)); }), ), { dispatch: false }, @@ -233,10 +237,7 @@ export class AuthEffects { user: authState.user, message: '', }; - localStorage.setItem( - 'LocalCragAuth', - JSON.stringify(autoLoginObject), - ); + writeAuthStorage(this.apiHost, JSON.stringify(autoLoginObject)); // Start a timer for refreshing the access token if (action.loginResponse.accessToken !== null) { this.store.dispatch(startAccessTokenRefreshTimer()); @@ -386,10 +387,9 @@ export class AuthEffects { this.actions$.pipe( ofType(tryAutoLogin), tap(() => { - if (localStorage.getItem('LocalCragAuth') !== null) { - const autoLoginObject: LoginResponse = JSON.parse( - localStorage.getItem('LocalCragAuth') as string, - ); + const stored = readAuthStorage(this.apiHost); + if (stored !== null) { + const autoLoginObject: LoginResponse = JSON.parse(stored); this.store.dispatch( newAuthCredentials({ loginResponse: autoLoginObject, @@ -487,7 +487,7 @@ export class AuthEffects { this.actions$.pipe( ofType(AuthActions.cleanupCredentials), tap((action) => { - localStorage.removeItem('LocalCragAuth'); + clearAuthStorage(this.apiHost); if (action.navigateToLogin) { this.router.navigate(['login']); } diff --git a/client/src/app/services/core/api.service.spec.ts b/client/src/app/services/core/api.service.spec.ts new file mode 100644 index 000000000..ccce81d07 --- /dev/null +++ b/client/src/app/services/core/api.service.spec.ts @@ -0,0 +1,27 @@ +import { TestBed } from '@angular/core/testing'; +import { ApiService } from './api.service'; +import { RUNTIME_API_HOST } from './runtime-api-host'; + +describe('ApiService', () => { + let api: ApiService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: RUNTIME_API_HOST, useValue: 'https://runtime.test' }, + ], + }); + api = TestBed.inject(ApiService); + }); + + it('builds auth URLs from RUNTIME_API_HOST', () => { + expect(api.auth.login()).toBe('https://runtime.test/api/login'); + expect(api.auth.loginRefresh()).toBe( + 'https://runtime.test/api/token/refresh', + ); + }); + + it('builds upload URL from RUNTIME_API_HOST', () => { + expect(api.uploader.uploadFile()).toBe('https://runtime.test/api/upload'); + }); +}); diff --git a/client/src/app/services/core/api.service.ts b/client/src/app/services/core/api.service.ts index 686dd6b50..3886c25de 100644 --- a/client/src/app/services/core/api.service.ts +++ b/client/src/app/services/core/api.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; -import { environment } from '../../../environments/environment'; +import { Injectable, inject } from '@angular/core'; import { LineType } from '../../enums/line-type'; +import { RUNTIME_API_HOST } from './runtime-api-host'; /** * Simple container service holding api route definitions. @@ -316,5 +316,5 @@ export class ApiService { `${this.apiHost}rock-explorer/features/${id}/clone`, }; - private apiHost = `${environment.apiHost}/api/`; + private apiHost = `${inject(RUNTIME_API_HOST)}/api/`; } diff --git a/client/src/app/services/core/auth-storage.spec.ts b/client/src/app/services/core/auth-storage.spec.ts new file mode 100644 index 000000000..5e971f19d --- /dev/null +++ b/client/src/app/services/core/auth-storage.spec.ts @@ -0,0 +1,55 @@ +import { + authStorageKey, + clearAuthStorage, + LEGACY_AUTH_STORAGE_KEY, + migrateLegacyAuthStorageIfNeeded, + readAuthStorage, + writeAuthStorage, +} from './auth-storage'; + +describe('auth-storage', () => { + let store: Record; + const deps = { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + }; + + beforeEach(() => { + store = {}; + }); + + it('builds a stable key for trailing-slash variants', () => { + expect(authStorageKey('https://a.example/')).toBe( + authStorageKey('https://a.example'), + ); + expect(authStorageKey('https://a.example')).toBe( + 'LocalCragAuth:https://a.example', + ); + }); + + it('migrates legacy LocalCragAuth once into the host key', () => { + store[LEGACY_AUTH_STORAGE_KEY] = '{"accessToken":"t"}'; + migrateLegacyAuthStorageIfNeeded('https://a.example/', deps); + expect(store['LocalCragAuth:https://a.example']).toBe( + '{"accessToken":"t"}', + ); + expect(store[LEGACY_AUTH_STORAGE_KEY]).toBeUndefined(); + expect(readAuthStorage('https://a.example', deps)).toBe( + '{"accessToken":"t"}', + ); + }); + + it('write and clear are host-scoped', () => { + writeAuthStorage('https://a.example', 'A', deps); + writeAuthStorage('https://b.example', 'B', deps); + expect(readAuthStorage('https://a.example', deps)).toBe('A'); + clearAuthStorage('https://a.example', deps); + expect(readAuthStorage('https://a.example', deps)).toBeNull(); + expect(readAuthStorage('https://b.example', deps)).toBe('B'); + }); +}); diff --git a/client/src/app/services/core/auth-storage.ts b/client/src/app/services/core/auth-storage.ts new file mode 100644 index 000000000..c1154a652 --- /dev/null +++ b/client/src/app/services/core/auth-storage.ts @@ -0,0 +1,73 @@ +import { normalizeApiHostUrl } from './runtime-api-host'; + +export const LEGACY_AUTH_STORAGE_KEY = 'LocalCragAuth'; + +/** + * Host-scoped localStorage key for JWT persistence (INST-04 / D-09). + */ +export function authStorageKey(apiHost: string): string { + return `${LEGACY_AUTH_STORAGE_KEY}:${normalizeApiHostUrl(apiHost)}`; +} + +export type AuthStorageDeps = { + getItem: (key: string) => string | null; + setItem: (key: string, value: string) => void; + removeItem: (key: string) => void; +}; + +function defaultDeps(): AuthStorageDeps { + return { + getItem: (key) => localStorage.getItem(key), + setItem: (key, value) => localStorage.setItem(key, value), + removeItem: (key) => localStorage.removeItem(key), + }; +} + +/** + * One-time move of legacy global LocalCragAuth into the active host key. + */ +export function migrateLegacyAuthStorageIfNeeded( + apiHost: string, + deps: Partial = {}, +): void { + const d = { ...defaultDeps(), ...deps }; + const key = authStorageKey(apiHost); + if (d.getItem(key) !== null) { + return; + } + const legacy = d.getItem(LEGACY_AUTH_STORAGE_KEY); + if (legacy === null) { + return; + } + d.setItem(key, legacy); + d.removeItem(LEGACY_AUTH_STORAGE_KEY); +} + +export function readAuthStorage( + apiHost: string, + deps: Partial = {}, +): string | null { + const d = { ...defaultDeps(), ...deps }; + migrateLegacyAuthStorageIfNeeded(apiHost, d); + return d.getItem(authStorageKey(apiHost)); +} + +export function writeAuthStorage( + apiHost: string, + value: string, + deps: Partial = {}, +): void { + const d = { ...defaultDeps(), ...deps }; + d.setItem(authStorageKey(apiHost), value); + // Avoid leaving a stale global key after first namespaced write. + d.removeItem(LEGACY_AUTH_STORAGE_KEY); +} + +export function clearAuthStorage( + apiHost: string, + deps: Partial = {}, +): void { + const d = { ...defaultDeps(), ...deps }; + d.removeItem(authStorageKey(apiHost)); + d.removeItem(LEGACY_AUTH_STORAGE_KEY); +} diff --git a/client/src/app/services/core/hardware-back-button.service.spec.ts b/client/src/app/services/core/hardware-back-button.service.spec.ts new file mode 100644 index 000000000..f1b48f1fe --- /dev/null +++ b/client/src/app/services/core/hardware-back-button.service.spec.ts @@ -0,0 +1,89 @@ +import { TestBed } from '@angular/core/testing'; +import { Location } from '@angular/common'; +import { + EXIT_APP, + HardwareBackButtonService, +} from './hardware-back-button.service'; + +describe('HardwareBackButtonService', () => { + let service: HardwareBackButtonService; + let location: jasmine.SpyObj; + let exitApp: jasmine.Spy<() => void>; + let keydownHandler: jasmine.Spy; + + beforeEach(() => { + location = jasmine.createSpyObj('Location', ['back']); + exitApp = jasmine.createSpy('exitApp'); + + TestBed.configureTestingModule({ + providers: [ + { provide: Location, useValue: location }, + { provide: EXIT_APP, useValue: exitApp }, + ], + }); + service = TestBed.inject(HardwareBackButtonService); + + keydownHandler = jasmine.createSpy('keydownHandler'); + document.addEventListener('keydown', keydownHandler); + }); + + afterEach(() => { + document.removeEventListener('keydown', keydownHandler); + document + .querySelectorAll('.p-dialog-mask, .p-popover') + .forEach((el) => el.remove()); + }); + + it('dismissTopOverlayIfAny() returns false and dispatches no keydown event when no overlay is present', () => { + expect(service.dismissTopOverlayIfAny()).toBeFalse(); + expect(keydownHandler).not.toHaveBeenCalled(); + }); + + it('dismissTopOverlayIfAny() returns true and dispatches one Escape keydown when a .p-dialog-mask is open', () => { + const mask = document.createElement('div'); + mask.className = 'p-dialog-mask'; + document.body.appendChild(mask); + + expect(service.dismissTopOverlayIfAny()).toBeTrue(); + expect(keydownHandler).toHaveBeenCalledTimes(1); + const event = keydownHandler.calls.mostRecent().args[0] as KeyboardEvent; + expect(event.key).toBe('Escape'); + }); + + it('dismissTopOverlayIfAny() returns true and dispatches one Escape keydown when a .p-popover is open', () => { + const popover = document.createElement('div'); + popover.className = 'p-popover'; + document.body.appendChild(popover); + + expect(service.dismissTopOverlayIfAny()).toBeTrue(); + expect(keydownHandler).toHaveBeenCalledTimes(1); + const event = keydownHandler.calls.mostRecent().args[0] as KeyboardEvent; + expect(event.key).toBe('Escape'); + }); + + it('handleBackPress(true) with an open overlay returns overlay-dismissed and does not navigate or exit', () => { + const mask = document.createElement('div'); + mask.className = 'p-dialog-mask'; + document.body.appendChild(mask); + + expect(service.handleBackPress(true)).toBe('overlay-dismissed'); + expect(location.back).not.toHaveBeenCalled(); + expect(exitApp).not.toHaveBeenCalled(); + }); + + it('handleBackPress(true) with no overlay returns navigated and calls Location.back exactly once', () => { + expect(service.handleBackPress(true)).toBe('navigated'); + expect(location.back).toHaveBeenCalledTimes(1); + expect(exitApp).not.toHaveBeenCalled(); + }); + + it('handleBackPress(false) with no overlay returns exited and calls EXIT_APP exactly once', () => { + expect(service.handleBackPress(false)).toBe('exited'); + expect(exitApp).toHaveBeenCalledTimes(1); + expect(location.back).not.toHaveBeenCalled(); + }); + + it('register() does not throw when Capacitor.isNativePlatform() is false', () => { + expect(() => service.register()).not.toThrow(); + }); +}); diff --git a/client/src/app/services/core/hardware-back-button.service.ts b/client/src/app/services/core/hardware-back-button.service.ts new file mode 100644 index 000000000..be4c94c3c --- /dev/null +++ b/client/src/app/services/core/hardware-back-button.service.ts @@ -0,0 +1,78 @@ +import { Injectable, InjectionToken, inject } from '@angular/core'; +import { Location } from '@angular/common'; +import { App } from '@capacitor/app'; +import { Capacitor } from '@capacitor/core'; + +/** + * Overlay roots that PrimeNG self-closes on Escape. + * + * Verified against primeng 21 source: `Dialog` binds a document keydown listener + * (`closeOnEscape` defaults true) and `Popover.onEscapeKeydown()` is unconditional. + */ +export const OVERLAY_SELECTORS = + '.p-dialog-mask, .p-popover, .p-confirmdialog, .p-drawer-mask'; + +/** Injectable exit seam so unit tests never spy on the Capacitor plugin proxy. */ +export const EXIT_APP = new InjectionToken<() => void>('EXIT_APP', { + providedIn: 'root', + factory: () => () => { + void App.exitApp(); + }, +}); + +export type BackPressOutcome = 'overlay-dismissed' | 'navigated' | 'exited'; + +@Injectable({ providedIn: 'root' }) +export class HardwareBackButtonService { + private location = inject(Location); + private exitApp = inject(EXIT_APP); + private registered = false; + + /** Registers the Capacitor `backButton` listener once. No-op off native platforms. */ + register(): void { + if (!Capacitor.isNativePlatform() || this.registered) { + return; + } + this.registered = true; + void App.addListener('backButton', ({ canGoBack }) => { + try { + this.handleBackPress(canGoBack); + } catch (error) { + // A thrown error here (e.g. from overlay detection) must never leave + // the hardware back button permanently unresponsive (T-14-06). + console.error( + 'HardwareBackButtonService: backButton handling failed', + error, + ); + } + }); + } + + /** Reacts to a single hardware back press: overlay first, then route, then exit. */ + handleBackPress(canGoBack: boolean): BackPressOutcome { + if (this.dismissTopOverlayIfAny()) { + return 'overlay-dismissed'; + } + if (canGoBack) { + this.location.back(); + return 'navigated'; + } + this.exitApp(); + return 'exited'; + } + + /** Dispatches a synthetic Escape when an overlay is open. True when handled. */ + dismissTopOverlayIfAny(): boolean { + if (!document.querySelector(OVERLAY_SELECTORS)) { + return false; + } + document.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + code: 'Escape', + bubbles: true, + }), + ); + return true; + } +} diff --git a/client/src/app/services/core/instance-registry.spec.ts b/client/src/app/services/core/instance-registry.spec.ts new file mode 100644 index 000000000..0719b4bdf --- /dev/null +++ b/client/src/app/services/core/instance-registry.spec.ts @@ -0,0 +1,67 @@ +import { + addInstance, + hasCompletedInstanceOnboarding, + isInstanceListEmpty, + listInstances, + removeInstance, + SAVED_INSTANCES_PREFERENCE_KEY, + setActiveHost, +} from './instance-registry'; +import { API_HOST_PREFERENCE_KEY } from './runtime-api-host'; + +describe('instance-registry', () => { + let store: Record; + const deps = { + getPreference: async ({ key }: { key: string }) => ({ + value: store[key] ?? null, + }), + setPreference: async ({ key, value }: { key: string; value: string }) => { + store[key] = value; + }, + removePreference: async ({ key }: { key: string }) => { + delete store[key]; + }, + }; + + beforeEach(() => { + store = {}; + }); + + it('treats empty saved list as incomplete onboarding even if apiHost is seeded', async () => { + store[API_HOST_PREFERENCE_KEY] = 'http://10.0.2.2:5000'; + await expectAsync(isInstanceListEmpty(deps)).toBeResolvedTo(true); + await expectAsync(hasCompletedInstanceOnboarding(deps)).toBeResolvedTo( + false, + ); + }); + + it('adds, lists, and removes instances with normalized URLs', async () => { + await addInstance( + { url: 'https://crag.example/', instanceName: 'Example' }, + deps, + ); + const list = await listInstances(deps); + expect(list).toEqual([ + { url: 'https://crag.example', instanceName: 'Example' }, + ]); + expect(store[SAVED_INSTANCES_PREFERENCE_KEY]).toContain( + 'https://crag.example', + ); + + await removeInstance('https://crag.example/', deps); + await expectAsync(listInstances(deps)).toBeResolvedTo([]); + await expectAsync(hasCompletedInstanceOnboarding(deps)).toBeResolvedTo( + false, + ); + }); + + it('setActiveHost writes API_HOST_PREFERENCE_KEY', async () => { + await addInstance({ url: 'https://a.example' }, deps); + const active = await setActiveHost('https://a.example/', deps); + expect(active).toBe('https://a.example'); + expect(store[API_HOST_PREFERENCE_KEY]).toBe('https://a.example'); + await expectAsync(hasCompletedInstanceOnboarding(deps)).toBeResolvedTo( + true, + ); + }); +}); diff --git a/client/src/app/services/core/instance-registry.ts b/client/src/app/services/core/instance-registry.ts new file mode 100644 index 000000000..cd2e69f02 --- /dev/null +++ b/client/src/app/services/core/instance-registry.ts @@ -0,0 +1,147 @@ +import { Preferences } from '@capacitor/preferences'; +import { + API_HOST_PREFERENCE_KEY, + normalizeApiHostUrl, +} from './runtime-api-host'; + +/** Preferences key for the saved instance list (JSON array). */ +export const SAVED_INSTANCES_PREFERENCE_KEY = 'savedInstances'; + +export type SavedInstance = { + url: string; + instanceName?: string | null; + apiVersion?: string | null; + lastUsedAt?: string | null; +}; + +export type InstanceRegistryDeps = { + getPreference: (options: { + key: string; + }) => Promise<{ value: string | null }>; + setPreference: (options: { key: string; value: string }) => Promise; + removePreference: (options: { key: string }) => Promise; +}; + +function defaultDeps(): InstanceRegistryDeps { + return { + getPreference: (options) => Preferences.get(options), + setPreference: (options) => Preferences.set(options), + removePreference: (options) => Preferences.remove(options), + }; +} + +function mergeDeps( + deps: Partial = {}, +): InstanceRegistryDeps { + return { ...defaultDeps(), ...deps }; +} + +async function readList(deps: InstanceRegistryDeps): Promise { + const { value } = await deps.getPreference({ + key: SAVED_INSTANCES_PREFERENCE_KEY, + }); + if (!value) { + return []; + } + try { + const parsed = JSON.parse(value) as unknown; + if (!Array.isArray(parsed)) { + return []; + } + return parsed + .filter( + (item): item is SavedInstance => + !!item && + typeof item === 'object' && + typeof (item as SavedInstance).url === 'string', + ) + .map((item) => ({ + ...item, + url: normalizeApiHostUrl(item.url), + })); + } catch { + return []; + } +} + +async function writeList( + deps: InstanceRegistryDeps, + list: SavedInstance[], +): Promise { + await deps.setPreference({ + key: SAVED_INSTANCES_PREFERENCE_KEY, + value: JSON.stringify(list), + }); +} + +export async function listInstances( + deps: Partial = {}, +): Promise { + return readList(mergeDeps(deps)); +} + +export async function isInstanceListEmpty( + deps: Partial = {}, +): Promise { + const list = await listInstances(deps); + return list.length === 0; +} + +/** + * Onboarding is complete only when the user has at least one saved instance. + * A Phase 15 auto-seeded apiHost alone does NOT count (D-01 / RESEARCH). + */ +export async function hasCompletedInstanceOnboarding( + deps: Partial = {}, +): Promise { + return !(await isInstanceListEmpty(deps)); +} + +export async function addInstance( + instance: SavedInstance, + deps: Partial = {}, +): Promise { + const d = mergeDeps(deps); + const url = normalizeApiHostUrl(instance.url); + const list = await readList(d); + const without = list.filter((item) => item.url !== url); + without.push({ + ...instance, + url, + }); + await writeList(d, without); + return without; +} + +export async function removeInstance( + url: string, + deps: Partial = {}, +): Promise { + const d = mergeDeps(deps); + const normalized = normalizeApiHostUrl(url); + const next = (await readList(d)).filter((item) => item.url !== normalized); + await writeList(d, next); + return next; +} + +export async function setActiveHost( + url: string, + deps: Partial = {}, +): Promise { + const d = mergeDeps(deps); + const normalized = normalizeApiHostUrl(url); + await d.setPreference({ + key: API_HOST_PREFERENCE_KEY, + value: normalized, + }); + const list = await readList(d); + const idx = list.findIndex((item) => item.url === normalized); + if (idx >= 0) { + list[idx] = { + ...list[idx], + lastUsedAt: new Date().toISOString(), + }; + await writeList(d, list); + } + return normalized; +} diff --git a/client/src/app/services/core/localcrag-health.spec.ts b/client/src/app/services/core/localcrag-health.spec.ts new file mode 100644 index 000000000..b97036ad6 --- /dev/null +++ b/client/src/app/services/core/localcrag-health.spec.ts @@ -0,0 +1,61 @@ +import { + isBelowMinApiVersion, + isLocalCragHealthResponse, + LOCALCRAG_HEALTH_PRODUCT, + LOCALCRAG_MIN_API_VERSION, +} from './localcrag-health'; + +describe('isLocalCragHealthResponse', () => { + it('accepts legacy health with server healthy (no product/version)', () => { + expect( + isLocalCragHealthResponse({ + server: 'healthy', + database: 'healthy', + s3: 'healthy', + }), + ).toBeTrue(); + }); + + it('accepts health with product constant and version', () => { + expect( + isLocalCragHealthResponse({ + server: 'healthy', + product: LOCALCRAG_HEALTH_PRODUCT, + version: '1.51.0', + }), + ).toBeTrue(); + }); + + it('rejects wrong product constant when present', () => { + expect( + isLocalCragHealthResponse({ + server: 'healthy', + product: 'other', + version: '1.51.0', + }), + ).toBeFalse(); + }); + + it('rejects when server is not healthy', () => { + expect( + isLocalCragHealthResponse({ + server: 'down', + }), + ).toBeFalse(); + }); +}); + +describe('isBelowMinApiVersion', () => { + it('detects when remote is below min (helper reserved for follow-up)', () => { + expect(isBelowMinApiVersion('1.50.0', '1.51.0')).toBeTrue(); + }); + + it('does not flag when remote equals or exceeds min', () => { + expect( + isBelowMinApiVersion('1.51.0', LOCALCRAG_MIN_API_VERSION), + ).toBeFalse(); + expect( + isBelowMinApiVersion('1.52.0', LOCALCRAG_MIN_API_VERSION), + ).toBeFalse(); + }); +}); diff --git a/client/src/app/services/core/localcrag-health.ts b/client/src/app/services/core/localcrag-health.ts new file mode 100644 index 000000000..a0d4eb9ba --- /dev/null +++ b/client/src/app/services/core/localcrag-health.ts @@ -0,0 +1,74 @@ +/** + * LocalCrag /api/health helpers. + * Server may emit product+version (forward-compatible); save validation stays + * legacy-friendly until operators upgrade (see deferred Phase 16 follow-up). + */ +export const LOCALCRAG_HEALTH_PRODUCT = 'localcrag'; + +/** Reserved for a later version-negotiation follow-up. */ +export const LOCALCRAG_MIN_API_VERSION = '1.51.0'; + +export type LocalCragHealthResponse = { + server?: string; + database?: string | null; + s3?: string | null; + product?: string; + version?: string; +}; + +/** + * Reachability check for instance save: legacy `{ server: "healthy" }` is enough. + * If `product` is present it must be LocalCrag (rejects spoofed product values). + */ +export function isLocalCragHealthResponse( + body: unknown, +): body is LocalCragHealthResponse { + if (!body || typeof body !== 'object') { + return false; + } + const record = body as Record; + if (record['server'] !== 'healthy') { + return false; + } + if ( + 'product' in record && + record['product'] !== undefined && + record['product'] !== null && + record['product'] !== LOCALCRAG_HEALTH_PRODUCT + ) { + return false; + } + return true; +} + +/** + * Compare dotted semver-ish strings (major.minor.patch); non-numeric segments → 0. + * Reserved for a later soft-warn / negotiation follow-up — not used in picker UX yet. + */ +export function isBelowMinApiVersion( + remoteVersion: string, + minVersion: string = LOCALCRAG_MIN_API_VERSION, +): boolean { + const remote = parseVersionParts(remoteVersion); + const min = parseVersionParts(minVersion); + for (let i = 0; i < 3; i++) { + if (remote[i] < min[i]) { + return true; + } + if (remote[i] > min[i]) { + return false; + } + } + return false; +} + +function parseVersionParts(version: string): [number, number, number] { + const parts = version + .trim() + .split('.') + .map((p) => { + const n = parseInt(p, 10); + return Number.isFinite(n) ? n : 0; + }); + return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0]; +} diff --git a/client/src/app/services/core/rock-explorer-live-session.guard.spec.ts b/client/src/app/services/core/rock-explorer-live-session.guard.spec.ts new file mode 100644 index 000000000..0333cfe61 --- /dev/null +++ b/client/src/app/services/core/rock-explorer-live-session.guard.spec.ts @@ -0,0 +1,155 @@ +import { TestBed } from '@angular/core/testing'; +import { Subject } from 'rxjs'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { TranslocoService } from '@jsverse/transloco'; +import { + LiveSessionEndHandlers, + RockExplorerLiveSessionGuard, +} from './rock-explorer-live-session.guard'; + +/** + * Wave 0 RED specs for INST-07 (D-01…D-05, D-14). + * Plan 02 makes these GREEN by wiring DynamicDialog via DialogService.open + * returning a ref whose onClose emits 'finish' | 'discard' | 'cancel'. + * Do not rename these expects when implementing. + */ +describe('RockExplorerLiveSessionGuard', () => { + let guard: RockExplorerLiveSessionGuard; + let dialogService: jasmine.SpyObj; + let handlers: jasmine.SpyObj; + let onClose$: Subject<'finish' | 'discard' | 'cancel' | undefined>; + + beforeEach(() => { + onClose$ = new Subject(); + const dialogRef = { + onClose: onClose$.asObservable(), + close: jasmine.createSpy('close'), + } as unknown as DynamicDialogRef; + + dialogService = jasmine.createSpyObj('DialogService', [ + 'open', + ]); + dialogService.open.and.returnValue(dialogRef); + + handlers = jasmine.createSpyObj( + 'LiveSessionEndHandlers', + ['finish', 'discard'], + ); + handlers.finish.and.callFake(async () => { + guard.setLiveSession(false); + }); + handlers.discard.and.callFake(async () => { + guard.setLiveSession(false); + }); + + TestBed.configureTestingModule({ + providers: [ + RockExplorerLiveSessionGuard, + { provide: DialogService, useValue: dialogService }, + { + provide: TranslocoService, + useValue: { translate: (key: string) => key }, + }, + ], + }); + guard = TestBed.inject(RockExplorerLiveSessionGuard); + }); + + it('setLiveSession(true) → isLive() true; setLiveSession(false) → isLive() false', () => { + expect(guard.isLive()).toBeFalse(); + guard.setLiveSession(true, handlers); + expect(guard.isLive()).toBeTrue(); + guard.setLiveSession(false); + expect(guard.isLive()).toBeFalse(); + }); + + it('isLive is independent of idle Dexie drafts (D-05) — only setLiveSession controls the predicate', () => { + // Idle drafts / hasRecordingSession / Dexie queue must NOT flip isLive. + expect(guard.isLive()).toBeFalse(); + guard.setLiveSession(true, handlers); + expect(guard.isLive()).toBeTrue(); + // Clearing live (exit Record) leaves any idle draft out of scope for the guard. + guard.setLiveSession(false); + expect(guard.isLive()).toBeFalse(); + }); + + it('when !isLive, runGuardedAction runs action immediately with no resolve dialog (passthrough)', async () => { + const action = jasmine.createSpy('action').and.resolveTo(undefined); + expect(guard.isLive()).toBeFalse(); + + await guard.runGuardedAction(action); + + expect(action).toHaveBeenCalledTimes(1); + expect(dialogService.open).not.toHaveBeenCalled(); + expect(handlers.finish).not.toHaveBeenCalled(); + expect(handlers.discard).not.toHaveBeenCalled(); + }); + + it('when isLive + Cancel: pending action does not run; session stays live (D-02)', async () => { + guard.setLiveSession(true, handlers); + const action = jasmine.createSpy('action'); + + const pending = guard.runGuardedAction(action); + expect(dialogService.open).toHaveBeenCalled(); + onClose$.next('cancel'); + onClose$.complete(); + await pending; + + expect(action).not.toHaveBeenCalled(); + expect(guard.isLive()).toBeTrue(); + expect(handlers.finish).not.toHaveBeenCalled(); + expect(handlers.discard).not.toHaveBeenCalled(); + }); + + it('when isLive + Finish: awaits handlers.finish then runs pending action; isLive false (D-03)', async () => { + guard.setLiveSession(true, handlers); + const action = jasmine.createSpy('action').and.resolveTo(undefined); + + const pending = guard.runGuardedAction(action); + expect(dialogService.open).toHaveBeenCalled(); + onClose$.next('finish'); + onClose$.complete(); + await pending; + + expect(handlers.finish).toHaveBeenCalledTimes(1); + expect(handlers.discard).not.toHaveBeenCalled(); + expect(action).toHaveBeenCalledTimes(1); + expect(guard.isLive()).toBeFalse(); + }); + + it('when isLive + Discard: awaits handlers.discard then runs pending action; isLive false (D-04)', async () => { + guard.setLiveSession(true, handlers); + const action = jasmine.createSpy('action').and.resolveTo(undefined); + + const pending = guard.runGuardedAction(action); + expect(dialogService.open).toHaveBeenCalled(); + onClose$.next('discard'); + onClose$.complete(); + await pending; + + expect(handlers.discard).toHaveBeenCalledTimes(1); + expect(handlers.finish).not.toHaveBeenCalled(); + expect(action).toHaveBeenCalledTimes(1); + expect(guard.isLive()).toBeFalse(); + }); + + it('never runs pending action while isLive remains true (D-01 — no soft continue)', async () => { + guard.setLiveSession(true, { + finish: async () => { + /* deliberately leave live true — soft-continue / orphan forbid */ + }, + discard: async () => { + /* deliberately leave live true */ + }, + }); + const action = jasmine.createSpy('action'); + + const pending = guard.runGuardedAction(action); + onClose$.next('finish'); + onClose$.complete(); + await pending; + + expect(guard.isLive()).toBeTrue(); + expect(action).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/app/services/core/rock-explorer-live-session.guard.ts b/client/src/app/services/core/rock-explorer-live-session.guard.ts new file mode 100644 index 000000000..b70cbb707 --- /dev/null +++ b/client/src/app/services/core/rock-explorer-live-session.guard.ts @@ -0,0 +1,90 @@ +import { Injectable, inject } from '@angular/core'; +import { TranslocoService } from '@jsverse/transloco'; +import { marker } from '@jsverse/transloco-keys-manager/marker'; +import { DialogService } from 'primeng/dynamicdialog'; +import { firstValueFrom } from 'rxjs'; +import { + LiveSessionResolveDialogComponent, + LiveSessionResolveResult, +} from '../../modules/core/live-session-resolve-dialog/live-session-resolve-dialog.component'; + +/** End handlers registered while Rock Explorer Record chrome is live (D-05). */ +export type LiveSessionEndHandlers = { + /** Finish live session — MUST call facade.exitRecordModeAsync() (research resolution #1 / D-03). */ + finish: () => Promise; + /** Discard active live draft — MUST stop FGS/shim and clear recordModeActive (D-04). */ + discard: () => Promise; +}; + +/** + * Root-scoped registry + pending-action runner (D-01…D-08). + * Predicate = live Record chrome (recordModeActive mirror), INCLUDING paused. + * Do NOT use hasRecordingSession, Dexie queue, or nativeGpsTrackingActive alone. + */ +@Injectable({ providedIn: 'root' }) +export class RockExplorerLiveSessionGuard { + private live = false; + private endHandlers: LiveSessionEndHandlers | null = null; + private readonly dialogService = inject(DialogService); + private readonly transloco = inject(TranslocoService); + + setLiveSession(active: boolean, handlers?: LiveSessionEndHandlers): void { + this.live = active; + this.endHandlers = active ? (handlers ?? null) : null; + } + + isLive(): boolean { + return this.live; + } + + /** + * If !isLive(): await action() immediately. + * If live: show Finish/Discard/Cancel resolve UI; Cancel → no action; + * Finish → await handlers.finish() then action iff !isLive(); + * Discard → await handlers.discard() then action iff !isLive(). + * Never soft-continue while live (D-01). + */ + async runGuardedAction(action: () => void | Promise): Promise { + if (!this.isLive()) { + await action(); + return; + } + + const ref = this.dialogService.open(LiveSessionResolveDialogComponent, { + header: this.transloco.translate( + marker('rockExplorer.liveSessionGuardHeader'), + ), + modal: true, + closable: true, + closeOnEscape: true, + dismissableMask: true, + width: 'min(420px, 92vw)', + }); + + if (!ref) { + // Duplicate dialog blocked — treat as Cancel (do not proceed while live). + return; + } + + const result = (await firstValueFrom(ref.onClose)) as + LiveSessionResolveResult | undefined; + + if (result === 'finish') { + await this.endHandlers?.finish(); + if (!this.isLive()) { + await action(); + } + return; + } + + if (result === 'discard') { + await this.endHandlers?.discard(); + if (!this.isLive()) { + await action(); + } + return; + } + + // Cancel, mask dismiss, Escape, or undefined — leave session + host unchanged (D-02). + } +} diff --git a/client/src/app/services/core/runtime-api-host.spec.ts b/client/src/app/services/core/runtime-api-host.spec.ts new file mode 100644 index 000000000..b6c8302f6 --- /dev/null +++ b/client/src/app/services/core/runtime-api-host.spec.ts @@ -0,0 +1,107 @@ +import { + API_HOST_PREFERENCE_KEY, + isAllowedApiHostUrl, + normalizeApiHostUrl, + resolveApiHost, +} from './runtime-api-host'; + +describe('resolveApiHost', () => { + it('returns defaultHost on web without touching Preferences', async () => { + const getPreference = jasmine.createSpy('getPreference'); + const setPreference = jasmine.createSpy('setPreference'); + + await expectAsync( + resolveApiHost({ + isNativePlatform: () => false, + defaultHost: 'http://127.0.0.1:5000', + getPreference, + setPreference, + }), + ).toBeResolvedTo('http://127.0.0.1:5000'); + expect(getPreference).not.toHaveBeenCalled(); + expect(setPreference).not.toHaveBeenCalled(); + }); + + it('returns stored Preferences value on native when set', async () => { + const getPreference = jasmine + .createSpy('getPreference') + .and.resolveTo({ value: 'https://example.localcrag.test' }); + const setPreference = jasmine.createSpy('setPreference'); + + await expectAsync( + resolveApiHost({ + isNativePlatform: () => true, + defaultHost: 'http://10.0.2.2:5000', + getPreference, + setPreference, + }), + ).toBeResolvedTo('https://example.localcrag.test'); + expect(getPreference).toHaveBeenCalledWith({ + key: API_HOST_PREFERENCE_KEY, + }); + expect(setPreference).not.toHaveBeenCalled(); + }); + + it('seeds Preferences from defaultHost on native when unset', async () => { + const getPreference = jasmine + .createSpy('getPreference') + .and.resolveTo({ value: null }); + const setPreference = jasmine + .createSpy('setPreference') + .and.resolveTo(undefined); + + await expectAsync( + resolveApiHost({ + isNativePlatform: () => true, + defaultHost: 'http://10.0.2.2:5000', + getPreference, + setPreference, + }), + ).toBeResolvedTo('http://10.0.2.2:5000'); + expect(setPreference).toHaveBeenCalledWith({ + key: API_HOST_PREFERENCE_KEY, + value: 'http://10.0.2.2:5000', + }); + }); +}); + +describe('API_HOST_PREFERENCE_KEY', () => { + it('is the literal apiHost key Phase 16 must reuse (D-03)', () => { + expect(API_HOST_PREFERENCE_KEY).toBe('apiHost'); + }); +}); + +describe('normalizeApiHostUrl', () => { + it('trims whitespace and strips trailing slashes', () => { + expect(normalizeApiHostUrl(' https://example.com/ ')).toBe( + 'https://example.com', + ); + }); +}); + +describe('isAllowedApiHostUrl', () => { + it('allows https hosts', () => { + expect(isAllowedApiHostUrl('https://example.com')).toBeTrue(); + }); + + it('allows emulator loopback http://10.0.2.2', () => { + expect(isAllowedApiHostUrl('http://10.0.2.2:5000')).toBeTrue(); + }); + + it('allows private LAN http hosts for physical-device debug', () => { + expect(isAllowedApiHostUrl('http://192.168.178.20:5000')).toBeTrue(); + expect(isAllowedApiHostUrl('http://10.0.0.5:5000')).toBeTrue(); + expect(isAllowedApiHostUrl('http://172.16.1.2:5000')).toBeTrue(); + expect(isAllowedApiHostUrl('http://127.0.0.1:5000')).toBeTrue(); + }); + + it('rejects public cleartext http hosts', () => { + expect(isAllowedApiHostUrl('http://evil.example')).toBeFalse(); + expect(isAllowedApiHostUrl('http://8.8.8.8:5000')).toBeFalse(); + }); + + it('rejects empty or invalid URLs', () => { + expect(isAllowedApiHostUrl('')).toBeFalse(); + expect(isAllowedApiHostUrl('not-a-url')).toBeFalse(); + }); +}); diff --git a/client/src/app/services/core/runtime-api-host.ts b/client/src/app/services/core/runtime-api-host.ts new file mode 100644 index 000000000..f6bba96ad --- /dev/null +++ b/client/src/app/services/core/runtime-api-host.ts @@ -0,0 +1,117 @@ +import { InjectionToken } from '@angular/core'; +import { Capacitor } from '@capacitor/core'; +import { Preferences } from '@capacitor/preferences'; +import { environment } from '../../../environments/environment'; + +/** + * Preferences key for the single runtime API base URL (no trailing /api/). + * Phase 16's instance picker MUST reuse this exact key (D-03). + */ +export const API_HOST_PREFERENCE_KEY = 'apiHost'; + +/** + * Resolved once per app load before Angular bootstraps (see main.ts). + * ApiService builds all `/api/` URLs from this value (INST-01). + */ +export const RUNTIME_API_HOST = new InjectionToken('RUNTIME_API_HOST'); + +/** Testable seams for resolveApiHost (Karma may duplicate @capacitor/core modules). */ +export type ResolveApiHostDeps = { + isNativePlatform: () => boolean; + getPreference: (options: { + key: string; + }) => Promise<{ value: string | null }>; + setPreference: (options: { key: string; value: string }) => Promise; + defaultHost: string; +}; + +function defaultResolveApiHostDeps(): ResolveApiHostDeps { + return { + isNativePlatform: () => Capacitor.isNativePlatform(), + getPreference: (options) => Preferences.get(options), + setPreference: (options) => Preferences.set(options), + defaultHost: environment.apiHost, + }; +} + +/** + * Trim and strip a trailing slash for consistency with environment.apiHost. + */ +export function normalizeApiHostUrl(raw: string): string { + return raw.trim().replace(/\/+$/, ''); +} + +/** + * True for RFC1918 / link-local IPv4 hosts used for local LAN debug + * (physical device → Mac) and the Android emulator alias 10.0.2.2. + */ +function isPrivateOrEmulatorHttpHostname(hostname: string): boolean { + if (hostname === '10.0.2.2' || hostname === 'localhost') { + return true; + } + const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname); + if (!m) { + return false; + } + const oct = m.slice(1).map((p) => Number(p)); + if (oct.some((n) => n > 255)) { + return false; + } + const [a, b] = oct; + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 + return ( + a === 10 || + a === 127 || + (a === 172 && b >= 16 && b <= 31) || + (a === 192 && b === 168) + ); +} + +/** + * HTTPS for real instances; cleartext HTTP only for emulator loopback (10.0.2.2) + * and private LAN / localhost hosts (physical-device → local Flask). + */ +export function isAllowedApiHostUrl(url: string): boolean { + if (!url) { + return false; + } + try { + const parsed = new URL(url); + if (parsed.protocol === 'https:') { + return true; + } + return ( + parsed.protocol === 'http:' && + isPrivateOrEmulatorHttpHostname(parsed.hostname) + ); + } catch { + return false; + } +} + +/** + * Resolve the API base host for this process. + * - Web: synchronous environment.apiHost (D-07), no Preferences. + * - Native: Preferences-backed; seed from environment.apiHost when unset (D-01, D-02). + */ +export async function resolveApiHost( + deps: Partial = {}, +): Promise { + const { isNativePlatform, getPreference, setPreference, defaultHost } = { + ...defaultResolveApiHostDeps(), + ...deps, + }; + + if (!isNativePlatform()) { + return defaultHost; + } + const { value } = await getPreference({ key: API_HOST_PREFERENCE_KEY }); + if (value) { + return value; + } + await setPreference({ + key: API_HOST_PREFERENCE_KEY, + value: defaultHost, + }); + return defaultHost; +} diff --git a/client/src/app/services/core/theme.service.ts b/client/src/app/services/core/theme.service.ts index b9dfcf46d..db78521d9 100644 --- a/client/src/app/services/core/theme.service.ts +++ b/client/src/app/services/core/theme.service.ts @@ -1,12 +1,16 @@ import { DestroyRef, Injectable, + Injector, computed, + effect, inject, signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Actions, ofType } from '@ngrx/effects'; +import { Capacitor } from '@capacitor/core'; +import { StatusBar, Style } from '@capacitor/status-bar'; import { cleanupCredentials, newAuthCredentials, @@ -30,6 +34,7 @@ export class ThemeService { private actions$ = inject(Actions); private destroyRef = inject(DestroyRef); + private injector = inject(Injector); private mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); private mediaListener = () => { if (this.colorScheme() === 'system') { @@ -57,6 +62,8 @@ export class ThemeService { .subscribe(() => { this.applyColorScheme(this.readGuestColorScheme() ?? 'system'); }); + + this.syncNativeStatusBar(); } toggleGuestColorScheme(): void { @@ -88,4 +95,31 @@ export class ThemeService { } return null; } + + private syncNativeStatusBar(): void { + if (!Capacitor.isNativePlatform()) { + return; + } + effect( + () => { + const dark = this.isDarkMode(); + // Style.Dark means light content drawn on dark chrome. + void StatusBar.setStyle({ style: dark ? Style.Dark : Style.Light }); + void StatusBar.setBackgroundColor({ color: this.statusBarColor(dark) }); + }, + { injector: this.injector }, + ); + } + + /** Reads the theme surface the site header already uses, so native chrome cannot drift. */ + private statusBarColor(dark: boolean): string { + const token = getComputedStyle(document.documentElement) + .getPropertyValue('--p-content-background') + .trim(); + return /^#[0-9a-fA-F]{6}$/.test(token) + ? token + : dark + ? '#18181b' + : '#ffffff'; + } } diff --git a/client/src/app/utility/rewrite-loopback-media-url.spec.ts b/client/src/app/utility/rewrite-loopback-media-url.spec.ts new file mode 100644 index 000000000..43230d535 --- /dev/null +++ b/client/src/app/utility/rewrite-loopback-media-url.spec.ts @@ -0,0 +1,30 @@ +import { rewriteLoopbackMediaUrlForAndroid } from './rewrite-loopback-media-url'; + +describe('rewriteLoopbackMediaUrlForAndroid', () => { + // Capacitor.isNativePlatform is false under Karma (browser), so these assert + // the web no-op path. Native rewrite is covered by the Capacitor Android smoke + // checkpoint (images load from MinIO via 10.0.2.2). + it('returns null/empty unchanged', () => { + expect(rewriteLoopbackMediaUrlForAndroid(null)).toBeNull(); + expect(rewriteLoopbackMediaUrlForAndroid(undefined)).toBeNull(); + expect(rewriteLoopbackMediaUrlForAndroid('')).toBe(''); + }); + + it('leaves URLs unchanged in the browser test environment', () => { + expect( + rewriteLoopbackMediaUrlForAndroid( + 'http://127.0.0.1:9000/bucket/file.jpg', + ), + ).toBe('http://127.0.0.1:9000/bucket/file.jpg'); + expect( + rewriteLoopbackMediaUrlForAndroid( + 'http://localhost:9000/bucket/file.jpg', + ), + ).toBe('http://localhost:9000/bucket/file.jpg'); + expect( + rewriteLoopbackMediaUrlForAndroid( + '

', + ), + ).toBe('

'); + }); +}); diff --git a/client/src/app/utility/rewrite-loopback-media-url.ts b/client/src/app/utility/rewrite-loopback-media-url.ts new file mode 100644 index 000000000..24c2222e6 --- /dev/null +++ b/client/src/app/utility/rewrite-loopback-media-url.ts @@ -0,0 +1,65 @@ +import { Capacitor } from '@capacitor/core'; + +/** + * Host used to rewrite developer-machine loopback media URLs on Android. + * Defaults to the emulator alias; overridden at bootstrap from the runtime API + * host so physical devices using LAN IP or adb-reverse 127.0.0.1 keep working. + */ +let androidMediaLoopbackHost = '10.0.2.2'; + +/** + * Matches absolute http(s) URLs whose host is the developer machine loopback. + * Global + unanchored so it also rewrites URLs embedded in rich-text HTML + * (blog posts, descriptions, rules) — not only standalone File.filename values. + */ +const LOOPBACK_HOST_IN_URL = /(https?:\/\/)(127\.0\.0\.1|localhost)(?=[:/])/gi; + +/** + * Point loopback media rewrites at the same machine as the runtime API host. + * Call once after `resolveApiHost()` (see main.ts). + * + * - `http://10.0.2.2:5000` → media via `10.0.2.2` (emulator) + * - `http://127.0.0.1:5000` → keep `127.0.0.1` (needs `adb reverse` for :9000) + * - `http://192.168.x.x:5000` → media via that LAN IP (MinIO must listen on LAN) + */ +export function configureAndroidMediaHostRewrite(apiHost: string): void { + if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== 'android') { + return; + } + try { + const hostname = new URL(apiHost).hostname; + if (!hostname) { + return; + } + androidMediaLoopbackHost = + hostname === 'localhost' ? '127.0.0.1' : hostname; + } catch { + // keep emulator default + } +} + +/** Test seam — reset rewrite host between specs. */ +export function resetAndroidMediaHostRewriteForTests(): void { + androidMediaLoopbackHost = '10.0.2.2'; +} + +/** + * Rewrites absolute media URLs that point at the host loopback so they load on + * Android. Server-serialized file URLs use S3_ACCESS_ENDPOINT + * (typically http://127.0.0.1:9000/...), which is unreachable as-is from the + * device/emulator unless rewritten (and, for 127.0.0.1, adb-reversed). + * + * Accepts a single URL or a larger string (e.g. HTML) containing such URLs. + * No-op on web, iOS, and when no loopback host is present. + */ +export function rewriteLoopbackMediaUrlForAndroid( + url: string | null | undefined, +): string | null { + if (url == null || url === '') { + return url ?? null; + } + if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== 'android') { + return url; + } + return url.replace(LOOPBACK_HOST_IN_URL, `$1${androidMediaLoopbackHost}`); +} diff --git a/client/src/assets/i18n/de.json b/client/src/assets/i18n/de.json index fd895cb56..3a12d4fd3 100644 --- a/client/src/assets/i18n/de.json +++ b/client/src/assets/i18n/de.json @@ -1048,6 +1048,16 @@ "rockExplorer.recordGroupInfo": "Info", "rockExplorer.recordGroupSession": "Sitzung", "rockExplorer.recordGeoDenied": "Standortzugriff verweigert. Aufzeichnung gestoppt.", + "rockExplorer.gpsBackgroundDisclosureHeader": "Standort im Hintergrund", + "rockExplorer.gpsBackgroundDisclosureMessage": "LocalCrag erfasst Standortdaten, um die Pfadaufzeichnung im Rock Explorer auch dann fortzusetzen, wenn die App geschlossen ist oder nicht verwendet wird.", + "rockExplorer.gpsBackgroundDisclosureAccept": "Weiter", + "rockExplorer.gpsBackgroundDisclosureReject": "Nicht jetzt", + "rockExplorer.liveSessionGuardHeader": "Aufnahmesitzung beenden?", + "rockExplorer.liveSessionGuardMessage": "Beende oder verwirf deine Rock-Explorer-Aufnahme, bevor du die Instanz wechselst oder dich abmeldest.", + "rockExplorer.liveSessionGuardFinish": "Aufnahme beenden", + "rockExplorer.liveSessionGuardDiscard": "Entwurf verwerfen", + "rockExplorer.liveSessionGuardCancel": "Abbrechen", + "rockExplorer.gpsTrackingActive": "GPS-Tracking aktiv", "rockExplorer.recordFinishPathTooShort": "Mindestens zwei GPS-Punkte nötig, um einen Pfad zu beenden.", "rockExplorer.syncStatus.pending": "Sync ausstehend", "rockExplorer.syncStatus.syncing": "Synchronisiere…", @@ -1220,5 +1230,17 @@ "rockExplorer.finishPath": "Weg abschließen", "rockExplorer.pathNeedsVertices": "Jeder Weg braucht mindestens zwei Punkte", "rockExplorer.pathDeleteConfirm": "Diesen Weg löschen?", - "rockExplorer.deletePathVertex": "Punkt löschen" + "rockExplorer.deletePathVertex": "Punkt löschen", + "instancePicker.title": "LocalCrag-Instanz wählen", + "instancePicker.description": "Gib die Basis-URL eines LocalCrag-Servers ein (https). Nach der Prüfung speichern und auswählen.", + "instancePicker.webOnlyHint": "Instanzauswahl ist nur in der Android-App verfügbar.", + "instancePicker.urlPlaceholder": "https://deine-instanz.example", + "instancePicker.saveButton": "Prüfen und speichern", + "instancePicker.savedHeading": "Gespeicherte Instanzen", + "instancePicker.emptyList": "Noch keine Instanzen gespeichert.", + "instancePicker.deleteButton": "Entfernen", + "instancePicker.errorInvalidUrl": "Nur https://… oder http:// für lokales LAN / Emulator (z. B. 192.168.… oder 10.0.2.2).", + "instancePicker.errorNotLocalCrag": "Diese URL lieferte keine gesunde LocalCrag-API-Antwort. URL prüfen.", + "instancePicker.errorUnreachable": "URL nicht erreichbar.", + "menu.switchInstance": "Instanz wechseln" } diff --git a/client/src/assets/i18n/en.json b/client/src/assets/i18n/en.json index e919ac7ce..0a1242e03 100644 --- a/client/src/assets/i18n/en.json +++ b/client/src/assets/i18n/en.json @@ -294,6 +294,19 @@ "login.loginButtonLabel": "Log in", "login.forgotPasswordButtonLabel": "Forgotten password?", "login.registerButtonLabel": "Create new account", + "instancePicker.title": "Choose a LocalCrag instance", + "instancePicker.description": "Enter the base URL of a LocalCrag server (https). Save after validation, then pick one to use.", + "instancePicker.webOnlyHint": "Instance picking is only available in the Android app.", + "instancePicker.urlPlaceholder": "https://your-instance.example", + "instancePicker.saveButton": "Validate and save", + "instancePicker.savedHeading": "Saved instances", + "instancePicker.emptyList": "No instances saved yet.", + "instancePicker.deleteButton": "Remove", + "instancePicker.errorInvalidUrl": "Use https://…, or http:// for a local LAN / emulator host (e.g. 192.168.… or 10.0.2.2).", + "instancePicker.errorNotLocalCrag": "That URL did not return a healthy LocalCrag API response. Check the URL.", + "instancePicker.errorUnreachable": "Could not reach that URL.", + "menu.logout": "Log out", + "menu.switchInstance": "Switch instance", "instanceSettings.instanceSettingsForm.commonSettings": "Global Settings", "instanceSettings.instanceSettingsForm.instanceNameLabel": "Instance name", "instanceSettings.instanceSettingsForm.instanceNamePlaceholder": "e.g. Gleesbouldering", @@ -950,7 +963,6 @@ "menu.todos": "ToDo list", "menu.account": "Account settings", "menu.changePassword": "Change Password", - "menu.logout": "Log out", "menu.news": "News", "menu.topo": "Topo", "menu.ascents": "Logbook", @@ -1048,6 +1060,16 @@ "rockExplorer.recordGroupInfo": "Info", "rockExplorer.recordGroupSession": "Session", "rockExplorer.recordGeoDenied": "Location permission denied. Recording stopped.", + "rockExplorer.gpsBackgroundDisclosureHeader": "Background location", + "rockExplorer.gpsBackgroundDisclosureMessage": "LocalCrag collects location data to enable Rock Explorer path recording even when the app is closed or not in use.", + "rockExplorer.gpsBackgroundDisclosureAccept": "Continue", + "rockExplorer.gpsBackgroundDisclosureReject": "Not now", + "rockExplorer.liveSessionGuardHeader": "End recording session?", + "rockExplorer.liveSessionGuardMessage": "Finish or discard your Rock Explorer recording before switching instance or logging out.", + "rockExplorer.liveSessionGuardFinish": "Finish recording", + "rockExplorer.liveSessionGuardDiscard": "Discard draft", + "rockExplorer.liveSessionGuardCancel": "Cancel", + "rockExplorer.gpsTrackingActive": "GPS tracking active", "rockExplorer.recordFinishPathTooShort": "Need at least two GPS points to finish a path.", "rockExplorer.syncStatus.pending": "Pending sync", "rockExplorer.syncStatus.syncing": "Syncing…", diff --git a/client/src/assets/i18n/fr.json b/client/src/assets/i18n/fr.json index d840264de..7e898d921 100644 --- a/client/src/assets/i18n/fr.json +++ b/client/src/assets/i18n/fr.json @@ -1048,6 +1048,16 @@ "rockExplorer.recordGroupInfo": "Infos", "rockExplorer.recordGroupSession": "Session", "rockExplorer.recordGeoDenied": "Autorisation de localisation refusée. Enregistrement arrêté.", + "rockExplorer.gpsBackgroundDisclosureHeader": "Localisation en arrière-plan", + "rockExplorer.gpsBackgroundDisclosureMessage": "LocalCrag collecte des données de localisation pour permettre l'enregistrement des parcours Rock Explorer même lorsque l'application est fermée ou non utilisée.", + "rockExplorer.gpsBackgroundDisclosureAccept": "Continuer", + "rockExplorer.gpsBackgroundDisclosureReject": "Pas maintenant", + "rockExplorer.liveSessionGuardHeader": "Terminer la session d'enregistrement ?", + "rockExplorer.liveSessionGuardMessage": "Terminez ou ignorez votre enregistrement Rock Explorer avant de changer d'instance ou de vous déconnecter.", + "rockExplorer.liveSessionGuardFinish": "Terminer l'enregistrement", + "rockExplorer.liveSessionGuardDiscard": "Ignorer le brouillon", + "rockExplorer.liveSessionGuardCancel": "Annuler", + "rockExplorer.gpsTrackingActive": "Suivi GPS actif", "rockExplorer.recordFinishPathTooShort": "Il faut au moins deux points GPS pour terminer un chemin.", "rockExplorer.syncStatus.pending": "Sync en attente", "rockExplorer.syncStatus.syncing": "Synchronisation…", @@ -1220,5 +1230,17 @@ "rockExplorer.finishPath": "Terminer le chemin", "rockExplorer.pathNeedsVertices": "Tracez au moins deux points pour chaque chemin", "rockExplorer.pathDeleteConfirm": "Supprimer ce chemin ?", - "rockExplorer.deletePathVertex": "Supprimer le sommet" + "rockExplorer.deletePathVertex": "Supprimer le sommet", + "instancePicker.title": "Choisir une instance LocalCrag", + "instancePicker.description": "Saisissez l’URL de base d’un serveur LocalCrag (https). Enregistrez après validation, puis choisissez.", + "instancePicker.webOnlyHint": "Le choix d’instance n’est disponible que dans l’application Android.", + "instancePicker.urlPlaceholder": "https://votre-instance.example", + "instancePicker.saveButton": "Valider et enregistrer", + "instancePicker.savedHeading": "Instances enregistrées", + "instancePicker.emptyList": "Aucune instance enregistrée.", + "instancePicker.deleteButton": "Supprimer", + "instancePicker.errorInvalidUrl": "Utilisez https://…, ou http:// pour un hôte LAN / émulateur (ex. 192.168.… ou 10.0.2.2).", + "instancePicker.errorNotLocalCrag": "Cette URL n’a pas renvoyé une réponse API LocalCrag saine. Vérifiez l’URL.", + "instancePicker.errorUnreachable": "Impossible d’atteindre cette URL.", + "menu.switchInstance": "Changer d’instance" } diff --git a/client/src/assets/i18n/it.json b/client/src/assets/i18n/it.json index 79feb10c9..70d339db3 100644 --- a/client/src/assets/i18n/it.json +++ b/client/src/assets/i18n/it.json @@ -1048,6 +1048,16 @@ "rockExplorer.recordGroupInfo": "Info", "rockExplorer.recordGroupSession": "Sessione", "rockExplorer.recordGeoDenied": "Autorizzazione posizione negata. Registrazione interrotta.", + "rockExplorer.gpsBackgroundDisclosureHeader": "Posizione in background", + "rockExplorer.gpsBackgroundDisclosureMessage": "LocalCrag raccoglie dati sulla posizione per abilitare la registrazione dei percorsi di Rock Explorer anche quando l'app è chiusa o non in uso.", + "rockExplorer.gpsBackgroundDisclosureAccept": "Continua", + "rockExplorer.gpsBackgroundDisclosureReject": "Non ora", + "rockExplorer.liveSessionGuardHeader": "Terminare la sessione di registrazione?", + "rockExplorer.liveSessionGuardMessage": "Termina o elimina la registrazione Rock Explorer prima di cambiare istanza o disconnetterti.", + "rockExplorer.liveSessionGuardFinish": "Termina registrazione", + "rockExplorer.liveSessionGuardDiscard": "Elimina bozza", + "rockExplorer.liveSessionGuardCancel": "Annulla", + "rockExplorer.gpsTrackingActive": "Tracciamento GPS attivo", "rockExplorer.recordFinishPathTooShort": "Servono almeno due punti GPS per terminare un percorso.", "rockExplorer.syncStatus.pending": "Sync in sospeso", "rockExplorer.syncStatus.syncing": "Sincronizzazione…", @@ -1220,5 +1230,17 @@ "rockExplorer.finishPath": "Termina sentiero", "rockExplorer.pathNeedsVertices": "Disegna almeno due punti per ogni sentiero", "rockExplorer.pathDeleteConfirm": "Eliminare questo sentiero?", - "rockExplorer.deletePathVertex": "Elimina vertice" + "rockExplorer.deletePathVertex": "Elimina vertice", + "instancePicker.title": "Scegli un’istanza LocalCrag", + "instancePicker.description": "Inserisci l’URL di base di un server LocalCrag (https). Salva dopo la convalida e seleziona.", + "instancePicker.webOnlyHint": "La selezione dell’istanza è disponibile solo nell’app Android.", + "instancePicker.urlPlaceholder": "https://tua-istanza.example", + "instancePicker.saveButton": "Convalida e salva", + "instancePicker.savedHeading": "Istanze salvate", + "instancePicker.emptyList": "Nessuna istanza salvata.", + "instancePicker.deleteButton": "Rimuovi", + "instancePicker.errorInvalidUrl": "Usa https://…, oppure http:// per host LAN / emulatore (es. 192.168.… o 10.0.2.2).", + "instancePicker.errorNotLocalCrag": "Questo URL non ha restituito una risposta API LocalCrag sana. Controlla l’URL.", + "instancePicker.errorUnreachable": "Impossibile raggiungere l’URL.", + "menu.switchInstance": "Cambia istanza" } diff --git a/client/src/assets/i18n/nl.json b/client/src/assets/i18n/nl.json index 2376606e7..632983299 100644 --- a/client/src/assets/i18n/nl.json +++ b/client/src/assets/i18n/nl.json @@ -1048,6 +1048,16 @@ "rockExplorer.recordGroupInfo": "Info", "rockExplorer.recordGroupSession": "Sessie", "rockExplorer.recordGeoDenied": "Locatietoegang geweigerd. Opname gestopt.", + "rockExplorer.gpsBackgroundDisclosureHeader": "Locatie op de achtergrond", + "rockExplorer.gpsBackgroundDisclosureMessage": "LocalCrag verzamelt locatiegegevens om Rock Explorer-padopnames mogelijk te maken, ook wanneer de app gesloten is of niet in gebruik is.", + "rockExplorer.gpsBackgroundDisclosureAccept": "Doorgaan", + "rockExplorer.gpsBackgroundDisclosureReject": "Niet nu", + "rockExplorer.liveSessionGuardHeader": "Opnamesessie beëindigen?", + "rockExplorer.liveSessionGuardMessage": "Beëindig of verwerp je Rock Explorer-opname voordat je van instantie wisselt of uitlogt.", + "rockExplorer.liveSessionGuardFinish": "Opname beëindigen", + "rockExplorer.liveSessionGuardDiscard": "Concept verwerpen", + "rockExplorer.liveSessionGuardCancel": "Annuleren", + "rockExplorer.gpsTrackingActive": "GPS-tracking actief", "rockExplorer.recordFinishPathTooShort": "Er zijn minstens twee GPS-punten nodig om een pad af te ronden.", "rockExplorer.syncStatus.pending": "Sync in behandeling", "rockExplorer.syncStatus.syncing": "Synchroniseren…", @@ -1220,5 +1230,17 @@ "rockExplorer.finishPath": "Pad afronden", "rockExplorer.pathNeedsVertices": "Teken minstens twee punten voor elk pad", "rockExplorer.pathDeleteConfirm": "Dit pad verwijderen?", - "rockExplorer.deletePathVertex": "Hoekpunt verwijderen" + "rockExplorer.deletePathVertex": "Hoekpunt verwijderen", + "instancePicker.title": "Kies een LocalCrag-instantie", + "instancePicker.description": "Voer de basis-URL van een LocalCrag-server in (https). Sla op na validatie en kies er een.", + "instancePicker.webOnlyHint": "Instantiekeuze is alleen beschikbaar in de Android-app.", + "instancePicker.urlPlaceholder": "https://jouw-instantie.example", + "instancePicker.saveButton": "Valideren en opslaan", + "instancePicker.savedHeading": "Opgeslagen instanties", + "instancePicker.emptyList": "Nog geen instanties opgeslagen.", + "instancePicker.deleteButton": "Verwijderen", + "instancePicker.errorInvalidUrl": "Gebruik https://…, of http:// voor een lokaal LAN-/emulatorhost (bijv. 192.168.… of 10.0.2.2).", + "instancePicker.errorNotLocalCrag": "Deze URL gaf geen gezonde LocalCrag-API-respons. Controleer de URL.", + "instancePicker.errorUnreachable": "URL niet bereikbaar.", + "menu.switchInstance": "Instantie wisselen" } diff --git a/client/src/environments/environment.android.ts b/client/src/environments/environment.android.ts new file mode 100644 index 000000000..adbde9518 --- /dev/null +++ b/client/src/environments/environment.android.ts @@ -0,0 +1,10 @@ +import packageInfo from '../../package.json'; + +export const mockGpsRecording = false; + +export const environment = { + production: false, + apiHost: 'http://10.0.2.2:5000', + skippedSlug: '_default', + version: packageInfo.version, +}; diff --git a/client/src/main.ts b/client/src/main.ts index 8bbea65cc..f4f033e06 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -8,19 +8,57 @@ import { Chart } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import BlotFormatter from 'quill-blot-formatter'; import { bootstrapApplication } from '@angular/platform-browser'; +import { Capacitor } from '@capacitor/core'; +import { SplashScreen } from '@capacitor/splash-screen'; import { appConfig } from './app/modules/core/app.config'; import { CoreComponent } from './app/modules/core/core.component'; +import { + resolveApiHost, + RUNTIME_API_HOST, +} from './app/services/core/runtime-api-host'; +import { configureAndroidMediaHostRewrite } from './app/utility/rewrite-loopback-media-url'; if (environment.production) { enableProdMode(); } -bootstrapApplication(CoreComponent, { - ...appConfig, - providers: [provideZoneChangeDetection(), ...appConfig.providers], -}).catch((err) => console.error(err)); - Quill.register('modules/imageUploader', ImageUploader); Quill.register('modules/blotFormatter', BlotFormatter); Chart.register(ChartDataLabels); + +async function main(): Promise { + // Resolve Preferences-backed host before any Angular initializer can construct + // ApiService (INST-01 / D-01 — avoids APP_INITIALIZER race with instance-settings). + const apiHost = await resolveApiHost(); + configureAndroidMediaHostRewrite(apiHost); + + // Never leave the native splash up for the full CapacitorHttp timeout when the + // API host is unreachable (e.g. leftover emulator 10.0.2.2 on a physical device). + let splashWatchdog: ReturnType | undefined; + if (Capacitor.isNativePlatform()) { + splashWatchdog = setTimeout(() => { + void SplashScreen.hide(); + }, 4000); + } + + try { + await bootstrapApplication(CoreComponent, { + ...appConfig, + providers: [ + provideZoneChangeDetection(), + { provide: RUNTIME_API_HOST, useValue: apiHost }, + ...appConfig.providers, + ], + }); + } finally { + if (splashWatchdog !== undefined) { + clearTimeout(splashWatchdog); + } + if (Capacitor.isNativePlatform()) { + void SplashScreen.hide(); + } + } +} + +main().catch((err) => console.error(err)); diff --git a/server/src/app.py b/server/src/app.py index 1920e2d1e..fd8684bca 100644 --- a/server/src/app.py +++ b/server/src/app.py @@ -21,7 +21,13 @@ def register_extensions(application): jwt.init_app(application) ma.init_app(application) migrate.init_app(application, db=db) - cors.init_app(application, origins=[application.config["FRONTEND_HOST"]]) + origins = [application.config["FRONTEND_HOST"]] + origins.extend( + origin.strip() + for origin in (application.config.get("ADDITIONAL_CORS_ORIGINS") or "").split(",") + if origin.strip() + ) + cors.init_app(application, origins=origins) def configure_extensions(application): diff --git a/server/src/config/default.py b/server/src/config/default.py index 2eaba5760..2d2b3ca79 100644 --- a/server/src/config/default.py +++ b/server/src/config/default.py @@ -18,6 +18,7 @@ class DefaultConfig(object): SMTP_PORT = "YOUR_SMTP_PORT" SMTP_TYPE = None # smtps, starttls, plain, disabled FRONTEND_HOST = None # Base URL of the SPA + ADDITIONAL_CORS_ORIGINS = "" # Comma-separated extra allowed CORS origins, e.g. native app WebView origins SUPERADMIN_FIRSTNAME = "" SUPERADMIN_LASTNAME = "" SUPERADMIN_EMAIL = "" diff --git a/server/src/config/env_var_config.py b/server/src/config/env_var_config.py index 498a368f3..63c6edc08 100644 --- a/server/src/config/env_var_config.py +++ b/server/src/config/env_var_config.py @@ -32,6 +32,7 @@ def overwrite_config_by_env_vars(app): "S3_ACCESS_ENDPOINT", "S3_ADDRESSING", "FRONTEND_HOST", + "ADDITIONAL_CORS_ORIGINS", "SENTRY_DSN", "SENTRY_ENABLED", "SUPERADMIN_EMAIL", diff --git a/server/src/config/template.cfg b/server/src/config/template.cfg index dded7c497..95e9f68e0 100644 --- a/server/src/config/template.cfg +++ b/server/src/config/template.cfg @@ -9,6 +9,9 @@ SMTP_PASSWORD = '' SMTP_PORT = '465' SMTP_TYPE = 'smpts' FRONTEND_HOST = 'http://localhost:4200' +# Extra allowed CORS origins beyond FRONTEND_HOST, comma-separated. Needed for the Capacitor +# Android Capacitor WebView origins (androidScheme http → http://localhost; legacy https also fine): +# ADDITIONAL_CORS_ORIGINS = 'http://localhost,https://localhost' SQLALCHEMY_ECHO = False SUPERADMIN_FIRSTNAME = '' SUPERADMIN_LASTNAME = '' diff --git a/server/src/resources/health_resources.py b/server/src/resources/health_resources.py index ce52f8c69..4b5c8fcd8 100644 --- a/server/src/resources/health_resources.py +++ b/server/src/resources/health_resources.py @@ -4,6 +4,7 @@ from extensions import db from uploader.do_s3 import get_s3_client +from util.localcrag_product import LOCALCRAG_PRODUCT, LOCALCRAG_VERSION class Health(MethodView): @@ -11,9 +12,16 @@ class Health(MethodView): def get(self): """ Health route to check if the server, database and s3 datastorage are healthy. + Always includes LocalCrag product identity + version (Phase 16 D-05). """ status = 200 - response = {"server": "healthy", "database": None, "s3": None} + response = { + "server": "healthy", + "database": None, + "s3": None, + "product": LOCALCRAG_PRODUCT, + "version": LOCALCRAG_VERSION, + } # Test database connection try: diff --git a/server/src/util/localcrag_product.py b/server/src/util/localcrag_product.py new file mode 100644 index 000000000..b978e8805 --- /dev/null +++ b/server/src/util/localcrag_product.py @@ -0,0 +1,5 @@ +"""LocalCrag product identity for health / client discovery (Phase 16).""" + +# Must match client LOCALCRAG_HEALTH_PRODUCT / LOCALCRAG_API_VERSION. +LOCALCRAG_PRODUCT = "localcrag" +LOCALCRAG_VERSION = "1.51.0" diff --git a/server/tests/test_health_resources.py b/server/tests/test_health_resources.py index ff7d90798..d2bf3e533 100644 --- a/server/tests/test_health_resources.py +++ b/server/tests/test_health_resources.py @@ -1,10 +1,32 @@ +from util.localcrag_product import LOCALCRAG_PRODUCT, LOCALCRAG_VERSION + + def test_successful_get_server_health(client, s3_mock, admin_token): response = client.get("/api/health", token=admin_token) - assert response.json == {"server": "healthy", "database": "healthy", "s3": "healthy"} + assert response.json == { + "server": "healthy", + "database": "healthy", + "s3": "healthy", + "product": LOCALCRAG_PRODUCT, + "version": LOCALCRAG_VERSION, + } assert response.status_code == 200 def test_get_server_health_unreachable_s3(client, admin_token): response = client.get("/api/health", token=admin_token) - assert response.json == {"server": "healthy", "database": "healthy", "s3": "Connection failed"} + assert response.json == { + "server": "healthy", + "database": "healthy", + "s3": "Connection failed", + "product": LOCALCRAG_PRODUCT, + "version": LOCALCRAG_VERSION, + } assert response.status_code == 503 + + +def test_health_always_includes_localcrag_identity(client, s3_mock, admin_token): + response = client.get("/api/health", token=admin_token) + assert response.json["product"] == "localcrag" + assert isinstance(response.json["version"], str) + assert response.json["version"]