diff --git a/.github/workflows/build-gui-cli.yml b/.github/workflows/build-gui-cli.yml index be292a3..bc60879 100644 --- a/.github/workflows/build-gui-cli.yml +++ b/.github/workflows/build-gui-cli.yml @@ -3,18 +3,18 @@ name: Build and Release GUI and CLI on: push: tags: - - 'v*' + - "v*" workflow_dispatch: inputs: version: - description: 'Release Version (e.g., v3.1)' + description: "Release Version (e.g., v3.1)" required: true update_jsons_only: - description: 'Only update the JSONs and Changelog (skip build and release)' + description: "Only update the JSONs and Changelog (skip build and release)" type: boolean default: false only_build_update_zip: - description: 'Only build the update ZIP (skip rootfs and full ZIP)' + description: "Only build the update ZIP (skip rootfs and full ZIP)" type: boolean default: false @@ -34,6 +34,10 @@ jobs: ref: main fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest - name: Install dependencies run: sudo apt update && sudo apt install -y jq @@ -112,7 +116,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ matrix.branch }} - fetch-depth: 0 # Fetch all history for proper tag comparison + fetch-depth: 0 # Fetch all history for proper tag comparison - name: Install dependencies run: sudo apt update && sudo apt install -y rsync python3 diff --git a/.gitignore b/.gitignore index 70fb8c2..4aae490 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ *.tar* *.zip *.conf + +**/.nuxt +**/.output +**/dist +**/node_modules diff --git a/build_zip.sh b/build_zip.sh index 9d47d56..8807bc9 100755 --- a/build_zip.sh +++ b/build_zip.sh @@ -2,7 +2,7 @@ set -e # Check for required dependencies -deps=("rsync" "python3") +deps=("rsync" "python3" "bun") for dep in "${deps[@]}"; do if ! command -v "$dep" >/dev/null 2>&1; then echo "$dep is required but not installed." @@ -44,8 +44,18 @@ rm -f "$ZIP_NAME" TMP_DIR=$(mktemp -d) +# Build webroot +cd webroot +bun i +bun run generate +cd .. + # Copy all files except excluded -rsync -a --exclude='.git*' --exclude='Screenshots' --exclude='Docker' --exclude='CHANGELOG.md' --exclude='out' --exclude='update-*.json' --exclude='update_meta.sh' --exclude='build_zip.sh' "$PWD/" "$TMP_DIR/" +rsync -a --exclude='.git*' --exclude='node_modules' --exclude='Screenshots' --exclude='Docker' --exclude='CHANGELOG.md' --exclude='out' --exclude='update-*.json' --exclude='update_meta.sh' --exclude='build_zip.sh' "$PWD/" "$TMP_DIR/" + +# Replace webroot/app with built .output +rm -rf "$TMP_DIR/webroot" +cp -r webroot/.output/public "$TMP_DIR/webroot" # For update builds, remove tar.gz files and add update marker if [ "$UPDATE_FLAG" = "--update" ]; then diff --git a/tools/chroot.sh b/tools/chroot.sh index 9794ce1..8cf9ce4 100755 --- a/tools/chroot.sh +++ b/tools/chroot.sh @@ -22,6 +22,7 @@ HOLDER_PID_FILE="${BASE_CHROOT_DIR}/holder.pid" SILENT=0 SKIP_POST_EXEC=0 CHROOT_SETUP_IN_PROGRESS=0 +NO_AUTO_START=0 # --- Debug mode --- LOGGING_ENABLED=${LOGGING_ENABLED:-0} @@ -71,6 +72,7 @@ usage() { echo " [user] Username to log in as (default: root)." echo " --no-shell Setup chroot without entering an interactive shell." echo " --skip-post-exec Skip running post-execution scripts." + echo " --no-auto-start Prevent automatic chroot startup for commands." echo " -s Silent mode (suppress informational output)." exit 1 } @@ -139,7 +141,7 @@ run_in_chroot() { local command="$*" # Ensure chroot is started if not running - but prevent recursion during setup - if [ "$CHROOT_SETUP_IN_PROGRESS" -eq 0 ]; then + if [ "$CHROOT_SETUP_IN_PROGRESS" -eq 0 ] && [ "$NO_AUTO_START" -eq 0 ]; then if ! is_chroot_running; then log "Starting chroot for command execution..." start_chroot > /dev/null 2>&1 || { @@ -773,11 +775,35 @@ show_status() { fi } +show_raw_status() { + if is_chroot_running; then + echo "RUNNING" + else + echo "STOPPED" + fi +} + list_users() { + if [ "$NO_AUTO_START" -eq 1 ] && ! is_chroot_running; then + echo "" + return + fi run_in_chroot "awk -F: '\$3 >= 1000 && \$3 < 65534 {print \$1}' /etc/passwd 2>/dev/null | tr '\n' ',' | sed 's/,$//'" } +check_existing() { + if [ -d "$CHROOT_PATH" ] || [ -f "$ROOTFS_IMG" ]; then + echo "exists" + else + echo "not_exists" + fi +} + run_command() { + if [ "$NO_AUTO_START" -eq 1 ] && ! is_chroot_running; then + error "Chroot is not running and auto-start is disabled" + return 1 + fi local command="$*" run_in_chroot "$command" } @@ -1157,11 +1183,12 @@ WEBUI_MODE=0 for arg in "$@"; do case "$arg" in - start|stop|restart|status|umount|fstrim|backup|restore|uninstall|list-users|run|resize) + start|stop|restart|status|raw-status|umount|fstrim|backup|restore|uninstall|list-users|run|resize|check_existing) COMMAND="$arg" ;; --no-shell) NO_SHELL_FLAG=1 ;; --webui) WEBUI_MODE=1 ;; --skip-post-exec) SKIP_POST_EXEC=1 ;; + --no-auto-start) NO_AUTO_START=1 ;; -s) SILENT=1 ;; -h|--help) usage ;; -*) echo "Unknown option: $arg"; usage ;; @@ -1191,6 +1218,7 @@ case "$COMMAND" in if [ "$NO_SHELL_FLAG" -eq 0 ]; then enter_chroot "$USER_ARG"; else log "Chroot setup complete (no-shell mode). Use 'sh $0 start' to enter."; fi ;; status) show_status ;; + raw-status) show_raw_status ;; umount) log "Umounting chroot filesystems..."; umount_chroot; log "Chroot filesystems unmounted successfully." ;; fstrim) @@ -1221,5 +1249,6 @@ case "$COMMAND" in exit 1 fi resize_sparse "$RESIZE_SIZE" ;; + check_existing) check_existing ;; *) error "Invalid command: $COMMAND"; usage ;; esac diff --git a/webroot/app/app.vue b/webroot/app/app.vue new file mode 100644 index 0000000..60376f1 --- /dev/null +++ b/webroot/app/app.vue @@ -0,0 +1,13 @@ + + + +> diff --git a/webroot/app/assets/css/animations.css b/webroot/app/assets/css/animations.css new file mode 100644 index 0000000..9042b10 --- /dev/null +++ b/webroot/app/assets/css/animations.css @@ -0,0 +1,80 @@ +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translate3d(0, 8px, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +@keyframes chunkFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes dotFill { + 0%, + 20.1%, + 100% { + opacity: 0.3; + } + 0.1%, + 20% { + opacity: 1; + } +} + +.loading-dot { + animation: dotFill 3.75s infinite; +} + +.loading-dot:nth-child(1) { + animation-delay: 0s; +} +.loading-dot:nth-child(2) { + animation-delay: 0.75s; +} +.loading-dot:nth-child(3) { + animation-delay: 1.5s; +} +.loading-dot:nth-child(4) { + animation-delay: 2.25s; +} +.loading-dot:nth-child(5) { + animation-delay: 3s; +} + +.console > div.progress-indicator.log-immediate { + animation: pulse 1.5s ease-in-out infinite !important; +} + +.console > div.log-immediate { + animation: none; +} + +.console > div.log-chunk-fade { + animation: chunkFadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.console > div.log-fade-in { + animation: fadeInUp 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.console .progress-indicator { + animation: pulse 1.5s ease-in-out infinite; +} diff --git a/webroot/app/assets/css/dark.css b/webroot/app/assets/css/dark.css new file mode 100644 index 0000000..b3e08e1 --- /dev/null +++ b/webroot/app/assets/css/dark.css @@ -0,0 +1,327 @@ +[data-theme="dark"] { + --bg: #0f0f0f; + --card: #1a1a1a; + --text: #ffffff; + --muted: #a0a0a0; + --accent: #60a5fa; + --danger: #fb7185; + --console-bg: rgba(255, 255, 255, 0.03); +} +[data-theme="dark"] .console-copy-btn { + border-color: rgba(255, 255, 255, 0.08); + outline: none; +} +[data-theme="dark"] + .console-copy-btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1) !important; + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.15); +} +[data-theme="dark"] .console-copy-btn:active, +[data-theme="dark"] .console-copy-btn:focus { + background: transparent !important; + border-color: rgba(255, 255, 255, 0.08) !important; + transform: none !important; + outline: none !important; +} +[data-theme="dark"] + .console-copy-btn:not(:hover):not(.btn-pressed):not(.btn-released) { + box-shadow: none !important; + transform: none !important; +} +[data-theme="dark"] .switch { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.15); +} +[data-theme="dark"] .switch:hover { + background: rgba(255, 255, 255, 0.15); +} +[data-theme="dark"] .slider { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); +} +[data-theme="dark"] .switch input:checked + .slider { + box-shadow: 0 2px 8px rgba(var(--accent-rgb, 96, 165, 250), 0.4); +} +[data-theme="dark"] .card { + border: 1px solid rgba(255, 255, 255, 0.12); +} + +[data-theme="dark"] .btn-box { + background: var(--card); + border: 1px solid rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .section-separator { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.12) 20%, + rgba(255, 255, 255, 0.12) 80%, + transparent 100% + ); +} +[data-theme="dark"] .icon-moon { + display: inline-block; +} +[data-theme="dark"] .icon-sun { + display: none; +} +/* Dark mode theme button borders */ +[data-theme="dark"] .theme-btn { + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme="dark"] .theme-btn[disabled] { + opacity: 0.5; +} + +/* Ensure outline buttons remain visible in dark mode (lighten the border) */ +[data-theme="dark"] .btn.outline { + border-color: rgba(255, 255, 255, 0.15); + color: var(--text); +} + +[data-theme="dark"] .btn { + background: rgba(37, 99, 235, 0.1); + border: 1px solid var(--accent); + color: var(--accent); +} + +[data-theme="dark"] .btn.primary { + background: rgba(37, 99, 235, 0.1); + border: 1px solid var(--accent); + color: var(--accent); +} + +[data-theme="dark"] .btn.stop { + background: rgba(220, 38, 38, 0.1) !important; + border: 1px solid #dc2626 !important; + color: #dc2626 !important; +} + +[data-theme="dark"] #restart-btn { + background: rgba(234, 88, 12, 0.1) !important; + border: 1px solid #ea580c !important; + color: #ea580c !important; +} + +[data-theme="dark"] #start-btn { + background: rgba(22, 163, 74, 0.1) !important; + border: 1px solid #16a34a !important; + color: #16a34a !important; +} + +[data-theme="dark"] .btn[disabled] { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.5); +} + +[data-theme="dark"] + .btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +[data-theme="dark"] + .btn.primary:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +[data-theme="dark"] + .btn.stop:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: #dc2626 !important; + color: white !important; + border-color: #dc2626 !important; +} + +[data-theme="dark"] + #restart-btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: #ea580c !important; + color: white !important; + border-color: #ea580c !important; +} + +[data-theme="dark"] + #start-btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: #16a34a !important; + color: white !important; + border-color: #16a34a !important; +} +[data-theme="dark"] .debug-indicator-tiny { + background: #f59e0b; + color: #92400e; + border-color: #d97706; +} +[data-theme="dark"] .console .err { + color: #fb7185; +} +[data-theme="dark"] .console .warn { + color: #fbbf24; +} +[data-theme="dark"] .console .success { + color: #4ade80; +} +[data-theme="dark"] .console .info { + color: #60a5fa; +} +[data-theme="dark"] .console .progress-indicator { + color: #60a5fa; +} +[data-theme="dark"] .console::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + transition: + opacity 0.3s ease, + background 0.2s ease; +} +[data-theme="dark"] .console::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} +[data-theme="dark"] .console::-webkit-scrollbar-corner { + background: transparent; +} + +[data-theme="dark"] .top { + border-bottom-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .header-logo { + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)) brightness(1.2) + contrast(1.1); +} +[data-theme="dark"] .form-group input, +[data-theme="dark"] .form-group select { + border-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .form-group input:focus, +[data-theme="dark"] .form-group select:focus { + border-color: #60a5fa; +} +[data-theme="dark"] .password-toggle:hover { + background: rgba(255, 255, 255, 0.05); +} +[data-theme="dark"] .user-select { + border-color: rgba(255, 255, 255, 0.08); + background: var(--card); +} +[data-theme="dark"] .user-select:hover:not(:disabled) { + border-color: rgba(255, 255, 255, 0.15); +} +[data-theme="dark"] .user-select:disabled { + background: rgba(255, 255, 255, 0.03); +} +[data-theme="dark"] .popup-content { + border-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .popup-header { + border-bottom-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .close-btn { + border-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .close-btn:hover { + background: rgba(255, 255, 255, 0.05); +} +[data-theme="dark"] .setting-section { + border-color: rgba(255, 255, 255, 0.08); +} +[data-theme="dark"] .danger-zone-section { + border-color: rgba(251, 113, 133, 0.2) !important; +} + +[data-theme="dark"] .danger-zone-section:hover { + border-color: var(--danger) !important; +} + +[data-theme="dark"] .danger-zone-section:focus, +[data-theme="dark"] .danger-zone-section:focus-within { + outline: none; + border-color: var(--danger) !important; +} +[data-theme="dark"] .script-editor { + background: var(--card); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme="dark"] .btn.danger { + background: rgba(220, 38, 38, 0.1); + border: 1px solid var(--danger); + color: var(--danger); + box-shadow: none !important; +} + +[data-theme="dark"] + .btn.danger:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { + background: var(--danger); + color: white; + border-color: var(--danger); + box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3) !important; +} +[data-theme="dark"] + .btn.danger:not(:hover):not(.btn-pressed):not(.btn-released) { + box-shadow: none !important; +} + +[data-theme="dark"] .warning-banner { + background: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.3); +} +[data-theme="dark"] .warning-text { + color: #fbbf24; +} +[data-theme="dark"] .warning-close { + border-color: rgba(251, 191, 36, 0.3); + color: #fbbf24; +} +[data-theme="dark"] .warning-close:hover { + background: rgba(251, 191, 36, 0.1); +} +[data-theme="dark"] .experimental-section { + border-color: rgba(251, 191, 36, 0.3) !important; +} + +[data-theme="dark"] .experimental-section:hover { + border-color: rgba(251, 191, 36, 0.5) !important; +} + +[data-theme="dark"] .experimental-section:focus, +[data-theme="dark"] .experimental-section:focus-within { + outline: none; + border-color: #fbbf24 !important; +} + +[data-theme="dark"] .optional-section { + border-color: rgba(251, 113, 133, 0.2) !important; +} + +[data-theme="dark"] .optional-section:hover { + border-color: var(--danger) !important; +} + +[data-theme="dark"] .optional-section:focus, +[data-theme="dark"] .optional-section:focus-within { + outline: none; + border-color: var(--danger) !important; +} +[data-theme="dark"] .setting-subsection { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.06); +} +[data-theme="dark"] .btn.warning { + background: rgba(245, 158, 11, 0.1) !important; + border: 1px solid #f59e0b; + color: #92400e !important; +} +[data-theme="dark"] .btn.warning:hover { + background: #f59e0b; + color: #92400e; + border-color: #f59e0b; +} + +[data-theme="dark"] .storage-info-table tbody tr { + border-bottom-color: rgba(255, 255, 255, 0.06); +} +[data-theme="dark"] .debug-warning-text { + color: #fb7185; +} diff --git a/webroot/assets/styles.css b/webroot/app/assets/css/light.css similarity index 50% rename from webroot/assets/styles.css rename to webroot/app/assets/css/light.css index c1e62da..7da503c 100644 --- a/webroot/assets/styles.css +++ b/webroot/app/assets/css/light.css @@ -1,5 +1,5 @@ /* Minimal modern styles: light/dark using data-theme attribute */ -:root{ +:root { --bg: #f6f8fa; --card: #ffffff; --text: #0f1720; @@ -7,25 +7,68 @@ --accent: #2563eb; --danger: #dc2626; --surface-radius: 12px; - --console-bg: rgba(0,0,0,0.04); + --console-bg: rgba(0, 0, 0, 0.04); /* Safe area insets like Tricky Addon */ /* Use max() to ensure minimum status bar height on Android devices where safe-area-inset-top may not work */ --top-inset: max(env(safe-area-inset-top, 0px), 29px); --bottom-inset: env(safe-area-inset-bottom, 0px); } -[data-theme="dark"]{ - --bg: #0f0f0f; - --card: #1a1a1a; - --text: #ffffff; - --muted: #a0a0a0; - --accent: #60a5fa; - --danger: #fb7185; - --console-bg: rgba(255,255,255,0.03); -} -*{box-sizing:border-box} -html,body{height:100%;min-height:100vh;background:var(--bg)} + +* { + box-sizing: border-box; +} +html, +body { + height: 100%; + min-height: 100vh; + background: var(--bg); +} /* Prevent horizontal page scrolling; allow vertical scroll when content exceeds viewport */ -body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial; background:var(--bg); color:var(--text);overflow-x:hidden;padding-top:var(--top-inset);padding-bottom:var(--bottom-inset);box-sizing:border-box} +body { + margin: 0; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial; + background: var(--bg); + color: var(--text); + overflow-x: hidden; + padding-top: var(--top-inset); + padding-bottom: var(--bottom-inset); + box-sizing: border-box; +} + +/* Global button styles to prevent active state sticking on mobile */ +button { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +button:active { + transform: none !important; + box-shadow: none !important; + outline: none !important; +} + +/* Prevent hover state from sticking on touch devices */ +@media (hover: none) { + button:hover, + [class*="btn"]:hover, + .password-toggle:hover, + .user-select:hover { + transform: none !important; + box-shadow: none !important; + background: inherit !important; + border-color: inherit !important; + } +} /* Disable text selection globally */ * { @@ -47,30 +90,135 @@ textarea { user-select: text; } -.app{max-width:1200px;width:100%;box-sizing:border-box;margin:0 auto;padding:12px;display:flex;flex-direction:column;min-height:calc(100vh - var(--top-inset) - var(--bottom-inset))} - -.top{display:flex;justify-content:space-between;align-items:center;position:fixed;top:0;left:50%;transform:translateX(-50%);padding:calc(var(--top-inset) + 8px) 12px 8px;background:var(--bg);z-index:10;width:100%;max-width:1200px;box-sizing:border-box;border-bottom:1px solid rgba(0,0,0,0.05)} -.panel{display:flex;flex-direction:column;gap:14px;min-width:0;margin-top:calc(44px + var(--top-inset));flex:1 1 auto} -.card{background:var(--card);border-radius:var(--surface-radius);padding:16px;box-shadow:0 6px 20px rgba(6,8,14,0.06);box-sizing:border-box;min-width:0;border:1px solid rgba(0,0,0,0.12)} -.title{margin:0 0 10px 0;display:flex;align-items:center} -.row{display:flex;align-items:center;justify-content:space-between;margin-top:8px} -.status-row{align-items:center} -.status-pill{display:flex;align-items:center;gap:10px} -.dot{width:12px;height:12px;border-radius:999px;display:inline-block} -.dot-on{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,0.18)} -.dot-off{background:#ef4444;opacity:0.95} -.dot-unknown{background:#f59e0b;box-shadow:0 0 8px rgba(245,158,11,0.12)} -.dot-warn{background:#f59e0b;box-shadow:0 0 8px rgba(245,158,11,0.18)} -.actions{display:flex;gap:8px} -.btn{background:var(--accent);color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;box-shadow:none} -.btn.small{padding:6px 8px;font-size:13px} -.btn.outline{background:transparent;border:1px solid rgba(15,23,32,0.08);color:var(--text);box-shadow:none !important} - -.btn.primary{background:var(--accent)} -.btn.stop{background:#dc2626 !important;} -#restart-btn{background:#ea580c !important;} -#start-btn{background:#16a34a !important;} -.btn[disabled]{opacity:0.5;cursor:not-allowed} +.app { + max-width: 1200px; + width: 100%; + box-sizing: border-box; + margin: 0 auto; + padding: 12px; + display: flex; + flex-direction: column; + min-height: calc(100vh - var(--top-inset) - var(--bottom-inset)); +} + +.top { + display: flex; + justify-content: space-between; + align-items: center; + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + padding: calc(var(--top-inset) + 8px) 12px 8px; + background: var(--bg); + z-index: 10; + width: 100%; + max-width: 1200px; + box-sizing: border-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} +.panel { + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; + margin-top: calc(44px + var(--top-inset)); + flex: 1 1 auto; +} +.card { + background: var(--card); + border-radius: var(--surface-radius); + padding: 16px; + box-shadow: 0 6px 20px rgba(6, 8, 14, 0.06); + box-sizing: border-box; + min-width: 0; + border: 1px solid rgba(0, 0, 0, 0.12); +} +.title { + margin: 0 0 10px 0; + display: flex; + align-items: center; +} +.row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; +} +.status-row { + align-items: center; +} +.status-pill { + display: flex; + align-items: center; + gap: 10px; +} +.dot { + width: 12px; + height: 12px; + border-radius: 999px; + display: inline-block; +} +.dot-on { + background: #22c55e; + box-shadow: 0 0 8px rgba(34, 197, 94, 0.18); +} +.dot-off { + background: #ef4444; + opacity: 0.95; +} +.dot-unknown { + background: #f59e0b; + box-shadow: 0 0 8px rgba(245, 158, 11, 0.12); +} +.dot-warn { + background: #f59e0b; + box-shadow: 0 0 8px rgba(245, 158, 11, 0.18); +} +.actions { + display: flex; + gap: 8px; +} +.btn { + background: var(--accent); + color: white; + border: 0; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + box-shadow: none; +} +.btn.small { + padding: 6px 8px; + font-size: 13px; +} +.btn.outline { + background: transparent; + border: 1px solid rgba(15, 23, 32, 0.08); + color: var(--text); + box-shadow: none !important; +} + +.btn.primary { + background: var(--accent); +} +.btn.stop { + background: #dc2626 !important; +} +#restart-btn { + background: #ea580c !important; +} +#start-btn { + background: #16a34a !important; +} +.btn[disabled] { + opacity: 0.5; + cursor: not-allowed; +} /* Enhanced hover effects for main action buttons */ .btn.stop:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { @@ -97,15 +245,44 @@ textarea { #start-btn:not(:hover):not(.btn-pressed):not(.btn-released) { box-shadow: none !important; } -.row-label{color:var(--muted);font-size:14px;font-weight:500} -.toggle-inline{display:inline-flex;gap:8px;align-items:center;font-size:14px} -.note{color:var(--muted);font-size:14px} -.console-card{display:flex;flex-direction:column;min-height:200px;min-width:0;flex:1 1 auto;overflow:hidden;max-height:100%} -.console-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} -.console-title{font-weight:600;font-size:16px;color:var(--muted)} +.row-label { + color: var(--muted); + font-size: 14px; + font-weight: 500; +} +.toggle-inline { + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 14px; +} +.note { + color: var(--muted); + font-size: 14px; +} +.console-card { + display: flex; + flex-direction: column; + min-height: 200px; + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + max-height: 100%; +} +.console-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} +.console-title { + font-weight: 600; + font-size: 16px; + color: var(--muted); +} .console-copy-btn { background: transparent; - border: 1px solid rgba(15,23,32,0.08); + border: 1px solid rgba(15, 23, 32, 0.08); color: var(--text); border-radius: 4px; width: 32px; @@ -114,10 +291,11 @@ textarea { align-items: center; justify-content: center; font-size: 13px; - transition: transform 0.12s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.12s cubic-bezier(0.4, 0, 0.2, 1), - background 0.15s ease, - border-color 0.15s ease; + transition: + transform 0.12s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.12s cubic-bezier(0.4, 0, 0.2, 1), + background 0.15s ease, + border-color 0.15s ease; padding: 0; min-height: auto; -webkit-tap-highlight-color: transparent; @@ -127,22 +305,22 @@ textarea { will-change: transform, box-shadow; } .console-copy-btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { - box-shadow: 0 2px 6px rgba(0,0,0,0.1) !important; - background: rgba(0,0,0,0.05); - border-color: rgba(15,23,32,0.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1) !important; + background: rgba(0, 0, 0, 0.05); + border-color: rgba(15, 23, 32, 0.15); } .console-copy-btn:active, .console-copy-btn:focus { background: transparent !important; - border-color: rgba(15,23,32,0.08) !important; + border-color: rgba(15, 23, 32, 0.08) !important; transform: none !important; outline: none !important; } .console-copy-btn.btn-pressed { - box-shadow: 0 2px 8px rgba(0,0,0,0.2) !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important; transform: scale(0.96) !important; background: transparent !important; - border-color: rgba(15,23,32,0.08) !important; + border-color: rgba(15, 23, 32, 0.08) !important; } .console-copy-btn.btn-released { box-shadow: none !important; @@ -152,125 +330,276 @@ textarea { box-shadow: none !important; transform: none !important; } -.console-copy-btn svg{width:16px;height:16px} +.console-copy-btn svg { + width: 16px; + height: 16px; +} -[data-theme="dark"] .console-copy-btn { - border-color: rgba(255,255,255,0.08); - outline: none; +.console { + flex: 1 1 0; + background: var(--console-bg); + border-radius: 10px; + padding: 12px; + min-height: 0; + max-height: 100%; + overflow: auto; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; + border: 1px solid rgba(0, 0, 0, 0.06); + font-size: 13px; + line-height: 1.5; + white-space: pre; + box-sizing: border-box; + min-width: 0; + width: 100%; + transform: translateZ(0); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -[data-theme="dark"] .console-copy-btn:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { - box-shadow: 0 2px 6px rgba(0,0,0,0.1) !important; - background: rgba(255,255,255,0.05); - border-color: rgba(255,255,255,0.15); +.console-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 10px; } -[data-theme="dark"] .console-copy-btn:active, -[data-theme="dark"] .console-copy-btn:focus { - background: transparent !important; - border-color: rgba(255,255,255,0.08) !important; - transform: none !important; - outline: none !important; +.foot { + margin-top: auto; + padding-top: 14px; + padding-bottom: calc(12px + var(--bottom-inset)); + color: var(--muted); + font-size: 14px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + font-weight: 500; + flex-shrink: 0; } -[data-theme="dark"] .console-copy-btn:not(:hover):not(.btn-pressed):not(.btn-released) { - box-shadow: none !important; - transform: none !important; +.foot a { + color: var(--muted); + text-decoration: none; + transition: color 0.2s ease; + font-weight: 500; + -webkit-tap-highlight-color: transparent; +} +.foot a:hover { + color: var(--accent); + text-decoration: underline; +} +.foot .separator { + color: var(--muted); + font-weight: 400; +} +.switch { + position: relative; + display: inline-block; + width: 48px; + height: 26px; + border-radius: 13px; + background: var(--muted); + border: 1px solid rgba(0, 0, 0, 0.08); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} +.switch input { + display: none; +} +.slider { + position: absolute; + top: 1px; + left: 1px; + width: 22px; + height: 22px; + background: white; + border-radius: 50%; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} +.switch input:checked + .slider { + transform: translateX(22px); +} +.switch input:checked + .slider { + background: var(--accent); + box-shadow: 0 2px 8px rgba(var(--accent-rgb, 37, 99, 235), 0.3); +} +.switch input:focus-visible + .slider { + box-shadow: 0 0 0 3px rgba(var(--accent-rgb, 37, 99, 235), 0.2); +} +.switch:hover { + background: rgba(0, 0, 0, 0.05); +} +.switch input:checked:hover { + background: var(--accent); +} + +.small { + font-size: 12px; } -.console{flex:1 1 0;background:var(--console-bg);border-radius:10px;padding:12px;min-height:0;max-height:100%;overflow:auto;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;border:1px solid rgba(0,0,0,0.06);font-size:13px;line-height:1.5;white-space:pre;box-sizing:border-box;min-width:0;width:100%;transform:translateZ(0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} -.console-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px} -.foot{margin-top:auto;padding-top:14px;padding-bottom:calc(12px + var(--bottom-inset));color:var(--muted);font-size:14px;text-align:center;display:flex;align-items:center;justify-content:center;gap:12px;font-weight:500;flex-shrink:0} -.foot a{color:var(--muted);text-decoration:none;transition:color 0.2s ease;font-weight:500;-webkit-tap-highlight-color:transparent} -.foot a:hover{color:var(--accent);text-decoration:underline} -.foot .separator{color:var(--muted);font-weight:400} -.switch{position:relative;display:inline-block;width:48px;height:26px;border-radius:13px;background:var(--muted);border:1px solid rgba(0,0,0,0.08);transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1);cursor:pointer;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none} -.switch input{display:none} -.slider{position:absolute;top:1px;left:1px;width:22px;height:22px;background:white;border-radius:50%;transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1);box-shadow:0 2px 4px rgba(0,0,0,0.2)} -.switch input:checked + .slider{transform:translateX(22px)} -.switch input:checked + .slider{background:var(--accent);box-shadow:0 2px 8px rgba(var(--accent-rgb, 37, 99, 235), 0.3)} -.switch input:focus-visible + .slider{box-shadow:0 0 0 3px rgba(var(--accent-rgb, 37, 99, 235), 0.2)} -.switch:hover{background:rgba(0,0,0,0.05)} -.switch input:checked:hover{background:var(--accent)} - -[data-theme="dark"] .switch{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15)} -[data-theme="dark"] .switch:hover{background:rgba(255,255,255,0.15)} -[data-theme="dark"] .slider{box-shadow:0 2px 4px rgba(0,0,0,0.4)} -[data-theme="dark"] .switch input:checked + .slider{box-shadow:0 2px 8px rgba(var(--accent-rgb, 96, 165, 250), 0.4)} -.small{font-size:12px} /* responsive behavior */ -@media (min-width:720px){ - .panel{display:grid;grid-template-columns:1fr 1fr;flex:1 1 auto} - .console-card{min-height:300px;flex:1 1 auto;overflow:hidden} -} - -@media (max-width:719px){ - .app{max-width:100%;margin:8px auto;padding:10px} - .title{font-size:1.05rem} - .brand{font-size:16px} - .header-logo{height:24px;max-width:120px} - .btn-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px} - .btn-row .btn{width:100%;min-width:0;padding:8px 6px;font-size:14px} - .btn-box{padding:8px} +@media (min-width: 720px) { + .panel { + display: grid; + grid-template-columns: 1fr 1fr; + flex: 1 1 auto; + } + .console-card { + min-height: 300px; + flex: 1 1 auto; + overflow: hidden; + } +} + +@media (max-width: 719px) { + .app { + max-width: 100%; + margin: 8px auto; + padding: 10px; + } + .title { + font-size: 1.05rem; + } + .brand { + font-size: 16px; + } + .header-logo { + height: 24px; + max-width: 120px; + } + .btn-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + } + .btn-row .btn { + width: 100%; + min-width: 0; + padding: 8px 6px; + font-size: 14px; + } + .btn-box { + padding: 8px; + } /* Console fills available space on mobile with proper scrolling */ - .console{min-height:0;overflow:auto} - .console-card{flex:1 1 auto;overflow:hidden} + .console { + min-height: 0; + overflow: auto; + } + .console-card { + flex: 1 1 auto; + overflow: hidden; + } /* Extra footer spacing on mobile */ - .foot{padding-top:16px;padding-bottom:calc(16px + var(--bottom-inset))} + .foot { + padding-top: 16px; + padding-bottom: calc(16px + var(--bottom-inset)); + } } - /* header controls alignment tweaks */ -.top .controls{display:flex;align-items:center;gap:12px} -.actions button{min-height:36px;display:inline-flex;align-items:center;justify-content:center} -.btn{min-height:36px} - -/* box containing the three main buttons */ -.btn-box{background:var(--card);border-radius:10px;padding:8px;border:1px solid rgba(0,0,0,0.12)} -.btn-row{display:flex;gap:10px} -.btn-row .btn{flex:1;text-align:center} -.btn-row .btn.stop{flex:1} - -[data-theme="dark"] .card { - border: 1px solid rgba(255,255,255,0.12); +.top .controls { + display: flex; + align-items: center; + gap: 12px; +} +.actions button { + min-height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.btn { + min-height: 36px; } -[data-theme="dark"] .btn-box{ +/* box containing the three main buttons */ +.btn-box { background: var(--card); - border: 1px solid rgba(255,255,255,0.08); + border-radius: 10px; + padding: 8px; + border: 1px solid rgba(0, 0, 0, 0.12); +} +.btn-row { + display: flex; + gap: 10px; +} +.btn-row .btn { + flex: 1; + text-align: center; +} +.btn-row .btn.stop { + flex: 1; } /* Section separator */ -.section-separator{ +.section-separator { height: 1px; - background: linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.08) 20%, rgba(0,0,0,0.08) 80%, transparent 100%); + background: linear-gradient( + 90deg, + transparent 0%, + rgba(0, 0, 0, 0.08) 20%, + rgba(0, 0, 0, 0.08) 80%, + transparent 100% + ); margin: 12px -2px 8px -2px; border-radius: 1px; } -[data-theme="dark"] .section-separator{ - background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.12) 20%, rgba(255,255,255,0.12) 80%, transparent 100%); +/* theme button */ +.theme-btn { + display: inline-flex; + gap: 8px; + align-items: center; + background: transparent; + border: 1px solid rgba(15, 23, 32, 0.1); + padding: 6px 10px; + border-radius: 999px; + cursor: pointer; + color: var(--text); + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + box-shadow: none !important; + transition: box-shadow 0.15s ease; } -/* theme button */ -.theme-btn{display:inline-flex;gap:8px;align-items:center;background:transparent;border:1px solid rgba(15,23,32,0.1);padding:6px 10px;border-radius:999px;cursor:pointer;color:var(--text);-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;box-shadow:none !important;transition:box-shadow 0.15s ease} -.theme-btn .icon{opacity:1;transition:transform .18s,opacity .18s} -.theme-btn:focus{outline:none;box-shadow:0 0 0 4px rgba(37,99,235,0.08) !important} -.theme-btn:not(:focus){box-shadow:none !important} -.theme-btn:active{box-shadow:none !important} +.theme-btn[disabled] { + opacity: 0.5; + cursor: not-allowed; +} +.theme-btn .icon { + opacity: 1; + transition: + transform 0.18s, + opacity 0.18s; +} +.theme-btn:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.08) !important; +} +.theme-btn:not(:focus) { + box-shadow: none !important; +} +.theme-btn:active { + box-shadow: none !important; +} /* when dark, show moon and hide sun via css */ -:root .icon-moon{display:none} -[data-theme="dark"] .icon-moon{display:inline-block} -[data-theme="dark"] .icon-sun{display:none} +:root .icon-moon { + display: none; +} /* smaller icons spacing */ -.theme-btn svg{display:block;width:18px;height:18px} - -/* Dark mode theme button borders */ -[data-theme="dark"] .theme-btn { - border-color: rgba(255,255,255,0.08); +.theme-btn svg { + display: block; + width: 18px; + height: 18px; } -/* Ensure outline buttons remain visible in dark mode (lighten the border) */ -[data-theme="dark"] .btn.outline{border-color: rgba(255,255,255,0.15); color:var(--text);} - /* Debug indicator styling */ .debug-indicator-tiny { display: inline-block; @@ -290,75 +619,51 @@ textarea { flex-shrink: 0; } -[data-theme="dark"] .debug-indicator-tiny { - background: #f59e0b; - color: #92400e; - border-color: #d97706; -} - /* Console message styling */ -.console > div{margin:2px 0;padding:2px 0} -.console .err{color:#ef4444;font-weight:500} -.console .warn{color:#f59e0b;font-weight:500} -.console .success{color:#22c55e;font-weight:500} -.console .info{color:var(--accent);font-weight:500} -.console .progress-indicator{color:#3b82f6;font-weight:500;animation:pulse 1.5s ease-in-out infinite} -[data-theme="dark"] .console .err{color:#fb7185} -[data-theme="dark"] .console .warn{color:#fbbf24} -[data-theme="dark"] .console .success{color:#4ade80} -[data-theme="dark"] .console .info{color:#60a5fa} -[data-theme="dark"] .console .progress-indicator{color:#60a5fa} - -@keyframes pulse{ - 0%, 100%{opacity:1} - 50%{opacity:0.6} -} - -@keyframes fadeInUp{ - from{ - opacity:0; - transform:translate3d(0, 8px, 0); - } - to{ - opacity:1; - transform:translate3d(0, 0, 0); - } +.console > div { + margin: 2px 0; + padding: 2px 0; +} +.console .err { + color: #ef4444; + font-weight: 500; +} +.console .warn { + color: #f59e0b; + font-weight: 500; +} +.console .success { + color: #22c55e; + font-weight: 500; +} +.console .info { + color: var(--accent); + font-weight: 500; +} +.console .progress-indicator { + color: #3b82f6; + font-weight: 500; } -.console > div.log-fade-in{ - animation:fadeInUp 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards; - opacity:0; +.console > div.log-fade-in { + opacity: 0; will-change: opacity, transform; backface-visibility: hidden; - transform:translateZ(0); + transform: translateZ(0); } -/* Chunk fade-in animation for batched logs */ -.console > div.log-chunk-fade{ - animation:chunkFadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; - opacity:0; +.console > div.log-chunk-fade { + opacity: 0; will-change: opacity; backface-visibility: hidden; } -@keyframes chunkFadeIn{ - from{ - opacity:0; - } - to{ - opacity:1; - } -} - -.console > div.log-immediate{ - animation:none; - opacity:1; +.console > div.log-immediate { + opacity: 1; } -/* Progress indicators should always pulse, even with log-immediate class */ -.console > div.progress-indicator.log-immediate{ - animation:pulse 1.5s ease-in-out infinite !important; - opacity:1; +.console > div.progress-indicator.log-immediate { + opacity: 1; } /* ============================================================================ @@ -370,10 +675,11 @@ textarea { /* Base button styles - NO shadow by default */ .btn { - transition: transform 0.12s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.12s cubic-bezier(0.4, 0, 0.2, 1), - background 0.15s ease, - border-color 0.15s ease; + transition: + transform 0.12s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.12s cubic-bezier(0.4, 0, 0.2, 1), + background 0.15s ease, + border-color 0.15s ease; -webkit-tap-highlight-color: transparent; outline: none; box-shadow: none; @@ -436,46 +742,84 @@ textarea { transform: none !important; } -/* Smooth console scrolling - * NOTE: We let JavaScript control smooth auto-scroll via scrollTo({ behavior: 'smooth' }) - * to avoid animation stacking. Here we only style visible scrollbars. - */ -.console::-webkit-scrollbar{width:8px;height:8px} -.console::-webkit-scrollbar-track{background:transparent} -.console::-webkit-scrollbar-thumb{background:rgba(0,0,0,0.2);border-radius:4px;transition:opacity 0.3s ease, background 0.2s ease} -.console::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,0.3)} - -[data-theme="dark"] .console::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.2);transition:opacity 0.3s ease, background 0.2s ease} -[data-theme="dark"] .console::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.3)} -[data-theme="dark"] .console::-webkit-scrollbar-corner{background:transparent} +.console::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.console::-webkit-scrollbar-track { + background: transparent; +} +.console::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + transition: + opacity 0.3s ease, + background 0.2s ease; +} +.console::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.3); +} /* Fade-out / fade-in helper class for console scrollbar. * JS toggles .console-scrollbar-hidden while long actions are running. */ -.console-scrollbar-hidden{ +.console-scrollbar-hidden { scrollbar-color: transparent transparent; /* Firefox */ } -.console-scrollbar-hidden::-webkit-scrollbar-thumb{ - background:transparent !important; - opacity:0; +.console-scrollbar-hidden::-webkit-scrollbar-thumb { + background: transparent !important; + opacity: 0; } /* header controls alignment tweaks */ -.top{border-bottom-color: rgba(0,0,0,0.05)} -[data-theme="dark"] .top{border-bottom-color: rgba(255,255,255,0.08)} +.top { + border-bottom-color: rgba(0, 0, 0, 0.05); +} /* Logo styles */ -.brand{display:flex;align-items:center;font-weight:700;font-size:18px} -.controls{flex:0 0 auto} -.header-logo{height:32px;width:auto;max-width:200px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.1))} -[data-theme="dark"] .header-logo{filter:drop-shadow(0 1px 2px rgba(0,0,0,0.5)) brightness(1.2) contrast(1.1)} - -.form-group{margin-bottom:16px} -.form-group label{display:block;margin-bottom:6px;font-weight:500;color:var(--text)} -.form-group input,.form-group select{padding:8px 12px;border:1px solid rgba(0,0,0,0.08);border-radius:8px;background:var(--card);color:var(--text);font-size:14px;width:100%;box-sizing:border-box} -.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(59,130,246,0.2)} -[data-theme="dark"] .form-group input,[data-theme="dark"] .form-group select{border-color:rgba(255,255,255,0.08)} -[data-theme="dark"] .form-group input:focus,[data-theme="dark"] .form-group select:focus{border-color:#60a5fa} +.brand { + display: flex; + align-items: center; + font-weight: 700; + font-size: 18px; +} +.controls { + flex: 0 0 auto; +} +.header-logo { + height: 32px; + width: auto; + max-width: 200px; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); +} + +.form-group { + margin-bottom: 16px; +} +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--text); +} +.form-group input, +.form-group select { + padding: 8px 12px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 8px; + background: var(--card); + color: var(--text); + font-size: 14px; + width: 100%; + box-sizing: border-box; +} +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); +} /* Password input with toggle */ .password-input-container { @@ -509,43 +853,42 @@ textarea { } .password-toggle:hover { - background: rgba(0,0,0,0.05); + background: rgba(0, 0, 0, 0.05); color: var(--text); } -[data-theme="dark"] .password-toggle:hover { - background: rgba(255,255,255,0.05); -} - .password-toggle:active { transform: translateY(-50%) scale(0.95); } /* User select */ -.user-select{ - padding:6px 10px; - border:1px solid rgba(0,0,0,0.08); - border-radius:6px; - background:var(--card); - color:var(--text); - font-size:14px; - cursor:pointer; - transition:all 0.2s ease; - min-width:100px; +.user-select { + padding: 6px 10px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 6px; + background: var(--card); + color: var(--text); + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; } -.user-select:focus{outline:2px solid var(--accent);outline-offset:1px} -.user-select:hover:not(:disabled){border-color:rgba(0,0,0,0.15)} -.user-select:disabled{opacity:0.5;cursor:not-allowed;background:rgba(0,0,0,0.03)} -[data-theme="dark"] .user-select{ - border-color:rgba(255,255,255,0.08); - background:var(--card); +.user-select:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; +} +.user-select:hover:not(:disabled) { + border-color: rgba(0, 0, 0, 0.15); +} +.user-select:disabled { + opacity: 0.5; + cursor: not-allowed; + background: rgba(0, 0, 0, 0.03); } -[data-theme="dark"] .user-select:hover:not(:disabled){border-color:rgba(255,255,255,0.15)} -[data-theme="dark"] .user-select:disabled{background:rgba(255,255,255,0.03)} /* Settings Popup - matches primary UI styling */ .popup-overlay { @@ -561,7 +904,9 @@ textarea { z-index: 1000; opacity: 0; visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; + transition: + opacity 0.3s ease, + visibility 0.3s ease; } .popup-overlay.active { @@ -572,18 +917,14 @@ textarea { .popup-content { background: var(--card); border-radius: var(--surface-radius); - box-shadow: 0 6px 20px rgba(6,8,14,0.06); + box-shadow: 0 6px 20px rgba(6, 8, 14, 0.06); width: 95%; - max-width: 600px; + max-width: 500px; max-height: 85vh; overflow-y: auto; - transform: scale(0.95) translateY(-10px); + transform: scale(1); transition: transform 0.3s ease; - border: 1px solid rgba(0,0,0,0.08); -} - -[data-theme="dark"] .popup-content { - border-color: rgba(255,255,255,0.08); + border: 1px solid rgba(0, 0, 0, 0.08); } .popup-overlay.active .popup-content { @@ -595,14 +936,10 @@ textarea { align-items: center; justify-content: space-between; padding: 20px 24px 16px; - border-bottom: 1px solid rgba(0,0,0,0.08); + border-bottom: 1px solid rgba(0, 0, 0, 0.08); border-radius: var(--surface-radius) var(--surface-radius) 0 0; } -[data-theme="dark"] .popup-header { - border-bottom-color: rgba(255,255,255,0.08); -} - .popup-header h3 { margin: 0; font-size: 18px; @@ -612,7 +949,7 @@ textarea { .close-btn { background: transparent; - border: 1px solid rgba(15,23,32,0.08); + border: 1px solid rgba(15, 23, 32, 0.08); width: 36px; height: 36px; border-radius: 8px; @@ -630,19 +967,11 @@ textarea { user-select: none; } -[data-theme="dark"] .close-btn { - border-color: rgba(255,255,255,0.08); -} - .close-btn:hover { - background: rgba(0,0,0,0.05); + background: rgba(0, 0, 0, 0.05); transform: rotate(90deg); } -[data-theme="dark"] .close-btn:hover { - background: rgba(255,255,255,0.05); -} - .popup-body { padding: 24px; } @@ -651,15 +980,11 @@ textarea { margin-bottom: 24px; padding: 20px; background: var(--card); - border: 1px solid rgba(0,0,0,0.08); + border: 1px solid rgba(0, 0, 0, 0.08); border-radius: var(--surface-radius); transition: border-color 0.2s ease; } -[data-theme="dark"] .setting-section { - border-color: rgba(255,255,255,0.08); -} - .setting-section:hover { border-color: var(--accent); } @@ -684,20 +1009,6 @@ textarea { border-color: var(--danger) !important; } -[data-theme="dark"] .danger-zone-section { - border-color: rgba(251, 113, 133, 0.2) !important; -} - -[data-theme="dark"] .danger-zone-section:hover { - border-color: var(--danger) !important; -} - -[data-theme="dark"] .danger-zone-section:focus, -[data-theme="dark"] .danger-zone-section:focus-within { - outline: none; - border-color: var(--danger) !important; -} - .setting-section h4 { margin: 0 0 6px 0; font-size: 16px; @@ -715,7 +1026,7 @@ textarea { .script-editor { width: 100%; padding: 12px 16px; - border: 1px solid rgba(0,0,0,0.08); + border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 8px; background: var(--card); color: var(--text); @@ -727,11 +1038,6 @@ textarea { min-height: 120px; } -[data-theme="dark"] .script-editor { - background: var(--card); - border-color: rgba(255,255,255,0.08); -} - .script-editor:focus { outline: none; border-color: var(--accent); @@ -745,7 +1051,8 @@ textarea { .script-actions { display: flex; gap: 8px; - margin-top: 16px; + justify-content: flex-end; + margin-top: 24px; } .script-actions .btn { @@ -783,22 +1090,6 @@ textarea { box-shadow: none !important; } -[data-theme="dark"] .btn.danger { - background: var(--danger); - border-color: var(--danger); - box-shadow: none !important; -} - -[data-theme="dark"] .btn.danger:hover:not([disabled]):not(.btn-pressed):not(.btn-released) { - background: #dc2626; - border-color: #dc2626; - box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3) !important; -} - -[data-theme="dark"] .btn.danger:not(:hover):not(.btn-pressed):not(.btn-released) { - box-shadow: none !important; -} - /* Console actions alignment */ .console-actions { display: flex; @@ -825,11 +1116,6 @@ textarea { gap: 12px; } -[data-theme="dark"] .warning-banner { - background: rgba(245, 158, 11, 0.1); - border-color: rgba(245, 158, 11, 0.3); -} - .warning-content { display: flex; align-items: flex-start; @@ -850,10 +1136,6 @@ textarea { margin: 0; } -[data-theme="dark"] .warning-text { - color: #fbbf24; -} - .warning-close { background: transparent; border: 1px solid rgba(146, 64, 14, 0.3); @@ -876,20 +1158,11 @@ textarea { user-select: none; } -[data-theme="dark"] .warning-close { - border-color: rgba(251, 191, 36, 0.3); - color: #fbbf24; -} - .warning-close:hover { background: rgba(146, 64, 14, 0.1); transform: scale(1.1); } -[data-theme="dark"] .warning-close:hover { - background: rgba(251, 191, 36, 0.1); -} - /* Hide warning banner when dismissed */ .warning-banner.hidden { display: none; @@ -898,7 +1171,11 @@ textarea { /* Experimental Section Styling */ .experimental-section { border-color: rgba(245, 158, 11, 0.3) !important; - background: linear-gradient(135deg, rgba(245, 158, 11, 0.05), rgba(245, 158, 11, 0.02)); + background: linear-gradient( + 135deg, + rgba(245, 158, 11, 0.05), + rgba(245, 158, 11, 0.02) + ); } .experimental-section:hover { @@ -911,20 +1188,6 @@ textarea { border-color: #f59e0b !important; } -[data-theme="dark"] .experimental-section { - border-color: rgba(251, 191, 36, 0.3) !important; -} - -[data-theme="dark"] .experimental-section:hover { - border-color: rgba(251, 191, 36, 0.5) !important; -} - -[data-theme="dark"] .experimental-section:focus, -[data-theme="dark"] .experimental-section:focus-within { - outline: none; - border-color: #fbbf24 !important; -} - .experimental-toggle { cursor: pointer; list-style: none; @@ -971,20 +1234,6 @@ textarea { border-color: var(--danger) !important; } -[data-theme="dark"] .optional-section { - border-color: rgba(251, 113, 133, 0.2) !important; -} - -[data-theme="dark"] .optional-section:hover { - border-color: var(--danger) !important; -} - -[data-theme="dark"] .optional-section:focus, -[data-theme="dark"] .optional-section:focus-within { - outline: none; - border-color: var(--danger) !important; -} - .optional-toggle { cursor: pointer; list-style: none; @@ -1019,14 +1268,9 @@ textarea { .setting-subsection { margin-top: 16px; padding: 16px; - background: rgba(0,0,0,0.02); + background: rgba(0, 0, 0, 0.02); border-radius: 8px; - border: 1px solid rgba(0,0,0,0.06); -} - -[data-theme="dark"] .setting-subsection { - background: rgba(255,255,255,0.02); - border-color: rgba(255,255,255,0.06); + border: 1px solid rgba(0, 0, 0, 0.06); } .setting-subsection h5 { @@ -1055,17 +1299,6 @@ textarea { box-shadow: none !important; } -[data-theme="dark"] .btn.warning { - background: #f59e0b; - border-color: #f59e0b; - color: #92400e; -} - -[data-theme="dark"] .btn.warning:hover { - background: #d97706; - border-color: #d97706; -} - /* Storage Information Table */ .storage-info-table { width: 100%; @@ -1075,11 +1308,7 @@ textarea { } .storage-info-table tbody tr { - border-bottom: 1px solid rgba(0,0,0,0.06); -} - -[data-theme="dark"] .storage-info-table tbody tr { - border-bottom-color: rgba(255,255,255,0.06); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); } .storage-info-table tbody tr:last-child { @@ -1181,10 +1410,6 @@ textarea { margin-top: 8px; } -[data-theme="dark"] .debug-warning-text { - color: #fb7185; -} - .hotspot-note { margin-top: 16px; margin-bottom: 0; @@ -1209,7 +1434,7 @@ textarea { .loading-screen { position: fixed; inset: 0; - background: #E95420; + background: #0f0f0f; display: flex; align-items: center; justify-content: center; @@ -1224,10 +1449,6 @@ textarea { display: none; } -[data-theme="dark"] .loading-screen { - background: #0f0f0f; -} - .loading-dots { display: flex; gap: 12px; @@ -1239,16 +1460,4 @@ textarea { border-radius: 15%; background: white; opacity: 0.3; - animation: dotFill 3.75s infinite; -} - -.loading-dot:nth-child(1) { animation-delay: 0s; } -.loading-dot:nth-child(2) { animation-delay: 0.75s; } -.loading-dot:nth-child(3) { animation-delay: 1.5s; } -.loading-dot:nth-child(4) { animation-delay: 2.25s; } -.loading-dot:nth-child(5) { animation-delay: 3s; } - -@keyframes dotFill { - 0%, 20.1%, 100% { opacity: 0.3; } - 0.1%, 20% { opacity: 1; } } diff --git a/webroot/assets/logo.png b/webroot/app/assets/logo.png similarity index 100% rename from webroot/assets/logo.png rename to webroot/app/assets/logo.png diff --git a/webroot/app/components/ConsoleSection.vue b/webroot/app/components/ConsoleSection.vue new file mode 100644 index 0000000..0a6f4eb --- /dev/null +++ b/webroot/app/components/ConsoleSection.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/webroot/app/components/FilePickerPopup.vue b/webroot/app/components/FilePickerPopup.vue new file mode 100644 index 0000000..837cff5 --- /dev/null +++ b/webroot/app/components/FilePickerPopup.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/webroot/app/components/Footer.vue b/webroot/app/components/Footer.vue new file mode 100644 index 0000000..b5c232c --- /dev/null +++ b/webroot/app/components/Footer.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/webroot/app/components/ForwardNatPopup.vue b/webroot/app/components/ForwardNatPopup.vue new file mode 100644 index 0000000..6b2550a --- /dev/null +++ b/webroot/app/components/ForwardNatPopup.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/webroot/app/components/Header.vue b/webroot/app/components/Header.vue new file mode 100644 index 0000000..3c27506 --- /dev/null +++ b/webroot/app/components/Header.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/webroot/app/components/HotspotPopup.vue b/webroot/app/components/HotspotPopup.vue new file mode 100644 index 0000000..69510b0 --- /dev/null +++ b/webroot/app/components/HotspotPopup.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/webroot/app/components/Icon.vue b/webroot/app/components/Icon.vue new file mode 100644 index 0000000..e638718 --- /dev/null +++ b/webroot/app/components/Icon.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/webroot/app/components/LoadingScreen.vue b/webroot/app/components/LoadingScreen.vue new file mode 100644 index 0000000..ad7f404 --- /dev/null +++ b/webroot/app/components/LoadingScreen.vue @@ -0,0 +1,15 @@ + + + diff --git a/webroot/app/components/NotFound.vue b/webroot/app/components/NotFound.vue new file mode 100644 index 0000000..0da00a0 --- /dev/null +++ b/webroot/app/components/NotFound.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/webroot/app/components/ProgressPopup.vue b/webroot/app/components/ProgressPopup.vue new file mode 100644 index 0000000..9b1d8e6 --- /dev/null +++ b/webroot/app/components/ProgressPopup.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/webroot/app/components/RestoreConfirmPopup.vue b/webroot/app/components/RestoreConfirmPopup.vue new file mode 100644 index 0000000..2f70832 --- /dev/null +++ b/webroot/app/components/RestoreConfirmPopup.vue @@ -0,0 +1,52 @@ + + + diff --git a/webroot/app/components/SettingsPopup.vue b/webroot/app/components/SettingsPopup.vue new file mode 100644 index 0000000..fa8dc71 --- /dev/null +++ b/webroot/app/components/SettingsPopup.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/webroot/app/components/SparseSettingsPopup.vue b/webroot/app/components/SparseSettingsPopup.vue new file mode 100644 index 0000000..30c219d --- /dev/null +++ b/webroot/app/components/SparseSettingsPopup.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/webroot/app/components/StatusSection.vue b/webroot/app/components/StatusSection.vue new file mode 100644 index 0000000..6cc95b7 --- /dev/null +++ b/webroot/app/components/StatusSection.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/webroot/app/components/UninstallConfirmPopup.vue b/webroot/app/components/UninstallConfirmPopup.vue new file mode 100644 index 0000000..53d326c --- /dev/null +++ b/webroot/app/components/UninstallConfirmPopup.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/webroot/app/components/UpdateConfirmPopup.vue b/webroot/app/components/UpdateConfirmPopup.vue new file mode 100644 index 0000000..7205c36 --- /dev/null +++ b/webroot/app/components/UpdateConfirmPopup.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/webroot/app/components/UserSection.vue b/webroot/app/components/UserSection.vue new file mode 100644 index 0000000..d9b3a2b --- /dev/null +++ b/webroot/app/components/UserSection.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/webroot/app/composables/constants.ts b/webroot/app/composables/constants.ts new file mode 100644 index 0000000..e27829e --- /dev/null +++ b/webroot/app/composables/constants.ts @@ -0,0 +1,9 @@ +export const CHROOT_DIR = "/data/local/ubuntu-chroot"; +export const ROOTFS_DIR = `${CHROOT_DIR}/rootfs`; +export const PATH_CHROOT_SH = `${CHROOT_DIR}/chroot.sh`; +export const HOTSPOT_SCRIPT = `${CHROOT_DIR}/start-hotspot`; +export const FORWARD_NAT_SCRIPT = `${CHROOT_DIR}/forward-nat.sh`; +export const OTA_UPDATER = `${CHROOT_DIR}/ota/updater.sh`; +export const BOOT_FILE = `${CHROOT_DIR}/boot-service`; +export const DOZE_OFF_FILE = `${CHROOT_DIR}/.doze_off`; +export const DEFAULT_BACKUP_DIR = "/sdcard"; diff --git a/webroot/app/composables/useChroot.ts b/webroot/app/composables/useChroot.ts new file mode 100644 index 0000000..83a1d2e --- /dev/null +++ b/webroot/app/composables/useChroot.ts @@ -0,0 +1,657 @@ +import { ref, computed, nextTick } from "vue"; +import useNativeCmd from "@/composables/useNativeCmd"; +import useConsole from "@/composables/useConsole"; +import { Storage, StateManager } from "@/composables/useStateManager"; +import ProgressIndicator from "@/services/progressIndicator"; +import { + CHROOT_DIR, + PATH_CHROOT_SH, + OTA_UPDATER, + BOOT_FILE, + DOZE_OFF_FILE, +} from "@/composables/constants"; + +export function useChroot(consoleApi: ReturnType) { + const cmd = useNativeCmd(); + const consoleRef = consoleApi.consoleRef; + + const statusText = ref("unknown"); + const startDisabled = ref(true); + const stopDisabled = ref(true); + const restartDisabled = ref(true); + const userSelectDisabled = ref(true); + const copyLoginDisabled = ref(true); + + const users = ref([]); + const selectedUser = ref("root"); + + const runAtBoot = ref(false); + const debugMode = ref(StateManager.get("debug")); + const androidOptimize = ref(true); + + const postExecScript = ref(""); + + const activeCommandId = ref(null); + + const rootAccessConfirmedRef = ref(cmd.isAvailable.value); + const showProgress = ref(false); + const progressTitle = ref(""); + const progressMessage = ref(""); + const showUpdateConfirm = ref(false); + + const statusDotClass = computed(() => { + switch (statusText.value) { + case "running": + return "dot dot-on"; + case "stopped": + return "dot dot-off"; + case "starting": + return "dot dot-on"; + case "stopping": + return "dot dot-off"; + case "not_found": + return "dot dot-off"; + default: + return "dot dot-unknown"; + } + }); + + function appendConsole(text: string, cls?: string) { + if (cls === "debug" && !debugMode.value) return; + consoleApi.append(text, cls); + } + + // Helper: Run a function while ensuring only one command is active + async function withCommandGuard(commandId: string, fn: () => Promise) { + if (activeCommandId.value) { + appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + if (!cmd.isAvailable.value) { + appendConsole("Cannot execute: root access not available", "err"); + return; + } + let timeoutId: number | null = null; + try { + activeCommandId.value = commandId; + timeoutId = window.setTimeout( + () => { + if (activeCommandId.value === commandId) { + appendConsole( + `⚠ Command '${commandId}' timed out and was forcibly cleared`, + "warn", + ); + activeCommandId.value = null; + } + }, + 10 * 60 * 1000, + ); // 10 minutes + await fn(); + } finally { + if (timeoutId) clearTimeout(timeoutId); + activeCommandId.value = null; + } + } + + async function fetchUsers(silent = false) { + if (!cmd.isAvailable.value) return; + try { + const out = await cmd.runCommandSync( + `${PATH_CHROOT_SH} list-users --no-auto-start`, + ); + const list = String(out || "").trim(); + users.value = list ? list.split(",").filter(Boolean) : []; + const saved = Storage.get("chroot_selected_user"); + if (saved && users.value.includes(saved)) selectedUser.value = saved; + if (!silent) { + const count = users.value.length; + const message = + count === 0 + ? "No users found (chroot may not be running)" + : `Found ${count} regular user(s) in chroot`; + appendConsole(message, "info"); + } + } catch (e: unknown) { + if (!silent) + appendConsole( + `Could not fetch users from chroot: ${String(e)}`, + "warn", + ); + users.value = []; + } + } + + async function copyLoginCommand() { + const user = selectedUser.value || "root"; + Storage.set("chroot_selected_user", user); + + let chrootCmd = "ubuntu-chroot"; + try { + const result = await cmd.runCommandAsyncPromise( + 'command -v ubuntu-chroot 2>/dev/null || echo ""', + ); + if (!String(result.output || "").trim()) { + chrootCmd = `${PATH_CHROOT_SH}`; + } + } catch { + chrootCmd = `sh ${PATH_CHROOT_SH}`; + } + + const loginCommand = `su -c "${chrootCmd} start ${user} -s"`; + try { + await navigator.clipboard.writeText(loginCommand); + appendConsole(`Login command for user '${user}' copied to clipboard`); + } catch { + appendConsole(loginCommand); + } + } + + async function checkRootAccess(silent = false) { + if (!cmd.isAvailable.value) { + if (!silent) + appendConsole( + "No root bridge detected — running offline. Actions disabled.", + ); + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + return false; + } + try { + const result = await cmd.runCommandAsyncPromise('echo "test"', { + timeoutMs: 10000, + }); + if (result.success) { + if (!silent) appendConsole("Root access available", "info"); + return true; + } else { + if (!silent) + appendConsole( + `Failed to detect root execution method: ${result.error || "Unknown error"}`, + "err", + ); + return false; + } + } catch (e: unknown) { + if (!silent) + appendConsole( + `Failed to detect root execution method: ${String(e)}`, + "err", + ); + return false; + } + } + + async function refreshStatus() { + const root = await checkRootAccess(true); + if (!root) { + statusText.value = "unknown"; + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + return; + } + + const maxRetries = 3; + let lastError: unknown; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const out = await cmd.runCommandSync(`${PATH_CHROOT_SH} raw-status`); + const s = String(out || "") + .trim() + .toUpperCase(); + + if (s === "RUNNING") { + statusText.value = "running"; + startDisabled.value = true; + stopDisabled.value = false; + restartDisabled.value = false; + userSelectDisabled.value = false; + copyLoginDisabled.value = false; + return; + } else if (s === "STOPPED") { + statusText.value = "stopped"; + startDisabled.value = false; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + return; + } else { + lastError = new Error(`Unknown status: ${s}`); + } + } catch (e: unknown) { + lastError = e; + } + + if (attempt < maxRetries - 1) { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + + statusText.value = "unknown"; + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + appendConsole( + `Failed to get status after ${maxRetries} attempts: ${String(lastError)}`, + "warn", + ); + } + + /** + * Update the overall UI status and button states in a single place. + * Accepts the same status strings used elsewhere (running, stopped, starting, stopping, not_found, updating, etc). + */ + function updateStatus(state: string) { + // Normalize incoming string + const s = String(state || "").trim(); + + if (s === "running") { + statusText.value = "running"; + startDisabled.value = true; + stopDisabled.value = false; + restartDisabled.value = false; + userSelectDisabled.value = false; + copyLoginDisabled.value = false; + } else if (s === "stopped") { + statusText.value = "stopped"; + startDisabled.value = false; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + } else if (s === "starting" || s === "stopping" || s === "restarting") { + // Transition states - treat them as in-progress and disable main actions + statusText.value = s; + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + } else if ( + s === "backing up" || + s === "restoring" || + s === "migrating" || + s === "uninstalling" || + s === "updating" || + s === "trimming" || + s === "resizing" + ) { + // Long-running maintenance operations - disable control actions + statusText.value = s; + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + } else if (s === "not_found") { + statusText.value = "not_found"; + startDisabled.value = true; // no chroot to start + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + } else { + // Unknown fallback + statusText.value = s || "unknown"; + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + } + } + + async function refreshStatusManual() { + try { + const rootOK = await checkRootAccess(true); + if (rootOK) { + await refreshStatus(); + await fetchUsers(true); + } + await readBootFile(true); + await readDozeOffFile(true); + } catch (e) { + appendConsole("Refresh failed", "warn"); + } + } + + async function doAction(action: "start" | "stop" | "restart") { + if (!cmd.isAvailable.value) { + appendConsole("Cannot execute commands: backend unavailable", "err"); + return; + } + + await withCommandGuard(action, async () => { + const actionText = + action === "start" + ? "Starting chroot" + : action === "stop" + ? "Stopping chroot" + : "Restarting chroot"; + const progress = ProgressIndicator.create( + actionText, + "dots", + consoleRef.value || document.getElementById("console"), + ); + + updateStatus( + action === "start" + ? "starting" + : action === "stop" + ? "stopping" + : "restarting", + ); + + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + consoleApi + .scrollToBottom({ behavior: "smooth", waitMs: 400 }) + .catch(() => {}); + + const cmdStr = `${PATH_CHROOT_SH} ${action} --no-shell`; + console.log(`Running command: ${cmdStr}`); + try { + const result = await cmd.runCommandAsyncPromise(cmdStr, { + asRoot: true, + debug: debugMode.value, + onOutput: (line: string) => appendConsole(line), + }); + if (result.success) { + appendConsole(`✓ ${action} completed successfully`, "success"); + await new Promise((resolve) => setTimeout(resolve, 500)); + await refreshStatus(); + } else { + appendConsole( + `✗ ${action} failed: ${result.error || "Unknown error"}`, + "err", + ); + await refreshStatus(); + } + } catch (e: unknown) { + appendConsole(`Failed to execute ${action}: ${String(e)}`, "err"); + await refreshStatus(); + } finally { + ProgressIndicator.remove(progress); + } + }); + } + + async function start() { + await doAction("start"); + } + async function stop() { + await doAction("stop"); + } + async function restart() { + await doAction("restart"); + } + + async function readBootFile(silent = false) { + try { + const result = await cmd.runCommandAsyncPromise( + `cat ${BOOT_FILE} 2>/dev/null || echo 0`, + { timeoutMs: 10000 }, + ); + runAtBoot.value = String(result.output || "").trim() === "1"; + if (!silent) + appendConsole( + `Run-at-boot: ${runAtBoot.value ? "enabled" : "disabled"}`, + "info", + ); + } catch { + runAtBoot.value = false; + } + } + + async function writeBootFile() { + try { + const result = await cmd.runCommandAsyncPromise( + `mkdir -p ${CHROOT_DIR} && echo ${runAtBoot.value ? 1 : 0} > ${BOOT_FILE}`, + { timeoutMs: 10000 }, + ); + if (result.success) { + appendConsole( + `Run-at-boot ${runAtBoot.value ? "enabled" : "disabled"}`, + "success", + ); + } else { + appendConsole( + `✗ Failed to set run-at-boot: ${result.error || "Unknown error"}`, + "err", + ); + await readBootFile(true); + } + } catch (e: unknown) { + appendConsole(`✗ Failed to set run-at-boot: ${String(e)}`, "err"); + await readBootFile(true); + } + } + + function toggleBoot() { + writeBootFile(); + } + + async function readDozeOffFile(silent = false) { + try { + const result = await cmd.runCommandAsyncPromise( + `cat ${DOZE_OFF_FILE} 2>/dev/null || echo 1`, + { timeoutMs: 10000 }, + ); + androidOptimize.value = String(result.output || "").trim() === "1"; + if (!silent) + appendConsole( + `Android optimizations: ${androidOptimize.value ? "enabled" : "disabled"}`, + "info", + ); + } catch { + androidOptimize.value = true; + } + } + + async function writeDozeOffFile() { + if (!cmd.isAvailable.value) { + appendConsole( + "Cannot set Android optimizations: command bridge unavailable", + "warn", + ); + return; + } + try { + const result = await cmd.runCommandAsyncPromise( + `mkdir -p ${CHROOT_DIR} && echo ${androidOptimize.value ? 1 : 0} > ${DOZE_OFF_FILE}`, + { timeoutMs: 10000 }, + ); + if (result.success) { + appendConsole( + `Android optimizations ${androidOptimize.value ? "enabled" : "disabled"}`, + "success", + ); + } else { + appendConsole( + `✗ Failed to set Android optimizations: ${result.error || "Unknown error"}`, + "err", + ); + await readDozeOffFile(true); + } + } catch (e: unknown) { + appendConsole( + `✗ Failed to set Android optimizations: ${String(e)}`, + "err", + ); + await readDozeOffFile(true); + } + } + + async function loadPostExecScript() { + try { + const result = await cmd.runCommandAsyncPromise( + `cat ${CHROOT_DIR}/post_exec.sh 2>/dev/null || echo ''`, + { timeoutMs: 10000 }, + ); + postExecScript.value = String(result.output || "").trim(); + } catch (e) { + appendConsole(`Failed to load post-exec script: ${String(e)}`, "warn"); + postExecScript.value = ""; + } + } + + async function savePostExecScript() { + try { + // encode as base64 safely + const utf8 = new TextEncoder().encode(postExecScript.value || ""); + let binary = ""; + for (let i = 0; i < utf8.length; i += 8192) { + const slice = utf8.subarray(i, i + 8192); + binary += String.fromCharCode.apply(null, Array.from(slice)); + } + const base64 = btoa(binary); + const result1 = await cmd.runCommandAsyncPromise( + `echo '${base64}' | base64 -d > ${CHROOT_DIR}/post_exec.sh`, + { timeoutMs: 10000 }, + ); + if (!result1.success) { + appendConsole( + `Failed to save post-exec script: ${result1.error || "Unknown error"}`, + "err", + ); + return; + } + const result2 = await cmd.runCommandAsyncPromise( + `chmod 755 ${CHROOT_DIR}/post_exec.sh`, + { timeoutMs: 10000 }, + ); + if (result2.success) { + appendConsole("Post-exec script saved successfully", "success"); + } else { + appendConsole( + `Failed to set permissions: ${result2.error || "Unknown error"}`, + "err", + ); + } + } catch (e: unknown) { + appendConsole(`Failed to save post-exec script: ${String(e)}`, "err"); + } + } + + async function clearPostExecScript() { + postExecScript.value = ""; + try { + const result = await cmd.runCommandAsyncPromise( + `echo '' > ${CHROOT_DIR}/post_exec.sh`, + { timeoutMs: 10000 }, + ); + if (result.success) { + appendConsole("Post-exec script cleared successfully", "info"); + } else { + appendConsole( + `Failed to clear post-exec script: ${result.error || "Unknown error"}`, + "err", + ); + } + } catch (e: unknown) { + appendConsole(`Failed to clear post-exec script: ${String(e)}`, "err"); + } + } + + async function updateChroot(closeSettingsPopup: () => void) { + if (!(await checkRootAccess(true))) { + appendConsole("Cannot update chroot: root access not available", "err"); + return; + } + + showUpdateConfirm.value = true; + } + + async function confirmUpdate(closeSettingsPopup: () => void) { + showUpdateConfirm.value = false; + + closeSettingsPopup(); + + statusText.value = "updating"; + + try { + if (statusText.value !== "running") { + await start(); + // Wait a bit + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + const cmdStr = `${OTA_UPDATER}`; + const result = await cmd.runCommandAsyncPromise(cmdStr, { + asRoot: true, + debug: debugMode.value, + }); + + if (result.success) { + appendConsole("✓ Chroot update completed successfully", "success"); + + await restart(); + + appendConsole("━━━ Update Complete ━━━", "success"); + } else { + appendConsole("✗ Chroot update failed", "err"); + } + } catch (e: unknown) { + appendConsole(`✗ Update failed: ${String(e)}`, "err"); + } finally { + await refreshStatus(); + } + } + + return { + statusText, + startDisabled, + stopDisabled, + restartDisabled, + userSelectDisabled, + copyLoginDisabled, + users, + selectedUser, + runAtBoot, + debugMode, + androidOptimize, + postExecScript, + activeCommandId, + rootAccessConfirmedRef, + showProgress, + progressTitle, + progressMessage, + showUpdateConfirm, + statusDotClass, + appendConsole, + withCommandGuard, + fetchUsers, + copyLoginCommand, + checkRootAccess, + refreshStatus, + updateStatus, + refreshStatusManual, + doAction, + start, + stop, + restart, + readBootFile, + writeBootFile, + toggleBoot, + readDozeOffFile, + writeDozeOffFile, + loadPostExecScript, + savePostExecScript, + clearPostExecScript, + updateChroot, + confirmUpdate, + }; +} diff --git a/webroot/app/composables/useConsole.ts b/webroot/app/composables/useConsole.ts new file mode 100644 index 0000000..7dfe2b3 --- /dev/null +++ b/webroot/app/composables/useConsole.ts @@ -0,0 +1,393 @@ +import { nextTick, onUnmounted, ref, watch } from "vue"; +import type { Ref } from "vue"; + +export type UseConsoleOptions = { + key?: string; + maxLines?: number; + batchSize?: number; + scrollThreshold?: number; + saveDebounceMs?: number; + consoleRef?: Ref; +}; + +export function useConsole(options: UseConsoleOptions = {}) { + const { + key = "chroot_console_logs", + maxLines = 250, + batchSize = 50, + scrollThreshold = 10, + saveDebounceMs = 500, + } = options; + + const consoleRef: Ref = + options.consoleRef || ref(null); + + const buffer: Array<{ text: string; cls?: string }> = []; + + let flushFrame: number | null = null; + let isFlushing = false; + let scrollScheduled = false; + let isUserScrolledUp = false; + let lastScrollTop = 0; + let saveTimer: number | null = null; + + function isAtBottom() { + const el = consoleRef.value; + if (!el) return true; + const maxScroll = el.scrollHeight - el.clientHeight; + return Math.abs(el.scrollTop - maxScroll) <= scrollThreshold; + } + + function append(text: string, cls?: string) { + if (!text && text !== "0") return; + buffer.push({ text: String(text), cls }); + scheduleFlush(); + } + + function appendBatch(lines: string[] | string, cls?: string) { + if (!Array.isArray(lines)) { + append(lines as string, cls); + return; + } + for (const l of lines) { + if (l != null && String(l).trim()) { + buffer.push({ text: String(l), cls }); + } + } + if (buffer.length > 0) scheduleFlush(); + } + + function clearConsole() { + if (consoleRef.value) { + consoleRef.value.textContent = ""; + } + try { + localStorage.removeItem(key); + } catch { + // ignore errors in restricted browsers / private mode + } + } + + async function copyLogs() { + const el = consoleRef.value; + if (!el) return false; + const text = el.textContent ?? ""; + if (!text.trim()) { + append("Console is empty - nothing to copy", "warn"); + return false; + } + + if ( + navigator.clipboard && + typeof navigator.clipboard.writeText === "function" + ) { + try { + await navigator.clipboard.writeText(text); + append("Console logs copied to clipboard"); + return true; + } catch (err) { + // fall through to fallback + } + } + + try { + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.left = "-9999px"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + append("Console logs copied to clipboard"); + return true; + } catch (err) { + append("Failed to copy console logs - please copy manually", "warn"); + append(text); + return false; + } + } + + function scheduleSave() { + if (saveTimer != null) { + window.clearTimeout(saveTimer); + } + saveTimer = window.setTimeout(() => { + saveLogs(); + saveTimer = null; + }, saveDebounceMs); + } + + function saveLogs() { + try { + if (!consoleRef.value) return; + const content = consoleRef.value.innerHTML; + localStorage.setItem(key, content); + } catch { + // ignore localStorage quota and strict mode errors + } + } + + function loadLogs() { + if (!consoleRef.value) return; + const stored = (() => { + try { + return localStorage.getItem(key) || ""; + } catch { + return ""; + } + })(); + + if (!stored) return; + + consoleRef.value.innerHTML = stored; + + const lines = consoleRef.value.querySelectorAll("div"); + if (lines.length > maxLines) { + const toRemove = lines.length - maxLines; + for (let i = 0; i < toRemove; i++) { + const node = lines[i]; + if (node && node.parentNode) node.parentNode.removeChild(node); + } + saveLogs(); + } + + const finalLines = consoleRef.value.querySelectorAll("div"); + const hasScrollbar = + consoleRef.value.scrollHeight > consoleRef.value.clientHeight; + const shouldAnimate = finalLines.length < 15 && !hasScrollbar; + + if (shouldAnimate) { + finalLines.forEach((line, i) => { + if (!line.classList.contains("progress-indicator")) { + line.classList.add("log-fade-in"); + (line as HTMLElement).style.animationDelay = `${i * 40}ms`; + } else { + line.classList.add("log-immediate"); + } + }); + } + + setTimeout(() => { + scrollInstant(); + isUserScrolledUp = false; + }, 0); + } + + function createLineNode(text: string, cls?: string, index = 0) { + const node = document.createElement("div"); + node.textContent = text + "\n"; + if (cls) node.className = cls; + + const isProgressIndicator = + cls === "progress-indicator" || + String(text).includes("⏳") || + String(text).includes("...") || + String(text).includes("⏳"); + + if (isProgressIndicator) { + node.classList.add("log-immediate"); + } else { + node.classList.add("log-chunk-fade"); + node.style.animationDelay = `${index * 20}ms`; + } + return node; + } + + function scheduleFlush() { + if (flushFrame || isFlushing) return; + flushFrame = window.setTimeout(() => { + flushFrame = null; + flush(); + }, 50); + } + + function flush() { + if (isFlushing) return; + if (buffer.length === 0) return; + + const el = consoleRef.value; + if (!el) { + scheduleFlush(); + return; + } + + isFlushing = true; + const batch = buffer.splice(0, batchSize); + const fragment = document.createDocumentFragment(); + + const wasAtBottom = isAtBottom(); + + const existingLines = Array.from(el.querySelectorAll("div")); + const regularExisting = existingLines.filter( + (n) => !n.classList.contains("progress-indicator"), + ); + + const totalAfterAdd = regularExisting.length + batch.length; + if (totalAfterAdd > maxLines) { + const toRemove = Math.min( + totalAfterAdd - maxLines, + regularExisting.length, + ); + for (let i = 0; i < toRemove; i++) { + const node = regularExisting[i]; + if (node && node.parentNode) node.parentNode.removeChild(node); + } + } + + batch.forEach((item, idx) => { + const node = createLineNode(item.text, item.cls, idx); + fragment.appendChild(node); + }); + + el.appendChild(fragment); + + if (wasAtBottom && !isUserScrolledUp) { + scheduleScroll(); + } + + scheduleSave(); + + isFlushing = false; + + if (buffer.length > 0) scheduleFlush(); + } + + function scheduleScroll() { + if (scrollScheduled) return; + scrollScheduled = true; + window.setTimeout(() => { + scrollScheduled = false; + if (!consoleRef.value) return; + consoleRef.value.scrollTop = consoleRef.value.scrollHeight; + }, 0); + } + + function scrollInstant() { + if (!consoleRef.value) return; + consoleRef.value.scrollTop = consoleRef.value.scrollHeight; + } + + function scrollToBottom( + { behavior = "smooth", waitMs = 400 } = { behavior: "smooth", waitMs: 400 }, + ) { + if (!consoleRef.value) return Promise.resolve(); + isUserScrolledUp = false; + return new Promise((resolve) => { + consoleRef.value!.scrollTo({ + top: consoleRef.value!.scrollHeight, + behavior: behavior as ScrollBehavior, + }); + window.setTimeout(() => resolve(), waitMs); + }); + } + + async function waitForFlush() { + while (buffer.length > 0 || isFlushing) { + await new Promise((r) => setTimeout(r, 40)); + } + + await nextTick(); + } + + function handleUserScroll() { + window.setTimeout(() => { + if (!consoleRef.value) return; + if (!isAtBottom()) { + isUserScrolledUp = true; + } else { + isUserScrolledUp = false; + } + lastScrollTop = consoleRef.value!.scrollTop; + }, 150); + } + + const stopWatchConsole = watch( + consoleRef, + (el, prevEl) => { + try { + if (prevEl) { + (prevEl as HTMLElement).removeEventListener?.( + "scroll", + handleUserScroll, + ); + } + } catch { + // ignore removal errors + } + + if (el) { + try { + (el as HTMLElement).addEventListener("scroll", handleUserScroll, { + passive: true, + }); + } catch { + // no-op on add listener errors + } + + try { + loadLogs(); + } catch { + // ignore load errors + } + + try { + if (typeof scheduleFlush === "function") scheduleFlush(); + } catch {} + } + }, + { immediate: true }, + ); + + onUnmounted(() => { + try { + stopWatchConsole?.(); + } catch { + // ignore + } + + if (consoleRef.value) { + try { + consoleRef.value.removeEventListener("scroll", handleUserScroll); + } catch { + // ignore removal errors + } + } + + if (flushFrame) { + window.clearTimeout(flushFrame); + flushFrame = null; + } + if (saveTimer) { + window.clearTimeout(saveTimer); + saveTimer = null; + } + }); + + const LogBuffer = { + consoleRef, + append, + appendBatch, + clearConsole, + copyLogs, + saveLogs, + loadLogs, + scheduleFlush, + flush, + waitForFlush, + scrollToBottom, + scrollInstant, + isAtBottom, + handleUserScroll, + getBuffer() { + return buffer.slice(); + }, + isUserScrolledUpRef() { + return isUserScrolledUp; + }, + }; + + return LogBuffer; +} + +export default useConsole; diff --git a/webroot/app/composables/useFeatures.ts b/webroot/app/composables/useFeatures.ts new file mode 100644 index 0000000..fab7a8d --- /dev/null +++ b/webroot/app/composables/useFeatures.ts @@ -0,0 +1,317 @@ +import { ref } from "vue"; +import useNativeCmd from "@/composables/useNativeCmd"; +import useConsole from "@/composables/useConsole"; +import { Storage, StateManager } from "@/composables/useStateManager"; +import ProgressIndicator from "@/services/progressIndicator"; +import { + ButtonState, + PopupManager, + disableAllActions, + disableSettingsPopup, +} from "@/composables/utils"; +import { + CHROOT_DIR, + PATH_CHROOT_SH, + HOTSPOT_SCRIPT, + FORWARD_NAT_SCRIPT, + OTA_UPDATER, +} from "@/composables/constants"; +import HotspotFeature from "@/features/hotspot"; +import ForwardNatFeature from "@/features/forward-nat"; +import BackupRestoreFeature from "@/features/backup-restore"; +import UninstallFeature from "@/features/uninstall"; +import ResizeFeature from "@/features/resize"; +import MigrateFeature from "@/features/migrate"; + +export function useFeatures( + updateStatus: (state: string) => void, + refreshStatus: () => Promise, + consoleApi: ReturnType, + showProgress: { value: boolean }, + progressTitle: { value: string }, + progressMessage: { value: string }, +) { + const cmd = useNativeCmd(); + + const consoleRef = consoleApi.consoleRef; + + const activeCommandId = ref(null); + const rootAccessConfirmed = ref(cmd.isAvailable.value); + const hotspotActive = ref(StateManager.get("hotspot")); + const forwardingActive = ref(StateManager.get("forwarding")); + const sparseMigrated = ref(StateManager.get("sparse")); + + function appendConsole(text: string, cls?: string) { + consoleApi.append(text, cls); + } + + // Helper: Run a function while ensuring only one command is active + async function withCommandGuard(commandId: string, fn: () => Promise) { + if (activeCommandId.value) { + appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + if (!cmd.isAvailable.value) { + appendConsole("Cannot execute: root access not available", "err"); + return; + } + try { + activeCommandId.value = commandId; + await fn(); + } finally { + activeCommandId.value = null; + } + } + + function copyConsole() { + consoleApi.copyLogs(); + } + + function clearConsole() { + consoleApi.clearConsole(); + } + + // Initialize feature modules (hotspot, forwarding, backup/restore, etc) + function initFeatureModules() { + try { + const els = { + get hotspotIface() { + return ( + (document.getElementById("hotspot-iface") as HTMLSelectElement) ?? + undefined + ); + }, + get hotspotSsid() { + return ( + (document.getElementById("hotspot-ssid") as HTMLInputElement) ?? + undefined + ); + }, + get hotspotPassword() { + return ( + (document.getElementById("hotspot-password") as HTMLInputElement) ?? + undefined + ); + }, + get hotspotBand() { + return ( + (document.getElementById("hotspot-band") as HTMLSelectElement) ?? + undefined + ); + }, + get hotspotChannel() { + return ( + (document.getElementById("hotspot-channel") as HTMLSelectElement) ?? + undefined + ); + }, + get hotspotPopup() { + return document.getElementById("hotspot-popup") ?? undefined; + }, + get startHotspotBtn() { + const el = document.getElementById("start-hotspot-btn"); + return el ? (el as HTMLButtonElement) : undefined; + }, + get stopHotspotBtn() { + const el = document.getElementById("stop-hotspot-btn"); + return el ? (el as HTMLButtonElement) : undefined; + }, + get dismissHotspotWarning() { + return ( + document.getElementById("dismiss-hotspot-warning") ?? undefined + ); + }, + + get forwardNatIface() { + return ( + (document.getElementById( + "forward-nat-iface", + ) as HTMLSelectElement) ?? undefined + ); + }, + get forwardNatPopup() { + return document.getElementById("forward-nat-popup") ?? undefined; + }, + get startForwardingBtn() { + const el = document.getElementById("start-forwarding-btn"); + return el ? (el as HTMLButtonElement) : undefined; + }, + get stopForwardingBtn() { + const el = document.getElementById("stop-forwarding-btn"); + return el ? (el as HTMLButtonElement) : undefined; + }, + }; + + const StateManagerAdapter = { + get: (name: string) => + (StateManager.get as unknown as any)(name as any), + set: (name: string, value: boolean) => + (StateManager.set as unknown as any)(name as any, value), + }; + + const ProgressIndicatorAdapter = { + create: ( + text: string, + type?: "spinner" | "dots", + el?: HTMLElement | null, + ) => { + const h = ProgressIndicator.create( + text, + (type as any) || "spinner", + el, + ); + // Always return a compatible object (features expect an object, not null) + return { + progressLine: (h as any).element as HTMLElement, + interval: undefined, + __internalHandle: h, + } as any; + }, + remove: (handle?: any) => { + if (!handle) return; + const el = + (handle as any)?.progressLine ?? (handle as any)?.element ?? handle; + ProgressIndicator.remove(el as any); + }, + update: (handle?: any, text?: string) => { + if (!handle) return; + const el = + (handle as any)?.progressLine ?? (handle as any)?.element ?? handle; + ProgressIndicator.update(el as any, text || ""); + }, + }; + + const ButtonStateAdapter = { + setButtonPair: ( + startBtn?: HTMLElement | null | undefined, + stopBtn?: HTMLElement | null | undefined, + isActive?: boolean, + ) => { + ButtonState.setButtonPair( + startBtn as any, + stopBtn as any, + !!isActive, + ); + }, + setButton: ( + btn?: HTMLElement | null | undefined, + enabled?: boolean, + visible = true, + opacity: string | null = null, + ) => { + ButtonState.setButton( + btn as any, + !!enabled, + visible, + opacity ?? null, + ); + }, + setButtons: (buttons: Array) => { + const adapted = buttons.map((b) => ({ + btn: b.btn as any, + enabled: b.enabled, + visible: b.visible, + opacity: b.opacity, + })); + ButtonState.setButtons(adapted); + }, + }; + + const commonDeps = { + els, + Storage, + StateManager: StateManagerAdapter, + CHROOT_DIR, + PATH_CHROOT_SH, + HOTSPOT_SCRIPT, + FORWARD_NAT_SCRIPT, + OTA_UPDATER, + appendConsole, + runCmdSync: (cmdStr: string) => cmd.runCommandSync(cmdStr), + runCmdAsync: (cmdStr: string, onComplete?: (res: any) => void) => + cmd.runCommandAsync(cmdStr, { + asRoot: true, + debug: false, + callbacks: { onComplete }, + }), + runCommandAsyncPromise: ( + cmdStr: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => cmd.runCommandAsyncPromise(cmdStr, { asRoot: true, ...options }), + showConfirmDialog: async ( + title: string, + message: string, + confirmText?: string, + cancelText?: string, + ) => Promise.resolve(window.confirm(message)), + withCommandGuard, + ANIMATION_DELAYS: { + POPUP_CLOSE: 450, + POPUP_CLOSE_LONG: 750, + POPUP_CLOSE_VERY_LONG: 850, + STATUS_REFRESH: 500, + INPUT_FOCUS: 100, + PROGRESS_SPINNER: 200, + PROGRESS_DOTS: 400, + }, + ProgressIndicator: ProgressIndicatorAdapter, + disableSettingsPopup, + ButtonState: ButtonStateAdapter, + PopupManager, + activeCommandId: activeCommandId, + rootAccessConfirmed: rootAccessConfirmed, + hotspotActive: hotspotActive, + forwardingActive: forwardingActive, + sparseMigrated: sparseMigrated, + updateStatus: updateStatus, + refreshStatus: refreshStatus, + showProgress: showProgress, + progressTitle: progressTitle, + progressMessage: progressMessage, + }; + + try { + HotspotFeature?.init?.(commonDeps); + } catch {} + try { + ForwardNatFeature?.init?.(commonDeps); + } catch {} + try { + BackupRestoreFeature?.init?.(commonDeps); + } catch {} + try { + UninstallFeature?.init?.(commonDeps); + } catch {} + try { + ResizeFeature?.init?.(commonDeps); + } catch {} + try { + MigrateFeature?.init?.(commonDeps); + } catch {} + } catch (e) { + appendConsole( + "Failed to initialize feature modules: " + String(e), + "warn", + ); + } + } + + return { + activeCommandId, + rootAccessConfirmed, + hotspotActive, + forwardingActive, + sparseMigrated, + withCommandGuard, + copyConsole, + clearConsole, + initFeatureModules, + }; +} diff --git a/webroot/app/composables/useForwardNat.ts b/webroot/app/composables/useForwardNat.ts new file mode 100644 index 0000000..afb3b89 --- /dev/null +++ b/webroot/app/composables/useForwardNat.ts @@ -0,0 +1,95 @@ +import { ref } from "vue"; +import useConsole from "@/composables/useConsole"; +import useNativeCmd from "@/composables/useNativeCmd"; +import { Storage } from "@/composables/useStateManager"; +import { NetworkInterfaceManager } from "@/services/NetworkInterfaceManager"; +import ForwardNatFeature from "@/features/forward-nat"; + +export function useForwardNat(consoleApi: ReturnType) { + const cmd = useNativeCmd(); + + function appendConsole(text: string, cls?: string) { + consoleApi.append(text, cls); + } + const forwardNatIfaces = ref>([]); + const forwardNatIface = ref(""); + + const interfaceManager = new NetworkInterfaceManager( + { + Storage, + appendConsole, + runCmdSync: (cmdStr: string) => cmd.runCommandSync(cmdStr), + rootAccessConfirmed: cmd.isAvailable, + }, + "/data/local/ubuntu-chroot/forward-nat.sh", + "chroot_forward_nat_interfaces_cache", + null, + "chroot_selected_interface", + (interfaces: string[]) => { + forwardNatIfaces.value = interfaces + .map((s: string) => { + const trimmed = String(s || "").trim(); + if (!trimmed) return null; + if (trimmed.includes(":")) { + const parts = trimmed.split(":").map((p) => p.trim()); + return { + value: parts[0], + label: `${parts[0]} (${parts[1] || ""})`, + }; + } + return { value: trimmed, label: trimmed }; + }) + .filter(Boolean) as Array<{ value: string; label: string }>; + }, + ); + + function openForwardNatPopup() { + const el = document.getElementById("forward-nat-popup"); + const selectEl = document.getElementById( + "forward-nat-iface", + ) as HTMLSelectElement | null; + + interfaceManager.updateSelectElement(selectEl); + + if (el) el.classList.add("active"); + + interfaceManager.fetchInterfaces(false, true).catch(() => {}); + interfaceManager.fetchInterfaces(false, false).catch(() => {}); + } + + function closeForwardNatPopup() { + const el = document.getElementById("forward-nat-popup"); + if (el) el.classList.remove("active"); + } + + function startForwarding() { + if ( + typeof ForwardNatFeature !== "undefined" && + ForwardNatFeature.startForwarding + ) { + ForwardNatFeature.startForwarding(); + } else { + appendConsole("No forwarding implementation available", "warn"); + } + } + + function stopForwarding() { + if ( + typeof ForwardNatFeature !== "undefined" && + ForwardNatFeature.stopForwarding + ) { + ForwardNatFeature.stopForwarding(); + } else { + appendConsole("No forwarding implementation available", "warn"); + } + } + + return { + forwardNatIfaces, + forwardNatIface, + openForwardNatPopup, + closeForwardNatPopup, + startForwarding, + stopForwarding, + }; +} diff --git a/webroot/app/composables/useHotspot.ts b/webroot/app/composables/useHotspot.ts new file mode 100644 index 0000000..92905aa --- /dev/null +++ b/webroot/app/composables/useHotspot.ts @@ -0,0 +1,159 @@ +import { ref } from "vue"; +import useConsole from "@/composables/useConsole"; +import useNativeCmd from "@/composables/useNativeCmd"; +import { Storage } from "@/composables/useStateManager"; +import { NetworkInterfaceManager } from "@/services/NetworkInterfaceManager"; +import HotspotFeature from "@/features/hotspot"; + +export function useHotspot(consoleApi: ReturnType) { + const cmd = useNativeCmd(); + + const hotspotWarningVisible = ref(true); + const hotspotIfaces = ref>([]); + const hotspotIfacesLoading = ref(false); + const hotspotIfaceError = ref(""); + const hotspotIface = ref(""); + const hotspotSsid = ref(""); + const hotspotPassword = ref(""); + const hotspotBand = ref("2"); + const hotspotChannel = ref("6"); + const hotspotChannels = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + + const interfaceManager = new NetworkInterfaceManager( + { + Storage, + appendConsole, + runCmdSync: (cmdStr: string) => cmd.runCommandSync(cmdStr), + rootAccessConfirmed: cmd.isAvailable, + }, + "/data/local/ubuntu-chroot/start-hotspot", + "chroot_hotspot_interfaces_cache", + null, + "chroot_hotspot_iface", + (interfaces: string[]) => { + hotspotIfaces.value = interfaces + .filter((s) => !s.startsWith("ap0")) + .map((s: string) => { + const trimmed = String(s || "").trim(); + if (!trimmed) return null; + if (trimmed.includes(":")) { + const parts = trimmed.split(":").map((p) => p.trim()); + return { + value: parts[0], + label: `${parts[0]} (${parts[1] || ""})`, + }; + } + return { value: trimmed, label: trimmed }; + }) + .filter(Boolean) as Array<{ value: string; label: string }>; + }, + ); + + function appendConsole(text: string, cls?: string) { + consoleApi.append(text, cls); + } + + function openHotspotPopup() { + const el = document.getElementById("hotspot-popup"); + const selectEl = document.getElementById( + "hotspot-iface", + ) as HTMLSelectElement | null; + + hotspotIfaceError.value = ""; + + interfaceManager.updateSelectElement(selectEl); + + try { + const cached = Storage.getJSON + ? Storage.getJSON("chroot_hotspot_interfaces_cache") + : null; + if (Array.isArray(cached) && cached.length > 0) { + hotspotIfaces.value = cached + .filter((s) => !s.startsWith("ap0")) + .map((s: string) => { + const trimmed = String(s || "").trim(); + if (!trimmed) return null; + if (trimmed.includes(":")) { + const parts = trimmed.split(":").map((p) => p.trim()); + return { + value: parts[0], + label: `${parts[0]} (${parts[1] || ""})`, + }; + } + return { value: trimmed, label: trimmed }; + }) + .filter(Boolean) as Array<{ value: string; label: string }>; + } + } catch (e) { + // ignore storage read errors + } + + if (el) el.classList.add("active"); + + interfaceManager.fetchInterfaces(false, true).catch(() => {}); + interfaceManager.fetchInterfaces(false, false).catch((err) => { + hotspotIfaceError.value = "Failed to load interfaces"; + }); + } + + function closeHotspotPopup() { + const el = document.getElementById("hotspot-popup"); + if (el) el.classList.remove("active"); + } + + async function refreshHotspotIfaces() { + hotspotIfacesLoading.value = true; + hotspotIfaceError.value = ""; + try { + await interfaceManager.fetchInterfaces(true, false); + } catch (err) { + hotspotIfaceError.value = "Failed to load interfaces"; + } finally { + hotspotIfacesLoading.value = false; + } + } + + function startHotspot() { + if (typeof HotspotFeature !== "undefined" && HotspotFeature.startHotspot) { + HotspotFeature.startHotspot(); + } + } + + function stopHotspot() { + if (typeof HotspotFeature !== "undefined" && HotspotFeature.stopHotspot) { + HotspotFeature.stopHotspot(); + } + } + + function dismissHotspotWarning() { + hotspotWarningVisible.value = false; + } + + function toggleHotspotPassword() { + const el = document.getElementById( + "hotspot-password", + ) as HTMLInputElement | null; + if (!el) return; + el.type = el.type === "password" ? "text" : "password"; + } + + return { + hotspotWarningVisible, + hotspotIfaces, + hotspotIfacesLoading, + hotspotIfaceError, + hotspotIface, + hotspotSsid, + hotspotPassword, + hotspotBand, + hotspotChannel, + hotspotChannels, + openHotspotPopup, + closeHotspotPopup, + refreshHotspotIfaces, + startHotspot, + stopHotspot, + dismissHotspotWarning, + toggleHotspotPassword, + }; +} diff --git a/webroot/app/composables/useNativeCmd.ts b/webroot/app/composables/useNativeCmd.ts new file mode 100644 index 0000000..72222a2 --- /dev/null +++ b/webroot/app/composables/useNativeCmd.ts @@ -0,0 +1,249 @@ +import { computed, readonly, ref, onMounted, onUnmounted } from "vue"; + +type ExecMethod = "ksu" | "sulib" | "none"; + +export type CommandResult = { + success: boolean; + exitCode?: number; + output?: string; + error?: string; +}; + +export type AsyncCallbacks = { + onOutput?: (line: string) => void; + onError?: (err: string) => void; + onComplete?: (res: CommandResult) => void; + onRawOutput?: (raw: string) => void; +}; + +export function useNativeCmd() { + const LOGGING_PREFIX = "LOGGING_ENABLED=1"; + + // reactive state for whether the native bridge is available + const _available = ref( + typeof window !== "undefined" && + !!(window as any).cmdExec && + (typeof (window as any).cmdExec.execute === "function" || + typeof (window as any).cmdExec.executeAsync === "function"), + ); + + const isAvailable = computed(() => { + return ( + typeof window !== "undefined" && + !!(window as any).cmdExec && + (typeof (window as any).cmdExec.execute === "function" || + typeof (window as any).cmdExec.executeAsync === "function") && + execMethod.value !== "none" + ); + }); + + onMounted(() => { + let attempts = 0; + const MAX_ATTEMPTS = 40; // ~10s polling window + const INTERVAL_MS = 250; + const interval = setInterval(() => { + attempts++; + const bridge = (window as any).cmdExec; + const execMethodCheck = bridge + ? (bridge.execMethod as ExecMethod) || "none" + : "none"; + const ok = + typeof window !== "undefined" && + !!bridge && + (typeof bridge.execute === "function" || + typeof bridge.executeAsync === "function") && + execMethodCheck !== "none"; + _available.value = ok; + if (ok || attempts >= MAX_ATTEMPTS) { + clearInterval(interval); + } + }, INTERVAL_MS); + + onUnmounted(() => clearInterval(interval)); + }); + + const execMethod = computed(() => { + try { + const bridge = (window as any).cmdExec; + if (!bridge) return "none"; + return (bridge.execMethod as ExecMethod) || "none"; + } catch { + return "none"; + } + }); + + function normalizeOutput(raw: string): string[] { + if (!raw) return []; + return String(raw) + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("[Executing:")); + } + + function runCommandAsync( + command: string, + options?: { asRoot?: boolean; debug?: boolean; callbacks?: AsyncCallbacks }, + ): string | null { + const bridge = (window as any).cmdExec; + if (!bridge || typeof bridge.executeAsync !== "function") return null; + + const asRoot = options?.asRoot !== undefined ? options.asRoot : true; + const enableDebug = !!options?.debug; + const callbacks = options?.callbacks || {}; + + const finalCommand = enableDebug ? `${LOGGING_PREFIX} ${command}` : command; + + try { + const commandId = bridge.executeAsync(finalCommand, asRoot, { + onOutput: (out: string) => { + if (callbacks.onRawOutput) callbacks.onRawOutput(out); + const lines = normalizeOutput(out); + lines.forEach((line) => { + if (callbacks.onOutput) callbacks.onOutput(line); + }); + }, + onError: (err: any) => { + if (callbacks.onError) callbacks.onError(String(err)); + }, + onComplete: (res: CommandResult) => { + if (callbacks.onComplete) callbacks.onComplete(res); + }, + }); + return commandId; + } catch { + return null; + } + } + + function runCommandAsyncPromise( + command: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + timeoutMs?: number; + }, + ): Promise { + const timeoutMs = options?.timeoutMs; + + return new Promise((resolve) => { + const callbacks: AsyncCallbacks = { + onOutput: (line) => options?.onOutput?.(line), + onError: (err) => resolve({ success: false, error: String(err) }), + onComplete: (result) => resolve(result), + }; + + if (!isAvailable.value) { + resolve({ success: false, error: "No command bridge available" }); + return; + } + + const commandId = runCommandAsync(command, { + asRoot: options?.asRoot, + debug: options?.debug, + callbacks, + }); + + if (!commandId) { + resolve({ success: false, error: "Failed to start command" }); + return; + } + + // Set timeout to prevent hanging, if specified + let timeoutId: NodeJS.Timeout | undefined; + if (timeoutMs && timeoutMs > 0) { + timeoutId = setTimeout(() => { + resolve({ success: false, error: "Command execution timed out" }); + }, timeoutMs); + } + + // Clear timeout when command completes + const originalOnComplete = callbacks.onComplete; + const originalOnError = callbacks.onError; + callbacks.onComplete = (result) => { + if (timeoutId) clearTimeout(timeoutId); + originalOnComplete?.(result); + }; + callbacks.onError = (err) => { + if (timeoutId) clearTimeout(timeoutId); + originalOnError?.(err); + }; + }); + } + + async function runCommandSync( + command: string, + options?: { asRoot?: boolean; debug?: boolean }, + ): Promise { + const bridge = (window as any).cmdExec; + if (!bridge || typeof bridge.execute !== "function") { + return ""; + } + + const asRoot = options?.asRoot !== undefined ? options.asRoot : true; + const enableDebug = !!options?.debug; + const finalCommand = enableDebug ? `${LOGGING_PREFIX} ${command}` : command; + + try { + const out = await bridge.execute(finalCommand, asRoot); + return String(out || ""); + } catch (err) { + throw err; + } + } + + function getRunningCommands(): Array<{ + id: string; + command: string; + startTime: number; + duration: number; + }> { + try { + const bridge = (window as any).cmdExec; + if (!bridge || typeof bridge.getRunningCommands !== "function") return []; + return bridge.getRunningCommands() || []; + } catch { + return []; + } + } + + function isCommandRunning(commandId: string): boolean { + try { + const bridge = (window as any).cmdExec; + if (!bridge || typeof bridge.isCommandRunning !== "function") + return false; + return bridge.isCommandRunning(commandId); + } catch { + return false; + } + } + + async function captureCommandOutput( + command: string, + options?: { asRoot?: boolean; debug?: boolean }, + ): Promise<{ result: CommandResult; lines: string[] }> { + const lines: string[] = []; + const res = await runCommandAsyncPromise(command, { + asRoot: options?.asRoot, + debug: options?.debug, + onOutput: (line) => lines.push(line), + }); + return { result: res, lines }; + } + + // Keep reactive availability in sync with dynamic computed + _available.value = isAvailable.value; + + return { + isAvailable: readonly(_available), + execMethod, + runCommandAsync, + runCommandAsyncPromise, + runCommandSync, + captureCommandOutput, + getRunningCommands, + isCommandRunning, + }; +} + +export default useNativeCmd; diff --git a/webroot/app/composables/useSettings.ts b/webroot/app/composables/useSettings.ts new file mode 100644 index 0000000..4ce7a4d --- /dev/null +++ b/webroot/app/composables/useSettings.ts @@ -0,0 +1,90 @@ +import { nextTick } from "vue"; +import useConsole from "@/composables/useConsole"; +import BackupRestoreFeature from "@/features/backup-restore"; +import UninstallFeature from "@/features/uninstall"; +import ResizeFeature from "@/features/resize"; + +export function useSettings(consoleApi: ReturnType) { + function appendConsole(text: string, cls?: string) { + consoleApi.append(text, cls); + } + + function openSettingsPopup() { + const el = document.getElementById("settings-popup"); + if (el) el.classList.add("active"); + // loadPostExecScript is handled in useChroot + } + + async function closeSettingsPopup() { + const el = document.getElementById("settings-popup"); + if (el) el.classList.remove("active"); + await nextTick(); + } + + function openSparseSettingsPopup() { + const el = document.getElementById("sparse-settings-popup"); + if (el) el.classList.add("active"); + } + + function closeSparseSettingsPopup() { + const el = document.getElementById("sparse-settings-popup"); + if (el) el.classList.remove("active"); + } + + async function backupChroot() { + if ( + (window as any).BackupRestoreFeature && + (window as any).BackupRestoreFeature.backupChroot + ) { + await (window as any).BackupRestoreFeature.backupChroot(); + } + } + + async function restoreChroot() { + if ( + (window as any).BackupRestoreFeature && + (window as any).BackupRestoreFeature.restoreChroot + ) { + await (window as any).BackupRestoreFeature.restoreChroot(); + } + } + + async function uninstallChroot() { + if ( + (window as any).UninstallFeature && + (window as any).UninstallFeature.uninstallChroot + ) { + await (window as any).UninstallFeature.uninstallChroot(); + } + } + + async function trimSparseImage() { + if ( + (window as any).ResizeFeature && + (window as any).ResizeFeature.trimSparseImage + ) { + await (window as any).ResizeFeature.trimSparseImage(); + } + } + + async function resizeSparseImage() { + if ( + (window as any).ResizeFeature && + (window as any).ResizeFeature.resizeSparseImage + ) { + await (window as any).ResizeFeature.resizeSparseImage(); + } + } + + return { + openSettingsPopup, + closeSettingsPopup, + openSparseSettingsPopup, + closeSparseSettingsPopup, + backupChroot, + restoreChroot, + uninstallChroot, + trimSparseImage, + resizeSparseImage, + }; +} diff --git a/webroot/app/composables/useStateManager.ts b/webroot/app/composables/useStateManager.ts new file mode 100644 index 0000000..dbe1356 --- /dev/null +++ b/webroot/app/composables/useStateManager.ts @@ -0,0 +1,205 @@ +import { reactive, toRefs } from "vue"; + +type StorageValue = string | null; + +type StorageAPI = { + get: (key: string, defaultValue?: string | null) => string | null; + set: (key: string, value: string) => void; + remove: (key: string) => void; + getBoolean: (key: string, defaultValue?: boolean) => boolean; + getJSON: (key: string, defaultValue?: T | null) => T | null; + setJSON: (key: string, value: any) => void; +}; + +type StateKeys = "hotspot" | "forwarding" | "debug" | "sparse"; + +type StateManagerAPI = { + // reactive state object (expose via toRefs for reactivity in components) + state: { + hotspot: boolean; + forwarding: boolean; + debug: boolean; + sparse: boolean; + }; + + // read/write single state value + get: (name: StateKeys) => boolean; + set: (name: StateKeys, value: boolean) => void; + + // load/save all states from/to storage + loadAll: () => void; + saveAll: () => void; +}; + +// Create a safe storage wrapper. +// It gracefully handles environments where localStorage isn't available. +function createStorage(): StorageAPI { + function safeLocalStorage() { + try { + if (typeof window === "undefined") return null; + if (!("localStorage" in window)) return null; + return window.localStorage; + } catch { + return null; + } + } + + function get(key: string, defaultValue: string | null = null): string | null { + const ls = safeLocalStorage(); + if (!ls) return defaultValue; + try { + const v = ls.getItem(key); + return v !== null ? v : defaultValue; + } catch { + return defaultValue; + } + } + + function set(key: string, value: string): void { + const ls = safeLocalStorage(); + if (!ls) return; + try { + ls.setItem(key, String(value)); + } catch { + // ignore + } + } + + function remove(key: string): void { + const ls = safeLocalStorage(); + if (!ls) return; + try { + ls.removeItem(key); + } catch { + // ignore + } + } + + function getBoolean(key: string, defaultValue = false): boolean { + const raw = get(key); + if (raw === null) return defaultValue; + return raw === "true"; + } + + function getJSON( + key: string, + defaultValue: T | null = null, + ): T | null { + const raw = get(key); + if (raw === null) return defaultValue; + try { + return JSON.parse(raw) as T; + } catch { + return defaultValue; + } + } + + function setJSON(key: string, value: any) { + try { + set(key, JSON.stringify(value)); + } catch { + // ignore + } + } + + return { + get, + set, + remove, + getBoolean, + getJSON, + setJSON, + }; +} + +// Create a simple state manager that persists boolean feature flags. +function createStateManager(storage: StorageAPI): StateManagerAPI { + // mapping of names -> storage key + defaults + const statesMeta: Record = { + hotspot: { key: "hotspot_active", default: false }, + forwarding: { key: "forwarding_active", default: false }, + debug: { key: "debug_mode_active", default: false }, + sparse: { key: "sparse_migrated", default: false }, + }; + + // Reactive object used by UI/components. + const state = reactive({ + hotspot: storage.getBoolean( + statesMeta.hotspot.key, + statesMeta.hotspot.default, + ), + forwarding: storage.getBoolean( + statesMeta.forwarding.key, + statesMeta.forwarding.default, + ), + debug: storage.getBoolean(statesMeta.debug.key, statesMeta.debug.default), + sparse: storage.getBoolean( + statesMeta.sparse.key, + statesMeta.sparse.default, + ), + }); + + function get(name: StateKeys): boolean { + return state[name]; + } + + function set(name: StateKeys, value: boolean) { + state[name] = !!value; + try { + storage.set(statesMeta[name].key, String(state[name])); + } catch { + // ignore storage issues silently + } + } + + function loadAll() { + (Object.keys(statesMeta) as StateKeys[]).forEach((k) => { + state[k] = storage.getBoolean(statesMeta[k].key, statesMeta[k].default); + }); + } + + function saveAll() { + (Object.keys(statesMeta) as StateKeys[]).forEach((k) => { + try { + storage.set(statesMeta[k].key, String(state[k])); + } catch { + // ignore + } + }); + } + + return { + state, + get, + set, + loadAll, + saveAll, + }; +} + +// Composable that exposes a singleton Storage object and StateManager. +// This returns the singleton instances to keep the app consistent. +export const Storage: StorageAPI = createStorage(); +export const StateManager: StateManagerAPI = createStateManager(Storage); + +// Named composable (useStateManager) to get reactive refs & helpers. +export function useStateManager() { + return { + state: toRefs(StateManager.state), // provides: { hotspot, forwarding, ... } as refs + get: StateManager.get, + set: StateManager.set, + loadAll: StateManager.loadAll, + saveAll: StateManager.saveAll, + Storage, + StateManager, + }; +} + +// Named composable for storage only (for explicit usage) +export function useStorage() { + return { + Storage, + }; +} + +export default useStateManager; diff --git a/webroot/app/composables/utils.ts b/webroot/app/composables/utils.ts new file mode 100644 index 0000000..0ceb6cd --- /dev/null +++ b/webroot/app/composables/utils.ts @@ -0,0 +1,123 @@ +export const ButtonState = { + setButton( + btn: HTMLButtonElement | null | undefined, + enabled: boolean, + visible = true, + opacity: string | null = null, + ) { + if (!btn) return; + btn.disabled = !enabled; + btn.style.display = visible ? "" : "none"; + btn.style.opacity = + opacity !== null ? (enabled ? "" : opacity) : enabled ? "" : "0.5"; + if (!enabled) { + btn.classList.remove("btn-pressed", "btn-released"); + btn.style.transform = ""; + btn.style.boxShadow = ""; + } + }, + + setButtonPair( + startBtn: HTMLButtonElement | null | undefined, + stopBtn: HTMLButtonElement | null | undefined, + isActive: boolean, + ) { + this.setButton(startBtn, !isActive, true, "0.5"); + this.setButton(stopBtn, isActive, true, "0.5"); + }, + + setButtons( + buttons: Array<{ + btn: HTMLButtonElement | null | undefined; + enabled: boolean; + visible?: boolean; + opacity?: string | null; + }>, + ) { + buttons.forEach(({ btn, enabled, visible, opacity }) => { + this.setButton( + btn, + enabled, + visible !== undefined ? visible : true, + opacity ?? null, + ); + }); + }, +}; + +export const PopupManager = { + open(popup: HTMLElement | null | undefined) { + if (!popup) return; + popup.classList.add("active"); + }, + close(popup: HTMLElement | null | undefined) { + if (!popup) return; + popup.classList.remove("active"); + }, + setupClickOutside( + popup: HTMLElement | null | undefined, + closeFn: (() => void) | undefined, + ) { + if (!popup || !closeFn) return; + const handler = (e: Event) => { + if (e.target === popup) closeFn(); + }; + popup.addEventListener("click", handler); + // return an unbind method if needed in future + return () => popup.removeEventListener("click", handler); + }, +}; + +export function disableAllActions( + startDisabled: { value: boolean }, + stopDisabled: { value: boolean }, + restartDisabled: { value: boolean }, + userSelectDisabled: { value: boolean }, + copyLoginDisabled: { value: boolean }, +) { + startDisabled.value = true; + stopDisabled.value = true; + restartDisabled.value = true; + userSelectDisabled.value = true; + copyLoginDisabled.value = true; + + // Disable console controls + const clearConsoleBtn = document.getElementById( + "clear-console", + ) as HTMLButtonElement | null; + const copyConsoleBtn = document.getElementById( + "copy-console", + ) as HTMLButtonElement | null; + const refreshBtn = document.getElementById( + "refresh-status", + ) as HTMLButtonElement | null; + + if (clearConsoleBtn) clearConsoleBtn.disabled = true; + if (copyConsoleBtn) copyConsoleBtn.disabled = true; + if (refreshBtn) refreshBtn.disabled = true; +} + +export function disableSettingsPopup(chrootExists = true) { + const buttonsToDisable = [ + "post-exec-script", + "save-script", + "clear-script", + "update-btn", + "backup-btn", + "restore-btn", + "uninstall-btn", + "trim-sparse-btn", + "resize-sparse-btn", + ]; + + buttonsToDisable.forEach((id) => { + const el = document.getElementById(id) as + | HTMLButtonElement + | HTMLInputElement + | null; + if (!el) return; + el.setAttribute("disabled", "true"); + el.style.pointerEvents = "none"; + el.style.opacity = "0.5"; + }); +} diff --git a/webroot/app/features/backup-restore.ts b/webroot/app/features/backup-restore.ts new file mode 100644 index 0000000..0dca563 --- /dev/null +++ b/webroot/app/features/backup-restore.ts @@ -0,0 +1,479 @@ +import { DEFAULT_BACKUP_DIR } from "../composables/constants"; +import { createApp, nextTick } from "vue"; +import type { CommandResult } from "@/composables/useNativeCmd"; + +// @ts-ignore +import FilePickerPopup from "../components/FilePickerPopup.vue"; + +export type BackupRestoreDeps = { + appendConsole: (text: string, cls?: string) => void; + runCmdSync: (cmd: string) => Promise; + runCmdAsync?: (cmd: string, onComplete?: (res: any) => void) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + Storage: { + get?: (key: string, defaultValue?: any) => any; + set?: (key: string, value: any) => void; + getJSON?: (key: string, defaultValue?: T | null) => T | null; + setJSON?: (key: string, value: any) => void; + }; + + showConfirmDialog: ( + title: string, + message: string, + confirmText?: string, + cancelText?: string, + ) => Promise; + + closeSettingsPopup?: () => void; + + ANIMATION_DELAYS?: Record; + + PATH_CHROOT_SH: string; + + ProgressIndicator?: { + create: ( + text: string, + type?: "spinner" | "dots", + el?: HTMLElement | null, + ) => { progressLine: HTMLElement; interval?: any } | null; + remove: ( + handle: { progressLine: HTMLElement; interval?: any } | null, + ) => void; + }; + + disableAllActions?: (disabled: boolean) => void; + disableSettingsPopup?: (disabled: boolean, chrootExists?: boolean) => void; + withCommandGuard?: (id: string, fn: () => Promise) => Promise; + + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise<{ progressLine: HTMLElement; interval?: any }>; + + executeCommandWithProgress?: (options: { + cmd: string; + progress: { progressLine: HTMLElement; progressInterval?: any } | null; + onSuccess?: (result?: any) => void; + onError?: (result?: any) => void; + onComplete?: (result?: any) => void; + useValue?: boolean; + activeCommandIdRef?: { value: string | null }; + }) => string | null; + + refreshStatus?: () => Promise; + ensureChrootStopped?: () => Promise; + updateStatus?: (s: string) => void; + updateModuleStatus?: () => void; + + activeCommandId?: { value: string | null }; + rootAccessConfirmed?: { value: boolean }; + showProgress?: { value: boolean }; + progressTitle?: { value: string }; + progressMessage?: { value: string }; +}; + +let deps: BackupRestoreDeps | null = null; + +/** + * File picker dialog using Vue component + */ +function showFilePickerDialog( + title: string, + message: string, + defaultPath?: string, + defaultFilename?: string, + forRestore = false, +): Promise { + return new Promise((resolve) => { + const app = createApp(FilePickerPopup, { + visible: true, + title, + message, + defaultPath, + defaultFilename, + forRestore, + onResolve: (path: string | null) => { + app.unmount(); + resolve(path); + }, + }); + const div = document.createElement("div"); + document.body.appendChild(div); + app.mount(div); + }); +} + +/** + * Initialize the feature module with dependencies + */ +export function init(d: BackupRestoreDeps) { + deps = d; +} + +/** + * Create a filename for backup with ISO timestamp + */ +function makeBackupFilename() { + const t = new Date().toISOString().slice(0, 19).replace(/:/g, "-"); + return `chroot-backup-${t}.tar.gz`; +} + +/** + * Backup the chroot using selected path and the PATH_CHROOT_SH wrapper + */ +export async function backupChroot() { + if (!deps) return; + const d = deps; + + d.appendConsole("Backup initiated", "info"); + + // Prevent concurrent backups via the active command check + if (d.activeCommandId && d.activeCommandId.value) { + d.appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + + const defaultDir = DEFAULT_BACKUP_DIR; + const defaultFilename = makeBackupFilename(); + + try { + let backupPath: string | null; + try { + backupPath = await showFilePickerDialog( + "Backup Chroot Environment", + "Select where to save the backup file.\n\nThe chroot will be stopped during backup if it's currently running.", + defaultDir, + defaultFilename, + false, + ); + } catch (err) { + deps!.appendConsole( + `File picker failed, using default path: ${(err as Error)?.message || String(err)}`, + "warn", + ); + backupPath = `${defaultDir}/${defaultFilename}`; + } + + if (!backupPath) { + d.appendConsole("Backup cancelled: no path selected", "warn"); + return; + } + + d.appendConsole(`Backup path selected: ${backupPath}`, "info"); + + if (d.showProgress) d.showProgress.value = true; + if (d.progressTitle) d.progressTitle.value = "Backup in Progress"; + if (d.progressMessage) + d.progressMessage.value = + "Please wait while the backup is being created..."; + + d.closeSettingsPopup?.(); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE_LONG ?? 400), + ); + + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + // Check and stop chroot if running + try { + const statusOut = d.runCmdSync(`${d.PATH_CHROOT_SH} raw-status`); + const status = String(statusOut || "") + .trim() + .toUpperCase(); + if (status === "RUNNING") { + d.appendConsole("Stopping chroot before backup...", "info"); + d.runCmdSync(`${d.PATH_CHROOT_SH} stop --no-shell`); + // Verify stopped + const verifyOut = d.runCmdSync(`${d.PATH_CHROOT_SH} raw-status`); + const verifyStatus = String(verifyOut || "") + .trim() + .toUpperCase(); + if (verifyStatus !== "STOPPED") { + d.appendConsole("✗ Failed to stop chroot - backup aborted", "err"); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + } + } catch (err: any) { + d.appendConsole( + `✗ Failed to check/stop chroot: ${String(err?.message || err)} - backup aborted`, + "err", + ); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + + d.updateStatus?.("backing up"); + + const progress = d.prepareActionExecution + ? await d.prepareActionExecution( + "Starting Chroot Backup", + "Backing up chroot", + "dots", + ) + : null; + + d.appendConsole(`Starting backup to: ${backupPath}`, "info"); + const cmdStr = `sh ${d.PATH_CHROOT_SH} backup --webui "${backupPath}"`; + + try { + const pendingOutput: string[] = []; + const flushOutput = () => { + if (pendingOutput.length > 0) { + pendingOutput.forEach((line) => console.log(line)); + pendingOutput.length = 0; + } + }; + const throttledFlush = () => { + flushOutput(); + setTimeout(throttledFlush, 100); + }; + throttledFlush(); + + const result = await d.runCommandAsyncPromise?.(cmdStr, { + onOutput: (line) => pendingOutput.push(line), + }); + flushOutput(); // Final flush + if (progress) d.ProgressIndicator?.remove(progress); + if (result?.success) { + d.appendConsole(`✓ Backup completed successfully`, "success"); + d.appendConsole(`Saved to: ${backupPath}`, "info"); + d.appendConsole("━━━ Backup Complete ━━━", "success"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } else { + d.appendConsole( + `✗ Backup failed: ${result?.error || "Unknown error"}`, + "err", + ); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } + } catch (err: any) { + if (progress) d.ProgressIndicator?.remove(progress); + d.appendConsole(`✗ Backup failed: ${String(err?.message || err)}`, "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } + } catch (err: any) { + d.appendConsole(`Backup aborted: ${String(err?.message || err)}`, "warn"); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } finally { + if (d.showProgress) d.showProgress.value = false; + if (d.progressTitle) d.progressTitle.value = ""; + if (d.progressMessage) d.progressMessage.value = ""; + } +} + +/** + * Restores a chroot from a backup file. + */ +export async function restoreChroot() { + if (!deps) return; + const d = deps; + + // Prevent concurrent operations + if (d.activeCommandId && d.activeCommandId.value) { + d.appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + + // Check root access presence if available + if (d.rootAccessConfirmed && !d.rootAccessConfirmed.value) { + d.appendConsole("Cannot restore chroot: root access not available", "err"); + return; + } + + try { + let backupPath: string | null; + try { + backupPath = await showFilePickerDialog( + "Restore Chroot Environment", + "Select the backup file to restore from.\n\nWARNING: This will permanently delete your current chroot environment!", + DEFAULT_BACKUP_DIR, + "", + true, // forRestore + ); + } catch (err) { + deps!.appendConsole( + `File picker error: ${(err as Error)?.message || String(err)}`, + "err", + ); + return; + } + + if (!backupPath) return; + + if (d.showProgress) d.showProgress.value = true; + if (d.progressTitle) d.progressTitle.value = "Restore in Progress"; + if (d.progressMessage) + d.progressMessage.value = + "Please wait while the restore is being performed..."; + + d.closeSettingsPopup?.(); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE_LONG ?? 400), + ); + + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + // Check and stop chroot if running + try { + const statusOut = d.runCmdSync(`${d.PATH_CHROOT_SH} raw-status`); + const status = String(statusOut || "") + .trim() + .toUpperCase(); + if (status === "RUNNING") { + d.appendConsole("Stopping chroot before restore...", "info"); + d.runCmdSync(`${d.PATH_CHROOT_SH} stop --no-shell`); + // Verify stopped + const verifyOut = d.runCmdSync(`${d.PATH_CHROOT_SH} raw-status`); + const verifyStatus = String(verifyOut || "") + .trim() + .toUpperCase(); + if (verifyStatus !== "STOPPED") { + d.appendConsole("✗ Failed to stop chroot - restore aborted", "err"); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + } + } catch (err: any) { + d.appendConsole( + `✗ Failed to check/stop chroot: ${String(err?.message || err)} - restore aborted`, + "err", + ); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + + d.updateStatus?.("restoring"); + + const progress = d.prepareActionExecution + ? await d.prepareActionExecution( + "Starting Chroot Restore", + "Restoring chroot", + "dots", + ) + : null; + + const cmdStr = `sh ${d.PATH_CHROOT_SH} restore --webui "${backupPath}"`; + + try { + const pendingOutput: string[] = []; + const flushOutput = () => { + if (pendingOutput.length > 0) { + pendingOutput.forEach((line) => console.log(line)); + pendingOutput.length = 0; + } + }; + const throttledFlush = () => { + flushOutput(); + setTimeout(throttledFlush, 100); + }; + throttledFlush(); + + const result = await d.runCommandAsyncPromise?.(cmdStr, { + onOutput: (line) => pendingOutput.push(line), + }); + flushOutput(); // Final flush + if (progress) d.ProgressIndicator?.remove?.(progress ?? null); + if (result?.success) { + d.appendConsole("✓ Restore completed successfully", "success"); + d.appendConsole("The chroot environment has been restored", "info"); + d.appendConsole("━━━ Restore Complete ━━━", "success"); + d.updateStatus?.("stopped"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + (d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500) * 2, + ); + } else { + d.appendConsole("✗ Restore failed", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + (d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500) * 2, + ); + } + } catch (err: any) { + if (progress) d.ProgressIndicator?.remove?.(progress ?? null); + d.appendConsole("✗ Restore failed", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + (d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500) * 2, + ); + } + } catch (err: any) { + d.appendConsole(`Restore aborted: ${String(err?.message || err)}`, "warn"); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } finally { + if (d.showProgress) d.showProgress.value = false; + if (d.progressTitle) d.progressTitle.value = ""; + if (d.progressMessage) d.progressMessage.value = ""; + } +} + +/** + * Minimal compatibility object available on window for legacy app.js usage + */ +export const BackupRestoreFeature = { + init, + backupChroot, + restoreChroot, +}; + +if (typeof window !== "undefined") { + try { + (window as any).BackupRestoreFeature = BackupRestoreFeature; + } catch {} +} + +export default BackupRestoreFeature; diff --git a/webroot/app/features/forward-nat.ts b/webroot/app/features/forward-nat.ts new file mode 100644 index 0000000..a010223 --- /dev/null +++ b/webroot/app/features/forward-nat.ts @@ -0,0 +1,442 @@ +import { NetworkInterfaceManager } from "@/services/NetworkInterfaceManager"; +import type { CommandResult } from "@/composables/useNativeCmd"; + +export type ForwardNatDeps = { + // UI element map (optional, but preferred) + els?: { + forwardNatIface?: HTMLSelectElement | null; + forwardNatPopup?: HTMLElement | null; + startForwardingBtn?: HTMLElement | null; + stopForwardingBtn?: HTMLElement | null; + }; + + // Small state refs (optional, if provided they are updated by the module) + forwardingActive?: { value: boolean }; + + // persistent storage helpers + Storage: { + get: (key: string, defaultValue?: any) => any; + set: (key: string, value: any) => void; + getJSON?: (key: string, defaultValue?: T | null) => T | null; + setJSON?: (key: string, value: any) => void; + }; + + // State manager (if present) for saving flags + StateManager?: { + get?: (name: string) => boolean; + set?: (name: string, value: boolean) => void; + }; + + // Script paths + FORWARD_NAT_SCRIPT: string; + + // Utilities provided by the main app + appendConsole: (text: string, cls?: string) => void; + runCmdSync: (cmd: string) => Promise; + runCmdAsync?: (cmd: string, onComplete?: (res: any) => void) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + + // Flow & UI management + withCommandGuard?: (id: string, fn: () => Promise) => Promise; + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise<{ progressLine: HTMLElement; interval: any }>; + ProgressIndicator?: { + create: ( + text: string, + type?: "spinner" | "dots", + el?: HTMLElement | null, + ) => { progressLine: HTMLElement; interval?: any } | null; + remove: ( + handle?: { progressLine: HTMLElement; interval?: any } | null, + ) => void; + update: ( + handle?: { progressLine: HTMLElement; interval?: any } | null, + text?: string, + ) => void; + }; + disableAllActions?: (disabled: boolean) => void; + disableSettingsPopup?: (disabled: boolean, chrootExists?: boolean) => void; + PopupManager?: { + open: (popup: HTMLElement | null | undefined) => void; + close: (popup: HTMLElement | null | undefined) => void; + }; + ButtonState?: { + setButtonPair?: ( + startBtn?: HTMLElement | null, + stopBtn?: HTMLElement | null, + isActive?: boolean, + ) => void; + }; + + // Flags used by other modules or core (optional) + rootAccessConfirmed?: { value: boolean }; + activeCommandId?: { value: string | null }; + updateStatus?: (s: string) => void; + refreshStatus?: () => Promise; + + // Optional delays + ANIMATION_DELAYS?: Record; +}; + +let deps: ForwardNatDeps | null = null; +let interfaceManager: NetworkInterfaceManager | null = null; + +/** + * Initialize the feature with dependencies. + */ +export function init(d: ForwardNatDeps) { + deps = d; + if (deps.forwardingActive == null) deps.forwardingActive = { value: false }; + + interfaceManager = new NetworkInterfaceManager( + { + Storage: deps.Storage, + appendConsole: deps.appendConsole, + runCmdSync: deps.runCmdSync, + rootAccessConfirmed: deps.rootAccessConfirmed, + }, + deps.FORWARD_NAT_SCRIPT, + "chroot_forward_nat_interfaces_cache", + deps.els?.forwardNatIface || null, + "chroot_selected_interface", + ); + + try { + loadForwardingStatus(); + } catch { + // ignore failures during init + } + + try { + fetchInterfaces(true, true).catch(() => {}); + } catch { + // ignore + } +} + +/** + * Load forwarding state from persistent store (StateManager or Storage). + */ +export function loadForwardingStatus() { + if (!deps) return; + if (!deps.forwardingActive) deps.forwardingActive = { value: false }; + + let value = false; + if (deps.StateManager && deps.StateManager.get) { + try { + const v = deps.StateManager.get("forwarding"); + value = Boolean(v); + } catch { + value = false; + } + } else { + // Fallback to Storage boolean + try { + const val = deps.Storage.get("forwarding_active"); + value = String(val) === "true"; + } catch { + value = false; + } + } + + deps.forwardingActive.value = value; + + try { + deps.ButtonState?.setButtonPair?.( + deps.els?.startForwardingBtn ?? null, + deps.els?.stopForwardingBtn ?? null, + Boolean(deps.forwardingActive.value), + ); + } catch { + // ignore + } +} + +/** + * Save forwarding state to persistent store. + */ +export function saveForwardingStatus() { + if (!deps) return; + const value = + deps.forwardingActive && deps.forwardingActive.value ? true : false; + if (deps.StateManager && deps.StateManager.set) { + deps.StateManager.set("forwarding", value); + } else { + try { + deps.Storage.set("forwarding_active", value ? "true" : "false"); + } catch { + // ignore + } + } +} + +/** + * Fetch network interfaces for forwarding. Uses caching (Storage) and prefers cached data unless forced. + * - forceRefresh: forces fetching from script and updating cache. + * - backgroundOnly: when true, only updates cache but does not update UI elements. + */ +export async function fetchInterfaces( + forceRefresh = false, + backgroundOnly = false, +) { + if (!interfaceManager) return; + await interfaceManager.fetchInterfaces(forceRefresh, backgroundOnly); +} + +/** + * Opens the forwarding popup and ensures UI has cached interfaces (no heavy background fetch). + */ +export async function openForwardNatPopup() { + if (!deps) return; + deps.PopupManager?.open?.(deps.els?.forwardNatPopup ?? null); + + // Update select element reference in case it wasn't available during init + if (interfaceManager) { + interfaceManager.updateSelectElement(deps.els?.forwardNatIface || null); + } + + try { + await fetchInterfaces(false, true); + } catch { + // ignore + } + + try { + await fetchInterfaces(false, false); + } catch { + // ignore + } +} + +/** + * Refresh interfaces (force refresh). + */ +export function refreshInterfaces() { + fetchInterfaces(true, false).catch(() => {}); +} + +/** + * Close forward-nat popup + */ +export function closeForwardNatPopup() { + if (!deps) return; + deps.PopupManager?.close?.(deps.els?.forwardNatPopup ?? null); +} + +/** + * Start forwarding on the selected interface. Uses `withCommandGuard` to avoid concurrent operations. + * Saves the selected interface to Storage, updates active UI state, and sets forwardingActive flag. + */ +export async function startForwarding() { + if (!deps) return; + const d = deps; + + // Validate UI props + const select = d.els?.forwardNatIface; + const iface = (select?.value || "").trim(); + if (!iface) { + d.appendConsole?.("Please select a network interface", "err"); + return; + } + + await d.withCommandGuard?.("forwarding-start", async () => { + try { + d.Storage.set("chroot_selected_interface", iface); + } catch { + // ignore + } + + // Close popup & small delay for UI + d.PopupManager?.close?.(d.els?.forwardNatPopup ?? null); + const delay = d.ANIMATION_DELAYS?.POPUP_CLOSE ?? 350; + await new Promise((r) => setTimeout(r, delay)); + + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + const actionText = `Starting forwarding on ${iface}`; + const progress = d.prepareActionExecution + ? await d.prepareActionExecution(actionText, actionText, "spinner") + : null; + + d.activeCommandId && (d.activeCommandId.value = "forwarding-start"); + + try { + const cmd = `sh ${d.FORWARD_NAT_SCRIPT} -i "${iface}" 2>&1`; + const result = await d.runCommandAsyncPromise?.(cmd, { + onOutput: (line) => d.appendConsole?.(line), + }); + if (!result || !result.success) { + d.appendConsole?.("✗ Failed to start forwarding", "err"); + return; + } + const outStr = result.output || ""; + + // Heuristic: treat as success unless we see explicit error keywords. + const failureRegex = /fail(ed)?|error|permission denied|not found/i; + const success = + outStr && + !failureRegex.test(outStr) && + (outStr.includes("Localhost routing active") || + outStr.includes("Gateway:") || + outStr.trim().length > 0); + + if (success) { + d.appendConsole?.( + `✓ Forwarding started successfully on ${iface}`, + "success", + ); + // Update state + if (d.forwardingActive) { + d.forwardingActive.value = true; + } + saveForwardingStatus(); + d.ButtonState?.setButtonPair?.( + d.els?.startForwardingBtn ?? null, + d.els?.stopForwardingBtn ?? null, + true, + ); + } else { + d.appendConsole?.("✗ Failed to start forwarding", "err"); + } + } catch (error: any) { + d.appendConsole?.( + `✗ Failed to start forwarding: ${String(error?.message || error)}`, + "err", + ); + } finally { + d.ProgressIndicator?.remove?.(progress ?? null); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 300, + ); + } + }); +} + +/** + * Stop forwarding - uses async execution to allow the background script to cleanup, and keeps UI responsive. + */ +export async function stopForwarding() { + if (!deps) return; + const d = deps; + + await d.withCommandGuard?.("forwarding-stop", async () => { + // Close popup & small delay for UI + try { + d.PopupManager?.close?.(d.els?.forwardNatPopup ?? null); + } catch {} + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE ?? 250), + ); + + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + const actionText = "Stopping forwarding"; + const progress = d.prepareActionExecution + ? await d.prepareActionExecution(actionText, actionText, "spinner") + : null; + + d.activeCommandId && (d.activeCommandId.value = "forwarding-stop"); + + try { + const cmd = `sh ${d.FORWARD_NAT_SCRIPT} -k 2>&1`; + const result = await d.runCommandAsyncPromise?.(cmd, { + onOutput: (line) => d.appendConsole?.(line), + }); + // cleanup + d.ProgressIndicator?.remove?.(progress ?? null); + // Always clear the state marker, even if there were errors + if (d.forwardingActive) { + d.forwardingActive.value = false; + } + saveForwardingStatus(); + d.ButtonState?.setButtonPair?.( + d.els?.startForwardingBtn ?? null, + d.els?.stopForwardingBtn ?? null, + false, + ); + + if (result?.success) { + d.appendConsole?.("✓ Forwarding stopped successfully", "success"); + } else { + const output = result?.output || result?.error || ""; + if ( + String(output).toLowerCase().includes("warn") || + String(output).toLowerCase().includes("warning") + ) { + d.appendConsole?.( + "⚠ Forwarding cleanup completed with warnings", + "warn", + ); + } else { + d.appendConsole?.( + "⚠ Forwarding stop completed (some rules may not have existed)", + "warn", + ); + } + // show possible debugging output + if (String(output).trim()) { + String(output) + .split("\n") + .forEach((line) => { + if (line && line.trim()) d.appendConsole?.(String(line).trim()); + }); + } + } + + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 300, + ); + } catch (err: any) { + d.ProgressIndicator?.remove?.(progress ?? null); + d.appendConsole?.( + `✗ Failed to stop forwarding: ${String(err?.message || err)}`, + "err", + ); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } + }); +} + +/** + * Export and attach to window for backward compatibility with legacy app.js style usage. + */ +const ForwardNatFeature = { + init, + loadForwardingStatus, + saveForwardingStatus, + fetchInterfaces, + openForwardNatPopup, + closeForwardNatPopup, + refreshInterfaces, + startForwarding, + stopForwarding, +}; + +if (typeof window !== "undefined") { + try { + (window as any).ForwardNatFeature = ForwardNatFeature; + } catch {} +} + +export default ForwardNatFeature; diff --git a/webroot/app/features/hotspot.ts b/webroot/app/features/hotspot.ts new file mode 100644 index 0000000..6d0cc8d --- /dev/null +++ b/webroot/app/features/hotspot.ts @@ -0,0 +1,532 @@ +import { NetworkInterfaceManager } from "@/services/NetworkInterfaceManager"; +import type { CommandResult } from "@/composables/useNativeCmd"; + +type Nullable = T | null | undefined; + +type DepEls = { + hotspotIface?: HTMLSelectElement; + hotspotSsid?: HTMLInputElement; + hotspotPassword?: HTMLInputElement; + hotspotBand?: HTMLSelectElement; + hotspotChannel?: HTMLSelectElement; + hotspotPopup?: HTMLElement; + startHotspotBtn?: HTMLElement; + stopHotspotBtn?: HTMLElement; + // optional refs in the DOM that feature may use: + dismissHotspotWarning?: HTMLElement; +}; + +type ProgressHandle = { progressLine: HTMLElement; interval?: number | null }; + +// Minimal interface for dependencies the feature expects. +interface HotspotDeps { + els: DepEls; + Storage: { + getJSON: (key: string, defaultValue?: any) => any; + setJSON: (key: string, value: any) => void; + get: (key: string, defaultValue?: any) => any; + set: (key: string, value: any) => void; + }; + StateManager?: { + set: (name: any, value: any) => void; + }; + HOTSPOT_SCRIPT: string; + FORWARD_NAT_SCRIPT?: string; + appendConsole: (text: string, cls?: string) => void; + runCmdSync: (cmd: string) => Promise; + runCmdAsync: ( + cmd: string, + onComplete?: (result: any) => void, + ) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + withCommandGuard: (id: string, fn: () => Promise) => Promise; + ANIMATION_DELAYS: Record; + ProgressIndicator: { + create: ( + text: string, + type?: "spinner" | "dots", + el?: HTMLElement | null, + ) => ProgressHandle; + remove: (handle?: ProgressHandle | null) => void; + update: (handle?: ProgressHandle | null, text?: string) => void; + }; + disableAllActions?: (disabled: boolean) => void; + disableSettingsPopup?: (disabled: boolean, chrootExists?: boolean) => void; + ButtonState?: { + setButtonPair: ( + startBtn: HTMLElement | undefined, + stopBtn: HTMLElement | undefined, + isActive: boolean, + ) => void; + }; + PopupManager?: { + open: (popup: HTMLElement | null | undefined) => void; + close: (popup: HTMLElement | null | undefined) => void; + }; + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise; + // Shared mutable state objects (refs-like) + activeCommandId?: { value: string | null }; + rootAccessConfirmed?: { value: boolean }; + hotspotActive?: { value: boolean }; + saveHotspotStatus?: () => void; + loadHotspotStatus?: () => void; + updateStatus?: (s: string) => void; + refreshStatus?: () => Promise; +} + +type PlayerProgress = { + progressLine: HTMLElement; + interval: any; +}; + +const HotspotFeature = (() => { + let deps: Nullable = null; + let interfaceManager: NetworkInterfaceManager | null = null; + + function init(d: HotspotDeps) { + deps = d; + + interfaceManager = new NetworkInterfaceManager( + { + Storage: deps.Storage, + appendConsole: deps.appendConsole, + runCmdSync: deps.runCmdSync, + rootAccessConfirmed: deps.rootAccessConfirmed, + }, + deps.HOTSPOT_SCRIPT, + "chroot_hotspot_interfaces_cache", + deps.els.hotspotIface || null, + "chroot_hotspot_iface", + ); + + try { + fetchInterfaces(true, true).catch(() => {}); + } catch { + // ignore + } + } + + function _getEl(name: K): Nullable { + if (!deps) return null; + return (deps.els || ({} as DepEls))[name]; + } + + function _appendConsole(text: string, cls?: string) { + deps?.appendConsole?.(text, cls); + } + + async function fetchInterfaces(forceRefresh = false, backgroundOnly = false) { + if (!interfaceManager) return; + await interfaceManager.fetchInterfaces(forceRefresh, backgroundOnly); + } + + async function openHotspotPopup() { + if (!deps) return; + deps.PopupManager?.open?.(deps.els.hotspotPopup ?? null); + + if (interfaceManager) { + interfaceManager.updateSelectElement(deps.els.hotspotIface || null); + } + + try { + await fetchInterfaces(false, false); + } catch { + // ignore + } + } + + function closeHotspotPopup() { + if (!deps) return; + deps.PopupManager?.close?.(deps.els.hotspotPopup); + } + + function showHotspotWarning() { + const els = deps?.els; + if (!els) return; + const warning = els.hotspotPopup + ? els.hotspotPopup.querySelector("#hotspot-warning") + : undefined; + if (warning && warning instanceof HTMLElement) { + warning.classList.remove("hidden"); + } + } + + function dismissHotspotWarning() { + const els = deps?.els; + if (!els) return; + const warning = els.hotspotPopup + ? els.hotspotPopup.querySelector("#hotspot-warning") + : undefined; + if (warning && warning instanceof HTMLElement) { + warning.classList.add("hidden"); + try { + deps!.Storage.set("chroot_hotspot_warning_dismissed", "true"); + } catch { + // ignore + } + } + } + + function saveHotspotSettings() { + if (!deps) return; + const ifaceEl = deps.els.hotspotIface; + const ssidEl = deps.els.hotspotSsid; + const passwordEl = deps.els.hotspotPassword; + const bandEl = deps.els.hotspotBand; + const channelEl = deps.els.hotspotChannel; + + const iface = ifaceEl ? ifaceEl.value.trim() : ""; + const ssid = ssidEl ? ssidEl.value.trim() : ""; + const password = passwordEl ? passwordEl.value : ""; + const band = bandEl ? bandEl.value : ""; + const channel = channelEl ? channelEl.value : ""; + + const settings = { iface, ssid, password, band, channel }; + try { + deps.Storage.setJSON("chroot_hotspot_settings", settings); + } catch { + // ignore + } + } + + async function loadHotspotSettings() { + if (!deps) return; + const storage = deps.Storage; + const saved: any = storage.getJSON?.("chroot_hotspot_settings") || null; + const selectIface = deps.els.hotspotIface; + const ssidEl = deps.els.hotspotSsid; + const passwordEl = deps.els.hotspotPassword; + const bandEl = deps.els.hotspotBand; + const channelEl = deps.els.hotspotChannel; + + const constants = (deps as any).APP_CONSTANTS || { + HOTSPOT: { + DEFAULT_BAND: "2", + DEFAULT_CHANNEL_2_4GHZ: "6", + DEFAULT_CHANNEL_5GHZ: "36", + CHANNELS_2_4GHZ: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + CHANNELS_5GHZ: [36, 40, 44, 48, 52, 56, 60, 64, 100, 104], + }, + }; + + try { + // Populate band -> channel options + if (bandEl && channelEl) { + const band = saved?.band || constants.HOTSPOT.DEFAULT_BAND || "2"; + bandEl.value = String(band); + + const channels = + band === "5" + ? constants.HOTSPOT.CHANNELS_5GHZ + : constants.HOTSPOT.CHANNELS_2_4GHZ; + channelEl.innerHTML = ""; + channels.forEach((c: number) => { + const opt = document.createElement("option"); + opt.value = String(c); + opt.textContent = String(c); + channelEl.appendChild(opt); + }); + + // set selected channel + const savedChannel = saved?.channel + ? String(saved.channel) + : band === "5" + ? String(constants.HOTSPOT.DEFAULT_CHANNEL_5GHZ) + : String(constants.HOTSPOT.DEFAULT_CHANNEL_2_4GHZ); + const exists = Array.from(channelEl.options).some( + (o) => o.value === savedChannel, + ); + channelEl.value = exists + ? savedChannel + : channelEl.options[0] + ? channelEl.options[0].value + : ""; + } + + // set SSID & password + if (ssidEl) ssidEl.value = saved?.ssid || ""; + if (passwordEl) passwordEl.value = saved?.password || ""; + + // set interface if option exists + if (selectIface && saved?.iface) { + const opt = Array.from(selectIface.options).find( + (o) => o.value === saved.iface, + ); + if (opt) selectIface.value = saved.iface; + } + } catch { + // ignore + } + } + + async function startHotspot() { + if (!deps) return; + const d = deps as HotspotDeps; + const { + withCommandGuard, + ANIMATION_DELAYS, + HOTSPOT_SCRIPT, + runCmdSync, + ProgressIndicator, + appendConsole, + disableAllActions, + disableSettingsPopup, + activeCommandId, + hotspotActive, + ButtonState, + prepareActionExecution, + els, + rootAccessConfirmed, + saveHotspotStatus, + refreshStatus, + Storage, + PopupManager, + } = d; + + await withCommandGuard?.("hotspot-start", async () => { + // collect settings + const ifaceEl = els.hotspotIface; + const ssidEl = els.hotspotSsid; + const passwordEl = els.hotspotPassword; + const bandEl = els.hotspotBand; + const channelEl = els.hotspotChannel; + + const iface = ifaceEl ? String(ifaceEl.value || "").trim() : ""; + const ssid = ssidEl ? String(ssidEl.value || "").trim() : ""; + const password = passwordEl ? String(passwordEl.value || "") : ""; + const band = bandEl ? String(bandEl.value || "") : "2"; + const channel = channelEl ? String(channelEl.value || "") : ""; + + const minLen = + (d as any).APP_CONSTANTS?.HOTSPOT?.PASSWORD_MIN_LENGTH ?? 8; + if (!iface) { + appendConsole("Please select a network interface", "err"); + return; + } + if (!ssid) { + appendConsole("Please provide a SSID", "err"); + return; + } + if (!password || password.length < minLen) { + appendConsole(`Password must be at least ${minLen} characters`, "err"); + return; + } + + // Save settings locally + try { + Storage.setJSON("chroot_hotspot_settings", { + iface, + ssid, + password, + band, + channel, + }); + } catch { + // ignore + } + + // close popup first + PopupManager?.close?.(els.hotspotPopup); + // small delay after closing the popup to let CSS animate out + await new Promise((r) => + setTimeout(r, ANIMATION_DELAYS?.POPUP_CLOSE || 350), + ); + + // Disable UI + disableAllActions?.(true); + disableSettingsPopup?.(true); + + const actionText = `Starting hotspot on ${iface}`; + const prepare = prepareActionExecution + ? await prepareActionExecution(actionText, actionText, "spinner") + : null; + const progressHandle = prepare + ? { progressLine: prepare.progressLine, interval: prepare.interval } + : null; + + try { + const cmdLine = `sh ${HOTSPOT_SCRIPT} -o "${iface}" -s "${ssid}" -p "${password}" -b "${band}" -c "${channel}" 2>&1`; + const result = await d.runCommandAsyncPromise?.(cmdLine, { + onOutput: (line) => appendConsole(line), + }); + if (!result || !result.success) { + appendConsole("✗ Failed to start hotspot", "err"); + } else { + appendConsole(`✓ Hotspot started on ${iface}`, "success"); + + // Update state + if (hotspotActive) { + hotspotActive.value = true; + saveHotspotStatus?.(); + } + // Update buttons + if (ButtonState) + ButtonState.setButtonPair( + els.startHotspotBtn, + els.stopHotspotBtn, + true, + ); + // Refresh status + if (refreshStatus) + setTimeout( + () => refreshStatus(), + ANIMATION_DELAYS?.STATUS_REFRESH || 300, + ); + } + } catch (err: any) { + appendConsole( + String(err?.message || err || "Failed to start hotspot"), + "err", + ); + } finally { + // remove progress indicator + if (progressHandle) ProgressIndicator?.remove(progressHandle); + // Re-enable UI + disableAllActions?.(false); + disableSettingsPopup?.(false, true); + } + }); + } + + async function stopHotspot() { + if (!deps) return; + const d = deps as HotspotDeps; + const { + withCommandGuard, + ANIMATION_DELAYS, + HOTSPOT_SCRIPT, + runCmdAsync, + ProgressIndicator, + appendConsole, + disableAllActions, + disableSettingsPopup, + activeCommandId, + hotspotActive, + saveHotspotStatus, + ButtonState, + prepareActionExecution, + refreshStatus, + els, + PopupManager, + } = d; + + await withCommandGuard?.("hotspot-stop", async () => { + // Close popup if open + PopupManager?.close?.(els.hotspotPopup); + await new Promise((r) => + setTimeout(r, ANIMATION_DELAYS?.POPUP_CLOSE || 250), + ); + + disableAllActions?.(true); + disableSettingsPopup?.(true); + + const actionText = "Stopping hotspot"; + const prepare = prepareActionExecution + ? await prepareActionExecution(actionText, actionText, "spinner") + : null; + const progressHandle = prepare + ? { progressLine: prepare.progressLine, interval: prepare.interval } + : null; + + try { + const cmdLine = `sh ${HOTSPOT_SCRIPT} -k 2>&1`; + const result = await d.runCommandAsyncPromise?.(cmdLine, { + onOutput: (line) => appendConsole(line), + }); + // Clean progress indicator + if (progressHandle) d.ProgressIndicator?.remove(progressHandle); + // Clear state + if (hotspotActive) { + hotspotActive.value = false; + saveHotspotStatus?.(); + } + if (ButtonState) + ButtonState.setButtonPair( + els.startHotspotBtn, + els.stopHotspotBtn, + false, + ); + + if (result?.success) { + appendConsole("✓ Hotspot stopped successfully", "success"); + } else { + const output = String(result?.output || result?.error || ""); + if ( + output && + (output.toLowerCase().includes("warn") || + output.toLowerCase().includes("warning")) + ) { + appendConsole("⚠ Hotspot stop completed with warnings", "warn"); + } else { + appendConsole( + "✗ Failed to stop hotspot (may already be stopped)", + "warn", + ); + } + } + + // Re-enable UI + disableAllActions?.(false); + disableSettingsPopup?.(false, true); + + // Refresh status if available + if (refreshStatus) + setTimeout( + () => refreshStatus?.(), + ANIMATION_DELAYS?.STATUS_REFRESH || 300, + ); + } catch (err: any) { + if (progressHandle) d.ProgressIndicator?.remove(progressHandle); + appendConsole( + `✗ Failed to stop hotspot: ${String(err?.message || err)}`, + "err", + ); + disableAllActions?.(false); + disableSettingsPopup?.(false, true); + } + }); + } + + function refreshInterfaces() { + fetchInterfaces(true, false).catch(() => {}); + } + + // Expose feature's public API + const API = { + init, + fetchInterfaces, + openHotspotPopup, + closeHotspotPopup, + showHotspotWarning, + dismissHotspotWarning, + saveHotspotSettings, + loadHotspotSettings, + startHotspot, + stopHotspot, + refreshInterfaces, + }; + + // Attach to window for compatibility (legacy code depends on it) + if (typeof window !== "undefined") { + try { + (window as any).HotspotFeature = API; + } catch { + // ignore attach errors in secure environments + } + } + + return API; +})(); + +export default HotspotFeature; diff --git a/webroot/app/features/migrate.ts b/webroot/app/features/migrate.ts new file mode 100644 index 0000000..7ec4975 --- /dev/null +++ b/webroot/app/features/migrate.ts @@ -0,0 +1,262 @@ +import { CommandResult } from "@/composables/useNativeCmd"; + +export type MigrateDeps = { + // UI & console helpers + appendConsole: (message: string, cls?: string) => void; + + // Dialogs and selection helpers + showSizeSelectionDialog?: () => Promise; + showConfirmDialog?: ( + title: string, + message: string, + confirmText?: string, + cancelText?: string, + ) => Promise; + + // Generic app helpers + closeSettingsPopup?: () => void; + ANIMATION_DELAYS?: Record; + + // Execution helpers + CHROOT_DIR: string; + PATH_CHROOT_SH: string; + runCmdSync?: (cmd: string) => Promise; + runCmdAsync?: (cmd: string, onComplete?: (res: any) => void) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + executeCommandWithProgress?: (options: { + cmd: string; + progress: { progressLine: HTMLElement; progressInterval?: any } | null; + onSuccess?: (result?: any) => void; + onError?: (result?: any) => void; + onComplete?: (result?: any) => void; + useValue?: boolean; + activeCommandIdRef?: { value: string | null } | undefined; + }) => string | null; + + // State & UI + activeCommandId?: { value: string | null }; + hotspotActive?: { value: boolean }; + sparseMigrated?: { value: boolean }; + disableAllActions?: (b: boolean) => void; + disableSettingsPopup?: (b: boolean, chrootExists?: boolean) => void; + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise<{ progressLine: HTMLElement; interval?: any } | null>; + ProgressIndicator?: { create: any; remove: any; update?: any }; + + updateStatus?: (s: string) => void; + updateModuleStatus?: () => void; + ensureChrootStopped?: () => Promise; + refreshStatus?: () => Promise; +}; + +let deps: MigrateDeps | null = null; + +/** + * Initialize the MigrateFeature module. + * The dependencies object should be provided by the main UI for full integration. + */ +export function init(d: MigrateDeps) { + deps = d; +} + +/** + * Convert the directory-based rootfs to a sparse ext4 image. + */ +export async function migrateToSparseImage(): Promise { + if (!deps) return; + + const d = deps; + + // 1) Prompt for size + const sizeChoice = d.showSizeSelectionDialog + ? await d.showSizeSelectionDialog() + : null; + if (!sizeChoice) { + // user canceled size selection + return; + } + const sizeGb = String(sizeChoice).trim(); + if (!sizeGb) return; + + // 2) Confirm potentially destructive action + const confirmFn = d.showConfirmDialog; + const confirmed = confirmFn + ? await confirmFn( + "Migrate to Sparse Image", + `This will convert your current rootfs to a ${sizeGb}GB sparse ext4 image.\n\n⚠️ IMPORTANT: If your chroot is currently running, it will be stopped automatically.\n\nℹ️ NOTE: Sparse images do not immediately use ${sizeGb}GB of storage. They only consume space as you write data to them, starting small and growing as needed.\n\nWARNING: This process cannot be undone. Make sure you have a backup!\n\nContinue with migration?`, + "Start Migration", + "Cancel", + ) + : true; + + if (!confirmed) return; + + // Close settings popup & give UI a moment to animate + d.closeSettingsPopup?.(); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE_LONG ?? 800), + ); + + // Lock UI + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + try { + // If chroot is running, stop it first (via helper) + if (d.ensureChrootStopped) { + // this will attempt to stop chroot using app-level helpers with the proper flow + const stopped = await d.ensureChrootStopped(); + if (!stopped) { + d.appendConsole("✗ Failed to stop chroot - migration aborted", "err"); + return; + } + } + + // Update status in UI + d.updateStatus?.("migrating"); + + // Show header & progress indicator using centralized flow when available + const progress = d.prepareActionExecution + ? await d.prepareActionExecution( + "Starting Sparse Image Migration", + "Migrating", + "dots", + ) + : null; + + // Post useful warnings in console for the user + d.appendConsole(`Target size: ${sizeGb}GB sparse ext4 image`, "info"); + d.appendConsole("DO NOT CLOSE THIS WINDOW!", "warn"); + + // Command to execute - use the sparsemgr.sh helper located in CHROOT_DIR + const migrateCommand = `sh ${d.CHROOT_DIR}/sparsemgr.sh migrate ${sizeGb}`; + + // Use the high-level executeCommandWithProgress if available (keeps consistent UX) + if (d.executeCommandWithProgress) { + const cmdId = d.executeCommandWithProgress({ + cmd: migrateCommand, + progress: progress + ? { + progressLine: progress.progressLine, + progressInterval: progress.interval, + } + : null, + onSuccess: () => { + d.appendConsole( + "✅ Sparse image migration completed successfully!", + "success", + ); + d.appendConsole( + "Your rootfs has been converted to a sparse image.", + "info", + ); + d.appendConsole("━━━ Migration Complete ━━━", "success"); + + if (d.sparseMigrated) d.sparseMigrated.value = true; + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + // Refresh status after a small delay to avoid UI flicker + setTimeout( + () => d.refreshStatus?.(), + (d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500) * 2, + ); + }, + onError: () => { + d.appendConsole("✗ Sparse image migration failed!", "err"); + d.appendConsole("Check the logs above for details.", "err"); + d.appendConsole("━━━ Migration Failed ━━━", "err"); + + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + }, + onComplete: () => { + // no-op here; the feature handlers above will do final steps + }, + useValue: true, + activeCommandIdRef: d.activeCommandId ?? undefined, + }); + + if (!cmdId) { + // validation failed (for example, no root access) — cancel UI lock gracefully + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } + } else { + // Fallback path: use runCommandAsyncPromise + try { + const result = await d.runCommandAsyncPromise?.(migrateCommand, { + onOutput: (line) => d.appendConsole(line), + }); + if (result?.success) { + d.appendConsole( + "✅ Sparse image migration completed successfully!", + "success", + ); + if (d.sparseMigrated) d.sparseMigrated.value = true; + d.updateModuleStatus?.(); + } else { + d.appendConsole("✗ Sparse image migration failed!", "err"); + d.updateModuleStatus?.(); + } + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + setTimeout( + () => d.refreshStatus?.(), + (d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500) * 2, + ); + } catch (err: any) { + d.appendConsole( + `✗ Sparse image migration failed: ${String(err?.message || err)}`, + "err", + ); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } finally { + if (progress && d.ProgressIndicator) { + try { + d.ProgressIndicator?.remove?.(progress); + } catch { + /* ignore */ + } + } + } + } + } catch (err: any) { + d.appendConsole( + `✗ Sparse image migration aborted: ${String(err?.message || err)}`, + "err", + ); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } +} + +/** + * Legacy compatibility and default export. + * The UI will still call `window.MigrateFeature.*` in legacy modules. + */ +const MigrateFeature = { + init, + migrateToSparseImage, +}; + +if (typeof window !== "undefined") { + try { + (window as any).MigrateFeature = MigrateFeature; + } catch {} +} + +export default MigrateFeature; diff --git a/webroot/app/features/resize.ts b/webroot/app/features/resize.ts new file mode 100644 index 0000000..99fe279 --- /dev/null +++ b/webroot/app/features/resize.ts @@ -0,0 +1,416 @@ +import { CommandResult } from "@/composables/useNativeCmd"; + +export type ResizeDeps = { + // Persistence & UI + Storage?: { + get?: (k: string, d?: any) => any; + set?: (k: string, v: any) => void; + getJSON?: (k: string, d?: any) => T | null; + setJSON?: (k: string, v: any) => void; + }; + appendConsole: (text: string, cls?: string) => void; + + // Dialog / helpers + showConfirmDialog?: ( + title: string, + message: string, + confirmText?: string, + cancelText?: string, + ) => Promise; + showSizeSelectionDialog?: () => Promise; + + // Commands and progress + PATH_CHROOT_SH: string; + CHROOT_DIR?: string; + + runCmdSync?: (cmd: string) => Promise; + runCmdAsync?: ( + cmd: string, + onComplete?: (result: any) => void, + ) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + + // A convenience, higher-level helper used by the app to orchestrate long-running jobs + executeCommandWithProgress?: (options: { + cmd: string; + progress: { progressLine: HTMLElement; progressInterval?: any } | null; + onSuccess?: (result?: any) => void; + onError?: (result?: any) => void; + onComplete?: (result?: any) => void; + useValue?: boolean; + activeCommandIdRef?: { value: string | null } | undefined; + }) => string | null; + + // UI helpers + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise<{ progressLine: HTMLElement; interval?: any } | null>; + ProgressIndicator?: { create: any; remove: any; update?: any }; + + disableAllActions?: (disabled: boolean) => void; + disableSettingsPopup?: (disabled: boolean, chrootExists?: boolean) => void; + + closeSettingsPopup?: () => void; + + updateStatus?: (status: string) => void; + + // Shared state & flags + activeCommandId?: { value: string | null }; + rootAccessConfirmed?: { value: boolean }; + sparseMigrated?: { value: boolean }; + + // Helpers from app + updateSparseInfo?: () => Promise; + refreshStatus?: () => Promise; + updateModuleStatus?: () => void; + + ANIMATION_DELAYS?: Record; +}; + +let deps: ResizeDeps | null = null; + +/** + * Initialize the module with a dependency bag. + */ +export function init(d: ResizeDeps) { + deps = d; +} + +/** + * Trim sparse image: + * - Ensures prerequisites are met (root access, sparse image exists) + * - Shows confirm dialog + * - Uses centralized prepareActionExecution + executeCommandWithProgress flow if available + */ +export async function trimSparseImage() { + if (!deps) return; + const d = deps; + + // Guard: concurrent command + if (d.activeCommandId && d.activeCommandId.value) { + d.appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + + // Guard: root access & sparse present + if (d.rootAccessConfirmed && !d.rootAccessConfirmed.value) { + d.appendConsole( + "Cannot trim sparse image: root access not available", + "err", + ); + return; + } + + if (d.sparseMigrated && !d.sparseMigrated.value) { + d.appendConsole("Sparse image not detected - cannot trim", "err"); + return; + } + + // Confirm action + const confirmed = d.showConfirmDialog + ? await d.showConfirmDialog( + "Trim Sparse Image", + "This will run fstrim to reclaim unused space in the sparse image.\n\nThe operation may take a few seconds and space reclamation happens gradually. Continue?", + "Trim", + "Cancel", + ) + : true; + + if (!confirmed) return; + + // Close popups (UI politely) + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + // Update status & create progress indicator via helper + d.updateSparseInfo?.(); // keep aside - update on completion + d.prepareActionExecution?.( + "Trimming Sparse Image", + "Trimming sparse image", + "dots", + ).catch(() => {}); + + // Use executeCommandWithProgress if available + const cmd = `sh ${d.PATH_CHROOT_SH} fstrim`; + + if (d.executeCommandWithProgress) { + const commandId = d.executeCommandWithProgress({ + cmd, + progress: null, // prepareActionExecution was used earlier by caller in original app + onSuccess: () => { + d.appendConsole("✓ Sparse image trimmed successfully", "success"); + d.appendConsole("Space may be reclaimed after a few minutes", "info"); + d.appendConsole("━━━ Trim Complete ━━━", "success"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH || 500, + ); + }, + onError: () => { + d.appendConsole("✗ Sparse image trim failed", "err"); + d.appendConsole("This may be expected on some Android kernels", "warn"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH || 500, + ); + }, + useValue: true, + activeCommandIdRef: d.activeCommandId, + }); + + if (!commandId) { + // validation failed - cleanup + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } + + return; + } + + // Fallback: run async + try { + const result = await d.runCommandAsyncPromise?.(cmd, { + onOutput: (line) => d.appendConsole(line), + }); + if (result?.success) { + d.appendConsole("✓ Sparse image trimmed successfully", "success"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } else { + d.appendConsole("✗ Sparse image trim failed", "err"); + d.appendConsole("This may be expected on some Android kernels", "warn"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } + } catch (e: any) { + d.appendConsole("✗ Sparse image trim failed", "err"); + d.appendConsole("This may be expected on some Android kernels", "warn"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } +} + +/** + * Resize sparse image: + * - Show size selection + * - Confirm destructive operation (with warnings about shrinking/growing) + * - Use migrate/resize script via PATH_CHROOT_SH + */ +export async function resizeSparseImage() { + if (!deps) return; + const d = deps; + + // Guard: concurrent command + if (d.activeCommandId && d.activeCommandId.value) { + d.appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + + // Guard: root access + if (d.rootAccessConfirmed && !d.rootAccessConfirmed.value) { + d.appendConsole( + "Cannot resize sparse image: root access not available", + "err", + ); + return; + } + + // Ask for new size via the optional dialog helper + const newSizeGb = d.showSizeSelectionDialog + ? await d.showSizeSelectionDialog() + : null; + if (!newSizeGb) return; + + // Attempt to detect current allocated size (best-effort) + let currentAllocatedGb = "Unknown"; + try { + if (d.runCmdSync && d.CHROOT_DIR) { + const apparentSizeCmd = `ls -lh ${d.CHROOT_DIR}/rootfs.img | tr -s ' ' | cut -d' ' -f5`; + const apparentSizeStr = await d.runCmdSync(apparentSizeCmd); + currentAllocatedGb = String(apparentSizeStr || "") + .trim() + .replace(/\\.0G$/, "GB") + .replace(/G$/, "GB"); + } + } catch { + // Keep Unknown + } + + // Confirm large operation + const confirm = d.showConfirmDialog + ? await d.showConfirmDialog( + "Resize Sparse Image", + `⚠️ EXTREME WARNING: This operation can CORRUPT your filesystem!\n\nYou MUST create a backup before proceeding.\n\nDO NOT close this window or interrupt the process.\n\nCurrent allocated: ${currentAllocatedGb}\nNew size: ${String(newSizeGb)}GB\n\nContinue?`, + "Resize", + "Cancel", + ) + : true; + + if (!confirm) return; + + // Close settings & start progress UI + d.closeSettingsPopup?.(); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE_LONG ?? 700), + ); + + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + d.updateStatus?.("resizing"); + + const prepare = d.prepareActionExecution + ? await d.prepareActionExecution( + `Resizing Sparse Image to ${String(newSizeGb)}GB`, + "Preparing resize operation", + "dots", + ) + : null; + + const cmdStr = `sh ${d.PATH_CHROOT_SH} resize --webui ${String(newSizeGb)}`; + + if (d.executeCommandWithProgress) { + const commandId = d.executeCommandWithProgress({ + cmd: cmdStr, + progress: prepare + ? { + progressLine: prepare.progressLine, + progressInterval: prepare.interval, + } + : null, + onSuccess: () => { + d.appendConsole("✅ Sparse image resized successfully", "success"); + d.appendConsole(`New size: ${newSizeGb}GB`, "info"); + d.appendConsole("━━━ Resize Complete ━━━", "success"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + }, + onError: () => { + d.appendConsole("✗ Sparse image resize failed", "err"); + d.appendConsole("Check the logs above for details", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + }, + useValue: true, + activeCommandIdRef: d.activeCommandId, + }); + + if (!commandId) { + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + } + return; + } + + // fallback to async command + try { + const result = await d.runCommandAsyncPromise?.(cmdStr, { + onOutput: (line) => d.appendConsole(line), + }); + if (result?.success) { + d.appendConsole("✅ Sparse image resized successfully", "success"); + d.appendConsole(`New size: ${newSizeGb}GB`, "info"); + d.appendConsole("━━━ Resize Complete ━━━", "success"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } else { + d.appendConsole("✗ Sparse image resize failed", "err"); + d.appendConsole("Check the logs above for details", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } + } catch (e: any) { + d.appendConsole("✗ Sparse image resize failed", "err"); + d.appendConsole("Check the logs above for details", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + d.updateSparseInfo?.(); + setTimeout( + () => d.refreshStatus?.(), + d.ANIMATION_DELAYS?.STATUS_REFRESH ?? 500, + ); + } +} + +/** + * Attach to window for compatibility with the legacy global object. + */ +const ResizeFeature = { + init, + trimSparseImage, + resizeSparseImage, +}; + +if (typeof window !== "undefined") { + try { + (window as any).ResizeFeature = ResizeFeature; + } catch {} +} + +export default ResizeFeature; diff --git a/webroot/app/features/uninstall.ts b/webroot/app/features/uninstall.ts new file mode 100644 index 0000000..4e50e8a --- /dev/null +++ b/webroot/app/features/uninstall.ts @@ -0,0 +1,286 @@ +import type { CommandResult } from "@/composables/useNativeCmd"; + +export type UninstallDeps = { + activeCommandId?: { value: string | null }; + rootAccessConfirmed?: { value: boolean }; + showProgress?: { value: boolean }; + progressTitle?: { value: string }; + progressMessage?: { value: string }; + appendConsole: (text: string, cls?: string) => void; + showConfirmDialog?: ( + title: string, + message: string, + confirmText?: string, + cancelText?: string, + ) => Promise; + closeSettingsPopup?: () => void; + ANIMATION_DELAYS?: Record; + PATH_CHROOT_SH: string; + ProgressIndicator?: { + create: ( + text: string, + type?: "spinner" | "dots", + el?: HTMLElement | null, + ) => { progressLine: HTMLElement; interval?: any } | null; + remove: ( + handle: { progressLine: HTMLElement; interval?: any } | null, + ) => void; + update?: ( + handle: { progressLine: HTMLElement; interval?: any } | null, + text?: string, + ) => void; + }; + disableAllActions?: (disabled: boolean) => void; + disableSettingsPopup?: (disabled: boolean, chrootExists?: boolean) => void; + runCmdAsync?: ( + cmd: string, + onComplete?: (result: any) => void, + ) => string | null; + runCommandAsyncPromise?: ( + cmd: string, + options?: { + asRoot?: boolean; + debug?: boolean; + onOutput?: (line: string) => void; + }, + ) => Promise; + runCmdSync?: (cmd: string) => Promise; + ensureChrootStopped?: () => Promise; + prepareActionExecution?: ( + headerText: string, + progressText: string, + progressType?: "spinner" | "dots", + ) => Promise<{ progressLine: HTMLElement; interval?: any } | null>; + executeCommandWithProgress?: (opts: { + cmd: string; + progress: { progressLine: HTMLElement; progressInterval?: any } | null; + onSuccess?: (res?: any) => void; + onError?: (res?: any) => void; + onComplete?: (res?: any) => void; + useValue?: boolean; + activeCommandIdRef?: { value: string | null }; + }) => string | null; + updateStatus?: (s: string) => void; + refreshStatus?: () => Promise; + updateModuleStatus?: () => void; +}; + +let deps: UninstallDeps | null = null; + +/** + * Initialize the uninstall module with a dependency bag. + */ +export function init(d: UninstallDeps) { + deps = d; +} + +export async function uninstallChroot() { + if (!deps) return; + const d = deps; + + try { + // Guard: don't run if a command is already active + if (d.activeCommandId && d.activeCommandId.value) { + d.appendConsole( + "⚠ Another command is already running. Please wait...", + "warn", + ); + return; + } + + // Ask for confirmation + alert( + "Are you sure you want to uninstall the chroot environment?\n\nThis will permanently delete all data in the chroot and cannot be undone.", + ); + const confirmed = true; + + if (d.showProgress) d.showProgress.value = true; + if (d.progressTitle) d.progressTitle.value = "Uninstall in Progress"; + if (d.progressMessage) + d.progressMessage.value = + "Please wait while the chroot environment is being uninstalled..."; + + d.closeSettingsPopup?.(); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.INPUT_FOCUS ?? 120), + ); + d.updateStatus?.("uninstalling"); + await new Promise((r) => + setTimeout(r, d.ANIMATION_DELAYS?.POPUP_CLOSE_VERY_LONG ?? 750), + ); + + // Disable UI while uninstalling + d.disableAllActions?.(true); + d.disableSettingsPopup?.(true); + + // Stop chroot if it's running, using the provided helper + if (d.ensureChrootStopped) { + try { + const stopped = await d.ensureChrootStopped(); + if (!stopped) { + d.appendConsole("✗ Failed to stop chroot - uninstall aborted", "err"); + if (d.activeCommandId) d.activeCommandId.value = null; + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + } catch (err: any) { + d.appendConsole( + "✗ Failed to ensure chroot stopped - uninstall aborted", + "err", + ); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, true); + return; + } + } + + // Prepare an action header and progress indicator + const progress = d.prepareActionExecution + ? await d.prepareActionExecution( + "Starting Uninstallation", + "Uninstalling chroot", + "dots", + ) + : null; + + const cmdStr = `sh ${d.PATH_CHROOT_SH} uninstall --webui`; + + // Prefer the higher-level executeCommandWithProgress if available + if (d.executeCommandWithProgress) { + const commandId = d.executeCommandWithProgress({ + cmd: cmdStr, + progress: progress + ? { + progressLine: progress.progressLine, + progressInterval: progress.interval, + } + : null, + onSuccess: async (result?: any) => { + d.appendConsole("✅ Chroot uninstalled successfully!", "success"); + d.appendConsole("All chroot data has been removed.", "info"); + d.appendConsole("━━━ Uninstallation Complete ━━━", "success"); + + // After uninstall, update status & refresh UI state + d.updateStatus?.("stopped"); + d.updateModuleStatus?.(); + d.disableAllActions?.(true); + d.disableSettingsPopup?.(false, false); + + // Force a status refresh so UI shows chroot missing and appropriate buttons + try { + await d.refreshStatus?.(); + } catch { + // ignore refresh errors + } + }, + onError: async (result?: any) => { + d.appendConsole("✗ Uninstallation failed", "err"); + d.appendConsole("Check the logs above for details.", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, false); + + try { + await d.refreshStatus?.(); + } catch { + // ignore + } + }, + useValue: true, + activeCommandIdRef: d.activeCommandId, + }); + + if (!commandId) { + // Validation probably failed; re-enable UI + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, false); + } + return; + } + + // Fallback: run asynchronously + try { + const result = await d.runCommandAsyncPromise?.(cmdStr, { + onOutput: (line) => d.appendConsole(line), + }); + if (result?.success) { + d.appendConsole("✅ Chroot uninstalled successfully!", "success"); + d.appendConsole("All chroot data has been removed.", "info"); + d.appendConsole("━━━ Uninstallation Complete ━━━", "success"); + d.updateStatus?.("stopped"); + d.updateModuleStatus?.(); + d.disableAllActions?.(true); + d.disableSettingsPopup?.(false, false); + + try { + await d.refreshStatus?.(); + } catch { + // ignore + } + } else { + d.appendConsole("✗ Uninstallation failed", "err"); + d.appendConsole("Check the logs above for details.", "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, false); + + try { + await d.refreshStatus?.(); + } catch { + // ignore + } + } + } catch (err: any) { + d.appendConsole("✗ Uninstallation failed", "err"); + d.appendConsole(String(err?.message || err), "err"); + d.updateModuleStatus?.(); + d.disableAllActions?.(false); + d.disableSettingsPopup?.(false, false); + + try { + await d.refreshStatus?.(); + } catch { + // ignore + } + } finally { + // Ensure progress indicator is removed + if (progress && d.ProgressIndicator) { + try { + d.ProgressIndicator.remove(progress); + } catch {} + } + } + } catch (error: any) { + console.error("Error in uninstallChroot:", error); + // Try to append to console if possible + if (deps?.appendConsole) { + deps.appendConsole( + `Uninstall error: ${String(error?.message || error)}`, + "err", + ); + } + } finally { + if (d.showProgress) d.showProgress.value = false; + if (d.progressTitle) d.progressTitle.value = ""; + if (d.progressMessage) d.progressMessage.value = ""; + } +} + +/** + * Minimal compatibility object (legacy window-based module) + */ +export const UninstallFeature = { + init, + uninstallChroot, +}; + +if (typeof window !== "undefined") { + try { + (window as any).UninstallFeature = UninstallFeature; + } catch { + /* ignore */ + } +} + +export default UninstallFeature; diff --git a/webroot/app/pages/index.vue b/webroot/app/pages/index.vue new file mode 100644 index 0000000..39ba462 --- /dev/null +++ b/webroot/app/pages/index.vue @@ -0,0 +1,430 @@ + + + + + diff --git a/webroot/app/plugins/loadCmdExec.client.ts b/webroot/app/plugins/loadCmdExec.client.ts new file mode 100644 index 0000000..f3f3ac4 --- /dev/null +++ b/webroot/app/plugins/loadCmdExec.client.ts @@ -0,0 +1,98 @@ +import { useRuntimeConfig } from "#imports"; + +export default defineNuxtPlugin(() => { + if (!process.client || typeof window === "undefined") { + return; + } + + const bridgeCheck = () => { + return ( + !!(window as any).cmdExec && + (typeof (window as any).cmdExec.execute === "function" || + typeof (window as any).cmdExec.executeAsync === "function") + ); + }; + + // If there's already a bridge, don't mess with it + if (bridgeCheck()) { + if (typeof console !== "undefined" && console.debug) { + console.debug( + "loadLegacyCmdExec: native cmdExec already present — skipping loader.", + ); + } + return; + } + + const runtimeConfig = useRuntimeConfig?.() as any; + const baseUrl = + runtimeConfig && runtimeConfig.app && runtimeConfig.app.baseURL + ? String(runtimeConfig.app.baseURL) + : "/"; + const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + const scriptUrl = `${normalizedBase}/command-executor.js`; + + if ( + document.querySelector(`script[src="${scriptUrl}"]`) || + document.querySelector('script[src*="command-executor.js"]') + ) { + // Script already present; start polling for injected bridge + const existingScript = + document.querySelector(`script[src="${scriptUrl}"]`) || + document.querySelector('script[src*="command-executor.js"]'); + if (typeof console !== "undefined" && console.debug) { + console.debug( + "loadLegacyCmdExec: command-executor script already included in DOM.", + ); + } + // We'll ensure the bridge becomes available + } else { + const scriptEl = document.createElement("script"); + scriptEl.src = scriptUrl; + scriptEl.async = true; + scriptEl.defer = true; + scriptEl.type = "text/javascript"; + scriptEl.onload = () => { + if (typeof console !== "undefined" && console.debug) { + console.debug(`loadLegacyCmdExec: loaded ${scriptUrl}`); + } + }; + scriptEl.onerror = (e) => { + if (typeof console !== "undefined" && console.warn) { + console.warn(`loadLegacyCmdExec: failed to load ${scriptUrl}`, e); + } + }; + (document.head || document.body || document.documentElement).appendChild( + scriptEl, + ); + } + + const MAX_WAIT_MS = 10_000; // 10s polling window + const INTERVAL_MS = 250; + let elapsed = 0; + const checkInterval = setInterval(() => { + if (bridgeCheck()) { + clearInterval(checkInterval); + if (typeof console !== "undefined" && console.debug) { + try { + const method = (window as any).cmdExec?.execMethod || "none"; + console.debug( + "loadLegacyCmdExec: cmdExec bridge detected (execMethod=%s).", + method, + ); + } catch { + console.debug("loadLegacyCmdExec: cmdExec bridge detected."); + } + } + return; + } + elapsed += INTERVAL_MS; + if (elapsed >= MAX_WAIT_MS) { + clearInterval(checkInterval); + if (typeof console !== "undefined" && console.warn) { + console.warn( + "loadLegacyCmdExec: cmdExec bridge not detected after 10s.", + ); + } + } + }, INTERVAL_MS); +}); diff --git a/webroot/app/services/NetworkInterfaceManager.ts b/webroot/app/services/NetworkInterfaceManager.ts new file mode 100644 index 0000000..e8d75af --- /dev/null +++ b/webroot/app/services/NetworkInterfaceManager.ts @@ -0,0 +1,162 @@ +import { CHROOT_DIR } from "../composables/constants"; + +export class NetworkInterfaceManager { + constructor( + private deps: { + Storage: { + getJSON?: (key: string, defaultValue?: T | null) => T | null; + setJSON?: (key: string, value: any) => void; + get: (key: string, defaultValue?: any) => any; + set: (key: string, value: any) => void; + }; + appendConsole?: (text: string, cls?: string) => void; + runCmdSync: (cmd: string) => Promise; + rootAccessConfirmed?: { value: boolean }; + }, + private scriptPath: string, + private cacheKey: string, + private selectElement?: HTMLSelectElement | null, + private selectedInterfaceKey?: string, + private onInterfacesUpdated?: (interfaces: string[]) => void, + ) {} + + /** + * Update the select element reference (useful when DOM elements are created after initialization) + */ + updateSelectElement(selectElement: HTMLSelectElement | null) { + this.selectElement = selectElement; + } + + /** + * Set callback for when interfaces are updated + */ + setOnInterfacesUpdated(callback: (interfaces: string[]) => void) { + this.onInterfacesUpdated = callback; + } + + /** + * Populate the interface select UI with a list of interface strings. + * The `interfacesRaw` array contains elements like: "wlan0:10.0.0.1" or "eth0". + */ + populateInterfaces(interfacesRaw: string[]) { + const select = this.selectElement; + if (!select) return; + + select.innerHTML = ""; + if (!interfacesRaw || interfacesRaw.length === 0) { + const option = document.createElement("option"); + option.value = ""; + option.textContent = "No interfaces found"; + select.appendChild(option); + select.disabled = true; + try { + if (this.selectedInterfaceKey) { + this.deps.Storage.set(this.selectedInterfaceKey, ""); + } + } catch { + // ignore + } + return; + } + + interfacesRaw.forEach((ifaceRaw) => { + const trimmed = String(ifaceRaw || "").trim(); + if (!trimmed) return; + + const option = document.createElement("option"); + if (trimmed.includes(":")) { + const [iface = "", ip = ""] = trimmed.split(":").map((s) => s.trim()); + option.value = iface; + option.textContent = `${iface} (${ip})`; + } else { + option.value = trimmed; + option.textContent = trimmed; + } + select.appendChild(option); + }); + + select.disabled = false; + try { + if (this.selectedInterfaceKey) { + const saved = this.deps.Storage.get(this.selectedInterfaceKey); + if (saved) { + const opt = Array.from(select.options).find((o) => o.value === saved); + if (opt) { + select.value = String(saved); + } else { + select.value = ""; + } + } + } + } catch { + // ignore + } + } + + /** + * Fetch network interfaces. Uses caching (Storage) and prefers cached data unless forced. + * - forceRefresh: forces fetching from script and updating cache. + * - backgroundOnly: when true, only updates cache but does not update UI elements. + */ + async fetchInterfaces(forceRefresh = false, backgroundOnly = false) { + const cached: string[] = + (this.deps.Storage.getJSON + ? this.deps.Storage.getJSON(this.cacheKey) + : null) || []; + + if (cached && Array.isArray(cached) && cached.length > 0 && !forceRefresh) { + if (!backgroundOnly) { + this.populateInterfaces(cached); + this.onInterfacesUpdated?.(cached); + } + return; + } + + // No cache or forced refresh + try { + const cmd = `sh ${CHROOT_DIR}/forward-nat.sh list-iface 2>&1`; + const out = await this.deps.runCmdSync(cmd); + const text = String(out || "").trim(); + + let interfacesRaw = text + ? text + .split(/[\r\n,]+/) + .map((s) => s.trim()) + .filter(Boolean) + : []; + + // Filter out ap0 interface for hotspot + if (this.cacheKey === "chroot_hotspot_interfaces_cache") { + interfacesRaw = interfacesRaw.filter((s) => !s.startsWith("ap0")); + } + + try { + this.deps.Storage.setJSON?.(this.cacheKey, interfacesRaw); + } catch { + // ignore + } + + if (!backgroundOnly) { + this.populateInterfaces(interfacesRaw); + this.onInterfacesUpdated?.(interfacesRaw); + } + } catch (err: any) { + // Show a polite warning in console unless backgroundOnly + if (!backgroundOnly) { + this.deps.appendConsole?.( + `Could not fetch interfaces: ${String(err?.message || err)}`, + "warn", + ); + const select = this.selectElement; + if (select) { + select.innerHTML = ""; + const option = document.createElement("option"); + option.value = ""; + option.textContent = "Failed to load interfaces"; + select.appendChild(option); + select.disabled = true; + } + } + } + } +} diff --git a/webroot/app/services/progressIndicator.ts b/webroot/app/services/progressIndicator.ts new file mode 100644 index 0000000..3f2b09a --- /dev/null +++ b/webroot/app/services/progressIndicator.ts @@ -0,0 +1,192 @@ +export type ProgressType = "spinner" | "dots"; + +export interface ProgressHandle { + element: HTMLElement; + type: ProgressType; + remove: () => void; + update: (text: string) => void; +} + +export function createProgressIndicator( + text: string, + type: ProgressType = "spinner", + container?: HTMLElement | null, +): ProgressHandle { + const CONTAINER_FALLBACK_ID = "console"; + const SPINNER_CHARS = ["|", "/", "-", "\\"]; + const SPINNER_INTERVAL_MS = 200; + const DOTS_INTERVAL_MS = 400; + const PREFIX = "⌛ "; + + // Defensive: resolve container + const resolvedContainer = + container || + (typeof document !== "undefined" + ? document.getElementById(CONTAINER_FALLBACK_ID) + : null) || + undefined; + + const el = document.createElement("div"); + el.className = "progress-indicator log-immediate"; + const baseText = String(text || "").trim(); + el.textContent = `${PREFIX}${baseText}`; + + if (resolvedContainer) { + resolvedContainer.appendChild(el); + try { + resolvedContainer.scrollTo({ + top: resolvedContainer.scrollHeight, + behavior: "smooth", + }); + } catch { + // ignore in environments where scrollTo throws + } + } + + let intervalId: number | null = null; + let spinnerIndex = 0; + let dotCount = 0; + let visible = true; + + function updateSpinner() { + if (!el.parentNode) { + clearIntervalSafe(); + return; + } + spinnerIndex = (spinnerIndex + 1) % SPINNER_CHARS.length; + el.textContent = `${PREFIX}${baseText} ${SPINNER_CHARS[spinnerIndex]}`; + } + + function updateDots() { + if (!el.parentNode) { + clearIntervalSafe(); + return; + } + dotCount = (dotCount + 1) % 4; // 0..3 + const dots = ".".repeat(dotCount); + el.textContent = dots + ? `${PREFIX}${baseText} ${dots}` + : `${PREFIX}${baseText}`; + } + + function clearIntervalSafe() { + if (intervalId !== null) { + try { + clearInterval(intervalId); + } catch { + // ignore + } + intervalId = null; + } + } + + if (type === "spinner") { + intervalId = window.setInterval( + updateSpinner, + SPINNER_INTERVAL_MS, + ) as unknown as number; + } else { + intervalId = window.setInterval( + updateDots, + DOTS_INTERVAL_MS, + ) as unknown as number; + } + + // Provide handles + function remove() { + clearIntervalSafe(); + try { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + } catch { + // ignore removal errors + } + } + + function update(newText: string) { + const t = String(newText || "").trim(); + const textToUse = `${PREFIX}${t}`; + if (type === "spinner") { + el.textContent = `${textToUse} ${SPINNER_CHARS[spinnerIndex % SPINNER_CHARS.length]}`; + } else { + const dots = ".".repeat(dotCount); + el.textContent = dots ? `${textToUse} ${dots}` : textToUse; + } + } + + const handle: ProgressHandle = { + element: el, + type, + remove, + update, + }; + + return handle; +} + +export function removeProgressIndicator( + handleOrElement: ProgressHandle | HTMLElement | null | undefined, +) { + if (!handleOrElement) return; + if (typeof (handleOrElement as ProgressHandle).remove === "function") { + try { + (handleOrElement as ProgressHandle).remove(); + return; + } catch { + // fallthrough to element logic + } + } + const el = (handleOrElement as HTMLElement) || null; + if (!el) return; + try { + if (el.parentNode) el.parentNode.removeChild(el); + } catch { + // ignore + } +} + +export function updateProgressIndicator( + handleOrElement: ProgressHandle | HTMLElement | null | undefined, + text: string, +) { + if (!handleOrElement) return; + if (typeof (handleOrElement as ProgressHandle).update === "function") { + try { + (handleOrElement as ProgressHandle).update(text); + return; + } catch { + // fallback + } + } + + const el = (handleOrElement as HTMLElement) || null; + if (!el) return; + const newText = String(text || "").trim(); + if (el.textContent) { + const prefix = "⏳ "; + const current = el.textContent || ""; + const afterPrefix = current.startsWith(prefix) + ? current.substring(prefix.length) + : current; + // Keep trailing spinner/dots if present + const trailingMatch = afterPrefix.match(/(\s[|\/\\\-.]+)$/); + const trailing = trailingMatch ? trailingMatch[1] : ""; + el.textContent = `${prefix}${newText}${trailing}`; + } else { + el.textContent = `⏳ ${newText}`; + } +} + +const ProgressIndicator = Object.freeze({ + create: (text: string, type?: ProgressType, container?: HTMLElement | null) => + createProgressIndicator( + text, + (type as ProgressType) || "spinner", + container, + ), + remove: removeProgressIndicator, + update: updateProgressIndicator, +}); + +export default ProgressIndicator; diff --git a/webroot/assets/app.js b/webroot/assets/app.js deleted file mode 100644 index 11d7f6c..0000000 --- a/webroot/assets/app.js +++ /dev/null @@ -1,3983 +0,0 @@ -// Chroot Control UI -// Copyright (c) 2025 ravindu644 -// This entire crap is AI generated, don't blame me for the mess - -(function(){ - // Use hardcoded paths provided by install.sh - const CHROOT_DIR = '/data/local/ubuntu-chroot'; - const PATH_CHROOT_SH = `${CHROOT_DIR}/chroot.sh`; - const UPDATE_STATUS_SCRIPT = `${CHROOT_DIR}/update-status.sh`; - const CHROOT_PATH_UI = `${CHROOT_DIR}/rootfs`; - const BOOT_FILE = `${CHROOT_DIR}/boot-service`; - const DOZE_OFF_FILE = `${CHROOT_DIR}/.doze_off`; - const POST_EXEC_SCRIPT = `${CHROOT_DIR}/post_exec.sh`; - const HOTSPOT_SCRIPT = `${CHROOT_DIR}/start-hotspot`; - const FORWARD_NAT_SCRIPT = `${CHROOT_DIR}/forward-nat.sh`; - const OTA_UPDATER = `${CHROOT_DIR}/ota/updater.sh`; - const LOG_DIR = `${CHROOT_DIR}/logs`; - - const els = { - statusDot: document.getElementById('status-dot'), - statusText: document.getElementById('status-text'), - startBtn: document.getElementById('start-btn'), - stopBtn: document.getElementById('stop-btn'), - restartBtn: document.getElementById('restart-btn'), - console: document.getElementById('console'), - clearConsole: document.getElementById('clear-console'), - copyConsole: document.getElementById('copy-console'), - refreshStatus: document.getElementById('refresh-status'), - bootToggle: document.getElementById('boot-toggle'), - themeToggle: document.getElementById('theme-toggle'), - userSelect: document.getElementById('user-select'), - settingsBtn: document.getElementById('settings-btn'), - settingsPopup: document.getElementById('settings-popup'), - closePopup: document.getElementById('close-popup'), - postExecScript: document.getElementById('post-exec-script'), - saveScript: document.getElementById('save-script'), - clearScript: document.getElementById('clear-script'), - updateBtn: document.getElementById('update-btn'), - backupBtn: document.getElementById('backup-btn'), - debugToggle: document.getElementById('debug-toggle'), - androidOptimizeToggle: document.getElementById('android-optimize-toggle'), - startHotspotBtn: document.getElementById('start-hotspot-btn'), - stopHotspotBtn: document.getElementById('stop-hotspot-btn'), - hotspotForm: document.getElementById('hotspot-form'), - hotspotWarning: document.getElementById('hotspot-warning'), - loadingScreen: document.getElementById('loading-screen'), - dismissHotspotWarning: document.getElementById('dismiss-hotspot-warning'), - sparseSettingsBtn: document.getElementById('sparse-settings-btn'), - sparseSettingsPopup: document.getElementById('sparse-settings-popup'), - closeSparsePopup: document.getElementById('close-sparse-popup'), - trimSparseBtn: document.getElementById('trim-sparse-btn'), - resizeSparseBtn: document.getElementById('resize-sparse-btn'), - sparseInfo: document.getElementById('sparse-info'), - restoreBtn: document.getElementById('restore-btn'), - uninstallBtn: document.getElementById('uninstall-btn'), - hotspotBtn: document.getElementById('hotspot-btn'), - hotspotPopup: document.getElementById('hotspot-popup'), - closeHotspotPopup: document.getElementById('close-hotspot-popup'), - forwardNatBtn: document.getElementById('forward-nat-btn'), - forwardNatPopup: document.getElementById('forward-nat-popup'), - closeForwardNatPopup: document.getElementById('close-forward-nat-popup'), - forwardNatIface: document.getElementById('forward-nat-iface'), - startForwardingBtn: document.getElementById('start-forwarding-btn'), - stopForwardingBtn: document.getElementById('stop-forwarding-btn') - }; - - // Track running commands to prevent UI blocking - let activeCommandId = null; - - // Track hotspot state - much more reliable than filesystem checks - let hotspotActive = false; - - // Track forward-nat state - let forwardingActive = false; - - // Feature module state refs (will be set by initFeatureModules) - let activeCommandIdRef = null; - let rootAccessConfirmedRef = null; - let hotspotActiveRef = null; - let forwardingActiveRef = null; - let sparseMigratedRef = null; - - // Track debug mode state - let debugModeActive = false; - - // Track sparse image migration status - let sparseMigrated = false; - - // ============================================================================ - // MODERN LOG BUFFER - Batched rendering with smooth animations - // ============================================================================ - const LogBuffer = { - buffer: [], - flushTimer: null, - isFlushing: false, - scrollScheduled: false, - isUserScrolledUp: false, - lastScrollTop: 0, - - // Constants - BATCH_SIZE: 50, // Max logs per batch - FLUSH_INTERVAL: 16, // Flush every 16ms (60fps) - SCROLL_THRESHOLD: 10, // Pixels from bottom to consider "at bottom" - USER_SCROLL_DEBOUNCE_MS: 150, // Debounce for detecting user scroll - - /** - * Check if console is at bottom - */ - isAtBottom() { - if(!els.console) return true; - const pre = els.console; - const maxScroll = pre.scrollHeight - pre.clientHeight; - const currentScroll = pre.scrollTop; - return Math.abs(currentScroll - maxScroll) <= this.SCROLL_THRESHOLD; - }, - - /** - * Add log to buffer (will be flushed in batches) - */ - add(text, cls) { - if(!text) return; - this.buffer.push({ text, cls }); - this.scheduleFlush(); - }, - - /** - * Schedule flush (batches multiple logs) - */ - scheduleFlush() { - if(this.flushTimer || this.isFlushing) return; - - this.flushTimer = requestAnimationFrame(() => { - this.flush(); - }); - }, - - /** - * Flush buffered logs to DOM in a single batch - */ - flush() { - if(this.isFlushing || this.buffer.length === 0) { - this.flushTimer = null; - return; - } - - this.isFlushing = true; - const pre = els.console; - if(!pre) { - this.buffer = []; - this.isFlushing = false; - this.flushTimer = null; - return; - } - - const maxLines = APP_CONSTANTS.CONSOLE.MAX_LINES; - const batch = this.buffer.splice(0, this.BATCH_SIZE); - - // Create document fragment for batch DOM update - const fragment = document.createDocumentFragment(); - const wasAtBottom = this.isAtBottom(); - - // Count existing lines for trimming - const allLines = pre.querySelectorAll('div'); - const regularLines = Array.from(allLines).filter( - line => !line.classList.contains('progress-indicator') - ); - - // Trim old lines if needed (before adding new ones) - const totalAfterAdd = regularLines.length + batch.length; - if(totalAfterAdd > maxLines) { - const toRemove = totalAfterAdd - maxLines; - for(let i = 0; i < toRemove && i < regularLines.length; i++) { - if(regularLines[i].parentNode) { - regularLines[i].remove(); - } - } - } - - // Create all log elements in fragment with fade-in animation - batch.forEach(({ text, cls }, index) => { - const line = document.createElement('div'); - if(cls) line.className = cls; - line.textContent = text + '\n'; - - // Determine if this is a progress indicator - const isProgressIndicator = cls === 'progress-indicator' || text.includes('⏳'); - - // Apply animation classes - if(isProgressIndicator) { - line.classList.add('log-immediate'); - } else { - line.classList.add('log-chunk-fade'); - // Stagger animation for smooth chunk appearance - line.style.animationDelay = `${index * 20}ms`; - } - - fragment.appendChild(line); - }); - - // Single DOM append for entire batch - pre.appendChild(fragment); - - // Single scroll operation per batch (only if user was at bottom or active command) - if((wasAtBottom || activeCommandId) && !this.isUserScrolledUp) { - this.scheduleScroll(); - } - - // Save logs (debounced) - saveConsoleLogs(); - - // Continue flushing if more logs in buffer - this.isFlushing = false; - this.flushTimer = null; - - if(this.buffer.length > 0) { - this.scheduleFlush(); - } - }, - - /** - * Schedule scroll (throttled to once per frame) - */ - scheduleScroll() { - if(this.scrollScheduled) return; - - this.scrollScheduled = true; - requestAnimationFrame(() => { - this.scrollScheduled = false; - if(!els.console) return; - - // Smooth scroll to bottom - els.console.scrollTo({ - top: els.console.scrollHeight, - behavior: 'smooth' - }); - }); - }, - - /** - * Handle user scroll event - */ - handleUserScroll() { - if(!els.console) return; - - // Debounce user scroll detection - setTimeout(() => { - if(!this.isAtBottom()) { - this.isUserScrolledUp = true; - } else { - this.isUserScrolledUp = false; - } - this.lastScrollTop = els.console.scrollTop; - }, this.USER_SCROLL_DEBOUNCE_MS); - }, - - /** - * Force scroll to bottom (for action buttons) - */ - scrollToBottom() { - if(!els.console) return Promise.resolve(); - this.isUserScrolledUp = false; - - return new Promise(resolve => { - els.console.scrollTo({ - top: els.console.scrollHeight, - behavior: 'smooth' - }); - // Wait for a reasonable amount of time for the scroll to finish. - setTimeout(resolve, 400); - }); - }, - - /** - * Instant scroll (for initial load) - */ - scrollInstant() { - if(!els.console) return; - els.console.scrollTop = els.console.scrollHeight; - }, - - /** - * Wait for all pending logs to be flushed - * Returns a promise that resolves when buffer is empty and flush is complete - */ - async waitForFlush() { - // Poll until the buffer is empty and the flush cycle is complete. - while (this.buffer.length > 0 || this.isFlushing) { - await new Promise(resolve => setTimeout(resolve, 50)); - } - // One extra frame for safety, to allow final DOM paint. - await new Promise(resolve => requestAnimationFrame(resolve)); - } - }; - - /** - * Helper: fade console scrollbar out/in via CSS class. - * We hide it while long-running actions are executing, then show it again - * after status/console refresh has fully completed. - */ - function setConsoleScrollbarHidden(hidden) { - if(!els.console) return; - if(hidden) { - els.console.classList.add('console-scrollbar-hidden'); - } else { - els.console.classList.remove('console-scrollbar-hidden'); - } - } - - // Hotspot status loading/saving is now handled by HotspotFeature module - // These functions are kept for backward compatibility during initialization - function loadHotspotStatus(){ - hotspotActive = StateManager.get('hotspot'); - } - - /** - * Load debug mode status from localStorage on page load - */ - function loadDebugMode(){ - debugModeActive = StateManager.get('debug'); - updateDebugIndicator(); - } - - /** - * Save debug mode status to localStorage - */ - function saveDebugMode(){ - StateManager.set('debug', debugModeActive); - } - - /** - * Update the debug indicator visibility in the header - */ - function updateDebugIndicator(){ - const indicator = document.getElementById('debug-indicator'); - if(indicator){ - // Use class instead of inline style - if(debugModeActive) { - indicator.classList.remove('debug-indicator-hidden'); - } else { - indicator.classList.add('debug-indicator-hidden'); - } - } - } - - // Track if chroot missing message was logged - let _chrootMissingLogged = false; - - // Start with actions disabled until we verify the chroot exists - disableAllActions(true); - - /** - * Save console logs to localStorage (debounced for performance) - * Limits to max lines to prevent localStorage overflow - */ - let saveConsoleLogsTimer = null; - function saveConsoleLogs(){ - // Debounce saves to avoid excessive localStorage writes - if(saveConsoleLogsTimer) { - clearTimeout(saveConsoleLogsTimer); - } - - saveConsoleLogsTimer = setTimeout(() => { - if(!els.console) return; - - const lines = els.console.querySelectorAll('div'); - const maxLines = APP_CONSTANTS.CONSOLE.MAX_LINES; - - // Trim if exceeding limit - if(lines.length > maxLines) { - const toRemove = lines.length - maxLines; - for(let i = 0; i < toRemove; i++) { - if(lines[i].parentNode) { - lines[i].remove(); - } - } - } - - // Save current state - try { - Storage.set('chroot_console_logs', els.console.innerHTML); - } catch(e) { - // Silently fail if storage quota exceeded - console.warn('Failed to save console logs:', e); - } - - saveConsoleLogsTimer = null; - }, 500); // Debounce: save 500ms after last log addition - } - - /** - * Load console logs from localStorage - * Enforces max line limit when loading - * Optimized loading with efficient DOM operations - */ - function loadConsoleLogs(){ - const logs = Storage.get('chroot_console_logs'); - if(!logs || !els.console) return; - - const pre = els.console; - - // Disable smooth scrolling for instant initial load - pre.style.setProperty('scroll-behavior', 'auto', 'important'); - - // Set content efficiently - pre.innerHTML = logs; - - // Enforce max line limit efficiently - const lines = pre.querySelectorAll('div'); - const maxLines = APP_CONSTANTS.CONSOLE.MAX_LINES; - if(lines.length > maxLines) { - const toRemove = lines.length - maxLines; - for(let i = 0; i < toRemove; i++) { - if(lines[i].parentNode) { - lines[i].remove(); - } - } - saveConsoleLogs(); - } - - // Apply fade-in animation only for small console (< 15 lines, no scrollbar) - const finalLines = pre.querySelectorAll('div'); - const hasScrollbar = pre.scrollHeight > pre.clientHeight; - const shouldAnimate = finalLines.length < 15 && !hasScrollbar; - - if(shouldAnimate) { - requestAnimationFrame(() => { - finalLines.forEach((line, index) => { - if(!line.classList.contains('progress-indicator')) { - // Ensure fade-in class exists (might already be in HTML) - if(!line.classList.contains('log-fade-in')) { - line.classList.add('log-fade-in'); - } - line.style.animationDelay = `${index * 40}ms`; // Slightly faster (40ms) - } else { - line.classList.add('log-immediate'); - } - }); - }); - } - - // Scroll to bottom instantly on load (no animation) - requestAnimationFrame(() => { - LogBuffer.scrollInstant(); - // Restore smooth scrolling for future interactions - pre.style.removeProperty('scroll-behavior'); - // Reset scroll state - LogBuffer.isUserScrolledUp = false; - }); - } - - /** - * Fetch available users from chroot using list-users command - */ - async function fetchUsers(silent = false){ - if(!rootAccessConfirmed){ - return; // Don't attempt command - root check already printed error - } - - try{ - // Use the new list-users command that runs inside the chroot - const cmd = `sh ${PATH_CHROOT_SH} list-users`; - const out = await runCmdSync(cmd); - const users = String(out || '').trim().split(',').filter(u => u && u.length > 0); - - // Clear existing options except root - const select = els.userSelect; - select.innerHTML = ''; - - // Add user options - users.forEach(user => { - if(user.length > 0){ - const option = document.createElement('option'); - option.value = user; - option.textContent = user; - select.appendChild(option); - } - }); - - // Try to restore previously selected user - const savedUser = Storage.get('chroot_selected_user'); - if(savedUser && select.querySelector(`option[value="${savedUser}"]`)){ - select.value = savedUser; - } - - if(!silent) { - appendConsole(`Found ${users.length} regular user(s) in chroot`, 'info'); - } - }catch(e){ - if(!silent) { - appendConsole(`Could not fetch users from chroot: ${e.message}`, 'warn'); - } - // Keep only root option - els.userSelect.innerHTML = ''; - } - } - - /** - * Append text to console (batched for performance) - * Logs are buffered and flushed in chunks for smooth streaming effect - */ - function appendConsole(text, cls) { - LogBuffer.add(text, cls); - } - - /** - * Append multiple lines at once (for command output batching) - */ - function appendConsoleBatch(lines, cls = null) { - if(!Array.isArray(lines)) { - LogBuffer.add(lines, cls); - return; - } - - lines.forEach(line => { - if(line && line.trim()) { - LogBuffer.add(line.trim(), cls); - } - }); - } - - /** - * Add button press animation - */ - function animateButton(btn, actionText = null){ - if(!btn || btn.disabled) return Promise.resolve(); - - // Remove any existing pressed state first - btn.classList.remove('btn-pressed', 'btn-released'); - // Clear any inline styles that might interfere - btn.style.transform = ''; - btn.style.boxShadow = ''; - // Force a reflow to ensure the class and style are reset - void btn.offsetWidth; - - // Add pressed state (this will apply scale(0.96) and shadow from CSS) - btn.classList.add('btn-pressed'); - - // Return a promise that resolves after animation completes - return new Promise((resolve) => { - // Show action message in console during animation - if(actionText) { - appendConsole(actionText, 'info'); - } - // Remove after animation delay - setTimeout(() => { - btn.classList.remove('btn-pressed'); - btn.classList.add('btn-released'); - // Force reflow - void btn.offsetWidth; - // After transition, remove released class and reset - setTimeout(() => { - btn.classList.remove('btn-released'); - btn.style.transform = ''; - btn.style.boxShadow = ''; - // Blur to remove :active state (fixes stuck buttons on touch devices) - btn.blur(); - resolve(); - }, ANIMATION_DELAYS.BUTTON_RELEASE); - }, ANIMATION_DELAYS.BUTTON_ANIMATION); - }); - } - - // ============================================================================ - // STORAGE UTILITY - Centralized localStorage operations - // ============================================================================ - const Storage = { - get(key, defaultValue = null) { - try { - const value = localStorage.getItem(key); - return value !== null ? value : defaultValue; - } catch(e) { - return defaultValue; - } - }, - set(key, value) { - try { - localStorage.setItem(key, String(value)); - } catch(e) { - // Silently fail - storage may be disabled - } - }, - remove(key) { - try { - localStorage.removeItem(key); - } catch(e) { - // Silently fail - } - }, - getBoolean(key, defaultValue = false) { - const value = this.get(key); - return value !== null ? value === 'true' : defaultValue; - }, - getJSON(key, defaultValue = null) { - try { - const value = this.get(key); - return value ? JSON.parse(value) : defaultValue; - } catch(e) { - return defaultValue; - } - }, - setJSON(key, value) { - try { - this.set(key, JSON.stringify(value)); - } catch(e) { - // Silently fail - } - } - }; - - // ============================================================================ - // ANIMATION DELAYS - Centralized timing constants - // ============================================================================ - const ANIMATION_DELAYS = { - POPUP_CLOSE: 450, - POPUP_CLOSE_LONG: 750, - POPUP_CLOSE_VERY_LONG: 850, - UI_UPDATE: 50, - STATUS_REFRESH: 500, - BUTTON_ANIMATION: 120, // Reduced for snappier feel - BUTTON_RELEASE: 120, // Delay for button release animation - INPUT_FOCUS: 100, // Delay for focusing inputs after DOM manipulation - INIT_DELAY: 0, // Initial page load delay (removed to speed up loading) - PRE_FETCH_DELAY: 500, // Delay before pre-fetching interfaces - SETTINGS_LOAD: 100, // Delay for loading settings after popup opens - CHANNEL_VERIFY: 100, // Delay for verifying channel value after load - CHANNEL_UPDATE_DELAY: 50, // Delay after updating channel options before setting value - DIALOG_CLOSE: 200, // Delay for dialog close animation - PROGRESS_SPINNER: 200, // Interval for spinner animation - PROGRESS_DOTS: 400 // Interval for dots animation - }; - - // ============================================================================ - // APPLICATION CONSTANTS - // ============================================================================ - const APP_CONSTANTS = { - HOTSPOT: { - PASSWORD_MIN_LENGTH: 8, - DEFAULT_BAND: '2', - DEFAULT_CHANNEL_2_4GHZ: '6', - DEFAULT_CHANNEL_5GHZ: '36', - CHANNELS_2_4GHZ: [1,2,3,4,5,6,7,8,9,10,11], - CHANNELS_5GHZ: [36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165] - }, - CONSOLE: { - MAX_LINES: 250 // Maximum number of console lines to keep - }, - SPARSE_IMAGE: { - SIZE_BASE: 1000, // Use base 1000 (GB) not 1024 (GiB) - DEFAULT_SIZE_GB: 8, - AVAILABLE_SIZES: [4, 8, 16, 32, 64, 128, 256, 512] - }, - UI: { - Z_INDEX_OVERLAY: 2000, // Z-index for overlay dialogs - BYTES_BASE: 1000 // Base for byte calculations (KB, MB, GB) - } - }; - - // ============================================================================ - // STATE MANAGER - Unified state management with persistence - // ============================================================================ - const StateManager = { - states: { - hotspot: { key: 'hotspot_active', default: false }, - forwarding: { key: 'forwarding_active', default: false }, - debug: { key: 'debug_mode_active', default: false }, - sparse: { key: 'sparse_migrated', default: false } - }, - get(name) { - const state = this.states[name]; - if(!state) return null; - return Storage.getBoolean(state.key, state.default); - }, - set(name, value) { - const state = this.states[name]; - if(!state) return; - Storage.set(state.key, value); - }, - loadAll() { - hotspotActive = this.get('hotspot'); - forwardingActive = this.get('forwarding'); - debugModeActive = this.get('debug'); - sparseMigrated = this.get('sparse'); - }, - saveAll() { - this.set('hotspot', hotspotActive); - this.set('forwarding', forwardingActive); - this.set('debug', debugModeActive); - this.set('sparse', sparseMigrated); - } - }; - - // ============================================================================ - // COMMAND GUARD - Prevents concurrent command execution - // ============================================================================ - async function withCommandGuard(commandId, fn) { - if(activeCommandId) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - if(!rootAccessConfirmed) { - appendConsole('Cannot execute: root access not available', 'err'); - return; - } - try { - activeCommandId = commandId; - await fn(); - } finally { - activeCommandId = null; - } - } - - // ============================================================================ - // DIALOG MANAGER - Centralized dialog creation - // ============================================================================ - const DialogManager = { - // Common dialog styles - styles: { - overlay: ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; /* APP_CONSTANTS.UI.Z_INDEX_OVERLAY */ - opacity: 0; - transition: opacity 0.2s ease; - `, - dialog: ` - background: var(--card); - border-radius: var(--surface-radius); - box-shadow: 0 6px 20px rgba(6,8,14,0.06); - border: 1px solid rgba(0,0,0,0.08); - max-width: 450px; - width: 90%; - padding: 24px; - transform: scale(0.9); - transition: transform 0.2s ease; - `, - title: ` - margin: 0 0 12px 0; - font-size: 18px; - font-weight: 600; - color: var(--text); - `, - message: ` - margin: 0 0 20px 0; - font-size: 14px; - color: var(--muted); - line-height: 1.5; - white-space: pre-line; - `, - buttonContainer: ` - display: flex; - gap: 12px; - justify-content: flex-end; - `, - button: ` - padding: 8px 16px; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; - -webkit-tap-highlight-color: transparent; - `, - buttonPrimary: ` - border: 1px solid var(--accent); - background: var(--accent); - color: white; - `, - buttonSecondary: ` - border: 1px solid rgba(0,0,0,0.08); - background: transparent; - color: var(--text); - `, - buttonDanger: ` - border: 1px solid var(--danger); - background: var(--danger); - color: white; - `, - input: ` - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: var(--card); - color: var(--text); - font-size: 14px; - box-sizing: border-box; - ` - }, - - createOverlay() { - const overlay = document.createElement('div'); - overlay.style.cssText = this.styles.overlay; - return overlay; - }, - - createDialog() { - const dialog = document.createElement('div'); - dialog.style.cssText = this.styles.dialog; - return dialog; - }, - - createTitle(text) { - const title = document.createElement('h3'); - title.textContent = text; - title.style.cssText = this.styles.title; - return title; - }, - - createMessage(text) { - const message = document.createElement('p'); - message.textContent = text; - message.style.cssText = this.styles.message; - return message; - }, - - createButton(text, type = 'secondary') { - const btn = document.createElement('button'); - btn.textContent = text; - const baseStyle = this.styles.button; - const typeStyle = type === 'primary' ? this.styles.buttonPrimary : - type === 'danger' ? this.styles.buttonDanger : - this.styles.buttonSecondary; - btn.style.cssText = baseStyle + typeStyle; - return btn; - }, - - createInput(placeholder = '', value = '') { - const input = document.createElement('input'); - input.type = 'text'; - input.placeholder = placeholder; - input.value = value; - input.style.cssText = this.styles.input; - return input; - }, - - createSelect(options = []) { - const select = document.createElement('select'); - select.style.cssText = this.styles.input; - options.forEach(opt => { - const option = document.createElement('option'); - option.value = opt.value; - option.textContent = opt.text; - select.appendChild(option); - }); - return select; - }, - - show(overlay, dialog) { - document.body.appendChild(overlay); - setTimeout(() => { - overlay.style.opacity = '1'; - dialog.style.transform = 'scale(1)'; - }, 10); - }, - - close(overlay, delay = ANIMATION_DELAYS.DIALOG_CLOSE) { - overlay.style.opacity = '0'; - const dialog = overlay.querySelector('div'); - if(dialog) dialog.style.transform = 'scale(0.9)'; - // Clean up keyboard handler - this.cleanupKeyboard(overlay); - setTimeout(() => { - if(overlay.parentNode) { - overlay.parentNode.removeChild(overlay); - } - }, delay); - }, - - setupKeyboard(overlay, onEnter, onEscape) { - const handleKeyDown = (e) => { - if(e.key === 'Escape') { - if(onEscape) onEscape(); - document.removeEventListener('keydown', handleKeyDown); - } else if(e.key === 'Enter') { - if(onEnter) onEnter(); - document.removeEventListener('keydown', handleKeyDown); - } - }; - document.addEventListener('keydown', handleKeyDown); - // Store handler on overlay for cleanup - overlay._keyboardHandler = handleKeyDown; - return handleKeyDown; - }, - - cleanupKeyboard(overlay) { - if(overlay && overlay._keyboardHandler) { - document.removeEventListener('keydown', overlay._keyboardHandler); - delete overlay._keyboardHandler; - } - } - }; - - /** - * Progress Indicator Manager - Centralizes progress indicator creation/management - */ - const ProgressIndicator = { - create(text, type = 'spinner') { - const progressLine = document.createElement('div'); - progressLine.className = 'progress-indicator log-immediate'; - const baseText = '⏳ ' + text; - progressLine.textContent = baseText; - els.console.appendChild(progressLine); - // Scroll smoothly when adding progress indicator - els.console.scrollTo({ - top: els.console.scrollHeight, - behavior: 'smooth' - }); - - let interval = null; - if(type === 'spinner') { - let spinIndex = 0; - const spinner = ['|', '/', '-', '\\']; - interval = setInterval(() => { - if(progressLine.parentNode) { // Check if still in DOM - spinIndex = (spinIndex + 1) % 4; - progressLine.textContent = baseText + ' ' + spinner[spinIndex]; - } - }, ANIMATION_DELAYS.PROGRESS_SPINNER); - } else if(type === 'dots') { - // Use blinking animation instead of dots to prevent getting stuck - let isVisible = true; - interval = setInterval(() => { - if(progressLine.parentNode) { // Check if still in DOM - progressLine.textContent = isVisible ? baseText : ''; - isVisible = !isVisible; - } - }, ANIMATION_DELAYS.PROGRESS_SPINNER); - } - - return { progressLine, interval }; - }, - - remove(progressLine, interval) { - if(interval) clearInterval(interval); - if(progressLine && progressLine.parentNode) { - progressLine.remove(); - } - }, - - update(progressLine, text) { - if(progressLine && progressLine.parentNode) { - progressLine.textContent = '⏳ ' + text; - } - } - }; - - /** - * Button State Manager - Centralizes all button state updates - */ - const ButtonState = { - setButton(btn, enabled, visible = true, opacity = null) { - if(!btn) return; - btn.disabled = !enabled; - if(opacity !== null) { - btn.style.opacity = enabled ? '' : opacity; - } else { - btn.style.opacity = enabled ? '' : '0.5'; - } - if(visible !== null) { - btn.style.display = visible ? '' : 'none'; - } - // Clear button states when disabled - if(!enabled) { - btn.classList.remove('btn-pressed', 'btn-released'); - btn.style.transform = ''; - btn.style.boxShadow = ''; - } - }, - - setButtonPair(startBtn, stopBtn, isActive) { - this.setButton(startBtn, !isActive, true, '0.5'); - this.setButton(stopBtn, isActive, true, '0.5'); - }, - - setButtons(buttons) { - // buttons: [{ btn, enabled, visible, opacity }, ...] - buttons.forEach(({ btn, enabled, visible, opacity }) => { - this.setButton(btn, enabled, visible, opacity); - }); - } - }; - - /** - * Command Execution Wrapper - Standardizes async command execution pattern - */ - async function executeCommand(config) { - const { - id, - checkActive = true, - checkRoot = true, - validate = null, - beforeExecute = null, - command, - progressText, - progressType = 'spinner', - closePopup = null, - onSuccess = null, - onError = null, - onComplete = null, - refreshAfter = true - } = config; - - // Check if another command is running - if(checkActive && activeCommandId) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - // Check root access - if(checkRoot && !rootAccessConfirmed) { - appendConsole(`Cannot execute: root access not available`, 'err'); - return; - } - - // Validate inputs - if(validate && !validate()) { - return; - } - - // Close popup if needed - if(closePopup) { - closePopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - } - - // Before execute hook - if(beforeExecute) { - await beforeExecute(); - } - - // Disable UI - disableAllActions(true); - disableSettingsPopup(true); - - // Use centralized flow for action execution - const { progressLine, interval } = await prepareActionExecution( - progressText, - progressText, - progressType - ); - - activeCommandId = id; - - // Execute command - return new Promise((resolve) => { - setTimeout(async () => { - try { - let output; - if(typeof command === 'function') { - output = await command(); - } else { - output = await runCmdSync(command); - } - - ProgressIndicator.remove(progressLine, interval); - - // Display output in batch (better performance) - if(output) { - const lines = String(output).split('\n').filter(line => line.trim()); - if(lines.length > 0) { - appendConsoleBatch(lines); - } - } - - // Handle success - if(onSuccess) { - onSuccess(output); - } - - // Force scroll to bottom after output - forceScrollAfterDOMUpdate(); - - // Cleanup - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(onComplete) onComplete(true); - if(refreshAfter) setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - resolve({ success: true, output }); - } catch(error) { - ProgressIndicator.remove(progressLine, interval); - - // Display error in batch (better performance) - const errorMsg = String(error.message || error); - const lines = errorMsg.split('\n').filter(line => line.trim()); - if(lines.length > 0) { - appendConsoleBatch(lines, 'err'); - } - - // Handle error - if(onError) { - onError(error); - } - - // Force scroll to bottom after error output - forceScrollAfterDOMUpdate(); - - // Cleanup - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(onComplete) onComplete(false); - if(refreshAfter) setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - resolve({ success: false, error }); - } - }, 50); - }); - } - - /** - * Popup Manager - Centralizes popup open/close logic - */ - const PopupManager = { - open(popup, onOpen = null) { - if(popup) { - popup.classList.add('active'); - if(onOpen) onOpen(); - } - }, - - close(popup, onClose = null) { - if(popup) { - popup.classList.remove('active'); - if(onClose) onClose(); - } - }, - - setupClickOutside(popup, closeFn) { - if(popup && closeFn) { - popup.addEventListener('click', (e) => { - if(e.target === popup) closeFn(); - }); - } - } - }; - - /** - * Force scroll console to absolute bottom - * Kept for backward compatibility - */ - function forceScrollToBottom() { - LogBuffer.scrollToBottom(); - } - - /** - * Helper: Force scroll after DOM updates complete - * Kept for backward compatibility - */ - function forceScrollAfterDOMUpdate() { - requestAnimationFrame(() => { - LogBuffer.scrollToBottom(); - }); - } - - /** - * Helper: Validate root access and backend availability before command execution - * Returns { valid: boolean, error?: string } - if !valid, caller should cleanup and return - * @param {Object} progress - { progressLine, progressInterval } to cleanup on error - * @param {boolean} useValue - Whether to check rootAccessConfirmed.value (for feature modules) - */ - function validateCommandExecution(progress = null, useValue = false) { - // When useValue is true, we're called from feature modules where rootAccessConfirmed is a ref object - // When useValue is false, we're called from app.js where rootAccessConfirmed is a boolean - let rootAccess; - if(useValue) { - // For feature modules: rootAccessConfirmed is passed as rootAccessConfirmedRef which has .value - // But we need to check the global rootAccessConfirmedRef if it exists, or fall back to rootAccessConfirmed - rootAccess = rootAccessConfirmedRef ? rootAccessConfirmedRef.value : rootAccessConfirmed; - } else { - // For app.js: rootAccessConfirmed is a direct boolean - rootAccess = rootAccessConfirmed; - } - - if(!rootAccess) { - if(progress) ProgressIndicator.remove(progress.progressLine, progress.progressInterval); - appendConsole('No root execution method available', 'err'); - return { valid: false, error: 'No root execution method available' }; - } - - if(!window.cmdExec || typeof cmdExec.executeAsync !== 'function') { - if(progress) ProgressIndicator.remove(progress.progressLine, progress.progressInterval); - appendConsole('Backend not available', 'err'); - return { valid: false, error: 'Backend not available' }; - } - - return { valid: true }; - } - - /** - * Helper: Execute command with full lifecycle management - * Handles validation, execution, cleanup, and scrolling automatically - * @param {Object} options - Command execution options - * @param {string} options.cmd - Command to execute - * @param {Object} options.progress - { progressLine, progressInterval } from prepareActionExecution - * @param {Function} options.onSuccess - Called on success with result - * @param {Function} options.onError - Called on error with result - * @param {Function} options.onComplete - Called after success/error (optional) - * @param {boolean} options.useValue - Whether to use .value for rootAccessConfirmed (feature modules) - * @param {Object} options.activeCommandIdRef - Reference object like {value: string} for feature modules, or null for app.js - * @returns {string|null} Command ID or null if validation failed - */ - function executeCommandWithProgress({ - cmd, - progress, - onSuccess = null, - onError = null, - onComplete = null, - useValue = false, - activeCommandIdRef = null - }) { - // Validate before execution - const validation = validateCommandExecution(progress, useValue); - if(!validation.valid) { - if(activeCommandIdRef && activeCommandIdRef.value !== undefined) { - activeCommandIdRef.value = null; - } else if(!useValue) { - activeCommandId = null; - } - return null; - } - - let localCommandId = null; - const commandId = runCmdAsync(cmd, (result) => { - // Cleanup progress indicator - if(progress) ProgressIndicator.remove(progress.progressLine, progress.progressInterval); - - // Clear active command ID - if(activeCommandIdRef && activeCommandIdRef.value !== undefined) { - if(activeCommandIdRef.value === localCommandId) { - activeCommandIdRef.value = null; - } - } else if(!useValue && activeCommandId === localCommandId) { - activeCommandId = null; - } - - // Handle result (callbacks may add console messages) - if(result.success && onSuccess) { - onSuccess(result); - } else if(!result.success && onError) { - onError(result); - } - - // Optional completion callback (may also add console messages) - if(onComplete) onComplete(result); - - // Force scroll after ALL DOM updates (callbacks may have added messages) - // Use setTimeout to ensure all synchronous console appends are complete - setTimeout(() => { - forceScrollAfterDOMUpdate(); - }, 50); - }); - - localCommandId = commandId; - - // Set active command ID - if(activeCommandIdRef && activeCommandIdRef.value !== undefined) { - activeCommandIdRef.value = commandId; - } else if(!useValue) { - activeCommandId = commandId; - } - - return commandId; - } - - /** - * Centralized function to prepare action execution - * Handles: scroll to bottom, print header, show animation, ensure DOM updates - * This is the core logic for handling console log flow - * - * @param {string} headerText - The header text to display (e.g., "Starting Chroot Backup") - * @param {string} progressText - The progress indicator text (e.g., "Backing up chroot") - * @param {string} progressType - Type of progress indicator ('spinner' or 'dots', default: 'dots') - * @returns {Object} Object with { progressLine, progressInterval } for cleanup - */ - async function prepareActionExecution(headerText, progressText, progressType = 'dots') { - // Hide scrollbar with a smooth fade while a long-running action is active. - // This avoids distraction from the thumb jumping during continuous auto-scroll. - setConsoleScrollbarHidden(true); - - // STEP 1: Print header message via LogBuffer. - // We intentionally do this BEFORE scrolling so the header is part of the - // flushed batch, then we scroll to the true bottom of the updated content. - appendConsole(`━━━ ${headerText} ━━━`, 'info'); - - // STEP 2: Ensure header is flushed to the DOM, then scroll to bottom so it - // is guaranteed to be visible even when a lot of old logs exist. - await LogBuffer.waitForFlush(); - await scrollConsoleToBottom({ smooth: true }); - - // STEP 3: Show animated progress indicator (keep visible during execution) - const { progressLine, interval: progressInterval } = ProgressIndicator.create(progressText, progressType); - - // Ensure DOM updates are painted before command execution starts - // This prevents UI freeze and ensures header/animation are visible - await new Promise(resolve => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - resolve(); - }); - }); - }); - - return { progressLine, progressInterval }; - } - - /** - * Unified console scroll function - * Smoothly scrolls to bottom; can be forced to ignore user scroll position - */ - async function scrollConsoleToBottom(options = {}) { - await LogBuffer.scrollToBottom(options); - } - - /** - * Run command asynchronously - * Note: KernelSU/libsuperuser don't support true streaming - * Does NOT scroll - caller must handle scrolling before calling this - */ - async function runCmdAsync(cmd, onComplete){ - if(!rootAccessConfirmed){ - const errorMsg = 'No root execution method available (KernelSU or libsuperuser not detected).'; - appendConsole(errorMsg, 'err'); - if(onComplete) onComplete({ success: false, error: errorMsg }); - return null; - } - - if(!window.cmdExec || typeof cmdExec.executeAsync !== 'function'){ - const msg = 'Backend not available (cmdExec missing in page).'; - appendConsole(msg, 'err'); - if(onComplete) onComplete({ success: false, error: msg }); - return null; - } - - // Prepend LOGGING_ENABLED=1 if debug mode is active - const finalCmd = debugModeActive ? `LOGGING_ENABLED=1 ${cmd}` : cmd; - - // Store local reference for callback to use (captured in closure) - let localCommandId = null; - - const commandId = cmdExec.executeAsync(finalCmd, true, { - onOutput: (output) => { - // Batch output processing - collect all lines and append in one go - if(output) { - const lines = output.split('\n') - .filter(line => line.trim() && !line.trim().startsWith('[Executing:')); - - if(lines.length > 0) { - // Use batch append for better performance - appendConsoleBatch(lines); - } - } - }, - onError: (error) => { - appendConsole(String(error), 'err'); - }, - onComplete: (result) => { - // Only clear if this is still the active command (prevents race conditions) - if(activeCommandId === localCommandId) { - activeCommandId = null; - } - if(onComplete) onComplete(result); - } - }); - - // Set activeCommandId immediately after getting commandId - localCommandId = commandId; - activeCommandId = commandId; - - return commandId; - } - - /** - * Legacy sync command for simple operations - * Does NOT scroll - caller must handle scrolling before calling this - */ - async function runCmdSync(cmd){ - if(!rootAccessConfirmed){ - throw new Error('No root execution method available (KernelSU or libsuperuser not detected).'); - } - - if(!window.cmdExec || typeof cmdExec.execute !== 'function'){ - const msg = 'Backend not available (cmdExec missing in page).'; - appendConsole(msg, 'err'); - throw new Error(msg); - } - - // Prepend LOGGING_ENABLED=1 if debug mode is active - const finalCmd = debugModeActive ? `LOGGING_ENABLED=1 ${cmd}` : cmd; - - try { - const out = await cmdExec.execute(finalCmd, true); - return out; - } catch(err) { - // Don't print duplicate error if root check already failed - if(rootAccessConfirmed) { - appendConsole(String(err), 'err'); - } - throw err; - } - } - - function disableAllActions(disabled, isErrorCondition = false){ - try{ - // Main action buttons - using centralized ButtonState - ButtonState.setButton(els.startBtn, !disabled); - ButtonState.setButton(els.stopBtn, !disabled); - ButtonState.setButton(els.restartBtn, !disabled); - ButtonState.setButton(els.settingsBtn, !disabled, true); - ButtonState.setButton(els.forwardNatBtn, !disabled, true); - ButtonState.setButton(els.hotspotBtn, !disabled, true); - - els.userSelect.disabled = disabled; - - // Additional UI elements that should be disabled during operations - // But kept enabled during error conditions (root access failed, chroot not found) - const shouldDisableAlwaysAvailable = disabled && !isErrorCondition; - ButtonState.setButton(els.clearConsole, !shouldDisableAlwaysAvailable); - ButtonState.setButton(els.copyConsole, !shouldDisableAlwaysAvailable); - ButtonState.setButton(els.refreshStatus, !shouldDisableAlwaysAvailable); - if(els.themeToggle){ - ButtonState.setButton(els.themeToggle, !shouldDisableAlwaysAvailable); - } - - const copyBtn = document.getElementById('copy-login'); - if(copyBtn) ButtonState.setButton(copyBtn, !disabled); - - // Disable boot toggle when root not available - if(els.bootToggle) { - els.bootToggle.disabled = disabled; - const toggleContainer = els.bootToggle.closest('.toggle-inline'); - if(toggleContainer) { - toggleContainer.style.opacity = disabled ? '0.5' : ''; - toggleContainer.style.pointerEvents = disabled ? 'none' : ''; - } - } - }catch(e){} - } - - /** - * Check if ap0 interface exists (indicates hotspot is running) - */ - async function checkAp0Interface(){ - if(!rootAccessConfirmed){ - return false; - } - try{ - const out = await runCmdSync(`ip link show ap0 2>/dev/null | grep -q ap0 && echo "exists" || echo "not_exists"`); - return String(out||'').trim() === 'exists'; - }catch(e){ - return false; - } - } - - /** - * Check if forward-nat is running (universal method - checks iptables rules) - */ - async function checkForwardNatRunning(){ - if(!rootAccessConfirmed){ - return false; - } - try{ - // Use the new check-status command that checks actual iptables rules - const out = await runCmdSync(`sh ${FORWARD_NAT_SCRIPT} check-status 2>&1`); - const status = String(out||'').trim(); - return status === 'active'; - }catch(e){ - // Fallback to state file check if command fails - try{ - const out = await runCmdSync(`test -f /data/local/tmp/localhost_router.state && echo "exists" || echo "not_exists"`); - return String(out||'').trim() === 'exists'; - }catch(e2){ - return false; - } - } - } - /** - * Execute chroot action (start/stop/restart) - * Clean implementation following the exact flow specified - */ - async function doAction(action, btn){ - await withCommandGuard(`chroot-${action}`, async () => { - // Disable buttons immediately (grey them out first) - btn.disabled = true; - btn.style.opacity = '0.5'; - btn.classList.remove('btn-pressed', 'btn-released'); - btn.style.transform = ''; - btn.style.boxShadow = ''; - disableAllActions(true); - disableSettingsPopup(true); - - // Update UI state IMMEDIATELY (before scrolling/preparation) - const statusState = action === 'start' ? 'starting' : action === 'stop' ? 'stopping' : 'restarting'; - updateStatus(statusState); - - // Stop network services on stop/restart BEFORE creating progress indicator - // This way the chroot action animation shows properly - if(action === 'stop' || action === 'restart'){ - if(window.StopNetServices) { - // Stop network services silently (no progress indicator interference) - await StopNetServices.stopNetworkServices({ silent: false }); - } - } - - // Use centralized flow: scroll, header, animation (after network services stopped) - // Fix typo: "stop" -> "stopping" (not "stoping") - const actionText = action === 'stop' ? 'Stopping chroot' : - action === 'start' ? 'Starting chroot' : - 'Restarting chroot'; - const { progressLine, interval: progressInterval } = await prepareActionExecution( - actionText, - actionText, - 'dots' - ); - - // STEP 4: Execute command (animation stays visible during execution) - const cmd = `sh ${PATH_CHROOT_SH} ${action} --no-shell`; - - if(!rootAccessConfirmed){ - ProgressIndicator.remove(progressLine, progressInterval); - appendConsole('No root execution method available', 'err'); - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - - if(!window.cmdExec || typeof cmdExec.executeAsync !== 'function'){ - ProgressIndicator.remove(progressLine, progressInterval); - appendConsole('Backend not available', 'err'); - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - - const finalCmd = debugModeActive ? `LOGGING_ENABLED=1 ${cmd}` : cmd; - let localCommandId = null; - - const commandId = runCmdAsync(finalCmd, (result) => { - // STEP 5: Clear animation ONLY when command completes (success or failure) - ProgressIndicator.remove(progressLine, progressInterval); - - if(activeCommandId === localCommandId) { - activeCommandId = null; - } - - // Print result - if(result.success) { - appendConsole(`✓ ${action} completed successfully`, 'success'); - // refreshStatus will handle scrollbar show with proper delay - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - // Update module status after successful action - updateModuleStatus(); - } else { - appendConsole(`✗ ${action} failed`, 'err'); - // refreshStatus will handle scrollbar show with proper delay - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - // Update module status even on failure (to reflect current state) - updateModuleStatus(); - } - - // Force scroll to bottom after completion messages are added - forceScrollAfterDOMUpdate(); - - // Cleanup UI - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(els.closePopup) els.closePopup.style.display = ''; - - [els.startBtn, els.stopBtn, els.restartBtn].forEach(btn => { - if(btn) { - btn.classList.remove('btn-pressed', 'btn-released'); - btn.style.transform = ''; - btn.style.boxShadow = ''; - } - }); - }); - - localCommandId = commandId; - activeCommandId = commandId; - }); - } - - - /** - * Stop chroot properly (like the Stop button does) - * Shows stopping status, stops network services, then executes stop command - * Uses centralized prepareActionExecution flow - * Returns true if chroot is now stopped, false otherwise - */ - async function ensureChrootStopped() { - if(!rootAccessConfirmed) { - return false; - } - - // Check current status - try { - const out = await runCmdSync(`sh ${PATH_CHROOT_SH} status`); - const s = String(out || ''); - const isRunning = /Status:\s*RUNNING/i.test(s); - - if(!isRunning) { - return true; // Already stopped - } - } catch(e) { - // Status check failed, try to stop anyway - } - - // Stop network services first BEFORE creating progress indicator - // This way the chroot action animation shows properly - updateStatus('stopping'); - - if(window.StopNetServices) { - await StopNetServices.stopNetworkServices({ silent: false }); - } - - // Chroot is running, stop it properly using centralized flow (after network services stopped) - const { progressLine, progressInterval } = await prepareActionExecution( - 'Stopping Chroot', - 'Stopping chroot', - 'dots' - ); - - return new Promise((resolve) => { - const stopCmd = `sh ${PATH_CHROOT_SH} stop --no-shell`; - let localStopCommandId = null; - - const stopCommandId = runCmdAsync(stopCmd, (stopResult) => { - // Clear progress indicator - ProgressIndicator.remove(progressLine, progressInterval); - - if(activeCommandId === localStopCommandId) { - activeCommandId = null; - } - - if(stopResult.success) { - // Update status immediately after stop completes (before verification) - updateStatus('stopped'); - - // Wait a bit and verify it's actually stopped - setTimeout(async () => { - try { - const verifyOut = await runCmdSync(`sh ${PATH_CHROOT_SH} status`); - const verifyStatus = String(verifyOut || ''); - const isRunning = /Status:\s*RUNNING/i.test(verifyStatus); - if(!isRunning) { - appendConsole('✓ Chroot stopped successfully', 'success'); - // Force scroll to bottom after completion message - forceScrollAfterDOMUpdate(); - resolve(true); - } else { - appendConsole('⚠ Chroot stop completed but status check failed', 'warn'); - resolve(false); - } - } catch(e) { - appendConsole('⚠ Chroot stop completed but status verification failed', 'warn'); - resolve(false); - } - }, 500); - } else { - appendConsole('✗ Failed to stop chroot', 'err'); - resolve(false); - } - }); - - localStopCommandId = stopCommandId; - activeCommandId = stopCommandId; - }); - } - - /** - * Ensure chroot is started and wait for it to be running - * Uses centralized prepareActionExecution flow - * Returns true if chroot is now running, false otherwise - */ - async function ensureChrootStarted() { - if(!rootAccessConfirmed) { - return false; - } - - // Check current status - try { - const out = await runCmdSync(`sh ${PATH_CHROOT_SH} status`); - const s = String(out || ''); - const isRunning = /Status:\s*RUNNING/i.test(s); - - if(isRunning) { - return true; // Already running - } - } catch(e) { - // Status check failed, try to start anyway - } - - // Chroot is not running, start it using centralized flow - const { progressLine, progressInterval } = await prepareActionExecution( - 'Starting Chroot', - 'Starting chroot', - 'dots' - ); - - updateStatus('starting'); - - return new Promise((resolve) => { - const startCmd = `sh ${PATH_CHROOT_SH} start --no-shell`; - let localStartCommandId = null; - - const startCommandId = runCmdAsync(startCmd, (startResult) => { - // Clear progress indicator - ProgressIndicator.remove(progressLine, progressInterval); - - if(activeCommandId === localStartCommandId) { - activeCommandId = null; - } - - if(startResult.success) { - // Update status immediately after start completes (before verification) - updateStatus('running'); - - // Wait a bit and verify it's actually running - setTimeout(async () => { - try { - const verifyOut = await runCmdSync(`sh ${PATH_CHROOT_SH} status`); - const verifyStatus = String(verifyOut || ''); - const isRunning = /Status:\s*RUNNING/i.test(verifyStatus); - if(isRunning) { - appendConsole('✓ Chroot started successfully', 'success'); - // Force scroll to bottom after completion message - forceScrollAfterDOMUpdate(); - resolve(true); - } else { - appendConsole('⚠ Chroot start completed but status check failed', 'warn'); - resolve(false); - } - } catch(e) { - appendConsole('⚠ Chroot start completed but status verification failed', 'warn'); - resolve(false); - } - }, 1000); - } else { - appendConsole('✗ Failed to start chroot', 'err'); - resolve(false); - } - }); - - localStartCommandId = startCommandId; - activeCommandId = startCommandId; - }); - } - - /** - * Refresh chroot status (non-blocking) - */ - async function refreshStatus(){ - if(!rootAccessConfirmed){ - updateStatus('unknown'); - disableAllActions(true, true); - return; // Don't attempt commands - root check already printed error - } - - // DISABLE ALL UI ELEMENTS FIRST to prevent flicker - disableAllActions(true); - - try{ - // Check if chroot directory exists - let exists = await cmdExec.execute(`test -d ${CHROOT_PATH_UI} && echo 1 || echo 0`, true); - const chrootExists = String(exists||'').trim() === '1'; - let running = false; - - // COLLECT ALL STATUS INFO WITHOUT TOUCHING UI - let fetchUsersPromise = Promise.resolve(); - - if(chrootExists){ - _chrootMissingLogged = false; - - // Check if sparse image exists FIRST - const sparseCheck = await runCmdSync(`[ -f "${CHROOT_DIR}/rootfs.img" ] && echo "sparse" || echo "directory"`); - sparseMigrated = sparseCheck && sparseCheck.trim() === 'sparse'; - - // Get status without blocking UI - const out = await runCmdSync(`sh ${PATH_CHROOT_SH} status`); - const s = String(out || ''); - // Check for "Status: RUNNING" from the status output - running = /Status:\s*RUNNING/i.test(s); - - // Fetch users if running - we'll await this to ensure logs are generated before showing scrollbar - if(running){ - fetchUsersPromise = fetchUsers(false).catch(() => {}); // Will print message when ready - } - - // Check hotspot state if running - sync with actual system state - let currentHotspotActive = false; - if(running && rootAccessConfirmed){ - currentHotspotActive = await checkAp0Interface(); - if(currentHotspotActive !== hotspotActive){ - // State mismatch - update our saved state to match reality - hotspotActive = currentHotspotActive; - StateManager.set('hotspot', currentHotspotActive); - if(hotspotActiveRef) hotspotActiveRef.value = currentHotspotActive; - // Don't log during refresh - keep it quiet - } - } - - // Check forward-nat state - sync with actual system state (check even if chroot stopped) - let currentForwardingActive = false; - if(rootAccessConfirmed){ - currentForwardingActive = await checkForwardNatRunning(); - if(currentForwardingActive !== forwardingActive){ - // State mismatch - update our saved state to match reality - forwardingActive = currentForwardingActive; - StateManager.set('forwarding', currentForwardingActive); - if(forwardingActiveRef) forwardingActiveRef.value = currentForwardingActive; - // Don't log during refresh - keep it quiet - } - } - } - - // NOW APPLY ALL UI CHANGES AT ONCE - NO MORE CHANGES AFTER THIS - - // Status update - but don't overwrite custom statuses during active operations - // Only preserve custom statuses if there's an active command running - const currentStatus = els.statusText ? els.statusText.textContent.trim() : ''; - const customStatuses = ['backing up', 'restoring', 'migrating', 'uninstalling', 'updating', 'trimming', 'resizing']; - const isCustomStatus = customStatuses.includes(currentStatus); - - // Only preserve custom status if there's an active command AND it's a long-running operation status - // Allow normal status updates for starting/stopping/restarting (these are quick transitions) - if(isCustomStatus && activeCommandId) { - // Don't overwrite restoring/migrating/uninstalling during active operations - // These will be updated by the operation itself when complete - } else { - // Normal status update - check actual chroot state - const status = chrootExists ? (running ? 'running' : 'stopped') : 'not_found'; - updateStatus(status); - } - - // Main action buttons - using centralized ButtonState - const canControl = rootAccessConfirmed && chrootExists; - ButtonState.setButton(els.startBtn, canControl && !running); - ButtonState.setButton(els.stopBtn, canControl && running); - ButtonState.setButton(els.restartBtn, canControl && running); - - // User select - if(chrootExists && running){ - els.userSelect.disabled = false; - } else { - els.userSelect.disabled = true; - if(!chrootExists){ - els.userSelect.innerHTML = ''; - } - } - - // Copy login button - const copyLoginBtn = document.getElementById('copy-login'); - if(copyLoginBtn) { - ButtonState.setButton(copyLoginBtn, chrootExists && running); - } - - // Forward NAT button - visible but disabled when chroot is not running - const forwardNatEnabled = chrootExists && running && rootAccessConfirmed; - ButtonState.setButton(els.forwardNatBtn, forwardNatEnabled, true); - ButtonState.setButtonPair(els.startForwardingBtn, els.stopForwardingBtn, forwardingActive && forwardNatEnabled); - - // Hotspot button - const hotspotEnabled = chrootExists && running && rootAccessConfirmed; - ButtonState.setButton(els.hotspotBtn, hotspotEnabled, true); - ButtonState.setButtonPair(els.startHotspotBtn, els.stopHotspotBtn, hotspotActive && hotspotEnabled); - - // Boot toggle - if(els.bootToggle) { - const toggleContainer = els.bootToggle.closest('.toggle-inline'); - if(chrootExists && rootAccessConfirmed){ - els.bootToggle.disabled = false; - if(toggleContainer) { - toggleContainer.style.opacity = ''; - toggleContainer.style.pointerEvents = ''; - toggleContainer.style.display = ''; - } - } else { - els.bootToggle.disabled = true; - if(toggleContainer) { - toggleContainer.style.opacity = '0.5'; - toggleContainer.style.pointerEvents = 'none'; - toggleContainer.style.display = ''; - } - } - } - - // Settings popup - if(chrootExists){ - disableSettingsPopup(false, true); - } else { - disableSettingsPopup(false, false); - } - - // Re-enable basic UI elements - els.clearConsole.disabled = false; - els.clearConsole.style.opacity = ''; - els.copyConsole.disabled = false; - els.copyConsole.style.opacity = ''; - els.refreshStatus.disabled = false; - els.refreshStatus.style.opacity = ''; - if(els.themeToggle){ - els.themeToggle.disabled = false; - els.themeToggle.style.opacity = ''; - } - els.settingsBtn.disabled = false; - els.settingsBtn.style.opacity = ''; - - // Wait for async operations to complete (fetchUsers generates logs) - await fetchUsersPromise; - - // Wait for log buffer to flush all pending logs - await LogBuffer.waitForFlush(); - - // Scroll to bottom to show all logs - await LogBuffer.scrollToBottom(); - - // Reveal console scrollbar again now that refresh + log flush are complete. - // This gives a smooth fade-in after it was hidden for the action. - setConsoleScrollbarHidden(false); - - }catch(e){ - updateStatus('unknown'); - disableAllActions(true); - // Wait for any pending logs - await LogBuffer.waitForFlush(); - await LogBuffer.scrollToBottom(); - // Even on error, ensure scrollbar becomes visible again. - setConsoleScrollbarHidden(false); - } - } - - function updateStatus(state){ - const dot = els.statusDot; const text = els.statusText; - if(state === 'running'){ - dot.className = 'dot dot-on'; - text.textContent = 'running'; - } else if(state === 'stopped'){ - dot.className = 'dot dot-off'; - text.textContent = 'stopped'; - } else if(state === 'starting'){ - dot.className = 'dot dot-on'; - text.textContent = 'starting'; - } else if(state === 'stopping'){ - dot.className = 'dot dot-off'; - text.textContent = 'stopping'; - } else if(state === 'restarting'){ - dot.className = 'dot dot-warn'; - text.textContent = 'restarting'; - } else if(state === 'backing up'){ - dot.className = 'dot dot-warn'; - text.textContent = 'backing up'; - } else if(state === 'restoring'){ - dot.className = 'dot dot-warn'; - text.textContent = 'restoring'; - } else if(state === 'migrating'){ - dot.className = 'dot dot-warn'; - text.textContent = 'migrating'; - } else if(state === 'uninstalling'){ - dot.className = 'dot dot-warn'; - text.textContent = 'uninstalling'; - } else if(state === 'updating'){ - dot.className = 'dot dot-warn'; - text.textContent = 'updating'; - } else if(state === 'trimming'){ - dot.className = 'dot dot-warn'; - text.textContent = 'trimming'; - } else if(state === 'resizing'){ - dot.className = 'dot dot-warn'; - text.textContent = 'resizing'; - } else if(state === 'not_found'){ - dot.className = 'dot dot-off'; - text.textContent = 'chroot not found'; - } else { - dot.className = 'dot dot-unknown'; - text.textContent = 'unknown'; - } - - // enable/disable buttons depending on state - try{ - if(state === 'running'){ - els.stopBtn.disabled = false; - els.restartBtn.disabled = false; - els.startBtn.disabled = true; - els.userSelect.disabled = false; - // Visual feedback - els.stopBtn.style.opacity = ''; - els.restartBtn.style.opacity = ''; - els.startBtn.style.opacity = '0.5'; - } else if(state === 'stopped'){ - els.stopBtn.disabled = true; - els.restartBtn.disabled = true; - els.startBtn.disabled = false; - els.userSelect.disabled = true; - // Visual feedback - els.stopBtn.style.opacity = '0.5'; - els.restartBtn.style.opacity = '0.5'; - els.startBtn.style.opacity = ''; - } else if(state === 'starting' || state === 'stopping' || state === 'restarting'){ - // Operation in progress - disable all action buttons - els.stopBtn.disabled = true; - els.restartBtn.disabled = true; - els.startBtn.disabled = true; - els.userSelect.disabled = true; - // Visual feedback - all buttons appear pressed/disabled - els.stopBtn.style.opacity = '0.5'; - els.restartBtn.style.opacity = '0.5'; - els.startBtn.style.opacity = '0.5'; - } else if( - state === 'backing up' || - state === 'restoring' || - state === 'migrating' || - state === 'uninstalling' || - state === 'updating' || - state === 'trimming' || - state === 'resizing' - ){ - // Long-running maintenance operations in progress: - // keep all main action buttons disabled so user can't start/stop/restart mid-task - els.stopBtn.disabled = true; - els.restartBtn.disabled = true; - els.startBtn.disabled = true; - els.userSelect.disabled = true; - els.stopBtn.style.opacity = '0.5'; - els.restartBtn.style.opacity = '0.5'; - els.startBtn.style.opacity = '0.5'; - } else if(state === 'not_found'){ - // Similar to stopped, but start button also disabled since no chroot to start - els.stopBtn.disabled = true; - els.restartBtn.disabled = true; - els.startBtn.disabled = true; - els.userSelect.disabled = true; - // Visual feedback - els.stopBtn.style.opacity = '0.5'; - els.restartBtn.style.opacity = '0.5'; - els.startBtn.style.opacity = '0.5'; - } else { - // unknown - els.stopBtn.disabled = true; - els.restartBtn.disabled = true; - els.startBtn.disabled = false; - els.userSelect.disabled = true; - // Visual feedback - els.stopBtn.style.opacity = '0.5'; - els.restartBtn.style.opacity = '0.5'; - els.startBtn.style.opacity = ''; - } - }catch(e){ /* ignore if elements missing */ } - } - - // boot toggle handlers - async function writeBootFile(val){ - if(!rootAccessConfirmed){ - return; // Silently fail - root check already printed error - } - - try{ - // Ensure directory exists and write file - const cmd = `mkdir -p ${CHROOT_DIR} && echo ${val} > ${BOOT_FILE}`; - await cmdExec.execute(cmd, true); - appendConsole(`Run-at-boot ${val === 1 ? 'enabled' : 'disabled'}`, 'success'); - }catch(e){ - console.error(e); - appendConsole(`✗ Failed to set run-at-boot: ${e.message}`, 'err'); - // Reset toggle on error - await readBootFile(); - } - } - async function readBootFile(silent = false){ - if(!rootAccessConfirmed){ - els.bootToggle.checked = false; // Default to disabled - return; // Don't attempt command - root check already printed error - } - - try{ - if(window.cmdExec && typeof cmdExec.execute === 'function'){ - const out = await cmdExec.execute(`cat ${BOOT_FILE} 2>/dev/null || echo 0`, true); - const v = String(out||'').trim(); - els.bootToggle.checked = v === '1'; - if(!silent) { - appendConsole('Run-at-boot: '+ (v==='1' ? 'enabled' : 'disabled')); - } - } else { - if(!silent) { - appendConsole('Backend not available', 'err'); - } - els.bootToggle.checked = false; - } - }catch(e){ - console.error(e); - if(!silent) { - appendConsole(`Failed to read boot setting: ${e.message}`, 'err'); - } - els.bootToggle.checked = false; - } - } - - async function writeDozeOffFile(val){ - if(!rootAccessConfirmed){ - return; // Silently fail - root check already printed error - } - - try{ - // Ensure directory exists and write file - const cmd = `mkdir -p ${CHROOT_DIR} && echo ${val} > ${DOZE_OFF_FILE}`; - await cmdExec.execute(cmd, true); - appendConsole(`Android optimizations ${val === 1 ? 'enabled' : 'disabled'}`, 'success'); - }catch(e){ - console.error(e); - appendConsole(`✗ Failed to set Android optimizations: ${e.message}`, 'err'); - // Reset toggle on error - await readDozeOffFile(); - } - } - - async function readDozeOffFile(silent = false){ - if(!rootAccessConfirmed){ - els.androidOptimizeToggle.checked = true; // Default to enabled - return; // Don't attempt command - root check already printed error - } - - try{ - if(window.cmdExec && typeof cmdExec.execute === 'function'){ - const out = await cmdExec.execute(`cat ${DOZE_OFF_FILE} 2>/dev/null || echo 1`, true); - const v = String(out||'').trim(); - els.androidOptimizeToggle.checked = v === '1'; - if(!silent) { - appendConsole('Android optimizations: '+ (v==='1' ? 'enabled' : 'disabled')); - } - } else { - if(!silent) { - appendConsole('Backend not available', 'err'); - } - els.androidOptimizeToggle.checked = true; // Default to enabled - } - }catch(e){ - console.error(e); - if(!silent) { - appendConsole(`Failed to read Android optimizations setting: ${e.message}`, 'err'); - } - els.androidOptimizeToggle.checked = true; // Default to enabled - } - } - - // Update module status in module.prop - async function updateModuleStatus(){ - if(!rootAccessConfirmed || !window.cmdExec || typeof window.cmdExec.execute !== 'function'){ - return; // Silently fail if root/backend not available - } - - try{ - // Run update-status.sh silently in background - await cmdExec.execute(`sh ${UPDATE_STATUS_SCRIPT} 2>/dev/null`, true); - }catch(e){ - // Silently fail - status update is not critical - console.debug('Failed to update module status:', e); - } - } - - // copy login command - async function copyLoginCommand(){ - const selectedUser = els.userSelect.value; - // Save selected user - Storage.set('chroot_selected_user', selectedUser); - - // Check if ubuntu-chroot command is available, otherwise fallback to full script path - let chrootCmd = 'ubuntu-chroot'; - try { - if(rootAccessConfirmed && window.cmdExec && typeof window.cmdExec.execute === 'function') { - const checkCmd = await runCmdSync('command -v ubuntu-chroot 2>/dev/null || echo ""'); - if(!checkCmd || !checkCmd.trim()) { - // Command not found, use full script path - chrootCmd = `sh ${PATH_CHROOT_SH}`; - } - } else { - // If we can't check, default to full path for safety - chrootCmd = `sh ${PATH_CHROOT_SH}`; - } - } catch(e) { - // On error, fallback to full script path - chrootCmd = `sh ${PATH_CHROOT_SH}`; - } - - const loginCommand = `su -c "${chrootCmd} start ${selectedUser} -s"`; - - if(navigator.clipboard && navigator.clipboard.writeText){ - navigator.clipboard.writeText(loginCommand).then(()=> appendConsole(`Login command for user '${selectedUser}' copied to clipboard`)) - .catch(()=> appendConsole('Failed to copy to clipboard')); - } else { - // fallback - appendConsole(loginCommand); - try{ window.prompt('Copy login command (Ctrl+C):', loginCommand); }catch(e){} - } - } - - // copy console logs - function copyConsoleLogs(){ - const consoleText = els.console.textContent || ''; - - // If console is empty, show a message - if(!consoleText.trim()){ - appendConsole('Console is empty - nothing to copy', 'warn'); - return; - } - - // Try modern clipboard API first - if(navigator.clipboard && navigator.clipboard.writeText){ - navigator.clipboard.writeText(consoleText).then(() => { - appendConsole('Console logs copied to clipboard'); - }).catch((err) => { - console.warn('Clipboard API failed:', err); - // Fall back to older methods - fallbackCopy(consoleText); - }); - } else { - // No clipboard API available, use fallback - fallbackCopy(consoleText); - } - - function fallbackCopy(text){ - try { - // Try to create a temporary textarea for selection - const textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - textArea.style.top = '-999999px'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - const successful = document.execCommand('copy'); - document.body.removeChild(textArea); - - if(successful){ - appendConsole('Console logs copied to clipboard'); - } else { - appendConsole('Failed to copy console logs - please copy manually:', 'warn'); - appendConsole(text); - } - } catch(err) { - console.warn('Fallback copy failed:', err); - appendConsole('Failed to copy console logs - please copy manually:', 'warn'); - appendConsole(text); - } - } - } - - // Master root detection function - checks backend once and sets UI state - async function checkRootAccess(silent = false){ - if(!window.cmdExec || typeof cmdExec.execute !== 'function'){ - if(!silent) { - appendConsole('No root bridge detected — running offline. Actions disabled.'); - } - disableAllActions(true, true); - disableSettingsPopup(true, true); // assume chroot exists for now - return; - } - - try{ - // Test root access with a simple command that requires root - await cmdExec.execute('echo "test"', true); - // If successful, root is available - rootAccessConfirmed = true; - disableAllActions(false); - disableSettingsPopup(false, true); // assume chroot exists for now - - // Pre-fetch interfaces in background when root access is confirmed - // This ensures cache is ready when user opens popups - setTimeout(() => { - if(window.HotspotFeature && HotspotFeature.fetchInterfaces) { - HotspotFeature.fetchInterfaces(false, true).catch(() => { - // Silently fail - will fetch when popup opens - }); - } - if(window.ForwardNatFeature && ForwardNatFeature.fetchInterfaces) { - ForwardNatFeature.fetchInterfaces(false, true).catch(() => { - // Silently fail - will fetch when popup opens - }); - } - }, ANIMATION_DELAYS.PRE_FETCH_DELAY); // Delay to not interfere with initial page load - }catch(e){ - // If failed, show the backend error message once - rootAccessConfirmed = false; - appendConsole(`Failed to detect root execution method: ${e.message}`, 'err'); - // Then disable all root-dependent UI elements - disableAllActions(true, true); - // Also disable boot toggle when no root access - if(els.bootToggle) { - els.bootToggle.disabled = true; - const toggleContainer = els.bootToggle.closest('.toggle-inline'); - if(toggleContainer) { - toggleContainer.style.opacity = '0.5'; - toggleContainer.style.pointerEvents = 'none'; - } - } - disableSettingsPopup(true, true); // assume chroot exists for now - } - } - - // Settings popup functions - async function openSettingsPopup(){ - // Start scroll in parallel (don't await - let it happen in background) - scrollConsoleToBottom(); - - // Open popup immediately (don't wait for scroll or script loading) - PopupManager.open(els.settingsPopup); - - // Load script in background (will update textarea after popup is already open) - loadPostExecScript().catch(() => { - // Silently fail - script loading shouldn't block popup - }); - - // Set debug toggle state immediately - if(els.debugToggle) { - els.debugToggle.checked = debugModeActive; - } - } - - function closeSettingsPopup(){ - PopupManager.close(els.settingsPopup); - } - - async function loadPostExecScript(){ - if(!rootAccessConfirmed){ - els.postExecScript.value = ''; - return; - } - try{ - const script = await runCmdSync(`cat ${POST_EXEC_SCRIPT} 2>/dev/null || echo ''`); - els.postExecScript.value = String(script || '').trim(); - }catch(e){ - appendConsole(`Failed to load post-exec script: ${e.message}`, 'err'); - els.postExecScript.value = ''; - } - } - - async function savePostExecScript(){ - if(!rootAccessConfirmed){ - appendConsole('Cannot save post-exec script: root access not available', 'err'); - return; - } - try{ - const script = els.postExecScript.value.trim(); - // Use base64 encoding to safely transfer complex scripts with special characters - // This avoids all shell escaping issues - // Properly encode UTF-8 to base64 (handle large scripts by chunking) - const utf8Bytes = new TextEncoder().encode(script); - let binaryString = ''; - const chunkSize = 8192; - for(let i = 0; i < utf8Bytes.length; i += chunkSize) { - const chunk = utf8Bytes.slice(i, i + chunkSize); - binaryString += String.fromCharCode.apply(null, chunk); - } - const base64Script = btoa(binaryString); - await runCmdSync(`echo '${base64Script}' | base64 -d > ${POST_EXEC_SCRIPT}`); - await runCmdSync(`chmod 755 ${POST_EXEC_SCRIPT}`); - appendConsole('Post-exec script saved successfully', 'success'); - }catch(e){ - appendConsole(`Failed to save post-exec script: ${e.message}`, 'err'); - } - } - - async function clearPostExecScript(){ - els.postExecScript.value = ''; - if(!rootAccessConfirmed){ - appendConsole('Cannot clear post-exec script: root access not available', 'err'); - return; - } - try{ - await runCmdSync(`echo '' > ${POST_EXEC_SCRIPT}`); - appendConsole('Post-exec script cleared successfully', 'info'); - }catch(e){ - appendConsole(`Failed to clear post-exec script: ${e.message}`, 'err'); - } - } - - // Hotspot functions - delegated to HotspotFeature module - async function openHotspotPopup() { - // Start scroll in parallel (don't await - let it happen in background) - scrollConsoleToBottom(); - - if(window.HotspotFeature) { - await HotspotFeature.openHotspotPopup(); - } - } - - function closeHotspotPopup() { - if(window.HotspotFeature) { - HotspotFeature.closeHotspotPopup(); - } - } - - function showHotspotWarning() { - if(window.HotspotFeature) { - HotspotFeature.showHotspotWarning(); - } - } - - function dismissHotspotWarning() { - if(window.HotspotFeature) { - HotspotFeature.dismissHotspotWarning(); - } - } - - async function startHotspot() { - if(window.HotspotFeature) { - await HotspotFeature.startHotspot(); - } - } - - async function stopHotspot() { - if(window.HotspotFeature) { - await HotspotFeature.stopHotspot(); - } - } - - // Forward NAT status loading/saving is now handled by ForwardNatFeature module - // These functions are kept for backward compatibility during initialization - function loadForwardingStatus() { - forwardingActive = StateManager.get('forwarding'); - } - - function openForwardNatPopup() { - // Start scroll in parallel (don't await - let it happen in background) - scrollConsoleToBottom(); - - if(window.ForwardNatFeature) { - ForwardNatFeature.openForwardNatPopup(); - } - } - - function closeForwardNatPopup() { - if(window.ForwardNatFeature) { - ForwardNatFeature.closeForwardNatPopup(); - } - } - - async function startForwarding() { - if(window.ForwardNatFeature) { - await ForwardNatFeature.startForwarding(); - } - } - - async function stopForwarding() { - if(window.ForwardNatFeature) { - await ForwardNatFeature.stopForwarding(); - } - } - - // Sparse image settings functions - function openSparseSettingsPopup(){ - updateSparseInfo(); - PopupManager.open(els.sparseSettingsPopup); - } - - function closeSparseSettingsPopup(){ - PopupManager.close(els.sparseSettingsPopup); - } - - // Helper function to format bytes to human readable format (base 1000, GB) - function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = APP_CONSTANTS.UI.BYTES_BASE; // Use base 1000 for GB instead of GiB - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - } - - async function updateSparseInfo(){ - if(!rootAccessConfirmed || !sparseMigrated){ - if(els.sparseInfo) els.sparseInfo.textContent = 'Sparse image not detected'; - return; - } - - try{ - // Get apparent size (visible to Android - the intended size) - const apparentSizeCmd = `ls -lh ${CHROOT_DIR}/rootfs.img | tr -s ' ' | cut -d' ' -f5`; - const apparentSizeStr = await runCmdSync(apparentSizeCmd); - const apparentSize = apparentSizeStr.trim().replace(/G$/, ' GB'); - - // Get actual usage (allocated space from du -h, then add proper unit) - const usageCmd = `du -h ${CHROOT_DIR}/rootfs.img | cut -f1`; - const actualUsageRaw = await runCmdSync(usageCmd); - const actualUsage = actualUsageRaw.trim().replace(/G$/, ' GB'); - - const info = ` - - - - - - - - - - - -
Visible size to Android${apparentSize}
Actual size of the image${String(actualUsage||'').trim()}
- `; - if(els.sparseInfo) els.sparseInfo.innerHTML = info; // Keep innerHTML for HTML table content - }catch(e){ - if(els.sparseInfo) els.sparseInfo.textContent = 'Unable to read sparse image information'; - } - } - - // Resize functions - delegated to ResizeFeature module - async function trimSparseImage() { - if(window.ResizeFeature) { - await ResizeFeature.trimSparseImage(); - } - } - - async function resizeSparseImage() { - if(window.ResizeFeature) { - await ResizeFeature.resizeSparseImage(); - } - } - - async function updateChroot(){ - await withCommandGuard('chroot-update', async () => { - if(!rootAccessConfirmed){ - appendConsole('Cannot update chroot: root access not available', 'err'); - return; - } - - // Custom confirmation dialog - const confirmed = await showConfirmDialog( - 'Update Chroot Environment', - 'This will apply any available updates to the chroot environment.\n\nThe chroot will be started if it\'s not running. Continue?', - 'Update', - 'Cancel' - ); - - if(!confirmed){ - return; - } - - closeSettingsPopup(); - if(els.closePopup) els.closePopup.style.display = 'none'; - // Update status immediately after closing popup for instant feedback - updateStatus('updating'); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Check if chroot is running, start if not (uses centralized flow internally) - const isRunning = els.statusText && els.statusText.textContent.trim() === 'running'; - if(!isRunning) { - // Update status to 'starting' IMMEDIATELY so it's visible - updateStatus('starting'); - // Start chroot first (this uses prepareActionExecution internally) - const started = await ensureChrootStarted(); - if(!started) { - appendConsole('✗ Failed to start chroot - update aborted', 'err'); - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(els.closePopup) els.closePopup.style.display = ''; - return; - } - // Restore updating status after starting - updateStatus('updating'); - } - - // Scroll to bottom BEFORE update header appears to ensure all previous logs are visible - // This prevents the update header from appearing halfway up the console - await scrollConsoleToBottom(); - // Small delay to ensure scroll completes and DOM settles - await new Promise(resolve => setTimeout(resolve, 350)); - - // Now use centralized flow for update action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Starting Chroot Update', - 'Updating chroot', - 'dots' - ); - - // STEP 4: Execute update command (animation stays visible) - const cmd = `sh ${OTA_UPDATER}`; - - if(!window.cmdExec || typeof cmdExec.executeAsync !== 'function'){ - ProgressIndicator.remove(progressLine, progressInterval); - appendConsole('Backend not available', 'err'); - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(els.closePopup) els.closePopup.style.display = ''; - return; - } - - const finalCmd = debugModeActive ? `LOGGING_ENABLED=1 ${cmd}` : cmd; - let localCommandId = null; - - const commandId = runCmdAsync(finalCmd, (result) => { - // STEP 5: Clear animation ONLY when command completes - ProgressIndicator.remove(progressLine, progressInterval); - - if(activeCommandId === localCommandId) { - activeCommandId = null; - } - - if(result.success) { - appendConsole('✓ Chroot update completed successfully', 'success'); - - // Force scroll to bottom after update completion message - forceScrollAfterDOMUpdate(); - - // Restart chroot after update (uses centralized flow) - // Note: scrollbar will be hidden again by prepareActionExecution for restart - setTimeout(async () => { - if(window.StopNetServices) { - await StopNetServices.stopNetworkServices(); - } - - // Update status first, then use centralized flow - updateStatus('restarting'); - - // Use centralized flow for restart action - const { progressLine: restartLine, interval: restartInterval } = await prepareActionExecution( - 'Restarting Chroot', - 'Restarting chroot', - 'dots' - ); - - let localRestartCommandId = null; - const restartCommandId = runCmdAsync(`sh ${PATH_CHROOT_SH} restart --no-shell`, (restartResult) => { - ProgressIndicator.remove(restartLine, restartInterval); - - if(activeCommandId === localRestartCommandId) { - activeCommandId = null; - } - - if(restartResult.success) { - appendConsole('✓ Chroot restarted successfully', 'success'); - updateModuleStatus(); - } else { - appendConsole('⚠ Chroot restart failed, but update was successful', 'warn'); - updateModuleStatus(); - } - - appendConsole('━━━ Update Complete ━━━', 'success'); - - // Ensure restart completion messages are visible immediately - forceScrollAfterDOMUpdate(); - - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(els.closePopup) els.closePopup.style.display = ''; - - // After "Update Complete" and a status refresh, smoothly scroll console once - // refreshStatus will handle scrollbar show with proper delay - setTimeout(async () => { - try { - await refreshStatus(); - } catch(e) { - console.error('refreshStatus error after update restart:', e); - } finally { - scrollConsoleToBottom({ force: true }); - } - }, ANIMATION_DELAYS.STATUS_REFRESH); - }); - - localRestartCommandId = restartCommandId; - activeCommandId = restartCommandId; - }, ANIMATION_DELAYS.POPUP_CLOSE_LONG); - } else { - appendConsole('✗ Chroot update failed', 'err'); - - // Force scroll to bottom after failure message - forceScrollAfterDOMUpdate(); - - activeCommandId = null; - disableAllActions(false); - disableSettingsPopup(false, true); - if(els.closePopup) els.closePopup.style.display = ''; - // refreshStatus will handle scrollbar show with proper delay - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - } - }); - - localCommandId = commandId; - activeCommandId = commandId; - }); - } - - // Backup/Restore functions - delegated to BackupRestoreFeature module - async function backupChroot() { - if(window.BackupRestoreFeature) { - await BackupRestoreFeature.backupChroot(); - } - } - - async function restoreChroot() { - if(window.BackupRestoreFeature) { - await BackupRestoreFeature.restoreChroot(); - } - } - - // Uninstall function - delegated to UninstallFeature module - async function uninstallChroot() { - if(window.UninstallFeature) { - await UninstallFeature.uninstallChroot(); - } - } - - // Disable settings popup when no root available - function disableSettingsPopup(disabled, chrootExists = true){ - try{ - if(els.settingsPopup){ - // Don't dim the entire popup when chroot doesn't exist - only dim individual elements - // Only dim when disabled due to no root access - if(disabled) { - els.settingsPopup.style.opacity = '0.5'; - // When disabled, allow closing but dim the content - els.settingsPopup.style.pointerEvents = 'auto'; - } else { - els.settingsPopup.style.opacity = ''; - // When not disabled, allow full interaction - els.settingsPopup.style.pointerEvents = 'auto'; - } - } - // Close button should remain functional - if(els.closePopup) { - // Close button stays enabled and visible - } - // Disable individual popup elements using centralized ButtonState - const buttonsToDisable = [ - { btn: els.postExecScript, disabled: disabled || !chrootExists }, - { btn: els.saveScript, disabled: disabled || !chrootExists }, - { btn: els.clearScript, disabled: disabled || !chrootExists }, - { btn: els.updateBtn, disabled: disabled || !chrootExists }, - { btn: els.backupBtn, disabled: disabled || !chrootExists }, - { btn: els.restoreBtn, disabled: disabled }, - { btn: els.uninstallBtn, disabled: disabled || !chrootExists }, - { btn: els.trimSparseBtn, disabled: disabled || !chrootExists || !sparseMigrated }, - { btn: els.resizeSparseBtn, disabled: disabled || !chrootExists || !sparseMigrated } - ]; - - buttonsToDisable.forEach(({ btn, disabled: btnDisabled }) => { - if(btn) { - btn.disabled = btnDisabled; - btn.style.opacity = btnDisabled ? '0.5' : ''; - btn.style.cursor = btnDisabled ? 'not-allowed' : ''; - btn.style.pointerEvents = btnDisabled ? 'none' : ''; - } - }); - - // Experimental features - migrate sparse button - const migrateSparseBtn = document.getElementById('migrate-sparse-btn'); - if(migrateSparseBtn) { - const migrateDisabled = disabled || !chrootExists || sparseMigrated; - migrateSparseBtn.disabled = migrateDisabled; - migrateSparseBtn.style.opacity = migrateDisabled ? '0.5' : ''; - migrateSparseBtn.style.cursor = migrateDisabled ? 'not-allowed' : ''; - migrateSparseBtn.style.pointerEvents = migrateDisabled ? 'none' : ''; - // Only show "Already Migrated" if chroot exists AND is migrated - // If chroot doesn't exist, always show "Migrate to Sparse Image" (disabled) - migrateSparseBtn.textContent = (chrootExists && sparseMigrated) ? 'Already Migrated' : 'Migrate to Sparse Image'; - } - - // Sparse settings button visibility - if(els.sparseSettingsBtn) { - els.sparseSettingsBtn.style.display = (!disabled && chrootExists && sparseMigrated) ? 'inline-block' : 'none'; - } - }catch(e){} - } - - // Show experimental section if enabled - function initExperimentalFeatures(){ - const experimentalSection = document.querySelector('.experimental-section'); - if(experimentalSection){ - // For now, always show experimental features (can be made conditional later) - experimentalSection.style.display = 'block'; - } - - const optionalSection = document.querySelector('.optional-section'); - if(optionalSection){ - // Always show optional section - optionalSection.style.display = 'block'; - } - } - - // Migrate function - delegated to MigrateFeature module - async function migrateToSparseImage() { - if(window.MigrateFeature) { - await MigrateFeature.migrateToSparseImage(); - } - } - - // Size selection dialog for sparse image migration - function showSizeSelectionDialog(){ - return new Promise((resolve) => { - // Create overlay - const overlay = document.createElement('div'); - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; /* APP_CONSTANTS.UI.Z_INDEX_OVERLAY */ - opacity: 0; - transition: opacity 0.2s ease; - `; - - // Create dialog - const dialog = document.createElement('div'); - dialog.style.cssText = ` - background: var(--card); - border-radius: var(--surface-radius); - box-shadow: 0 6px 20px rgba(6,8,14,0.06); - border: 1px solid rgba(0,0,0,0.08); - max-width: 400px; - width: 90%; - padding: 24px; - transform: scale(0.9); - transition: transform 0.2s ease; - `; - - // Create title - const titleEl = document.createElement('h3'); - titleEl.textContent = 'Select Sparse Image Size'; - titleEl.style.cssText = ` - margin: 0 0 12px 0; - font-size: 18px; - font-weight: 600; - color: var(--text); - `; - - // Create description - const descEl = document.createElement('p'); - descEl.textContent = 'Choose the maximum size for your sparse ext4 image. The actual disk usage will grow as you add data.'; - descEl.style.cssText = ` - margin: 0 0 20px 0; - font-size: 14px; - color: var(--muted); - line-height: 1.5; - `; - - // Create form - const formContainer = document.createElement('div'); - formContainer.style.cssText = ` - margin-bottom: 20px; - `; - - const sizeSelect = document.createElement('select'); - sizeSelect.style.cssText = ` - width: 100%; - padding: 12px 16px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: var(--card); - color: var(--text); - font-size: 16px; - margin-bottom: 8px; - `; - - // Add size options from constants - const sizes = APP_CONSTANTS.SPARSE_IMAGE.AVAILABLE_SIZES; - const defaultSize = APP_CONSTANTS.SPARSE_IMAGE.DEFAULT_SIZE_GB; - sizes.forEach(size => { - const option = document.createElement('option'); - option.value = size; - option.textContent = `${size}GB`; - if(size === defaultSize) option.selected = true; - sizeSelect.appendChild(option); - }); - - const sizeNote = document.createElement('p'); - sizeNote.textContent = 'Note: This sets the maximum size. Actual usage starts small and grows as needed.'; - sizeNote.style.cssText = ` - margin: 8px 0 0 0; - font-size: 12px; - color: var(--muted); - font-style: italic; - `; - - formContainer.appendChild(sizeSelect); - formContainer.appendChild(sizeNote); - - // Create button container - const buttonContainer = document.createElement('div'); - buttonContainer.style.cssText = ` - display: flex; - gap: 12px; - justify-content: flex-end; - `; - - // Create cancel button - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = 'Cancel'; - cancelBtn.style.cssText = ` - padding: 8px 16px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: transparent; - color: var(--text); - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; - -webkit-tap-highlight-color: transparent; - `; - - // Create select button - const selectBtn = document.createElement('button'); - selectBtn.textContent = 'Continue'; - selectBtn.style.cssText = ` - padding: 8px 16px; - border: 1px solid var(--accent); - border-radius: 8px; - background: var(--accent); - color: white; - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; - -webkit-tap-highlight-color: transparent; - `; - - // Dark mode adjustments - if(document.documentElement.getAttribute('data-theme') === 'dark'){ - dialog.style.borderColor = 'rgba(255,255,255,0.08)'; - cancelBtn.style.borderColor = 'rgba(255,255,255,0.08)'; - sizeSelect.style.borderColor = 'rgba(255,255,255,0.08)'; - cancelBtn.addEventListener('mouseenter', () => { - cancelBtn.style.background = 'rgba(255,255,255,0.05)'; - }); - cancelBtn.addEventListener('mouseleave', () => { - cancelBtn.style.background = 'transparent'; - }); - } - - // Event listeners - const closeDialog = (result) => { - overlay.style.opacity = '0'; - dialog.style.transform = 'scale(0.9)'; - // Clean up keyboard handler - if(overlay._keyboardHandler) { - document.removeEventListener('keydown', overlay._keyboardHandler); - delete overlay._keyboardHandler; - } - setTimeout(() => { - if(overlay.parentNode) { - document.body.removeChild(overlay); - } - resolve(result); - }, ANIMATION_DELAYS.DIALOG_CLOSE); - }; - - cancelBtn.addEventListener('click', () => closeDialog(null)); - - selectBtn.addEventListener('click', () => { - const selectedSize = sizeSelect.value; - closeDialog(selectedSize); - }); - - selectBtn.addEventListener('mouseenter', () => { - selectBtn.style.transform = 'translateY(-1px)'; - selectBtn.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.3)'; - }); - - selectBtn.addEventListener('mouseleave', () => { - selectBtn.style.transform = 'translateY(0)'; - selectBtn.style.boxShadow = 'none'; - }); - - // Close on overlay click - overlay.addEventListener('click', (e) => { - if(e.target === overlay) closeDialog(null); - }); - - // Keyboard support - store handler for cleanup - const handleKeyDown = (e) => { - if(e.key === 'Escape') { - closeDialog(null); - document.removeEventListener('keydown', handleKeyDown); - } else if(e.key === 'Enter') { - selectBtn.click(); - document.removeEventListener('keydown', handleKeyDown); - } - }; - document.addEventListener('keydown', handleKeyDown); - overlay._keyboardHandler = handleKeyDown; // Store for cleanup - - // Assemble dialog - buttonContainer.appendChild(cancelBtn); - buttonContainer.appendChild(selectBtn); - - dialog.appendChild(titleEl); - dialog.appendChild(descEl); - dialog.appendChild(formContainer); - dialog.appendChild(buttonContainer); - - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - // Animate in - setTimeout(() => { - overlay.style.opacity = '1'; - dialog.style.transform = 'scale(1)'; - }, 10); - }); - } - function showConfirmDialog(title, message, confirmText = 'Yes', cancelText = 'No'){ - return new Promise((resolve) => { - const overlay = DialogManager.createOverlay(); - const dialog = DialogManager.createDialog(); - const titleEl = DialogManager.createTitle(title); - const messageEl = DialogManager.createMessage(message); - const buttonContainer = document.createElement('div'); - buttonContainer.style.cssText = DialogManager.styles.buttonContainer; - - const cancelBtn = DialogManager.createButton(cancelText, 'secondary'); - const confirmBtn = DialogManager.createButton(confirmText, 'danger'); - - // Dark mode adjustments - if(document.documentElement.getAttribute('data-theme') === 'dark'){ - dialog.style.borderColor = 'rgba(255,255,255,0.08)'; - cancelBtn.style.borderColor = 'rgba(255,255,255,0.08)'; - cancelBtn.addEventListener('mouseenter', () => { - cancelBtn.style.background = 'rgba(255,255,255,0.05)'; - }); - cancelBtn.addEventListener('mouseleave', () => { - cancelBtn.style.background = 'transparent'; - }); - } - - const closeDialog = (result) => { - DialogManager.close(overlay, ANIMATION_DELAYS.DIALOG_CLOSE); - resolve(result); - }; - - cancelBtn.addEventListener('click', () => closeDialog(false)); - confirmBtn.addEventListener('click', () => closeDialog(true)); - - confirmBtn.addEventListener('mouseenter', () => { - confirmBtn.style.transform = 'translateY(-1px)'; - confirmBtn.style.boxShadow = '0 4px 12px rgba(220, 38, 38, 0.3)'; - }); - confirmBtn.addEventListener('mouseleave', () => { - confirmBtn.style.transform = 'translateY(0)'; - confirmBtn.style.boxShadow = 'none'; - }); - - overlay.addEventListener('click', (e) => { - if(e.target === overlay) closeDialog(false); - }); - - DialogManager.setupKeyboard(overlay, () => closeDialog(true), () => closeDialog(false)); - - buttonContainer.appendChild(cancelBtn); - buttonContainer.appendChild(confirmBtn); - dialog.appendChild(titleEl); - dialog.appendChild(messageEl); - dialog.appendChild(buttonContainer); - overlay.appendChild(dialog); - DialogManager.show(overlay, dialog); - }); - } - - // File picker dialog for backup/restore operations - function showFilePickerDialog(title, message, defaultPath, defaultFilename, forRestore = false){ - return new Promise((resolve) => { - // Create overlay - const overlay = document.createElement('div'); - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; /* APP_CONSTANTS.UI.Z_INDEX_OVERLAY */ - opacity: 0; - transition: opacity 0.2s ease; - `; - - // Create dialog - const dialog = document.createElement('div'); - dialog.style.cssText = ` - background: var(--card); - border-radius: var(--surface-radius); - box-shadow: 0 6px 20px rgba(6,8,14,0.06); - border: 1px solid rgba(0,0,0,0.08); - max-width: 450px; - width: 90%; - padding: 24px; - transform: scale(0.9); - transition: transform 0.2s ease; - `; - - // Create title - const titleEl = document.createElement('h3'); - titleEl.textContent = title; - titleEl.style.cssText = ` - margin: 0 0 12px 0; - font-size: 18px; - font-weight: 600; - color: var(--text); - `; - - // Create message - const messageEl = document.createElement('p'); - messageEl.textContent = message; - messageEl.style.cssText = ` - margin: 0 0 16px 0; - font-size: 14px; - color: var(--muted); - line-height: 1.5; - `; - - // Create form container - const formContainer = document.createElement('div'); - formContainer.style.cssText = ` - margin-bottom: 20px; - `; - - let pathInput; // Declare here for scope - - if(!forRestore){ - // For backup: path input + filename input - const pathLabel = document.createElement('label'); - pathLabel.textContent = 'Directory:'; - pathLabel.style.cssText = ` - display: block; - margin-bottom: 6px; - font-weight: 500; - color: var(--text); - font-size: 14px; - `; - - pathInput = document.createElement('input'); - pathInput.type = 'text'; - pathInput.value = defaultPath; - pathInput.placeholder = '/sdcard/backup'; - pathInput.style.cssText = ` - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: var(--card); - color: var(--text); - font-size: 14px; - margin-bottom: 12px; - box-sizing: border-box; - `; - - const filenameLabel = document.createElement('label'); - filenameLabel.textContent = 'Filename:'; - filenameLabel.style.cssText = ` - display: block; - margin-bottom: 6px; - font-weight: 500; - color: var(--text); - font-size: 14px; - `; - - const filenameInput = document.createElement('input'); - filenameInput.type = 'text'; - filenameInput.value = defaultFilename; - filenameInput.placeholder = 'chroot-backup.tar.gz'; - filenameInput.style.cssText = ` - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: var(--card); - color: var(--text); - font-size: 14px; - box-sizing: border-box; - `; - - // Auto-append .tar.gz if not present - filenameInput.addEventListener('input', () => { - if(!filenameInput.value.includes('.tar.gz') && filenameInput.value.length > 0){ - filenameInput.value = filenameInput.value.replace(/\.tar\.gz$/, '') + '.tar.gz'; - } - }); - - // Focus on filename input - setTimeout(() => filenameInput.focus(), ANIMATION_DELAYS.INPUT_FOCUS); - - formContainer.appendChild(pathLabel); - formContainer.appendChild(pathInput); - formContainer.appendChild(filenameLabel); - formContainer.appendChild(filenameInput); - } else { - // For restore: single file path input - const pathLabel = document.createElement('label'); - pathLabel.textContent = 'Backup File Path:'; - pathLabel.style.cssText = ` - display: block; - margin-bottom: 6px; - font-weight: 500; - color: var(--text); - font-size: 14px; - `; - - pathInput = document.createElement('input'); - pathInput.type = 'text'; - pathInput.value = defaultPath; - pathInput.placeholder = '/sdcard/chroot-backup.tar.gz'; - pathInput.style.cssText = ` - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: var(--card); - color: var(--text); - font-size: 14px; - box-sizing: border-box; - `; - - // Focus on path input - setTimeout(() => pathInput.focus(), ANIMATION_DELAYS.INPUT_FOCUS); - - formContainer.appendChild(pathLabel); - formContainer.appendChild(pathInput); - } - - // Create button container - const buttonContainer = document.createElement('div'); - buttonContainer.style.cssText = ` - display: flex; - gap: 12px; - justify-content: flex-end; - `; - - // Create cancel button - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = 'Cancel'; - cancelBtn.style.cssText = ` - padding: 8px 16px; - border: 1px solid rgba(0,0,0,0.08); - border-radius: 8px; - background: transparent; - color: var(--text); - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; - -webkit-tap-highlight-color: transparent; - `; - - // Create select button - const selectBtn = document.createElement('button'); - selectBtn.textContent = forRestore ? 'Select File' : 'Select Location'; - selectBtn.style.cssText = ` - padding: 8px 16px; - border: 1px solid var(--accent); - border-radius: 8px; - background: var(--accent); - color: white; - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; - -webkit-tap-highlight-color: transparent; - `; - - // Dark mode adjustments - if(document.documentElement.getAttribute('data-theme') === 'dark'){ - dialog.style.borderColor = 'rgba(255,255,255,0.08)'; - cancelBtn.style.borderColor = 'rgba(255,255,255,0.08)'; - if(!forRestore){ - formContainer.querySelectorAll('input').forEach(input => { - input.style.borderColor = 'rgba(255,255,255,0.08)'; - }); - } else { - pathInput.style.borderColor = 'rgba(255,255,255,0.08)'; - } - cancelBtn.addEventListener('mouseenter', () => { - cancelBtn.style.background = 'rgba(255,255,255,0.05)'; - }); - cancelBtn.addEventListener('mouseleave', () => { - cancelBtn.style.background = 'transparent'; - }); - } - - // Event listeners - const closeDialog = (result) => { - overlay.style.opacity = '0'; - dialog.style.transform = 'scale(0.9)'; - // Clean up keyboard handler - if(overlay._keyboardHandler) { - document.removeEventListener('keydown', overlay._keyboardHandler); - delete overlay._keyboardHandler; - } - setTimeout(() => { - if(overlay.parentNode) { - document.body.removeChild(overlay); - } - resolve(result); - }, ANIMATION_DELAYS.DIALOG_CLOSE); - }; - - cancelBtn.addEventListener('click', () => closeDialog(null)); - - selectBtn.addEventListener('click', () => { - let selectedPath = ''; - if(!forRestore){ - const pathInput = formContainer.querySelector('input:nth-child(2)'); - const filenameInput = formContainer.querySelector('input:nth-child(4)'); - const path = pathInput.value.trim(); - const filename = filenameInput.value.trim(); - if(path && filename){ - selectedPath = path + (path.endsWith('/') ? '' : '/') + filename; - } - } else { - const pathInput = formContainer.querySelector('input'); - selectedPath = pathInput.value.trim(); - } - - if(selectedPath){ - // Basic validation - if(forRestore && !selectedPath.endsWith('.tar.gz')){ - alert('Please select a valid .tar.gz backup file'); - return; - } - closeDialog(selectedPath); - } else { - alert('Please enter a valid path'); - } - }); - - selectBtn.addEventListener('mouseenter', () => { - selectBtn.style.transform = 'translateY(-1px)'; - selectBtn.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.3)'; - }); - - selectBtn.addEventListener('mouseleave', () => { - selectBtn.style.transform = 'translateY(0)'; - selectBtn.style.boxShadow = 'none'; - }); - - // Close on overlay click - overlay.addEventListener('click', (e) => { - if(e.target === overlay) closeDialog(null); - }); - - // Keyboard support - const handleKeyDown = (e) => { - if(e.key === 'Escape') { - closeDialog(null); - document.removeEventListener('keydown', handleKeyDown); - } else if(e.key === 'Enter') { - selectBtn.click(); - document.removeEventListener('keydown', handleKeyDown); - } - }; - document.addEventListener('keydown', handleKeyDown); - - // Assemble dialog - buttonContainer.appendChild(cancelBtn); - buttonContainer.appendChild(selectBtn); - - dialog.appendChild(titleEl); - dialog.appendChild(messageEl); - dialog.appendChild(formContainer); - dialog.appendChild(buttonContainer); - - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - // Animate in - setTimeout(() => { - overlay.style.opacity = '1'; - dialog.style.transform = 'scale(1)'; - }, 10); - }); - } - - - // Theme toggle - button with aria-pressed (checkbox code removed as HTML only has button) - function initTheme(){ - const stored = Storage.get('chroot_theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light'); - document.documentElement.setAttribute('data-theme', stored==='dark' ? 'dark' : ''); - - const t = els.themeToggle; - if(!t) return; - - // Button toggle with aria-pressed - const isDark = stored === 'dark'; - t.setAttribute('aria-pressed', isDark ? 'true' : 'false'); - t.addEventListener('click', ()=>{ - const pressed = t.getAttribute('aria-pressed') === 'true'; - const next = pressed ? 'light' : 'dark'; - t.setAttribute('aria-pressed', next === 'dark' ? 'true' : 'false'); - document.documentElement.setAttribute('data-theme', next==='dark' ? 'dark' : ''); - Storage.set('chroot_theme', next); - }); - } - - // Setup event handlers with button animations - els.startBtn.addEventListener('click', (e) => doAction('start', e.target)); - els.stopBtn.addEventListener('click', (e) => doAction('stop', e.target)); - els.restartBtn.addEventListener('click', (e) => doAction('restart', e.target)); - const copyLoginBtn = document.getElementById('copy-login'); - if(copyLoginBtn) { - copyLoginBtn.addEventListener('click', (e) => { - // Start animation (don't await - let it run in background) - animateButton(e.target); - // Copy immediately (don't wait for animation) - copyLoginCommand(); - }); - } - els.clearConsole.addEventListener('click', (e) => { - // Start animation (don't await - let it run in background) - animateButton(e.target); - // Clear console immediately (don't wait for animation) - els.console.textContent = ''; // Use textContent for clearing (safer than innerHTML) - // Clear saved logs - Storage.remove('chroot_console_logs'); - - // If debug mode is enabled, also clear the logs folder - if(debugModeActive){ - appendConsole('Console and logs are cleared', 'info'); - setTimeout(() => { - runCmdAsync(`rm -rf ${LOG_DIR}`, () => {}); - }, ANIMATION_DELAYS.INPUT_FOCUS); - } else { - appendConsole('Console cleared', 'info'); - } - }); - els.copyConsole.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - // Start animation (don't await - let it run in background) - animateButton(e.currentTarget); - // Copy immediately (don't wait for animation) - copyConsoleLogs(); - }); - els.refreshStatus.addEventListener('click', async (e) => { - // Disable button during refresh to prevent double-clicks - const btn = e.target; - btn.disabled = true; - btn.style.opacity = '0.5'; - - try { - // Do a comprehensive refresh: re-check root access, then refresh status - // No console messages, no scrolling - just quiet refresh - await checkRootAccess(true); // Silent mode - no console output - await refreshStatus(); - await readBootFile(true); // Also refresh boot toggle status (silent mode) - await readDozeOffFile(true); // Also refresh Android optimizations toggle status (silent mode) - updateModuleStatus(); // Update module status in module.prop - - // Pre-fetch interfaces in background (non-blocking) to update cache - // This prevents lag when opening popups later - // Use setTimeout to ensure it's truly non-blocking - if(rootAccessConfirmed) { - setTimeout(() => { - // Fetch hotspot interfaces in background (force refresh + background only) - if(window.HotspotFeature && HotspotFeature.fetchInterfaces) { - HotspotFeature.fetchInterfaces(true, true).catch(() => { - // Silently fail - cache will be used if fetch fails - }); - } - // Fetch forward-nat interfaces in background (force refresh + background only) - if(window.ForwardNatFeature && ForwardNatFeature.fetchInterfaces) { - ForwardNatFeature.fetchInterfaces(true, true).catch(() => { - // Silently fail - cache will be used if fetch fails - }); - } - }, ANIMATION_DELAYS.INPUT_FOCUS); // Small delay to ensure UI updates first - } - } catch(error) { - // Silently handle errors - refresh should never fail loudly - console.error('Refresh error:', error); - } finally { - // Re-enable button - btn.disabled = false; - btn.style.opacity = ''; - } - }); - els.bootToggle.addEventListener('change', () => writeBootFile(els.bootToggle.checked ? 1 : 0)); - els.debugToggle.addEventListener('change', () => { - debugModeActive = els.debugToggle.checked; - saveDebugMode(); - updateDebugIndicator(); - if(debugModeActive){ - appendConsole('Debug mode enabled. All scripts will now log to /data/logs/ubuntu-chroot/logs', 'warn'); - } else { - appendConsole('Debug mode disabled', 'info'); - } - }); - els.androidOptimizeToggle.addEventListener('change', () => writeDozeOffFile(els.androidOptimizeToggle.checked ? 1 : 0)); - - // Settings popup event handlers - els.settingsBtn.addEventListener('click', () => openSettingsPopup()); - els.closePopup.addEventListener('click', () => closeSettingsPopup()); - PopupManager.setupClickOutside(els.settingsPopup, closeSettingsPopup); - els.saveScript.addEventListener('click', async (e) => { - e.preventDefault(); - e.stopPropagation(); - await animateButton(e.currentTarget); - savePostExecScript(); - }); - els.clearScript.addEventListener('click', async (e) => { - e.preventDefault(); - e.stopPropagation(); - await animateButton(e.currentTarget); - clearPostExecScript(); - }); - els.updateBtn.addEventListener('click', () => updateChroot()); - els.backupBtn.addEventListener('click', () => { - if(window.BackupRestoreFeature) BackupRestoreFeature.backupChroot(); - }); - els.restoreBtn.addEventListener('click', () => { - if(window.BackupRestoreFeature) BackupRestoreFeature.restoreChroot(); - }); - els.uninstallBtn.addEventListener('click', () => { - if(window.UninstallFeature) UninstallFeature.uninstallChroot(); - }); - - // Experimental features event handlers - const migrateSparseBtn = document.getElementById('migrate-sparse-btn'); - if(migrateSparseBtn){ - migrateSparseBtn.addEventListener('click', () => { - if(window.MigrateFeature) MigrateFeature.migrateToSparseImage(); - }); - } - - // Sparse settings event handlers - if(els.sparseSettingsBtn){ - els.sparseSettingsBtn.addEventListener('click', () => openSparseSettingsPopup()); - } - if(els.closeSparsePopup){ - els.closeSparsePopup.addEventListener('click', () => closeSparseSettingsPopup()); - } - PopupManager.setupClickOutside(els.sparseSettingsPopup, closeSparseSettingsPopup); - if(els.trimSparseBtn){ - els.trimSparseBtn.addEventListener('click', () => { - if(window.ResizeFeature) ResizeFeature.trimSparseImage(); - }); - } - if(els.resizeSparseBtn){ - els.resizeSparseBtn.addEventListener('click', () => { - if(window.ResizeFeature) ResizeFeature.resizeSparseImage(); - }); - } - - // Hotspot event handlers - if(els.hotspotBtn) { - els.hotspotBtn.addEventListener('click', () => openHotspotPopup()); - } - if(els.closeHotspotPopup) { - els.closeHotspotPopup.addEventListener('click', () => closeHotspotPopup()); - } - if(els.hotspotPopup) { - PopupManager.setupClickOutside(els.hotspotPopup, closeHotspotPopup); - } - if(els.startHotspotBtn) { - els.startHotspotBtn.addEventListener('click', () => startHotspot()); - } - if(els.stopHotspotBtn) { - els.stopHotspotBtn.addEventListener('click', () => stopHotspot()); - } - if(els.dismissHotspotWarning) { - els.dismissHotspotWarning.addEventListener('click', () => dismissHotspotWarning()); - } - - // Forward NAT event handlers - if(els.forwardNatBtn) { - els.forwardNatBtn.addEventListener('click', () => openForwardNatPopup()); - } - if(els.closeForwardNatPopup) { - els.closeForwardNatPopup.addEventListener('click', () => closeForwardNatPopup()); - } - if(els.forwardNatPopup) { - PopupManager.setupClickOutside(els.forwardNatPopup, closeForwardNatPopup); - } - if(els.startForwardingBtn) { - els.startForwardingBtn.addEventListener('click', () => startForwarding()); - } - if(els.stopForwardingBtn) { - els.stopForwardingBtn.addEventListener('click', () => stopForwarding()); - } - - // Password toggle functionality - use data attribute instead of innerHTML replacement - const togglePasswordBtn = document.getElementById('toggle-password'); - if(togglePasswordBtn){ - const passwordInput = document.getElementById('hotspot-password'); - const iconEye = togglePasswordBtn.querySelector('svg'); - - // Store original SVG content - const eyeOpenSvg = ``; - const eyeClosedSvg = ``; - - togglePasswordBtn.addEventListener('click', () => { - if(passwordInput.type === 'password'){ - passwordInput.type = 'text'; - iconEye.innerHTML = eyeClosedSvg; - } else { - passwordInput.type = 'password'; - iconEye.innerHTML = eyeOpenSvg; - } - }); - } - if(els.dismissHotspotWarning) { - els.dismissHotspotWarning.addEventListener('click', () => dismissHotspotWarning()); - } - - // Band change updates channel limits and saves settings - const hotspotBandEl = document.getElementById('hotspot-band'); - if(hotspotBandEl) { - hotspotBandEl.addEventListener('change', function() { - const channelSelect = document.getElementById('hotspot-channel'); - const newBand = this.value; - - // Update channel options based on new band (wait for completion) - updateChannelLimits(newBand).then(() => { - // Save settings when band changes (after channel options are updated) - if(window.HotspotFeature && window.HotspotFeature.saveHotspotSettings) { - window.HotspotFeature.saveHotspotSettings(); - } - }); - }); - } - - // Save settings when channel changes - const hotspotChannelEl = document.getElementById('hotspot-channel'); - if(hotspotChannelEl) { - hotspotChannelEl.addEventListener('change', function() { - if(window.HotspotFeature && window.HotspotFeature.saveHotspotSettings) { - window.HotspotFeature.saveHotspotSettings(); - } - }); - } - - // Auto-save settings when SSID, password, or interface changes - const hotspotSsidEl = document.getElementById('hotspot-ssid'); - const hotspotPasswordEl = document.getElementById('hotspot-password'); - const hotspotIfaceEl = document.getElementById('hotspot-iface'); - - if(hotspotSsidEl) { - hotspotSsidEl.addEventListener('input', function() { - if(window.HotspotFeature && window.HotspotFeature.saveHotspotSettings) { - window.HotspotFeature.saveHotspotSettings(); - } - }); - } - - if(hotspotPasswordEl) { - hotspotPasswordEl.addEventListener('input', function() { - if(window.HotspotFeature && window.HotspotFeature.saveHotspotSettings) { - window.HotspotFeature.saveHotspotSettings(); - } - }); - } - - if(hotspotIfaceEl) { - hotspotIfaceEl.addEventListener('change', function() { - if(window.HotspotFeature && window.HotspotFeature.saveHotspotSettings) { - window.HotspotFeature.saveHotspotSettings(); - } - }); - } - - // Hotspot band change handler - // Update channel options based on band value (can be passed as parameter or read from dropdown) - // Returns a promise that resolves when options are populated (for race condition prevention) - function updateChannelLimits(bandValue = null){ - return new Promise((resolve) => { - const bandSelect = document.getElementById('hotspot-band'); - const channelSelect = document.getElementById('hotspot-channel'); - - if(!bandSelect || !channelSelect) { - resolve(); - return; - } - - // Use provided band value, or read from dropdown - const band = bandValue !== null ? bandValue : bandSelect.value; - - // Clear existing options - channelSelect.innerHTML = ''; - - // Get channels from constants - const channels = band === '5' - ? APP_CONSTANTS.HOTSPOT.CHANNELS_5GHZ - : APP_CONSTANTS.HOTSPOT.CHANNELS_2_4GHZ; - - // Add options - channels.forEach(ch => { - const option = document.createElement('option'); - option.value = String(ch); - option.textContent = String(ch); - channelSelect.appendChild(option); - }); - - // Set default value (will be overridden if saved channel exists) - const defaultChannel = band === '5' - ? APP_CONSTANTS.HOTSPOT.DEFAULT_CHANNEL_5GHZ - : APP_CONSTANTS.HOTSPOT.DEFAULT_CHANNEL_2_4GHZ; - channelSelect.value = defaultChannel; - - // Use requestAnimationFrame to ensure DOM is updated before resolving - requestAnimationFrame(() => { - setTimeout(resolve, ANIMATION_DELAYS.CHANNEL_UPDATE_DELAY); - }); - }); - } - - // Hotspot settings persistence - DELEGATED TO HotspotFeature MODULE - // Functions removed - use window.HotspotFeature.saveHotspotSettings() and loadHotspotSettings() instead - - // ============================================================================ - // INITIALIZE FEATURE MODULES - // ============================================================================ - function initFeatureModules() { - // Create dependency objects for mutable values (using refs to sync) - activeCommandIdRef = { - get value() { return activeCommandId; }, - set value(v) { activeCommandId = v; } - }; - rootAccessConfirmedRef = { - get value() { return rootAccessConfirmed; }, - set value(v) { rootAccessConfirmed = v; } - }; - hotspotActiveRef = { - get value() { return hotspotActive; }, - set value(v) { hotspotActive = v; } - }; - forwardingActiveRef = { - get value() { return forwardingActive; }, - set value(v) { forwardingActive = v; } - }; - sparseMigratedRef = { - get value() { return sparseMigrated; }, - set value(v) { sparseMigrated = v; } - }; - - // Common dependencies for all features - const commonDeps = { - // Mutable state (passed as refs) - activeCommandId: activeCommandIdRef, - rootAccessConfirmed: rootAccessConfirmedRef, - hotspotActive: hotspotActiveRef, - forwardingActive: forwardingActiveRef, - sparseMigrated: sparseMigratedRef, - - // Constants - CHROOT_DIR, - PATH_CHROOT_SH, - HOTSPOT_SCRIPT, - FORWARD_NAT_SCRIPT, - OTA_UPDATER, - - // Utilities - Storage, - StateManager, - ButtonState, - ProgressIndicator, - PopupManager, - DialogManager, - ANIMATION_DELAYS, - - // Functions - appendConsole, - runCmdSync, - runCmdAsync, - withCommandGuard, - disableAllActions, - disableSettingsPopup, - refreshStatus, - updateStatus, - updateModuleStatus, - checkForwardNatRunning, - scrollConsoleToBottom, - ensureChrootStarted, - ensureChrootStopped, - prepareActionExecution, - forceScrollToBottom, - forceScrollAfterDOMUpdate, - validateCommandExecution, - executeCommandWithProgress, - els - }; - - // Initialize Forward NAT feature - if(window.ForwardNatFeature) { - ForwardNatFeature.init({ - ...commonDeps, - forwardingActive: forwardingActiveRef, - loadForwardingStatus: () => { forwardingActiveRef.value = StateManager.get('forwarding'); }, - saveForwardingStatus: () => { StateManager.set('forwarding', forwardingActiveRef.value); } - }); - } - - // Initialize Hotspot feature - if(window.HotspotFeature) { - HotspotFeature.init({ - ...commonDeps, - hotspotActive: hotspotActiveRef, - loadHotspotStatus: () => { hotspotActiveRef.value = StateManager.get('hotspot'); }, - saveHotspotStatus: () => { StateManager.set('hotspot', hotspotActiveRef.value); }, - FORWARD_NAT_SCRIPT - }); - } - - // Initialize Backup/Restore feature - if(window.BackupRestoreFeature) { - BackupRestoreFeature.init({ - ...commonDeps, - showFilePickerDialog, - showConfirmDialog, - closeSettingsPopup - }); - } - - // Initialize Uninstall feature - if(window.UninstallFeature) { - UninstallFeature.init({ - ...commonDeps, - showConfirmDialog, - closeSettingsPopup - }); - } - - // Initialize Migrate feature - if(window.MigrateFeature) { - MigrateFeature.init({ - ...commonDeps, - showSizeSelectionDialog, - showConfirmDialog, - closeSettingsPopup, - updateStatus - }); - } - - // Initialize Stop Network Services feature - if(window.StopNetServices) { - StopNetServices.init({ - ...commonDeps, - checkAp0Interface, - checkForwardNatRunning - }); - } - - // Initialize Resize feature - if(window.ResizeFeature) { - ResizeFeature.init({ - ...commonDeps, - sparseMigrated: sparseMigratedRef, - showSizeSelectionDialog, - showConfirmDialog, - closeSettingsPopup, - updateSparseInfo - }); - } - - } - - // init - initTheme(); - loadConsoleLogs(); // Restore previous console logs - // Don't load hotspot settings here - will be loaded when popup opens (after interfaces are populated) - loadHotspotStatus(); // Load hotspot status (will be synced with actual state in refreshStatus) - loadForwardingStatus(); // Load forwarding status (will be synced with actual state in refreshStatus) - loadDebugMode(); // Load debug mode status - readDozeOffFile(true).catch(() => {}); // Load Android optimizations setting (silent mode) - - // Initialize channel options on page load based on saved settings - function initializeChannelOptions() { - const bandSelect = document.getElementById('hotspot-band'); - const channelSelect = document.getElementById('hotspot-channel'); - - if(!bandSelect || !channelSelect) return; - - // Get saved settings to determine which band to use - const savedSettings = Storage.getJSON('chroot_hotspot_settings'); - const defaultBand = APP_CONSTANTS.HOTSPOT.DEFAULT_BAND; - const defaultChannel2_4 = APP_CONSTANTS.HOTSPOT.DEFAULT_CHANNEL_2_4GHZ; - const defaultChannel5 = APP_CONSTANTS.HOTSPOT.DEFAULT_CHANNEL_5GHZ; - const band = savedSettings && savedSettings.band ? savedSettings.band : defaultBand; - - // Set band value first - bandSelect.value = band; - - // Populate channel options based on saved band (use promise to prevent race condition) - updateChannelLimits(band).then(() => { - // Set channel value if saved (after options are populated) - if(savedSettings && savedSettings.channel && channelSelect) { - const savedChannel = String(savedSettings.channel); - const channelExists = Array.from(channelSelect.options).some(opt => opt.value === savedChannel); - if(channelExists) { - channelSelect.value = savedChannel; - } else { - // Channel doesn't exist for this band, use default - channelSelect.value = band === '5' ? defaultChannel5 : defaultChannel2_4; - } - } else if(channelSelect) { - // No saved channel, use default - channelSelect.value = band === '5' ? defaultChannel5 : defaultChannel2_4; - } - }); - } - - // Initialize channel options on page load - initializeChannelOptions(); - - // Simple fix for stuck buttons on touch devices: blur on touchend - // Store handler reference for cleanup to prevent memory leaks - const touchEndHandler = (e) => { - if(e.target && e.target.classList && e.target.classList.contains('btn-pressed')) { - e.target.blur(); - e.target.classList.remove('btn-pressed', 'btn-released'); - } - }; - document.addEventListener('touchend', touchEndHandler, { passive: true }); - - // Initialize console scroll listener - if(els.console) { - els.console.addEventListener('scroll', () => { - LogBuffer.handleUserScroll(); - }, { passive: true }); - } - - // Store cleanup function for potential future use (e.g., page unload) - window._chrootUICleanup = () => { - document.removeEventListener('touchend', touchEndHandler); - }; - - initExperimentalFeatures(); // Initialize experimental features - initFeatureModules(); // Initialize feature modules - - /** - * Hide loading screen with fade-out animation - */ - function hideLoadingScreen() { - const screen = els.loadingScreen; - if(screen && !screen.classList.contains('hidden')) { - screen.classList.add('hidden'); - setTimeout(() => screen && (screen.style.display = 'none'), 300); - } - } - - // Check if first load and hide loading screen if not - const isFirstLoad = !sessionStorage.getItem('chroot_ui_loaded'); - if(!isFirstLoad && els.loadingScreen) { - els.loadingScreen.style.display = 'none'; - } - - // Initialize app - setTimeout(async ()=>{ - try { - await checkRootAccess(); - await refreshStatus(); - readBootFile(false).catch(() => {}); - readDozeOffFile(false).catch(() => {}); - updateModuleStatus(); // Update module status in module.prop on initial load - - if(isFirstLoad) { - hideLoadingScreen(); - sessionStorage.setItem('chroot_ui_loaded', 'true'); - } - } catch(e) { - appendConsole(`Initialization error: ${e.message}`, 'err'); - if(isFirstLoad) { - hideLoadingScreen(); - sessionStorage.setItem('chroot_ui_loaded', 'true'); - } - } - }, ANIMATION_DELAYS.INIT_DELAY); - - // Export some helpers for debug and expose constants to feature modules - window.chrootUI = { refreshStatus, doAction, appendConsole }; - window.APP_CONSTANTS = APP_CONSTANTS; // Expose constants for feature modules - window.updateChannelLimits = updateChannelLimits; // Expose for hotspot feature -})(); diff --git a/webroot/assets/command-executor.js b/webroot/assets/command-executor.js deleted file mode 100644 index ea561d9..0000000 --- a/webroot/assets/command-executor.js +++ /dev/null @@ -1,133 +0,0 @@ -// This entire crap is AI generated, don't blame me for the mess - -class CommandExecutor { - constructor() { - const scriptTag = document.querySelector('script[src*="command-executor.js"]'); - this.assetsPath = scriptTag ? scriptTag.src.replace(/\/command-executor\.js$/, '') : './assets'; - this.execMethod = this.detectExecutionMethod(); - this.runningCommands = new Map(); - } - - detectExecutionMethod() { - if (typeof ksu !== 'undefined' && ksu.exec) { - return 'ksu'; - } else if (window.SULib) { - return 'sulib'; - } - return 'none'; - } - - /** - * Execute a command with real-time output streaming - * @param {string} command - Command to execute - * @param {boolean} asRoot - Run as root - * @param {Object} callbacks - { onOutput, onError, onComplete } - * @returns {string} commandId - Unique ID for this command - */ - executeAsync(command, asRoot = true, callbacks = {}) { - const commandId = `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const fullCommand = asRoot ? `su -c "${command}"` : command; - - const { onOutput, onError, onComplete } = callbacks; - - this.runningCommands.set(commandId, { command: fullCommand, startTime: Date.now() }); - - if (this.execMethod === 'ksu') { - const callback = `ksu_callback_${commandId}`; - window[callback] = (exitCode, stdout, stderr) => { - delete window[callback]; - this.runningCommands.delete(commandId); - - if (exitCode === 0) { - if (stdout && onOutput) onOutput(stdout); - if (onComplete) onComplete({ success: true, exitCode, output: stdout }); - } else { - // --- FIXED --- - // Combine stdout and stderr for a more informative error message, - // as many scripts write errors to stdout before exiting. - const combinedOutput = [stdout, stderr].filter(Boolean).join('\n'); - const errorMessage = combinedOutput.trim() || `exit:${exitCode}`; - - if (errorMessage && onError) onError(errorMessage); - if (onComplete) onComplete({ success: false, exitCode, error: errorMessage }); - } - }; - - try { - ksu.exec(fullCommand, '{}', callback); - if (onOutput) onOutput(`[Executing: ${command}]\n`); - } catch (e) { - // Clean up on synchronous error - if (window[callback]) delete window[callback]; - this.runningCommands.delete(commandId); - if (onError) onError(String(e)); - if (onComplete) onComplete({ success: false, error: String(e) }); - } - } else if (this.execMethod === 'sulib') { - try { - if (onOutput) onOutput(`[Executing: ${command}]\n`); - - window.SULib.exec(fullCommand, (result) => { - this.runningCommands.delete(commandId); - - if (result.success) { - if (result.output && onOutput) onOutput(result.output); - if (onComplete) onComplete({ success: true, output: result.output }); - } else { - // --- FIXED --- - // Combine output and error for a more informative message on failure. - const combinedOutput = [result.output, result.error].filter(Boolean).join('\n'); - const errorMessage = combinedOutput.trim() || `Command failed with exit code ${result.exitCode || 'unknown'}`; - - if (errorMessage && onError) onError(errorMessage); - if (onComplete) onComplete({ success: false, error: errorMessage }); - } - }); - } catch (e) { - // Clean up on synchronous error - this.runningCommands.delete(commandId); - if (onError) onError(String(e)); - if (onComplete) onComplete({ success: false, error: String(e) }); - } - } else { - this.runningCommands.delete(commandId); - const errorMsg = 'No root execution method available (KernelSU or libsuperuser not detected).'; - if (onError) onError(errorMsg); - if (onComplete) onComplete({ success: false, error: errorMsg }); - } - - return commandId; - } - - /** - * Legacy execute method for backward compatibility - */ - async execute(command, asRoot = true) { - return new Promise((resolve, reject) => { - this.executeAsync(command, asRoot, { - onComplete: (result) => { - if (result.success) { - resolve(result.output || ''); - } else { - reject(new Error(result.error || 'Command failed')); - } - } - }); - }); - } - - isCommandRunning(commandId) { - return this.runningCommands.has(commandId); - } - - getRunningCommands() { - return Array.from(this.runningCommands.entries()).map(([id, info]) => ({ - id, - ...info, - duration: Date.now() - info.startTime - })); - } -} - -// Expose a tolerant global instance -window.cmdExec = new CommandExecutor(); diff --git a/webroot/assets/features/backup-restore.js b/webroot/assets/features/backup-restore.js deleted file mode 100644 index 9d0c15c..0000000 --- a/webroot/assets/features/backup-restore.js +++ /dev/null @@ -1,212 +0,0 @@ -// Backup and Restore Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - async function backupChroot() { - const { - activeCommandId, rootAccessConfirmed, appendConsole, showFilePickerDialog, showConfirmDialog, - closeSettingsPopup, ANIMATION_DELAYS, PATH_CHROOT_SH, ProgressIndicator, - disableAllActions, disableSettingsPopup, refreshStatus, runCmdAsync, - updateStatus, updateModuleStatus, ensureChrootStopped, prepareActionExecution, executeCommandWithProgress, els - } = dependencies; - - if(activeCommandId.value) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - const backupPath = await showFilePickerDialog( - 'Backup Chroot Environment', - 'Select where to save the backup file.\n\nThe chroot will be stopped during backup if it\'s currently running.', - '/sdcard', - `chroot-backup-${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.tar.gz` - ); - - if(!backupPath) return; - - const confirmed = await showConfirmDialog( - 'Backup Chroot Environment', - `This will create a compressed backup of your chroot environment.\n\nThe chroot will be stopped during backup if it's currently running.\n\nBackup location: ${backupPath}\n\nContinue?`, - 'Backup', - 'Cancel' - ); - - if(!confirmed) return; - - closeSettingsPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Stop chroot if running (uses centralized flow internally) - const isRunning = els.statusText && els.statusText.textContent.trim() === 'running'; - if(isRunning) { - const stopped = await ensureChrootStopped(); - if(!stopped) { - appendConsole('✗ Failed to stop chroot - backup aborted', 'err'); - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - } - - // Update status first, then use centralized flow - updateStatus('backing up'); - - // Now use centralized flow for backup action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Starting Chroot Backup', - 'Backing up chroot', - 'dots' - ); - - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${PATH_CHROOT_SH} backup --webui "${backupPath}"`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: (result) => { - appendConsole('✓ Backup completed successfully', 'success'); - appendConsole(`Saved to: ${backupPath}`, 'info'); - appendConsole('━━━ Backup Complete ━━━', 'success'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - onError: (result) => { - appendConsole('✗ Backup failed', 'err'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - - async function restoreChroot() { - const { - activeCommandId, rootAccessConfirmed, appendConsole, showFilePickerDialog, - showConfirmDialog, closeSettingsPopup, ANIMATION_DELAYS, PATH_CHROOT_SH, - ProgressIndicator, disableAllActions, disableSettingsPopup, updateStatus, updateModuleStatus, - refreshStatus, runCmdAsync, ensureChrootStopped, prepareActionExecution, executeCommandWithProgress, els - } = dependencies; - - if(activeCommandId.value) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - if(!rootAccessConfirmed.value) { - appendConsole('Cannot restore chroot: root access not available', 'err'); - return; - } - - const backupPath = await showFilePickerDialog( - 'Restore Chroot Environment', - 'Select the backup file to restore from.\n\nWARNING: This will permanently delete your current chroot environment!', - '/sdcard', - '', - true - ); - - if(!backupPath) return; - - const confirmed = await showConfirmDialog( - 'Restore Chroot Environment', - `⚠️ WARNING: This will permanently delete your current chroot environment and replace it with the backup!\n\nAll current data in the chroot will be lost.\n\nBackup file: ${backupPath}\n\nThis action cannot be undone. Continue?`, - 'Restore', - 'Cancel' - ); - - if(!confirmed) return; - - closeSettingsPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Stop chroot if running (uses centralized flow internally) - const isRunning = els.statusText && els.statusText.textContent.trim() === 'running'; - if(isRunning) { - const stopped = await ensureChrootStopped(); - if(!stopped) { - appendConsole('✗ Failed to stop chroot - restore aborted', 'err'); - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - } - - // Update status first, then use centralized flow - updateStatus('restoring'); - - // Now use centralized flow for restore action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Starting Chroot Restore', - 'Restoring chroot', - 'dots' - ); - - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${PATH_CHROOT_SH} restore --webui "${backupPath}"`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: (result) => { - appendConsole('✓ Restore completed successfully', 'success'); - appendConsole('The chroot environment has been restored', 'info'); - appendConsole('━━━ Restore Complete ━━━', 'success'); - updateStatus('stopped'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(true); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH * 2); - }, - onError: (result) => { - appendConsole('✗ Restore failed', 'err'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH * 2); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - - window.BackupRestoreFeature = { - init, - backupChroot, - restoreChroot - }; -})(window); - diff --git a/webroot/assets/features/forward-nat.js b/webroot/assets/features/forward-nat.js deleted file mode 100644 index 569d596..0000000 --- a/webroot/assets/features/forward-nat.js +++ /dev/null @@ -1,310 +0,0 @@ -// Forward NAT Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - // This module will be initialized with dependencies from app.js - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - function loadForwardingStatus() { - const { StateManager, forwardingActive } = dependencies; - forwardingActive.value = StateManager.get('forwarding'); - } - - function saveForwardingStatus() { - const { StateManager, forwardingActive } = dependencies; - StateManager.set('forwarding', forwardingActive.value); - } - - function populateInterfaces(interfacesRaw) { - const { els, Storage } = dependencies; - const select = els.forwardNatIface; - select.innerHTML = ''; - - if(interfacesRaw.length === 0) { - const option = document.createElement('option'); - option.value = ''; - option.textContent = 'No interfaces found'; - select.appendChild(option); - select.disabled = true; - return; - } - - interfacesRaw.forEach(ifaceRaw => { - const trimmed = ifaceRaw.trim(); - if(trimmed.length > 0) { - const option = document.createElement('option'); - - if(trimmed.includes(':')) { - const [iface, ip] = trimmed.split(':'); - option.value = iface.trim(); - option.textContent = `${iface.trim()} (${ip.trim()})`; - } else { - option.value = trimmed; - option.textContent = trimmed; - } - - select.appendChild(option); - } - }); - - select.disabled = false; - - const savedIface = Storage.get('chroot_selected_interface'); - if(savedIface) { - const exactMatch = Array.from(select.options).find(opt => opt.value === savedIface); - if(exactMatch) { - select.value = savedIface; - } else if(interfacesRaw.length > 0) { - const firstIface = interfacesRaw[0].trim(); - select.value = firstIface.includes(':') ? firstIface.split(':')[0].trim() : firstIface; - } - } else if(interfacesRaw.length > 0) { - const firstIface = interfacesRaw[0].trim(); - select.value = firstIface.includes(':') ? firstIface.split(':')[0].trim() : firstIface; - } - } - - async function fetchInterfaces(forceRefresh = false, backgroundOnly = false) { - const { rootAccessConfirmed, runCmdSync, FORWARD_NAT_SCRIPT, Storage, appendConsole, els } = dependencies; - - if(!rootAccessConfirmed.value) { - return; - } - - const cached = Storage.getJSON('chroot_forward_nat_interfaces_cache'); - - // Strategy: Show cached data immediately if available, only fetch if cache is empty or forced - // When opening popup: show cache only, NO background refresh (that causes lag!) - // Background refresh only happens on refresh button or pre-fetch - - // If we have cache and not forcing refresh, show it immediately and return - // NO background refresh when opening popup - that's what causes the lag! - if(cached && Array.isArray(cached) && cached.length > 0 && !forceRefresh) { - if(!backgroundOnly) { - populateInterfaces(cached); - } - // Return immediately - don't fetch in background when opening popup - return; - } - - // No cache or force refresh - fetch now (only if cache is empty or forced) - // This should only happen if cache is empty, or when refresh button is clicked - try { - const cmd = `sh ${FORWARD_NAT_SCRIPT} list-iface`; - const out = await runCmdSync(cmd); - const interfacesRaw = String(out || '').trim().split(',').filter(i => i && i.length > 0); - - // Always update cache - Storage.setJSON('chroot_forward_nat_interfaces_cache', interfacesRaw); - - // Only populate UI if not background-only mode - if(!backgroundOnly) { - populateInterfaces(interfacesRaw); - } - } catch(e) { - if(!backgroundOnly) { - appendConsole(`Could not fetch interfaces: ${e.message}`, 'warn'); - // Clear and add error option - els.forwardNatIface.innerHTML = ''; // Clear first - const errorOption = document.createElement('option'); - errorOption.value = ''; - errorOption.textContent = 'Failed to load interfaces'; - els.forwardNatIface.appendChild(errorOption); - els.forwardNatIface.disabled = true; - } - } - } - - function openForwardNatPopup() { - dependencies.PopupManager.open(dependencies.els.forwardNatPopup); - // Only show cached interfaces - NO fetching (that causes lag!) - // Fetch only happens if cache is empty - fetchInterfaces(false, false); - } - - function refreshInterfaces() { - fetchInterfaces(true); // Force refresh - } - - function closeForwardNatPopup() { - dependencies.PopupManager.close(dependencies.els.forwardNatPopup); - } - - async function startForwarding() { - const { - withCommandGuard, els, Storage, ANIMATION_DELAYS, - FORWARD_NAT_SCRIPT, runCmdSync, ProgressIndicator, appendConsole, - disableAllActions, disableSettingsPopup, activeCommandId, refreshStatus, - forwardingActive, saveForwardingStatus, ButtonState, prepareActionExecution, forceScrollAfterDOMUpdate - } = dependencies; - - await withCommandGuard('forwarding-start', async () => { - const iface = els.forwardNatIface.value.trim(); - if(!iface) { - appendConsole('Please select a network interface', 'err'); - return; - } - - Storage.set('chroot_selected_interface', iface); - closeForwardNatPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - - disableAllActions(true); - disableSettingsPopup(true); - - const actionText = `Starting forwarding on ${iface}`; - const { progressLine, interval: progressInterval } = await prepareActionExecution( - actionText, - actionText, - 'spinner' - ); - - activeCommandId.value = 'forwarding-start'; - - const cmd = `sh ${FORWARD_NAT_SCRIPT} -i "${iface}" 2>&1`; - - setTimeout(async () => { - try { - const output = await runCmdSync(cmd); - ProgressIndicator.remove(progressLine, progressInterval); - - if(output) { - const lines = String(output).split('\n'); - lines.forEach(line => { - if(line.trim()) { - appendConsole(line); - } - }); - } - - if(output && (output.includes('Localhost routing active') || output.includes('Gateway:'))) { - appendConsole(`✓ Forwarding started successfully on ${iface}`, 'success'); - forwardingActive.value = true; - saveForwardingStatus(); - ButtonState.setButtonPair(els.startForwardingBtn, els.stopForwardingBtn, true); - } else { - appendConsole(`✗ Failed to start forwarding`, 'err'); - } - - // Force scroll to bottom after completion messages - forceScrollAfterDOMUpdate(); - } catch(error) { - ProgressIndicator.remove(progressLine, progressInterval); - - const errorMsg = String(error.message || error); - const lines = errorMsg.split('\n'); - lines.forEach(line => { - if(line.trim()) { - appendConsole(line, 'err'); - } - }); - - appendConsole(`✗ Forwarding failed to start`, 'err'); - - // Force scroll to bottom after error messages - forceScrollAfterDOMUpdate(); - } finally { - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - } - }, ANIMATION_DELAYS.UI_UPDATE); - }); - } - - async function stopForwarding() { - const { - withCommandGuard, ANIMATION_DELAYS, FORWARD_NAT_SCRIPT, - runCmdAsync, ProgressIndicator, appendConsole, disableAllActions, - disableSettingsPopup, activeCommandId, refreshStatus, forwardingActive, - saveForwardingStatus, ButtonState, prepareActionExecution, forceScrollAfterDOMUpdate, els - } = dependencies; - - await withCommandGuard('forwarding-stop', async () => { - closeForwardNatPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - - disableAllActions(true); - disableSettingsPopup(true); - - const actionText = 'Stopping forwarding'; - const { progressLine, interval: progressInterval } = await prepareActionExecution( - actionText, - actionText, - 'spinner' - ); - - activeCommandId.value = 'forwarding-stop'; - - const cmd = `sh ${FORWARD_NAT_SCRIPT} -k 2>&1`; - - setTimeout(() => { - runCmdAsync(cmd, (result) => { - ProgressIndicator.remove(progressLine, progressInterval); - - // Always clear the state marker, even if there were errors - forwardingActive.value = false; - saveForwardingStatus(); - ButtonState.setButtonPair(els.startForwardingBtn, els.stopForwardingBtn, false); - - if(result.success) { - appendConsole(`✓ Forwarding stopped successfully`, 'success'); - } else { - // Check output for warnings - script now warns instead of exiting - const output = result.output || ''; - if(output.includes('warn') || output.includes('WARN') || output.includes('warning')) { - appendConsole(`⚠ Forwarding cleanup completed with warnings`, 'warn'); - if(output.trim()) { - const lines = output.split('\n'); - lines.forEach(line => { - if(line.trim() && !line.trim().startsWith('[Executing:')) { - appendConsole(line.trim(), 'warn'); - } - }); - } - } else { - appendConsole(`⚠ Forwarding stop completed (some rules may not have existed)`, 'warn'); - if(output.trim()) { - const lines = output.split('\n'); - lines.forEach(line => { - if(line.trim() && !line.trim().startsWith('[Executing:')) { - appendConsole(line.trim()); - } - }); - } - } - } - - // Force scroll to bottom after completion messages - forceScrollAfterDOMUpdate(); - - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }); - }, ANIMATION_DELAYS.UI_UPDATE); - }); - } - - // Export public API - window.ForwardNatFeature = { - init, - loadForwardingStatus, - saveForwardingStatus, - fetchInterfaces, - openForwardNatPopup, - closeForwardNatPopup, - startForwarding, - stopForwarding, - refreshInterfaces - }; -})(window); diff --git a/webroot/assets/features/hotspot.js b/webroot/assets/features/hotspot.js deleted file mode 100644 index c2a84b5..0000000 --- a/webroot/assets/features/hotspot.js +++ /dev/null @@ -1,507 +0,0 @@ -// Hotspot Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - function populateInterfaces(interfacesRaw, forceRefresh = false) { - const { Storage } = dependencies; - const select = document.getElementById('hotspot-iface'); - if(!select) return; - - select.innerHTML = ''; - - // Filter out ap0 interface - it should never be shown - const filteredInterfaces = interfacesRaw.filter(ifaceRaw => { - const trimmed = ifaceRaw.trim(); - if(trimmed.includes(':')) { - const [iface] = trimmed.split(':'); - return iface.trim() !== 'ap0'; - } - return trimmed !== 'ap0'; - }); - - if(filteredInterfaces.length === 0) { - const option = document.createElement('option'); - option.value = ''; - option.textContent = 'No interfaces found'; - select.appendChild(option); - select.disabled = true; - return; - } - - filteredInterfaces.forEach(ifaceRaw => { - const trimmed = ifaceRaw.trim(); - if(trimmed.length > 0) { - const option = document.createElement('option'); - - if(trimmed.includes(':')) { - const [iface, ip] = trimmed.split(':'); - option.value = iface.trim(); - option.textContent = `${iface.trim()} (${ip.trim()})`; - } else { - option.value = trimmed; - option.textContent = trimmed; - } - - select.appendChild(option); - } - }); - - select.disabled = false; - - // Try to restore previously selected interface or use saved hotspot settings - const savedHotspotIface = Storage.get('chroot_hotspot_iface') || Storage.get('chroot_selected_interface'); - if(savedHotspotIface) { - const exactMatch = Array.from(select.options).find(opt => opt.value === savedHotspotIface); - if(exactMatch) { - select.value = savedHotspotIface; - } else if(interfacesRaw.length > 0) { - const firstIface = interfacesRaw[0].trim(); - select.value = firstIface.includes(':') ? firstIface.split(':')[0].trim() : firstIface; - } - } else if(interfacesRaw.length > 0) { - // Default to first interface or 'wlan0' if available - const wlan0Option = Array.from(select.options).find(opt => opt.value === 'wlan0'); - if(wlan0Option) { - select.value = 'wlan0'; - } else { - const firstIface = interfacesRaw[0].trim(); - select.value = firstIface.includes(':') ? firstIface.split(':')[0].trim() : firstIface; - } - } - } - - async function fetchInterfaces(forceRefresh = false, backgroundOnly = false) { - const { rootAccessConfirmed, runCmdSync, FORWARD_NAT_SCRIPT, appendConsole, Storage } = dependencies; - - if(!rootAccessConfirmed.value) { - return; - } - - const cached = Storage.getJSON('chroot_hotspot_interfaces_cache'); - const select = document.getElementById('hotspot-iface'); - - // Strategy: Show cached data immediately if available, only fetch if cache is empty or forced - // When opening popup: show cache only, NO background refresh (that causes lag!) - // Background refresh only happens on refresh button or pre-fetch - - // If we have cache and not forcing refresh, show it immediately and return - // NO background refresh when opening popup - that's what causes the lag! - if(cached && Array.isArray(cached) && cached.length > 0 && !forceRefresh) { - if(!backgroundOnly && select) { - // Filter out ap0 from cached data (in case old cache contains it) - const filteredCached = cached.filter(ifaceRaw => { - const trimmed = ifaceRaw.trim(); - if(trimmed.includes(':')) { - const [iface] = trimmed.split(':'); - return iface.trim() !== 'ap0'; - } - return trimmed !== 'ap0'; - }); - populateInterfaces(filteredCached); - } - // Return immediately - don't fetch in background when opening popup - return; - } - - // No cache or force refresh - fetch now (only if cache is empty or forced) - // This should only happen if cache is empty, or when refresh button is clicked - try { - const cmd = `sh ${FORWARD_NAT_SCRIPT} list-all-iface`; - const out = await runCmdSync(cmd); - const interfacesRaw = String(out || '').trim().split(',').filter(i => i && i.length > 0); - - // Filter out ap0 before caching - const filteredForCache = interfacesRaw.filter(ifaceRaw => { - const trimmed = ifaceRaw.trim(); - if(trimmed.includes(':')) { - const [iface] = trimmed.split(':'); - return iface.trim() !== 'ap0'; - } - return trimmed !== 'ap0'; - }); - - // Always update cache (without ap0) - Storage.setJSON('chroot_hotspot_interfaces_cache', filteredForCache); - - // Only populate UI if not background-only mode - if(!backgroundOnly && select) { - populateInterfaces(filteredForCache); - } - } catch(e) { - if(!backgroundOnly) { - appendConsole(`Could not fetch interfaces: ${e.message}`, 'warn'); - if(select) { - // Clear and add error option - select.innerHTML = ''; // Clear first - const errorOption = document.createElement('option'); - errorOption.value = ''; - errorOption.textContent = 'Failed to load interfaces'; - select.appendChild(errorOption); - select.disabled = true; - } - } - } - } - - async function openHotspotPopup() { - showHotspotWarning(); - - // Load settings BEFORE opening popup to prevent visible flicker - // First ensure interfaces are populated (use cache, no fetch) - await fetchInterfaces(false, false); - - // Load settings and wait for completion before popup becomes visible - // This ensures all values (especially channel) are set correctly before user sees the popup - await loadHotspotSettings(); - - // Now open the popup - all values should already be correct, no flicker - dependencies.PopupManager.open(dependencies.els.hotspotPopup); - } - - function closeHotspotPopup() { - dependencies.PopupManager.close(dependencies.els.hotspotPopup); - } - - function showHotspotWarning() { - const { els, Storage } = dependencies; - if(!els.hotspotWarning) return; - - const dismissed = Storage.getBoolean('hotspot_warning_dismissed'); - if(dismissed) { - els.hotspotWarning.classList.add('hidden'); - } else { - els.hotspotWarning.classList.remove('hidden'); - } - } - - function dismissHotspotWarning() { - const { els, Storage } = dependencies; - if(!els.hotspotWarning) return; - - els.hotspotWarning.classList.add('hidden'); - Storage.set('hotspot_warning_dismissed', true); - } - - function saveHotspotSettings() { - const { Storage } = dependencies; - const ifaceEl = document.getElementById('hotspot-iface'); - const ssidEl = document.getElementById('hotspot-ssid'); - const passwordEl = document.getElementById('hotspot-password'); - const bandEl = document.getElementById('hotspot-band'); - const channelEl = document.getElementById('hotspot-channel'); - - if(!ifaceEl || !ssidEl || !passwordEl || !bandEl || !channelEl) { - return; // Elements not ready yet - } - - const iface = ifaceEl.value; - const settings = { - iface: iface || '', - ssid: ssidEl.value || '', - password: passwordEl.value || '', - band: bandEl.value || '2', - channel: channelEl.value || '6' - }; - - Storage.setJSON('chroot_hotspot_settings', settings); - // Also save interface separately for easier access - if(iface) Storage.set('chroot_hotspot_iface', iface); - } - - async function loadHotspotSettings() { - const { Storage } = dependencies; - const settings = Storage.getJSON('chroot_hotspot_settings'); - - const ifaceSelect = document.getElementById('hotspot-iface'); - const ssidEl = document.getElementById('hotspot-ssid'); - const passwordEl = document.getElementById('hotspot-password'); - const bandEl = document.getElementById('hotspot-band'); - const channelEl = document.getElementById('hotspot-channel'); - - if(!ifaceSelect || !ssidEl || !passwordEl || !bandEl || !channelEl) { - return; // Elements not ready yet - } - - // Temporarily disable auto-save during load to prevent conflicts - const originalSave = window.HotspotFeature?.saveHotspotSettings; - let isLoading = true; - if(window.HotspotFeature && originalSave) { - window.HotspotFeature.saveHotspotSettings = function() { - if(!isLoading) { - originalSave.call(this); - } - }; - } - - if(settings) { - // Load SSID - if(settings.ssid) { - ssidEl.value = settings.ssid; - } - - // Load password - if(settings.password) { - passwordEl.value = settings.password; - } - - // Load band and channel - Use promise-based update to prevent race condition - const constants = window.APP_CONSTANTS?.HOTSPOT || {}; - const defaultBand = constants.DEFAULT_BAND || '2'; - const defaultChannel2_4 = constants.DEFAULT_CHANNEL_2_4GHZ || '6'; - const defaultChannel5 = constants.DEFAULT_CHANNEL_5GHZ || '36'; - - const band = settings.band || defaultBand; - const savedChannel = settings.channel ? String(settings.channel) : null; - - // Step 1: Set band value (don't trigger change event) - bandEl.value = band; - - // Step 2: Update channel options and wait for completion (AWAIT to ensure it's done) - if(window.updateChannelLimits) { - await window.updateChannelLimits(band); - // Step 3: Set channel value AFTER options are populated (promise ensures this) - // Only update if the value is different to prevent visible flicker - if(savedChannel && channelEl) { - const channelExists = Array.from(channelEl.options).some(opt => opt.value === savedChannel); - if(channelExists) { - // Only update if different to prevent flicker - if(channelEl.value !== savedChannel) { - channelEl.value = savedChannel; - } - } else { - // Channel doesn't exist for this band, use default - const defaultChannel = band === '5' ? defaultChannel5 : defaultChannel2_4; - if(channelEl.value !== defaultChannel) { - channelEl.value = defaultChannel; - } - } - } else if(channelEl) { - // No saved channel, use default - only update if different - const defaultChannel = band === '5' ? defaultChannel5 : defaultChannel2_4; - if(channelEl.value !== defaultChannel) { - channelEl.value = defaultChannel; - } - } - } else { - // Fallback if updateChannelLimits not available - if(channelEl) { - const targetChannel = savedChannel || (band === '5' ? defaultChannel5 : defaultChannel2_4); - if(channelEl.value !== targetChannel) { - channelEl.value = targetChannel; - } - } - } - - // Load interface (must be done after interfaces are populated) - if(settings.iface && ifaceSelect.options.length > 1) { - const savedOption = Array.from(ifaceSelect.options).find(opt => opt.value === settings.iface); - if(savedOption) { - ifaceSelect.value = settings.iface; - } - } - } else { - // No saved settings - initialize with defaults - const constants = window.APP_CONSTANTS?.HOTSPOT || {}; - const defaultBand = constants.DEFAULT_BAND || '2'; - const defaultChannel2_4 = constants.DEFAULT_CHANNEL_2_4GHZ || '6'; - const defaultChannel5 = constants.DEFAULT_CHANNEL_5GHZ || '36'; - - const currentBand = bandEl.value || defaultBand; - if(window.updateChannelLimits) { - await window.updateChannelLimits(currentBand); - if(channelEl) { - const defaultChannel = currentBand === '5' ? defaultChannel5 : defaultChannel2_4; - // Only update if different to prevent flicker - if(channelEl.value !== defaultChannel) { - channelEl.value = defaultChannel; - } - } - } else if(channelEl) { - const defaultChannel = currentBand === '5' ? defaultChannel5 : defaultChannel2_4; - if(channelEl.value !== defaultChannel) { - channelEl.value = defaultChannel; - } - } - } - - // Re-enable save function - isLoading = false; - if(window.HotspotFeature && originalSave) { - window.HotspotFeature.saveHotspotSettings = originalSave; - } - } - - async function startHotspot() { - const { - withCommandGuard, ANIMATION_DELAYS, HOTSPOT_SCRIPT, - runCmdSync, ProgressIndicator, appendConsole, disableAllActions, - disableSettingsPopup, activeCommandId, refreshStatus, hotspotActive, - saveHotspotStatus, ButtonState, prepareActionExecution, forceScrollAfterDOMUpdate, els - } = dependencies; - - await withCommandGuard('hotspot-start', async () => { - const iface = document.getElementById('hotspot-iface').value.trim(); - const ssid = document.getElementById('hotspot-ssid').value.trim(); - const password = document.getElementById('hotspot-password').value; - const band = document.getElementById('hotspot-band').value; - const channel = document.getElementById('hotspot-channel').value; - - if(!iface || !ssid || !password || !channel) { - appendConsole('All fields are required', 'err'); - return; - } - - const minPasswordLength = window.APP_CONSTANTS?.HOTSPOT?.PASSWORD_MIN_LENGTH || 8; - if(password.length < minPasswordLength) { - appendConsole(`Password must be at least ${minPasswordLength} characters`, 'err'); - return; - } - - saveHotspotSettings(); - - closeHotspotPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - - disableAllActions(true); - disableSettingsPopup(true); - - const actionText = `Starting hotspot '${ssid}'`; - const { progressLine, interval: progressInterval } = await prepareActionExecution( - actionText, - actionText, - 'spinner' - ); - - activeCommandId.value = 'hotspot-start'; - - const cmd = `sh ${HOTSPOT_SCRIPT} -o "${iface}" -s "${ssid}" -p "${password}" -b "${band}" -c "${channel}" 2>&1`; - - setTimeout(async () => { - try { - const output = await runCmdSync(cmd); - ProgressIndicator.remove(progressLine, progressInterval); - - if(output) { - const lines = String(output).split('\n'); - lines.forEach(line => { - if(line.trim()) { - appendConsole(line); - } - }); - } - - if(output && output.includes('AP-ENABLED')) { - appendConsole(`✓ Hotspot started successfully`, 'success'); - hotspotActive.value = true; - saveHotspotStatus(); - ButtonState.setButtonPair(els.startHotspotBtn, els.stopHotspotBtn, true); - } else { - appendConsole(`✗ Failed to start hotspot`, 'err'); - } - - // Force scroll to bottom after completion messages - forceScrollAfterDOMUpdate(); - } catch(error) { - ProgressIndicator.remove(progressLine, progressInterval); - - const errorMsg = String(error.message || error); - const lines = errorMsg.split('\n'); - lines.forEach(line => { - if(line.trim()) { - appendConsole(line, 'err'); - } - }); - - appendConsole(`✗ Hotspot failed to start`, 'err'); - - // Force scroll to bottom after error messages - forceScrollAfterDOMUpdate(); - } finally { - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - } - }, ANIMATION_DELAYS.UI_UPDATE); - }); - } - - async function stopHotspot() { - const { - withCommandGuard, ANIMATION_DELAYS, HOTSPOT_SCRIPT, - runCmdAsync, ProgressIndicator, appendConsole, disableAllActions, - disableSettingsPopup, activeCommandId, refreshStatus, hotspotActive, - saveHotspotStatus, ButtonState, prepareActionExecution, forceScrollAfterDOMUpdate, els - } = dependencies; - - await withCommandGuard('hotspot-stop', async () => { - closeHotspotPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE)); - - disableAllActions(true); - disableSettingsPopup(true); - - const actionText = 'Stopping hotspot'; - const { progressLine, interval: progressInterval } = await prepareActionExecution( - actionText, - actionText, - 'spinner' - ); - - activeCommandId.value = 'hotspot-stop'; - - const cmd = `sh ${HOTSPOT_SCRIPT} -k 2>&1`; - - setTimeout(() => { - runCmdAsync(cmd, (result) => { - ProgressIndicator.remove(progressLine, progressInterval); - - if(result.success) { - appendConsole(`✓ Hotspot stopped successfully`, 'success'); - hotspotActive.value = false; - saveHotspotStatus(); - ButtonState.setButtonPair(els.startHotspotBtn, els.stopHotspotBtn, false); - } else { - appendConsole(`✗ Failed to stop hotspot (exit code: ${result.exitCode || 'unknown'})`, 'err'); - } - - // Force scroll to bottom after completion messages - forceScrollAfterDOMUpdate(); - - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }); - }, ANIMATION_DELAYS.UI_UPDATE); - }); - } - - function refreshInterfaces() { - fetchInterfaces(true); // Force refresh - } - - window.HotspotFeature = { - init, - openHotspotPopup, - closeHotspotPopup, - showHotspotWarning, - dismissHotspotWarning, - saveHotspotSettings, - loadHotspotSettings, - startHotspot, - stopHotspot, - fetchInterfaces, - refreshInterfaces - }; -})(window); - diff --git a/webroot/assets/features/migrate.js b/webroot/assets/features/migrate.js deleted file mode 100644 index 76cadb6..0000000 --- a/webroot/assets/features/migrate.js +++ /dev/null @@ -1,109 +0,0 @@ -// Migrate to Sparse Image Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - async function migrateToSparseImage() { - const { - showSizeSelectionDialog, showConfirmDialog, closeSettingsPopup, - ANIMATION_DELAYS, els, PATH_CHROOT_SH, CHROOT_DIR, appendConsole, - ProgressIndicator, disableAllActions, disableSettingsPopup, activeCommandId, - rootAccessConfirmed, refreshStatus, sparseMigrated, runCmdAsync, updateStatus, updateModuleStatus, ensureChrootStopped, prepareActionExecution, executeCommandWithProgress - } = dependencies; - - const sizeGb = await showSizeSelectionDialog(); - if(!sizeGb) return; - - const confirmed = await showConfirmDialog( - 'Migrate to Sparse Image', - `This will convert your current rootfs to a ${sizeGb}GB sparse ext4 image.\n\n⚠️ IMPORTANT: If your chroot is currently running, it will be stopped automatically.\n\nℹ️ NOTE: Sparse images do not immediately use ${sizeGb}GB of storage. They only consume space as you write data to them, starting small and growing as needed.\n\nWARNING: This process cannot be undone. Make sure you have a backup!\n\nContinue with migration?`, - 'Start Migration', - 'Cancel' - ); - - if(!confirmed) return; - - closeSettingsPopup(); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Stop chroot if running (uses centralized flow internally) - const isRunning = els.statusText.textContent.trim() === 'running'; - if(isRunning) { - const stopped = await ensureChrootStopped(); - if(!stopped) { - appendConsole('✗ Failed to stop chroot - migration aborted', 'err'); - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - } - - // Update status first, then use centralized flow - updateStatus('migrating'); - - // Now use centralized flow for migration action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Starting Sparse Image Migration', - 'Migrating', - 'dots' - ); - - appendConsole(`Target size: ${sizeGb}GB sparse ext4 image`, 'info'); - appendConsole('DO NOT CLOSE THIS WINDOW!', 'warn'); - - proceedToMigration(); - - function proceedToMigration() { - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${CHROOT_DIR}/sparsemgr.sh migrate ${sizeGb}`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: (result) => { - appendConsole('✅ Sparse image migration completed successfully!', 'success'); - appendConsole('Your rootfs has been converted to a sparse image.', 'info'); - appendConsole('━━━ Migration Complete ━━━', 'success'); - sparseMigrated.value = true; - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH * 2); - }, - onError: (result) => { - appendConsole('✗ Sparse image migration failed!', 'err'); - appendConsole('Check the logs above for details.', 'err'); - appendConsole('━━━ Migration Failed ━━━', 'err'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - } - - window.MigrateFeature = { - init, - migrateToSparseImage - }; -})(window); - diff --git a/webroot/assets/features/resize.js b/webroot/assets/features/resize.js deleted file mode 100644 index 7c5c434..0000000 --- a/webroot/assets/features/resize.js +++ /dev/null @@ -1,210 +0,0 @@ -// Resize Sparse Image Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - async function trimSparseImage() { - const { - activeCommandId, rootAccessConfirmed, sparseMigrated, appendConsole, - showConfirmDialog, closeSettingsPopup, els, ANIMATION_DELAYS, PATH_CHROOT_SH, - ProgressIndicator, disableAllActions, disableSettingsPopup, updateSparseInfo, - refreshStatus, runCmdAsync, updateStatus, updateModuleStatus, prepareActionExecution, - executeCommandWithProgress - } = dependencies; - - if(activeCommandId.value) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - if(!rootAccessConfirmed.value) { - appendConsole('Cannot trim sparse image: root access not available', 'err'); - return; - } - - if(!sparseMigrated.value) { - appendConsole('Sparse image not detected - cannot trim', 'err'); - return; - } - - const confirmed = await showConfirmDialog( - 'Trim Sparse Image', - 'This will run fstrim to reclaim unused space in the sparse image.\n\nThe operation may take a few seconds and space reclamation happens gradually. Continue?', - 'Trim', - 'Cancel' - ); - - if(!confirmed) return; - - closeSettingsPopup(); - const sparsePopup = els.sparseSettingsPopup; - if(sparsePopup && sparsePopup.classList.contains('active')) { - sparsePopup.classList.remove('active'); - } - - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Update status first, then use centralized flow - updateStatus('trimming'); - - // Use centralized flow for trim action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Trimming Sparse Image', - 'Trimming sparse image', - 'dots' - ); - - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${PATH_CHROOT_SH} fstrim`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: (result) => { - appendConsole('✓ Sparse image trimmed successfully', 'success'); - appendConsole('Space may be reclaimed after a few minutes', 'info'); - appendConsole('━━━ Trim Complete ━━━', 'success'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - updateSparseInfo(); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - onError: (result) => { - appendConsole('✗ Sparse image trim failed', 'err'); - appendConsole('This may be expected on some Android kernels', 'warn'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - updateSparseInfo(); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - - async function resizeSparseImage() { - const { - activeCommandId, rootAccessConfirmed, appendConsole, showSizeSelectionDialog, - showConfirmDialog, closeSettingsPopup, els, ANIMATION_DELAYS, CHROOT_DIR, - PATH_CHROOT_SH, runCmdSync, ProgressIndicator, disableAllActions, - disableSettingsPopup, updateSparseInfo, refreshStatus, runCmdAsync, updateStatus, - updateModuleStatus, prepareActionExecution, executeCommandWithProgress - } = dependencies; - - if(activeCommandId.value) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - if(!rootAccessConfirmed.value) { - appendConsole('Cannot resize sparse image: root access not available', 'err'); - return; - } - - const newSizeGb = await showSizeSelectionDialog(); - if(!newSizeGb) return; - - let currentAllocatedGb = 'Unknown'; - try { - // Use same method as updateSparseInfo - get visible size (what Android sees) - const apparentSizeCmd = `ls -lh ${CHROOT_DIR}/rootfs.img | tr -s ' ' | cut -d' ' -f5`; - const apparentSizeStr = await runCmdSync(apparentSizeCmd); - const apparentSize = apparentSizeStr.trim(); - // Extract numeric value and unit, remove .0 if present (e.g., "8.0G" -> "8GB", "8G" -> "8GB") - currentAllocatedGb = apparentSize.replace(/\.0G$/, 'GB').replace(/G$/, 'GB'); - } catch(e) { - // Keep as 'Unknown' if we can't determine - } - - const confirmed = await showConfirmDialog( - 'Resize Sparse Image', - `⚠️ EXTREME WARNING: This operation can CORRUPT your filesystem!\n\nYou MUST create a backup before proceeding.\n\nDO NOT close this window or interrupt the process.\n\nCurrent allocated: ${currentAllocatedGb}\nNew size: ${newSizeGb}GB\n\n${parseInt(newSizeGb) > parseInt(currentAllocatedGb) ? 'Operation: GROWING (safer)' : 'Operation: SHRINKING (VERY RISKY)'}\n\nContinue?`, - 'Resize', - 'Cancel' - ); - - if(!confirmed) return; - - closeSettingsPopup(); - const sparsePopup = els.sparseSettingsPopup; - if(sparsePopup && sparsePopup.classList.contains('active')) { - sparsePopup.classList.remove('active'); - } - - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Update status first, then use centralized flow - updateStatus('resizing'); - - // Use centralized flow for resize action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - `Resizing Sparse Image to ${newSizeGb}GB`, - 'Preparing resize operation', - 'dots' - ); - - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${PATH_CHROOT_SH} resize --webui ${newSizeGb}`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: (result) => { - appendConsole('✅ Sparse image resized successfully', 'success'); - appendConsole(`New size: ${newSizeGb}GB`, 'info'); - appendConsole('━━━ Resize Complete ━━━', 'success'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - updateSparseInfo(); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - onError: (result) => { - appendConsole('✗ Sparse image resize failed', 'err'); - appendConsole('Check the logs above for details', 'err'); - appendConsole('━━━ Resize Failed ━━━', 'err'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, true); - updateSparseInfo(); - setTimeout(() => refreshStatus(), ANIMATION_DELAYS.STATUS_REFRESH); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - - window.ResizeFeature = { - init, - trimSparseImage, - resizeSparseImage - }; -})(window); - diff --git a/webroot/assets/features/stop-net-services.js b/webroot/assets/features/stop-net-services.js deleted file mode 100644 index ceb1713..0000000 --- a/webroot/assets/features/stop-net-services.js +++ /dev/null @@ -1,126 +0,0 @@ -// Stop Network Services Feature Module -// Centralized module for stopping hotspot and forward-nat services -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - /** - * Stop all network services (hotspot and forward-nat) - * Called before stopping/restarting chroot or during backup/restore/uninstall/migrate - * @param {Object} options - Configuration options - * @param {Object} options.progressLine - Optional progress indicator line to update - * @param {boolean} options.silent - If true, don't log messages (default: false) - * @returns {Promise} Object with stopped services status - */ - async function stopNetworkServices(options = {}) { - const { - HOTSPOT_SCRIPT, - FORWARD_NAT_SCRIPT, - runCmdAsync, - appendConsole, - ProgressIndicator, - checkAp0Interface, - checkForwardNatRunning, - hotspotActive, - forwardingActive, - StateManager, - hotspotActiveRef, - forwardingActiveRef - } = dependencies; - - const { progressLine = null, silent = false } = options; - const results = { - hotspot: { wasRunning: false, stopped: false }, - forwardNat: { wasRunning: false, stopped: false } - }; - - // Stop hotspot if running - try { - results.hotspot.wasRunning = await checkAp0Interface(); - if(results.hotspot.wasRunning) { - if(progressLine) { - ProgressIndicator.update(progressLine, 'Stopping hotspot first'); - } - if(!silent) { - appendConsole('Stopping hotspot before operation...', 'info'); - } - - await new Promise((resolve) => { - runCmdAsync(`sh ${HOTSPOT_SCRIPT} -k 2>&1`, (result) => { - if(result.success) { - results.hotspot.stopped = true; - if(!silent) { - appendConsole('✓ Hotspot stopped successfully', 'success'); - } - // Update state through StateManager - hotspotActive.value = false; - StateManager.set('hotspot', false); - if(hotspotActiveRef) hotspotActiveRef.value = false; - } else { - if(!silent) { - appendConsole('✗ Failed to stop hotspot, continuing with operation', 'warn'); - } - } - resolve(); - }); - }); - } - } catch(e) { - if(!silent) { - appendConsole('⚠ Could not check hotspot status, proceeding with operation', 'warn'); - } - } - - // Stop forward-nat if running - try { - results.forwardNat.wasRunning = await checkForwardNatRunning(); - if(results.forwardNat.wasRunning) { - if(progressLine) { - ProgressIndicator.update(progressLine, 'Stopping forward NAT first'); - } - if(!silent) { - appendConsole('Stopping forward NAT before operation...', 'info'); - } - - await new Promise((resolve) => { - runCmdAsync(`sh ${FORWARD_NAT_SCRIPT} -k 2>&1`, (result) => { - if(result.success) { - results.forwardNat.stopped = true; - if(!silent) { - appendConsole('✓ Forward NAT stopped successfully', 'success'); - } - // Update state through StateManager - forwardingActive.value = false; - StateManager.set('forwarding', false); - if(forwardingActiveRef) forwardingActiveRef.value = false; - } else { - if(!silent) { - appendConsole('✗ Failed to stop forward NAT, continuing with operation', 'warn'); - } - } - resolve(); - }); - }); - } - } catch(e) { - if(!silent) { - appendConsole('⚠ Could not check forward NAT status, proceeding with operation', 'warn'); - } - } - - return results; - } - - window.StopNetServices = { - init, - stopNetworkServices - }; -})(window); - diff --git a/webroot/assets/features/uninstall.js b/webroot/assets/features/uninstall.js deleted file mode 100644 index eba2ae3..0000000 --- a/webroot/assets/features/uninstall.js +++ /dev/null @@ -1,111 +0,0 @@ -// Uninstall Feature Module -// This entire crap is AI generated, don't blame me for the mess - -(function(window) { - 'use strict'; - - let dependencies = {}; - - function init(deps) { - dependencies = deps; - } - - async function uninstallChroot() { - const { - activeCommandId, rootAccessConfirmed, appendConsole, showConfirmDialog, closeSettingsPopup, - ANIMATION_DELAYS, PATH_CHROOT_SH, ProgressIndicator, disableAllActions, - disableSettingsPopup, updateStatus, refreshStatus, runCmdAsync, ensureChrootStopped, - prepareActionExecution, executeCommandWithProgress, scrollConsoleToBottom, updateModuleStatus, els - } = dependencies; - - if(activeCommandId.value) { - appendConsole('⚠ Another command is already running. Please wait...', 'warn'); - return; - } - - const confirmed = await showConfirmDialog( - 'Uninstall Chroot Environment', - 'Are you sure you want to uninstall the chroot environment?\n\nThis will permanently delete all data in the chroot and cannot be undone.', - 'Uninstall', - 'Cancel' - ); - - if(!confirmed) { - return; - } - - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.INPUT_FOCUS)); - - closeSettingsPopup(); - // Update status immediately after closing popup for instant feedback - updateStatus('uninstalling'); - await new Promise(resolve => setTimeout(resolve, ANIMATION_DELAYS.POPUP_CLOSE_VERY_LONG)); - - disableAllActions(true); - disableSettingsPopup(true); - - // Stop chroot if running (uses centralized flow internally) - const isRunning = els.statusText && els.statusText.textContent.trim() === 'running'; - if(isRunning) { - const stopped = await ensureChrootStopped(); - if(!stopped) { - appendConsole('✗ Failed to stop chroot - uninstall aborted', 'err'); - activeCommandId.value = null; - disableAllActions(false); - disableSettingsPopup(false, true); - return; - } - } - - // Now use centralized flow for uninstall action - const { progressLine, interval: progressInterval } = await prepareActionExecution( - 'Starting Uninstallation', - 'Uninstalling chroot', - 'dots' - ); - - // Execute command using helper (handles validation, execution, cleanup, scrolling) - const cmd = `sh ${PATH_CHROOT_SH} uninstall --webui`; - - const commandId = executeCommandWithProgress({ - cmd, - progress: { progressLine, progressInterval }, - onSuccess: async (result) => { - appendConsole('✅ Chroot uninstalled successfully!', 'success'); - appendConsole('All chroot data has been removed.', 'info'); - appendConsole('━━━ Uninstallation Complete ━━━', 'success'); - updateStatus('stopped'); // temporary; refreshStatus will switch to "chroot not found" - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(true); - disableSettingsPopup(false, false); - // After logs and status update, smoothly scroll and then refresh UI state - await scrollConsoleToBottom({ force: true }); - await refreshStatus(); - }, - onError: async (result) => { - appendConsole('✗ Uninstallation failed', 'err'); - appendConsole('Check the logs above for details.', 'err'); - if(updateModuleStatus) updateModuleStatus(); - disableAllActions(false); - disableSettingsPopup(false, false); - // Ensure user sees error logs and UI is refreshed - await scrollConsoleToBottom({ force: true }); - await refreshStatus(); - }, - useValue: true, - activeCommandIdRef: activeCommandId - }); - - if(!commandId) { - // Validation failed - cleanup already done by helper - disableAllActions(false); - disableSettingsPopup(false, true); - } - } - - window.UninstallFeature = { - init, - uninstallChroot - }; -})(window); - diff --git a/webroot/bun.lock b/webroot/bun.lock new file mode 100644 index 0000000..901059c --- /dev/null +++ b/webroot/bun.lock @@ -0,0 +1,1529 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "ubuntu-webroot", + "dependencies": { + "nuxt": "^4.2.2", + "vue": "^3.5.25", + "vue-router": "^4.6.3", + }, + "devDependencies": { + "@types/node": "^25.0.1", + "prettier": "^3.7.4", + "terser": "^5.44.1", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@bomb.sh/tab": ["@bomb.sh/tab@0.0.9", "", { "peerDependencies": { "cac": "^6.7.14", "citty": "^0.1.6", "commander": "^13.1.0" }, "optionalPeers": ["cac", "citty", "commander"], "bin": { "tab": "dist/bin/cli.js" } }, "sha512-HUJ0b+LkZpLsyn0u7G/H5aJioAdSLqWMWX5ryuFS6n70MOEFu+SGrF8d8u6HzI1gINVQTvsfoxDLcjWkmI0AWg=="], + + "@clack/core": ["@clack/core@1.0.0-alpha.7", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ=="], + + "@clack/prompts": ["@clack/prompts@1.0.0-alpha.7", "", { "dependencies": { "@clack/core": "1.0.0-alpha.7", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-BLB8LYOdfI4q6XzDl8la69J/y/7s0tHjuU1/5ak+o8yB2BPZBNE22gfwbFUIEmlq/BGBD6lVUAMR7w+1K7Pr6Q=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.1", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg=="], + + "@dxup/nuxt": ["@dxup/nuxt@0.2.2", "", { "dependencies": { "@dxup/unimport": "^0.1.2", "@nuxt/kit": "^4.2.1", "chokidar": "^4.0.3", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-RNpJjDZs9+JcT9N87AnOuHsNM75DEd58itADNd/s1LIF6BZbTLZV0xxilJZb55lntn4TYvscTaXLCBX2fq9CXg=="], + + "@dxup/unimport": ["@dxup/unimport@0.1.2", "", {}, "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ=="], + + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], + + "@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], + + "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + + "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nuxt/cli": ["@nuxt/cli@3.31.2", "", { "dependencies": { "@bomb.sh/tab": "^0.0.9", "@clack/prompts": "1.0.0-alpha.7", "c12": "^3.3.2", "citty": "^0.1.6", "confbox": "^0.2.2", "consola": "^3.4.2", "copy-paste": "^2.2.0", "debug": "^4.4.3", "defu": "^6.1.4", "exsolve": "^1.0.8", "fuse.js": "^7.1.0", "giget": "^2.0.0", "jiti": "^2.6.1", "listhen": "^1.9.0", "nypm": "^0.6.2", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "scule": "^1.3.0", "semver": "^7.7.3", "srvx": "^0.9.7", "std-env": "^3.10.0", "tinyexec": "^1.0.2", "ufo": "^1.6.1", "youch": "^4.1.0-beta.13" }, "bin": { "nuxi": "bin/nuxi.mjs", "nuxi-ng": "bin/nuxi.mjs", "nuxt": "bin/nuxi.mjs", "nuxt-cli": "bin/nuxi.mjs" } }, "sha512-ud4KcfSdPeY96IR3UCtg/k7p6nUbJqF3IguQsolHo6EEJwiNM283EFXhRzU9cR+1iILExjaJvHMpFJ/7Xi++bg=="], + + "@nuxt/devalue": ["@nuxt/devalue@2.0.2", "", {}, "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA=="], + + "@nuxt/devtools": ["@nuxt/devtools@3.1.1", "", { "dependencies": { "@nuxt/devtools-kit": "3.1.1", "@nuxt/devtools-wizard": "3.1.1", "@nuxt/kit": "^4.2.1", "@vue/devtools-core": "^8.0.5", "@vue/devtools-kit": "^8.0.5", "birpc": "^2.8.0", "consola": "^3.4.2", "destr": "^2.0.5", "error-stack-parser-es": "^1.0.5", "execa": "^8.0.1", "fast-npm-meta": "^0.4.7", "get-port-please": "^3.2.0", "hookable": "^5.5.3", "image-meta": "^0.2.2", "is-installed-globally": "^1.0.0", "launch-editor": "^2.12.0", "local-pkg": "^1.1.2", "magicast": "^0.5.1", "nypm": "^0.6.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "semver": "^7.7.3", "simple-git": "^3.30.0", "sirv": "^3.0.2", "structured-clone-es": "^1.0.0", "tinyglobby": "^0.2.15", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-tracer": "^1.1.3", "which": "^5.0.0", "ws": "^8.18.3" }, "peerDependencies": { "@vitejs/devtools": "*", "vite": ">=6.0" }, "optionalPeers": ["@vitejs/devtools"], "bin": { "devtools": "cli.mjs" } }, "sha512-UG8oKQqcSyzwBe1l0z24zypmwn6FLW/HQMHK/F/gscUU5LeMHzgBhLPD+cuLlDvwlGAbifexWNMsS/I7n95KlA=="], + + "@nuxt/devtools-kit": ["@nuxt/devtools-kit@3.1.1", "", { "dependencies": { "@nuxt/kit": "^4.2.1", "execa": "^8.0.1" }, "peerDependencies": { "vite": ">=6.0" } }, "sha512-sjiKFeDCOy1SyqezSgyV4rYNfQewC64k/GhOsuJgRF+wR2qr6KTVhO6u2B+csKs74KrMrnJprQBgud7ejvOXAQ=="], + + "@nuxt/devtools-wizard": ["@nuxt/devtools-wizard@3.1.1", "", { "dependencies": { "consola": "^3.4.2", "diff": "^8.0.2", "execa": "^8.0.1", "magicast": "^0.5.1", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "prompts": "^2.4.2", "semver": "^7.7.3" }, "bin": { "devtools-wizard": "cli.mjs" } }, "sha512-6UORjapNKko2buv+3o57DQp69n5Z91TeJ75qdtNKcTvOfCTJrO78Ew0nZSgMMGrjbIJ4pFsHQEqXfgYLw3pNxg=="], + + "@nuxt/kit": ["@nuxt/kit@4.2.2", "", { "dependencies": { "c12": "^3.3.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "untyped": "^2.0.0" } }, "sha512-ZAgYBrPz/yhVgDznBNdQj2vhmOp31haJbO0I0iah/P9atw+OHH7NJLUZ3PK+LOz/0fblKTN1XJVSi8YQ1TQ0KA=="], + + "@nuxt/nitro-server": ["@nuxt/nitro-server@4.2.2", "", { "dependencies": { "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "4.2.2", "@unhead/vue": "^2.0.19", "@vue/shared": "^3.5.25", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "devalue": "^5.6.0", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.4", "impound": "^1.0.0", "klona": "^2.0.6", "mocked-exports": "^0.1.1", "nitropack": "^2.12.9", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "radix3": "^1.1.2", "std-env": "^3.10.0", "ufo": "^1.6.1", "unctx": "^2.4.1", "unstorage": "^1.17.3", "vue": "^3.5.25", "vue-bundle-renderer": "^2.2.0", "vue-devtools-stub": "^0.1.0" }, "peerDependencies": { "nuxt": "^4.2.2" } }, "sha512-lDITf4n5bHQ6a5MO7pvkpdQbPdWAUgSvztSHCfui/3ioLZsM2XntlN02ue6GSoh3oV9H4xSB3qGa+qlSjgxN0A=="], + + "@nuxt/schema": ["@nuxt/schema@4.2.2", "", { "dependencies": { "@vue/shared": "^3.5.25", "defu": "^6.1.4", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "std-env": "^3.10.0" } }, "sha512-lW/1MNpO01r5eR/VoeanQio8Lg4QpDklMOHa4mBHhhPNlBO1qiRtVYzjcnNdun3hujGauRaO9khGjv93Z5TZZA=="], + + "@nuxt/telemetry": ["@nuxt/telemetry@2.6.6", "", { "dependencies": { "@nuxt/kit": "^3.15.4", "citty": "^0.1.6", "consola": "^3.4.2", "destr": "^2.0.3", "dotenv": "^16.4.7", "git-url-parse": "^16.0.1", "is-docker": "^3.0.0", "ofetch": "^1.4.1", "package-manager-detector": "^1.1.0", "pathe": "^2.0.3", "rc9": "^2.1.2", "std-env": "^3.8.1" }, "bin": { "nuxt-telemetry": "bin/nuxt-telemetry.mjs" } }, "sha512-Zh4HJLjzvm3Cq9w6sfzIFyH9ozK5ePYVfCUzzUQNiZojFsI2k1QkSBrVI9BGc6ArKXj/O6rkI6w7qQ+ouL8Cag=="], + + "@nuxt/vite-builder": ["@nuxt/vite-builder@4.2.2", "", { "dependencies": { "@nuxt/kit": "4.2.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.2", "@vitejs/plugin-vue-jsx": "^5.1.2", "autoprefixer": "^10.4.22", "consola": "^3.4.2", "cssnano": "^7.1.2", "defu": "^6.1.4", "esbuild": "^0.27.1", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "h3": "^1.15.4", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.0", "mocked-exports": "^0.1.1", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.6", "rollup-plugin-visualizer": "^6.0.5", "seroval": "^1.4.0", "std-env": "^3.10.0", "ufo": "^1.6.1", "unenv": "^2.0.0-rc.24", "vite": "^7.2.7", "vite-node": "^5.2.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "nuxt": "4.2.2", "rolldown": "^1.0.0-beta.38", "vue": "^3.3.4" }, "optionalPeers": ["rolldown"] }, "sha512-Bot8fpJNtHZrM4cS1iSR7bEAZ1mFLAtJvD/JOSQ6kT62F4hSFWfMubMXOwDkLK2tnn3bnAdSqGy1nLNDBCahpQ=="], + + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.102.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pknM+ttJTwRr7ezn1v5K+o2P4RRjLAzKI10bjVDPybwWQ544AZW6jxm7/YDgF2yUbWEV9o7cAQPkIUOmCiW8vg=="], + + "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.102.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BDLiH41ZctNND38+GCEL3ZxFn9j7qMZJLrr6SLWMt8xlG4Sl64xTkZ0zeUy4RdVEatKKZdrRIhFZ2e5wPDQT6Q=="], + + "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.102.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-AcB8ZZ711w4hTDhMfMHNjT2d+hekTQ2XmNSUBqJdXB+a2bJbE50UCRq/nxXl44zkjaQTit3lcQbFvhk2wwKcpw=="], + + "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.102.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UlLEN9mR5QaviYVMWZQsN9DgAH3qyV67XUXDEzSrbVMLsqHsVHhFU8ZIeO0fxWTQW/cgpvldvKp9/+RdrggqWw=="], + + "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.102.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CWyCwedZrUt47n56/RwHSwKXxVI3p98hB0ntLaBNeH5qjjBujs9uOh4bQ0aAlzUWunT77b3/Y+xcQnmV42HN4A=="], + + "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-W/DCw+Ys8rXj4j38ylJ2l6Kvp6SV+eO5SUWA11imz7yCWntNL001KJyGQ9PJNUFHg0jbxe3yqm4M50v6miWzeA=="], + + "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DyH/t/zSZHuX4Nn239oBteeMC4OP7B13EyXWX18Qg8aJoZ+lZo90WPGOvhP04zII33jJ7di+vrtAUhsX64lp+A=="], + + "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.102.0", "", { "os": "linux", "cpu": "none" }, "sha512-CMvzrmOg+Gs44E7TRK/IgrHYp+wwVJxVV8niUrDR2b3SsrCO3NQz5LI+7bM1qDbWnuu5Cl1aiitoMfjRY61dSg=="], + + "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.102.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-tZWr6j2s0ddm9MTfWTI3myaAArg9GDy4UgvpF00kMQAjLcGUNhEEQbB9Bd9KtCvDQzaan8HQs0GVWUp+DWrymw=="], + + "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0YEKmAIun1bS+Iy5Shx6WOTSj3GuilVuctJjc5/vP8/EMTZ/RI8j0eq0Mu3UFPoT/bMULL3MBXuHuEIXmq7Ddg=="], + + "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Ew4QDpEsXoV+pG5+bJpheEy3GH436GBe6ASPB0X27Hh9cQ2gb1NVZ7cY7xJj68+fizwS/PtT8GHoG3uxyH17Pg=="], + + "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.102.0", "", { "os": "none", "cpu": "arm64" }, "sha512-wYPXS8IOu/sXiP3CGHJNPzZo4hfPAwJKevcFH2syvU2zyqUxym7hx6smfcK/mgJBiX7VchwArdGRwrEQKcBSaQ=="], + + "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.102.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-52SepCb9e+8cVisGa9S/F14K8PxW0AnbV1j4KEYi8uwfkUIxeDNKRHVHzPoBXNrr0yxW0EHLn/3i8J7a2YCpWw=="], + + "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.102.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kLs6H1y6sDBKcIimkNwu5th28SLkyvFpHNxdLtCChda0KIGeIXNSiupy5BqEutY+VlWJivKT1OV3Ev3KC5Euzg=="], + + "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.102.0", "", { "os": "win32", "cpu": "x64" }, "sha512-XdyJZdSMN8rbBXH10CrFuU+Q9jIP2+MnxHmNzjK4+bldbTI1UxqwjUMS9bKVC5VCaIEZhh8IE8x4Vf8gmCgrKQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.102.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pD2if3w3cxPvYbsBSTbhxAYGDaG6WVwnqYG0mYRQ142D6SJ6BpNs7YVQrqpRA2AJQCmzaPP5TRp/koFLebagfQ=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.102.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzMN6f6MrjjpQC2Dandyod3iOscofYBpHaTecmoRRbC5sJMwsurkqUMHzoJX9F6IM87kn8m/JcClnoOfx5Sesw=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.102.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Sr2/3K6GEcejY+HgWp5HaxRPzW5XHe9IfGKVn9OhLt8fzVLnXbK5/GjXj7JjMCNKI3G3ZPZDG2Dgm6CX3MaHCA=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.102.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-s9F2N0KJCGEpuBW6ChpFfR06m2Id9ReaHSl8DCca4HvFNt8SJFPp8fq42n2PZy68rtkremQasM0JDrK2BoBeBQ=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.102.0", "", { "os": "linux", "cpu": "arm" }, "sha512-zRCIOWzLbqhfY4g8KIZDyYfO2Fl5ltxdQI1v2GlePj66vFWRl8cf4qcBGzxKfsH3wCZHAhmWd1Ht59mnrfH/UQ=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5n5RbHgfjulRhKB0pW5p0X/NkQeOpI4uI9WHgIZbORUDATGFC8yeyPA6xYGEs+S3MyEAFxl4v544UEIWwqAgsA=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/XWcmglH/VJ4yKAGTLRgPKSSikh3xciNxkwGiURt8dS30b+3pwc4ZZmudMu0tQ3mjSu0o7V9APZLMpbHK8Bp5w=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.102.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jtIq4nswvy6xdqv1ndWyvVlaRpS0yqomLCvvHdCFx3pFXo5Aoq4RZ39kgvFWrbAtpeYSYeAGFnwgnqjx9ftdw=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.102.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Yp6HX/574mvYryiqj0jNvNTJqo4pdAsNP2LPBTxlDQ1cU3lPd7DUA4MQZadaeLI8+AGB2Pn50mPuPyEwFIxeFg=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-R4b0xZpDRhoNB2XZy0kLTSYm0ZmWeKjTii9fcv1Mk3/SIGPrrglwt4U6zEtwK54Dfi4Bve5JnQYduigR/gyDzw=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xM5A+03Ti3jvWYZoqaBRS3lusvnvIQjA46Fc9aBE/MHgvKgHSkrGEluLWg/33QEwBwxupkH25Pxc1yu97oZCtg=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.102.0", "", { "os": "none", "cpu": "arm64" }, "sha512-AieLlsliblyaTFq7Iw9Nc618tgwV02JT4fQ6VIUd/3ZzbluHIHfPjIXa6Sds+04krw5TvCS8lsegtDYAyzcyhg=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.102.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-w6HRyArs1PBb9rDsQSHlooe31buUlUI2iY8sBzp62jZ1tmvaJo9EIVTQlRNDkwJmk9DF9uEyIJ82EkZcCZTs9A=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.102.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-pqP5UuLiiFONQxqGiUFMdsfybaK1EOK4AXiPlvOvacLaatSEPObZGpyCkAcj9aZcvvNwYdeY9cxGM9IT3togaA=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.102.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ntMcL35wuLR1A145rLSmm7m7j8JBZGkROoB9Du0KFIFcfi/w1qk75BdCeiTl3HAKrreAnuhW3QOGs6mJhntowA=="], + + "@oxc-project/types": ["@oxc-project/types@0.102.0", "", {}, "sha512-8Skrw405g+/UJPKWJ1twIk3BIH2nXdiVlVNtYT23AXVwpsd79es4K+KYt06Fbnkc5BaTvk/COT2JuCLYdwnCdA=="], + + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.102.0", "", { "os": "android", "cpu": "arm64" }, "sha512-JLBT7EiExsGmB6LuBBnm6qTfg0rLSxBU+F7xjqy6UXYpL7zhqelGJL7IAq6Pu5UYFT55zVlXXmgzLOXQfpQjXA=="], + + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.102.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xmsBCk/NwE0khy8h6wLEexiS5abCp1ZqJUNHsAovJdGgIW21oGwhiC3VYg1vNLbq+zEXwOHuphVuNEYfBwyNTw=="], + + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.102.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-EhBsiq8hSd5BRjlWACB9MxTUiZT2He1s1b3tRP8k3lB8ZTt6sXnDXIWhxRmmM0h//xe6IJ2HuMlbvjXPo/tATg=="], + + "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.102.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-eujvuYf0x7BFgKyFecbXUa2JBEXT4Ss6vmyrrhVdN07jaeJRiobaKAmeNXBkanoWL2KQLELJbSBgs1ykWYTkzg=="], + + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.102.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2x7Ro356PHBVp1SS/dOsHBSnrfs5MlPYwhdKg35t6qixt2bv1kzEH0tDmn4TNEbdjOirmvOXoCTEWUvh8A4f4Q=="], + + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Rz/RbPvT4QwcHKIQ/cOt6Lwl4c7AhK2b6whZfyL6oJ7Uz8UiVl1BCwk8thedrB5h+FEykmaPHoriW1hmBev60g=="], + + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.102.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-I08iWABrN7zakn3wuNIBWY3hALQGsDLPQbZT1mXws7tyiQqJNGe49uS0/O50QhX3KXj+mbRGsmjVXLXGJE1CVQ=="], + + "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.102.0", "", { "os": "linux", "cpu": "none" }, "sha512-9+SYW1ARAF6Oj/82ayoqKRe8SI7O1qvzs3Y0kijvhIqAaaZWcFRjI5DToyWRAbnzTtHlMcSllZLXNYdmxBjFxA=="], + + "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.102.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-HV9nTyQw0TTKYPu+gBhaJBioomiM9O4LcGXi+s5IylCGG6imP0/U13q/9xJnP267QFmiWWqnnSFcv0QAWCyh8A=="], + + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4wcZ08mmdFk8OjsnglyeYGu5PW3TDh87AmcMOi7tZJ3cpJjfzwDfY27KTEUx6G880OpjAiF36OFSPwdKTKgp2g=="], + + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.102.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rUHZSZBw0FUnUgOhL/Rs7xJz9KjH2eFur/0df6Lwq/isgJc/ggtBtFoZ+y4Fb8ON87a3Y2gS2LT7SEctX0XdPQ=="], + + "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.102.0", "", { "os": "none", "cpu": "arm64" }, "sha512-98y4tccTQ/pA+r2KA/MEJIZ7J8TNTJ4aCT4rX8kWK4pGOko2YsfY3Ru9DVHlLDwmVj7wP8Z4JNxdBrAXRvK+0g=="], + + "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.102.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-M6myOXxHty3L2TJEB1NlJPtQm0c0LmivAxcGv/+DSDadOoB/UnOUbjM8W2Utlh5IYS9ARSOjqHtBiPYLWJ15XA=="], + + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.102.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-jzaA1lLiMXiJs4r7E0BHRxTPiwAkpoCfSNRr8npK/SqL4UQE4cSz3WDTX5wJWRrN2U+xqsDGefeYzH4reI8sgw=="], + + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.102.0", "", { "os": "win32", "cpu": "x64" }, "sha512-eYOm6mch+1cP9qlNkMdorfBFY8aEOxY/isqrreLmEWqF/hyXA0SbLKDigTbvh3JFKny/gXlHoCKckqfua4cwtg=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + + "@parcel/watcher-wasm": ["@parcel/watcher-wasm@2.5.1", "", { "dependencies": { "is-glob": "^4.0.3", "micromatch": "^4.0.5", "napi-wasm": "^1.1.0" } }, "sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + + "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="], + + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], + + "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], + + "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="], + + "@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.1.1", "", {}, "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.0.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg=="], + + "@types/parse-path": ["@types/parse-path@7.1.0", "", { "dependencies": { "parse-path": "*" } }, "sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@unhead/vue": ["@unhead/vue@2.0.19", "", { "dependencies": { "hookable": "^5.5.3", "unhead": "2.0.19" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg=="], + + "@vercel/nft": ["@vercel/nft@0.30.4", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.5.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-wE6eAGSXScra60N2l6jWvNtVK0m+sh873CpfZW4KI2v8EHuUQp+mSEi4T+IcdPCSEDgCdAS/7bizbhQlkjzrSA=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.53" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w=="], + + "@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5", "@rolldown/pluginutils": "^1.0.0-beta.50", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, "sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A=="], + + "@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="], + + "@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="], + + "@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="], + + "@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@2.0.1", "", {}, "sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA=="], + + "@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@2.0.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@vue/babel-helper-vue-transform-on": "2.0.1", "@vue/babel-plugin-resolve-type": "2.0.1", "@vue/shared": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA=="], + + "@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@2.0.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.4", "@vue/compiler-sfc": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.25", "", { "dependencies": { "@vue/compiler-core": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.25", "@vue/compiler-dom": "3.5.25", "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A=="], + + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + + "@vue/devtools-core": ["@vue/devtools-core@8.0.5", "", { "dependencies": { "@vue/devtools-kit": "^8.0.5", "@vue/devtools-shared": "^8.0.5", "mitt": "^3.0.1", "nanoid": "^5.1.5", "pathe": "^2.0.3", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@8.0.5", "", { "dependencies": { "@vue/devtools-shared": "^8.0.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@8.0.5", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg=="], + + "@vue/language-core": ["@vue/language-core@3.1.8", "", { "dependencies": { "@volar/language-core": "2.4.26", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.25", "", { "dependencies": { "@vue/shared": "3.5.25" } }, "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/runtime-core": "3.5.25", "@vue/shared": "3.5.25", "csstype": "^3.1.3" } }, "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.25", "", { "dependencies": { "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "vue": "3.5.25" } }, "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ=="], + + "@vue/shared": ["@vue/shared@3.5.25", "", {}, "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg=="], + + "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "alien-signals": ["alien-signals@3.1.1", "", {}, "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], + + "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], + + "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], + + "ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], + + "autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="], + + "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], + + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "compatx": ["compatx@0.2.0", "", {}, "sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA=="], + + "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + + "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + + "copy-paste": ["copy-paste@2.2.0", "", { "dependencies": { "iconv-lite": "^0.4.8" } }, "sha512-jqSL4r9DSeiIvJZStLzY/sMLt9ToTM7RsK237lYOTG+KcbQJHGala3R1TUpa8h1p9adswVgIdV4qGbseVhL4lg=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + + "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], + + "croner": ["croner@9.1.0", "", {}, "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "css-declaration-sorter": ["css-declaration-sorter@7.3.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssnano": ["cssnano@7.1.2", "", { "dependencies": { "cssnano-preset-default": "^7.0.10", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-HYOPBsNvoiFeR1eghKD5C3ASm64v9YVyJB4Ivnl2gqKoQYvjjN/G0rztvKQq8OxocUtC6sjqY8jwYngIB4AByA=="], + + "cssnano-preset-default": ["cssnano-preset-default@7.0.10", "", { "dependencies": { "browserslist": "^4.27.0", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", "postcss-colormin": "^7.0.5", "postcss-convert-values": "^7.0.8", "postcss-discard-comments": "^7.0.5", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", "postcss-merge-rules": "^7.0.7", "postcss-minify-font-values": "^7.0.1", "postcss-minify-gradients": "^7.0.1", "postcss-minify-params": "^7.0.5", "postcss-minify-selectors": "^7.0.5", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", "postcss-normalize-positions": "^7.0.1", "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", "postcss-normalize-unicode": "^7.0.5", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", "postcss-reduce-initial": "^7.0.5", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.1.0", "postcss-unique-selectors": "^7.0.4" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-6ZBjW0Lf1K1Z+0OKUAUpEN62tSXmYChXWi2NAA0afxEVsj9a+MbcB1l5qel6BHJHmULai2fCGRthCeKSFbScpA=="], + + "cssnano-utils": ["cssnano-utils@5.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "errx": ["errx@0.1.0", "", {}, "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-npm-meta": ["fast-npm-meta@0.4.7", "", {}, "sha512-aZU3i3eRcSb2NCq8i6N6IlyiTyF6vqAqzBGl2NBF6ngNx/GIqfYbkLDIKZ4z4P0o/RmtsFnVqHwdrSm13o4tnQ=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "git-up": ["git-up@8.1.1", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^9.2.0" } }, "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g=="], + + "git-url-parse": ["git-url-parse@16.1.0", "", { "dependencies": { "git-up": "^8.1.0" } }, "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + + "globby": ["globby@15.0.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], + + "h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "httpxy": ["httpxy@0.1.7", "", {}, "sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "image-meta": ["image-meta@0.2.2", "", {}, "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA=="], + + "impound": ["impound@1.0.0", "", { "dependencies": { "exsolve": "^1.0.5", "mocked-exports": "^0.1.1", "pathe": "^2.0.3", "unplugin": "^2.3.2", "unplugin-utils": "^0.2.4" } }, "sha512-8lAJ+1Arw2sMaZ9HE2ZmL5zOcMnt18s6+7Xqgq2aUVy4P1nlzAyPtzCDxsk51KVFwHEEdc6OWvUyqwHwhRYaug=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], + + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + + "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + + "is-ssh": ["is-ssh@1.4.1", "", { "dependencies": { "protocols": "^2.0.1" } }, "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="], + + "launch-editor": ["launch-editor@2.12.0", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg=="], + + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "listhen": ["listhen@1.9.0", "", { "dependencies": { "@parcel/watcher": "^2.4.1", "@parcel/watcher-wasm": "^2.4.1", "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.12.0", "http-shutdown": "^1.2.2", "jiti": "^2.1.2", "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg=="], + + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="], + + "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="], + + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "mocked-exports": ["mocked-exports@0.1.1", "", {}, "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "nanotar": ["nanotar@0.2.0", "", {}, "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ=="], + + "nitropack": ["nitropack@2.12.9", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.0", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.9", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@vercel/nft": "^0.30.3", "archiver": "^7.0.1", "c12": "^3.3.1", "chokidar": "^4.0.3", "citty": "^0.1.6", "compatx": "^0.2.0", "confbox": "^0.2.2", "consola": "^3.4.2", "cookie-es": "^2.0.0", "croner": "^9.1.0", "crossws": "^0.3.5", "db0": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "dot-prop": "^10.1.0", "esbuild": "^0.25.11", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.7", "globby": "^15.0.0", "gzip-size": "^7.0.0", "h3": "^1.15.4", "hookable": "^5.5.3", "httpxy": "^0.1.7", "ioredis": "^5.8.2", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "listhen": "^1.9.0", "magic-string": "^0.30.21", "magicast": "^0.5.0", "mime": "^4.1.0", "mlly": "^1.8.0", "node-fetch-native": "^1.6.7", "node-mock-http": "^1.0.3", "ofetch": "^1.5.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "pretty-bytes": "^7.1.0", "radix3": "^1.1.2", "rollup": "^4.52.5", "rollup-plugin-visualizer": "^6.0.5", "scule": "^1.3.0", "semver": "^7.7.3", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.0", "source-map": "^0.7.6", "std-env": "^3.10.0", "ufo": "^1.6.1", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unenv": "^2.0.0-rc.23", "unimport": "^5.5.0", "unplugin-utils": "^0.3.1", "unstorage": "^1.17.1", "untyped": "^2.0.0", "unwasm": "^0.3.11", "youch": "^4.1.0-beta.11", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-t6qqNBn2UDGMWogQuORjbL2UPevB8PvIPsPHmqvWpeGOlPr4P8Oc5oA8t3wFwGmaolM2M/s2SwT23nx9yARmOg=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nuxt": ["nuxt@4.2.2", "", { "dependencies": { "@dxup/nuxt": "^0.2.2", "@nuxt/cli": "^3.31.1", "@nuxt/devtools": "^3.1.1", "@nuxt/kit": "4.2.2", "@nuxt/nitro-server": "4.2.2", "@nuxt/schema": "4.2.2", "@nuxt/telemetry": "^2.6.6", "@nuxt/vite-builder": "4.2.2", "@unhead/vue": "^2.0.19", "@vue/shared": "^3.5.25", "c12": "^3.3.2", "chokidar": "^5.0.0", "compatx": "^0.2.0", "consola": "^3.4.2", "cookie-es": "^2.0.0", "defu": "^6.1.4", "destr": "^2.0.5", "devalue": "^5.6.0", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.4", "hookable": "^5.5.3", "ignore": "^7.0.5", "impound": "^1.0.0", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.0", "nanotar": "^0.2.0", "nypm": "^0.6.2", "ofetch": "^1.5.1", "ohash": "^2.0.11", "on-change": "^6.0.1", "oxc-minify": "^0.102.0", "oxc-parser": "^0.102.0", "oxc-transform": "^0.102.0", "oxc-walker": "^0.6.0", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "radix3": "^1.1.2", "scule": "^1.3.0", "semver": "^7.7.3", "std-env": "^3.10.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unimport": "^5.5.0", "unplugin": "^2.3.11", "unplugin-vue-router": "^0.19.0", "untyped": "^2.0.0", "vue": "^3.5.25", "vue-router": "^4.6.3" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "@types/node": ">=18.12.0" }, "optionalPeers": ["@parcel/watcher", "@types/node"], "bin": { "nuxi": "bin/nuxt.mjs", "nuxt": "bin/nuxt.mjs" } }, "sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw=="], + + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "on-change": ["on-change@6.0.1", "", {}, "sha512-P7o0hkMahOhjb1niG28vLNAXsJrRcfpJvYWcTmPt/Tf4xedcF2PA1E9++N1tufY8/vIsaiJgHhjQp53hJCe+zw=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "oxc-minify": ["oxc-minify@0.102.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.102.0", "@oxc-minify/binding-darwin-arm64": "0.102.0", "@oxc-minify/binding-darwin-x64": "0.102.0", "@oxc-minify/binding-freebsd-x64": "0.102.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.102.0", "@oxc-minify/binding-linux-arm64-gnu": "0.102.0", "@oxc-minify/binding-linux-arm64-musl": "0.102.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.102.0", "@oxc-minify/binding-linux-s390x-gnu": "0.102.0", "@oxc-minify/binding-linux-x64-gnu": "0.102.0", "@oxc-minify/binding-linux-x64-musl": "0.102.0", "@oxc-minify/binding-openharmony-arm64": "0.102.0", "@oxc-minify/binding-wasm32-wasi": "0.102.0", "@oxc-minify/binding-win32-arm64-msvc": "0.102.0", "@oxc-minify/binding-win32-x64-msvc": "0.102.0" } }, "sha512-FphAHDyTCNepQbiQTSyWFMbNc9zdUmj1WBsoLwvZhWm7rEe/IeIKYKRhy75lWOjwFsi5/i4Qucq43hgs3n2Exw=="], + + "oxc-parser": ["oxc-parser@0.102.0", "", { "dependencies": { "@oxc-project/types": "^0.102.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.102.0", "@oxc-parser/binding-darwin-arm64": "0.102.0", "@oxc-parser/binding-darwin-x64": "0.102.0", "@oxc-parser/binding-freebsd-x64": "0.102.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.102.0", "@oxc-parser/binding-linux-arm64-gnu": "0.102.0", "@oxc-parser/binding-linux-arm64-musl": "0.102.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.102.0", "@oxc-parser/binding-linux-s390x-gnu": "0.102.0", "@oxc-parser/binding-linux-x64-gnu": "0.102.0", "@oxc-parser/binding-linux-x64-musl": "0.102.0", "@oxc-parser/binding-openharmony-arm64": "0.102.0", "@oxc-parser/binding-wasm32-wasi": "0.102.0", "@oxc-parser/binding-win32-arm64-msvc": "0.102.0", "@oxc-parser/binding-win32-x64-msvc": "0.102.0" } }, "sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og=="], + + "oxc-transform": ["oxc-transform@0.102.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.102.0", "@oxc-transform/binding-darwin-arm64": "0.102.0", "@oxc-transform/binding-darwin-x64": "0.102.0", "@oxc-transform/binding-freebsd-x64": "0.102.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.102.0", "@oxc-transform/binding-linux-arm64-gnu": "0.102.0", "@oxc-transform/binding-linux-arm64-musl": "0.102.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.102.0", "@oxc-transform/binding-linux-s390x-gnu": "0.102.0", "@oxc-transform/binding-linux-x64-gnu": "0.102.0", "@oxc-transform/binding-linux-x64-musl": "0.102.0", "@oxc-transform/binding-openharmony-arm64": "0.102.0", "@oxc-transform/binding-wasm32-wasi": "0.102.0", "@oxc-transform/binding-win32-arm64-msvc": "0.102.0", "@oxc-transform/binding-win32-x64-msvc": "0.102.0" } }, "sha512-MR5ohiBS6/kvxRpmUZ3LIDTTJBEC4xLAEZXfYr7vrA0eP7WHewQaNQPFDgT4Bee89TdmVQ5ZKrifGwxLjSyHHw=="], + + "oxc-walker": ["oxc-walker@0.6.0", "", { "dependencies": { "magic-regexp": "^0.10.0" }, "peerDependencies": { "oxc-parser": ">=0.98.0" } }, "sha512-BA3hlxq5+Sgzp7TCQF52XDXCK5mwoIZuIuxv/+JuuTzOs2RXkLqWZgZ69d8pJDDjnL7wiREZTWHBzFp/UWH88Q=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "parse-path": ["parse-path@7.1.0", "", { "dependencies": { "protocols": "^2.0.0" } }, "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw=="], + + "parse-url": ["parse-url@9.2.0", "", { "dependencies": { "@types/parse-path": "^7.0.0", "parse-path": "^7.0.0" } }, "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-calc": ["postcss-calc@10.1.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.38" } }, "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw=="], + + "postcss-colormin": ["postcss-colormin@7.0.5", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-ekIBP/nwzRWhEMmIxHHbXHcMdzd1HIUzBECaj5KEdLz9DVP2HzT065sEhvOx1dkLjYW7jyD0CngThx6bpFi2fA=="], + + "postcss-convert-values": ["postcss-convert-values@7.0.8", "", { "dependencies": { "browserslist": "^4.27.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-+XNKuPfkHTCEo499VzLMYn94TiL3r9YqRE3Ty+jP7UX4qjewUONey1t7CG21lrlTLN07GtGM8MqFVp86D4uKJg=="], + + "postcss-discard-comments": ["postcss-discard-comments@7.0.5", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ=="], + + "postcss-discard-duplicates": ["postcss-discard-duplicates@7.0.2", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w=="], + + "postcss-discard-empty": ["postcss-discard-empty@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg=="], + + "postcss-discard-overridden": ["postcss-discard-overridden@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg=="], + + "postcss-merge-longhand": ["postcss-merge-longhand@7.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^7.0.5" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw=="], + + "postcss-merge-rules": ["postcss-merge-rules@7.0.7", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew=="], + + "postcss-minify-font-values": ["postcss-minify-font-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ=="], + + "postcss-minify-gradients": ["postcss-minify-gradients@7.0.1", "", { "dependencies": { "colord": "^2.9.3", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A=="], + + "postcss-minify-params": ["postcss-minify-params@7.0.5", "", { "dependencies": { "browserslist": "^4.27.0", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-FGK9ky02h6Ighn3UihsyeAH5XmLEE2MSGH5Tc4tXMFtEDx7B+zTG6hD/+/cT+fbF7PbYojsmmWjyTwFwW1JKQQ=="], + + "postcss-minify-selectors": ["postcss-minify-selectors@7.0.5", "", { "dependencies": { "cssesc": "^3.0.0", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug=="], + + "postcss-normalize-charset": ["postcss-normalize-charset@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ=="], + + "postcss-normalize-display-values": ["postcss-normalize-display-values@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ=="], + + "postcss-normalize-positions": ["postcss-normalize-positions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ=="], + + "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ=="], + + "postcss-normalize-string": ["postcss-normalize-string@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ=="], + + "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg=="], + + "postcss-normalize-unicode": ["postcss-normalize-unicode@7.0.5", "", { "dependencies": { "browserslist": "^4.27.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-X6BBwiRxVaFHrb2WyBMddIeB5HBjJcAaUHyhLrM2FsxSq5TFqcHSsK7Zu1otag+o0ZphQGJewGH1tAyrD0zX1Q=="], + + "postcss-normalize-url": ["postcss-normalize-url@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ=="], + + "postcss-normalize-whitespace": ["postcss-normalize-whitespace@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA=="], + + "postcss-ordered-values": ["postcss-ordered-values@7.0.2", "", { "dependencies": { "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw=="], + + "postcss-reduce-initial": ["postcss-reduce-initial@7.0.5", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA=="], + + "postcss-reduce-transforms": ["postcss-reduce-transforms@7.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "postcss-svgo": ["postcss-svgo@7.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w=="], + + "postcss-unique-selectors": ["postcss-unique-selectors@7.0.4", "", { "dependencies": { "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + + "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "protocols": ["protocols@2.0.2", "", {}, "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], + + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + + "rollup-plugin-visualizer": ["rollup-plugin-visualizer@6.0.5", "", { "dependencies": { "open": "^8.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "seroval": ["seroval@1.4.0", "", {}, "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg=="], + + "serve-placeholder": ["serve-placeholder@2.0.2", "", { "dependencies": { "defu": "^6.1.4" } }, "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], + + "srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], + + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "structured-clone-es": ["structured-clone-es@1.0.0", "", {}, "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ=="], + + "stylehacks": ["stylehacks@7.0.7", "", { "dependencies": { "browserslist": "^4.27.0", "postcss-selector-parser": "^7.1.0" }, "peerDependencies": { "postcss": "^8.4.32" } }, "sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g=="], + + "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], + + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], + + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + + "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], + + "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@5.3.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg=="], + + "type-level-regexp": ["type-level-regexp@0.1.17", "", {}, "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], + + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + + "unctx": ["unctx@2.4.1", "", { "dependencies": { "acorn": "^8.14.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "unplugin": "^2.1.0" } }, "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "unhead": ["unhead@2.0.19", "", { "dependencies": { "hookable": "^5.5.3" } }, "sha512-gEEjkV11Aj+rBnY6wnRfsFtF2RxKOLaPN4i+Gx3UhBxnszvV6ApSNZbGk7WKyy/lErQ6ekPN63qdFL7sa1leow=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unimport": ["unimport@5.5.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.19", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.10", "unplugin-utils": "^0.3.0" } }, "sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-utils": ["unplugin-utils@0.2.5", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg=="], + + "unplugin-vue-router": ["unplugin-vue-router@0.19.0", "", { "dependencies": { "@babel/generator": "^7.28.5", "@vue-macros/common": "^3.1.1", "@vue/language-core": "^3.1.5", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@vue/compiler-sfc": "^3.5.17", "vue-router": "^4.6.0" }, "optionalPeers": ["vue-router"] }, "sha512-UlqWIZgxg28gicggB2Zv4aUYq07i38q/dLDl0fzMgidjm+zuDeoAZSIr5uc/szKhGNZW1vMiqXQOzjgOUG0VIg=="], + + "unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="], + + "untun": ["untun@0.1.3", "", { "dependencies": { "citty": "^0.1.5", "consola": "^3.2.3", "pathe": "^1.1.1" }, "bin": { "untun": "bin/untun.mjs" } }, "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ=="], + + "untyped": ["untyped@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", "jiti": "^2.4.2", "knitwork": "^1.2.0", "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g=="], + + "unwasm": ["unwasm@0.3.11", "", { "dependencies": { "knitwork": "^1.2.0", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "unplugin": "^2.3.6" } }, "sha512-Vhp5gb1tusSQw5of/g3Q697srYgMXvwMgXMjcG4ZNga02fDX9coxJ9fAb0Ci38hM2Hv/U1FXRPGgjP2BYqhNoQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="], + + "uqr": ["uqr@0.1.2", "", {}, "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="], + + "vite-dev-rpc": ["vite-dev-rpc@1.1.0", "", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="], + + "vite-hot-client": ["vite-hot-client@2.1.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ=="], + + "vite-node": ["vite-node@5.2.0", "", { "dependencies": { "cac": "^6.7.14", "es-module-lexer": "^1.7.0", "obug": "^2.0.0", "pathe": "^2.0.3", "vite": "^7.2.2" }, "bin": { "vite-node": "dist/cli.mjs" } }, "sha512-7UT39YxUukIA97zWPXUGb0SGSiLexEGlavMwU3HDE6+d/HJhKLjLqu4eX2qv6SQiocdhKLRcusroDwXHQ6CnRQ=="], + + "vite-plugin-checker": ["vite-plugin-checker@0.12.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "chokidar": "^4.0.3", "npm-run-path": "^6.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tiny-invariant": "^1.3.3", "tinyglobby": "^0.2.15", "vscode-uri": "^3.1.0" }, "peerDependencies": { "@biomejs/biome": ">=1.7", "eslint": ">=9.39.1", "meow": "^13.2.0", "optionator": "^0.9.4", "oxlint": ">=1", "stylelint": ">=16", "typescript": "*", "vite": ">=5.4.21", "vls": "*", "vti": "*", "vue-tsc": "~2.2.10 || ^3.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "meow", "optionator", "oxlint", "stylelint", "typescript", "vls", "vti", "vue-tsc"] }, "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg=="], + + "vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="], + + "vite-plugin-vue-tracer": ["vite-plugin-vue-tracer@1.1.3", "", { "dependencies": { "estree-walker": "^3.0.3", "exsolve": "^1.0.7", "magic-string": "^0.30.21", "pathe": "^2.0.3", "source-map-js": "^1.2.1" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0", "vue": "^3.5.0" } }, "sha512-fM7hfHELZvbPnSn8EKZwHfzxm5EfYFQIclz8rwcNXfodNbRkwNvh0AGMtaBfMxQ9HC5KVa3KitwHnmE4ezDemw=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vue": ["vue@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", "@vue/runtime-dom": "3.5.25", "@vue/server-renderer": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g=="], + + "vue-bundle-renderer": ["vue-bundle-renderer@2.2.0", "", { "dependencies": { "ufo": "^1.6.1" } }, "sha512-sz/0WEdYH1KfaOm0XaBmRZOWgYTEvUDt6yPYaUzl4E52qzgWLlknaPPTTZmp6benaPTlQAI/hN1x3tAzZygycg=="], + + "vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="], + + "vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "youch": ["youch@4.1.0-beta.13", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.5", "@speed-highlight/core": "^1.2.9", "cookie-es": "^2.0.0", "youch-core": "^0.3.3" } }, "sha512-3+AG1Xvt+R7M7PSDudhbfbwiyveW6B8PLBIwTyEC598biEYIjHhC89i6DBEvR0EZUjGY3uGSnC429HpIa2Z09g=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], + + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "@dxup/nuxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "@nuxt/telemetry/@nuxt/kit": ["@nuxt/kit@3.20.2", "", { "dependencies": { "c12": "^3.3.2", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "untyped": "^2.0.0" } }, "sha512-laqfmMcWWNV1FsVmm1+RQUoGY8NIJvCRl0z0K8ikqPukoEry0LXMqlQ+xaf8xJRvoH2/78OhZmsEEsUBTXipcw=="], + + "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], + + "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/plugin-inject/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue-macros/common/unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "nitropack/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "nitropack/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "nitropack/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "nitropack/unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "rollup-plugin-visualizer/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "unimport/unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "unplugin-vue-router/unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "vite-plugin-checker/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "vite-plugin-checker/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "vite-plugin-inspect/unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@dxup/nuxt/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "nitropack/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "nitropack/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "nitropack/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "nitropack/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "nitropack/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "nitropack/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "nitropack/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "nitropack/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "nitropack/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "nitropack/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "nitropack/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "nitropack/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "nitropack/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "nitropack/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "nitropack/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "nitropack/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "nitropack/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "nitropack/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "nitropack/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "nitropack/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "nitropack/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "nitropack/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "nitropack/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "nitropack/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "nitropack/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "nitropack/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "nitropack/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "rollup-plugin-visualizer/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "rollup-plugin-visualizer/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "vite-plugin-checker/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "vite-plugin-checker/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + } +} diff --git a/webroot/index.html b/webroot/index.html deleted file mode 100644 index fda0c13..0000000 --- a/webroot/index.html +++ /dev/null @@ -1,396 +0,0 @@ - - - - - - - - Ubuntu Chroot Manager - - - - - - -
-
-
-
-
-
-
-
-
- -
-
-
- -
-
- - - - -
-
-
-
-

- Ubuntu Chroot - DEBUG -

-
-
- - unknown -
-
- - -
-
- - - -
-
-
-
-
- - -
- -
-
- - -
-
-
-
- -
-
- Console - -
-

-        
- - -
-
-
- - - - - - - - - - - - - - -
- - - - - - - - - - - - diff --git a/webroot/nuxt.config.ts b/webroot/nuxt.config.ts new file mode 100644 index 0000000..7f07da3 --- /dev/null +++ b/webroot/nuxt.config.ts @@ -0,0 +1,38 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +import { defineNuxtConfig } from "nuxt/config"; +export default defineNuxtConfig({ + compatibilityDate: "2025-07-15", + ssr: true, + devtools: { enabled: true }, + + vite: { + build: { + minify: "terser", + sourcemap: false, + terserOptions: { + ecma: 2020, + compress: { + drop_console: true, + drop_debugger: true, + pure_funcs: ["console.log", "console.info", "console.debug"], + sequences: true, + dead_code: true, + conditionals: true, + booleans: true, + unused: true, + if_return: true, + join_vars: true, + hoist_funs: true, + hoist_vars: true, + reduce_vars: true, + }, + mangle: true, + format: { + comments: false, + }, + keep_classnames: false, + keep_fnames: false, + }, + }, + }, +}); diff --git a/webroot/package.json b/webroot/package.json new file mode 100644 index 0000000..b50b768 --- /dev/null +++ b/webroot/package.json @@ -0,0 +1,22 @@ +{ + "name": "ubuntu-webroot", + "type": "module", + "private": true, + "scripts": { + "generate": "nuxt generate", + "postinstall": "nuxt prepare", + "format": "prettier --write .", + "lint": "prettier --check .", + "devtest": "bun generate; adb shell rm -rf /data/adb/modules/ubuntu-chroot/webroot/*; adb push .output/public/* /data/adb/modules/ubuntu-chroot/webroot" + }, + "dependencies": { + "nuxt": "^4.2.2", + "vue": "^3.5.25", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@types/node": "^25.0.1", + "prettier": "^3.7.4", + "terser": "^5.44.1" + } +} diff --git a/webroot/public/command-executor.js b/webroot/public/command-executor.js new file mode 100644 index 0000000..2ed6266 --- /dev/null +++ b/webroot/public/command-executor.js @@ -0,0 +1,148 @@ +// This entire crap is AI generated, don't blame me for the mess + +class CommandExecutor { + constructor() { + const scriptTag = document.querySelector( + 'script[src*="command-executor.js"]', + ); + this.assetsPath = scriptTag + ? scriptTag.src.replace(/\/command-executor\.js$/, "") + : "./assets"; + this.execMethod = this.detectExecutionMethod(); + this.runningCommands = new Map(); + } + + detectExecutionMethod() { + if (typeof ksu !== "undefined" && ksu.exec) { + return "ksu"; + } else if (window.SULib) { + return "sulib"; + } + return "none"; + } + + /** + * Execute a command with real-time output streaming + * @param {string} command - Command to execute + * @param {boolean} asRoot - Run as root + * @param {Object} callbacks - { onOutput, onError, onComplete } + * @returns {string} commandId - Unique ID for this command + */ + executeAsync(command, asRoot = true, callbacks = {}) { + const commandId = `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const fullCommand = asRoot ? `su -c "${command}"` : command; + + const { onOutput, onError, onComplete } = callbacks; + + this.runningCommands.set(commandId, { + command: fullCommand, + startTime: Date.now(), + }); + + if (this.execMethod === "ksu") { + const callback = `ksu_callback_${commandId}`; + window[callback] = (exitCode, stdout, stderr) => { + delete window[callback]; + this.runningCommands.delete(commandId); + + if (exitCode === 0) { + if (stdout && onOutput) onOutput(stdout); + if (onComplete) + onComplete({ success: true, exitCode, output: stdout }); + } else { + // --- FIXED --- + // Combine stdout and stderr for a more informative error message, + // as many scripts write errors to stdout before exiting. + const combinedOutput = [stdout, stderr].filter(Boolean).join("\n"); + const errorMessage = combinedOutput.trim() || `exit:${exitCode}`; + + if (errorMessage && onError) onError(errorMessage); + if (onComplete) + onComplete({ success: false, exitCode, error: errorMessage }); + } + }; + + try { + ksu.exec(fullCommand, "{}", callback); + if (onOutput) onOutput(`[Executing: ${command}]\n`); + } catch (e) { + // Clean up on synchronous error + if (window[callback]) delete window[callback]; + this.runningCommands.delete(commandId); + if (onError) onError(String(e)); + if (onComplete) onComplete({ success: false, error: String(e) }); + } + } else if (this.execMethod === "sulib") { + try { + if (onOutput) onOutput(`[Executing: ${command}]\n`); + + window.SULib.exec(fullCommand, (result) => { + this.runningCommands.delete(commandId); + + if (result.success) { + if (result.output && onOutput) onOutput(result.output); + if (onComplete) + onComplete({ success: true, output: result.output }); + } else { + // --- FIXED --- + // Combine output and error for a more informative message on failure. + const combinedOutput = [result.output, result.error] + .filter(Boolean) + .join("\n"); + const errorMessage = + combinedOutput.trim() || + `Command failed with exit code ${result.exitCode || "unknown"}`; + + if (errorMessage && onError) onError(errorMessage); + if (onComplete) onComplete({ success: false, error: errorMessage }); + } + }); + } catch (e) { + // Clean up on synchronous error + this.runningCommands.delete(commandId); + if (onError) onError(String(e)); + if (onComplete) onComplete({ success: false, error: String(e) }); + } + } else { + this.runningCommands.delete(commandId); + const errorMsg = + "No root execution method available (KernelSU or libsuperuser not detected)."; + if (onError) onError(errorMsg); + if (onComplete) onComplete({ success: false, error: errorMsg }); + } + + return commandId; + } + + /** + * Legacy execute method for backward compatibility + */ + async execute(command, asRoot = true) { + return new Promise((resolve, reject) => { + this.executeAsync(command, asRoot, { + onComplete: (result) => { + if (result.success) { + resolve(result.output || ""); + } else { + reject(new Error(result.error || "Command failed")); + } + }, + }); + }); + } + + isCommandRunning(commandId) { + return this.runningCommands.has(commandId); + } + + getRunningCommands() { + return Array.from(this.runningCommands.entries()).map(([id, info]) => ({ + id, + ...info, + duration: Date.now() - info.startTime, + })); + } +} + +// Expose a tolerant global instance +window.cmdExec = new CommandExecutor(); diff --git a/webroot/tsconfig.json b/webroot/tsconfig.json new file mode 100644 index 0000000..307b213 --- /dev/null +++ b/webroot/tsconfig.json @@ -0,0 +1,18 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +}