generated from nhcarrigan/template
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c829ec97c4
|
|||
|
dac875c413
|
@@ -15,3 +15,9 @@ Thumbs.db
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.lcov
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: help install install-ts install-py build lint lint-ts lint-py format format-py format-check format-check-py test clean run
|
||||
.PHONY: help install install-ts install-py build lint lint-ts lint-py format format-py format-check format-check-py test test-ts test-py coverage coverage-ts coverage-py clean run
|
||||
|
||||
# Default target - show help
|
||||
help:
|
||||
@@ -12,11 +12,17 @@ help:
|
||||
@echo " make lint-py - Run Python linter only"
|
||||
@echo " make format - Format Python code"
|
||||
@echo " make format-check - Check Python formatting without modifying"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make test - Run all tests (TypeScript and Python)"
|
||||
@echo " make test-ts - Run TypeScript tests only"
|
||||
@echo " make test-py - Run Python tests only"
|
||||
@echo " make coverage - Run all tests with coverage"
|
||||
@echo " make coverage-ts - Run TypeScript tests with coverage"
|
||||
@echo " make coverage-py - Run Python tests with coverage"
|
||||
@echo " make clean - Clean build artifacts and caches"
|
||||
@echo ""
|
||||
@echo "Running scripts:"
|
||||
@echo " make run - Interactive script runner (select language, category, script)"
|
||||
@echo " make run-bash - Run a bash script (e.g., make run-bash SCRIPT=bash/adb/push.sh)"
|
||||
|
||||
# Install all dependencies
|
||||
install: install-ts install-py
|
||||
@@ -60,10 +66,27 @@ format-check: format-check-py
|
||||
format-check-py:
|
||||
cd python && uv run ruff format --check .
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
@echo "No tests configured yet"
|
||||
@exit 0
|
||||
# Run all tests
|
||||
test: test-ts test-py
|
||||
|
||||
# Run TypeScript tests
|
||||
test-ts:
|
||||
cd typescript && pnpm test
|
||||
|
||||
# Run Python tests
|
||||
test-py:
|
||||
cd python && uv run pytest -v
|
||||
|
||||
# Run all tests with coverage
|
||||
coverage: coverage-ts coverage-py
|
||||
|
||||
# Run TypeScript tests with coverage
|
||||
coverage-ts:
|
||||
cd typescript && pnpm test:coverage
|
||||
|
||||
# Run Python tests with coverage
|
||||
coverage-py:
|
||||
cd python && uv run pytest --cov=. --cov-report=term-missing -v
|
||||
|
||||
# Clean build artifacts and caches
|
||||
clean:
|
||||
@@ -76,3 +99,16 @@ clean:
|
||||
# Interactive script runner
|
||||
run:
|
||||
@./run.sh
|
||||
|
||||
# Run a specific bash script
|
||||
run-bash:
|
||||
@if [ -z "$(SCRIPT)" ]; then \
|
||||
echo "Please specify a script: make run-bash SCRIPT=bash/adb/push.sh"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f "$(SCRIPT)" ]; then \
|
||||
echo "Script not found: $(SCRIPT)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Running bash script: $(SCRIPT)"
|
||||
@op run --env-file=prod.env --no-masking -- bash "$(SCRIPT)"
|
||||
@@ -0,0 +1,150 @@
|
||||
# ADB File Transfer Scripts
|
||||
|
||||
Easy-to-use bash scripts for transferring files between your computer and Android device using ADB.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android Debug Bridge (ADB) installed and in your PATH
|
||||
- USB debugging enabled on your Android device
|
||||
- Device connected via USB cable
|
||||
|
||||
### Installing ADB
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install android-tools-adb
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download from [Android Developer website](https://developer.android.com/studio/releases/platform-tools)
|
||||
|
||||
## Scripts Overview
|
||||
|
||||
### 🚀 push.sh - Push files to Android
|
||||
Transfer files from your computer to your Android device.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Interactive mode (recommended)
|
||||
./push.sh
|
||||
|
||||
# Direct mode
|
||||
./push.sh ~/Documents/file.pdf /sdcard/Download/
|
||||
./push.sh ~/Pictures/vacation/ /sdcard/Pictures/
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Interactive mode with common destination suggestions
|
||||
- Automatic path validation
|
||||
- Directory creation if needed
|
||||
- Progress feedback
|
||||
- Support for both files and directories
|
||||
|
||||
### 📥 pull.sh - Pull files from Android
|
||||
Transfer files from your Android device to your computer.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Interactive mode (recommended)
|
||||
./pull.sh
|
||||
|
||||
# Direct mode
|
||||
./pull.sh /sdcard/DCIM/Camera/ ~/Pictures/phone-backup/
|
||||
./pull.sh /sdcard/Download/document.pdf ~/Downloads/
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Browse Android filesystem interactively
|
||||
- Quick access to common folders
|
||||
- File count and size summary
|
||||
- Creates destination directories automatically
|
||||
- Support for both files and directories
|
||||
|
||||
### 📱 adb-transfer.sh - All-in-One Menu
|
||||
Interactive menu system for all file transfer operations.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./adb-transfer.sh
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Device status and information display
|
||||
- Quick actions (backup photos, screenshots, WhatsApp media)
|
||||
- Access to push/pull scripts
|
||||
- Device information viewer
|
||||
- Beautiful CLI interface
|
||||
|
||||
## Common Android Paths
|
||||
|
||||
- `/sdcard/Download/` - Downloads folder
|
||||
- `/sdcard/DCIM/Camera/` - Camera photos and videos
|
||||
- `/sdcard/Pictures/` - General pictures folder
|
||||
- `/sdcard/Screenshots/` - Screenshots (varies by device)
|
||||
- `/sdcard/WhatsApp/Media/` - WhatsApp media files
|
||||
- `/sdcard/Documents/` - Documents folder
|
||||
- `/sdcard/Music/` - Music files
|
||||
- `/sdcard/Movies/` - Video files
|
||||
|
||||
## Making Scripts Executable
|
||||
|
||||
First time setup:
|
||||
```bash
|
||||
chmod +x push.sh pull.sh adb-transfer.sh
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Backup all camera photos
|
||||
```bash
|
||||
./pull.sh /sdcard/DCIM/Camera/ ~/Pictures/android-backup/
|
||||
```
|
||||
|
||||
### Push multiple PDFs to Downloads
|
||||
```bash
|
||||
./push.sh ~/Documents/*.pdf /sdcard/Download/
|
||||
```
|
||||
|
||||
### Interactive file browser
|
||||
```bash
|
||||
./pull.sh
|
||||
# Then select option 1 to browse filesystem
|
||||
```
|
||||
|
||||
### Quick backup using menu
|
||||
```bash
|
||||
./adb-transfer.sh
|
||||
# Select option 3 for quick actions
|
||||
# Select option 1 to backup all camera photos
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No device connected" error
|
||||
1. Check USB cable connection
|
||||
2. Enable USB debugging: Settings → Developer options → USB debugging
|
||||
3. Accept the authorization prompt on your phone
|
||||
4. Try `adb devices` to verify connection
|
||||
|
||||
### "Permission denied" errors
|
||||
- Some system directories require root access
|
||||
- Stick to `/sdcard/` paths for normal usage
|
||||
|
||||
### Slow transfer speeds
|
||||
- Use USB 3.0 ports and cables when possible
|
||||
- Large files/directories take time - the scripts show progress
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use interactive mode** - It's easier and prevents typos
|
||||
2. **Backup regularly** - Use the quick actions menu for easy backups
|
||||
3. **Check free space** - Use device info option to see available storage
|
||||
4. **Organize transfers** - The scripts create timestamped folders for backups
|
||||
|
||||
## Created with 💕 by Naomi & Hikari
|
||||
Executable
+225
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
# Interactive ADB file transfer menu
|
||||
# Author: Naomi Carrigan & Hikari 💕
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Color codes for pretty output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ASCII art banner
|
||||
banner() {
|
||||
echo -e "${CYAN}"
|
||||
echo "╔═══════════════════════════════════╗"
|
||||
echo "║ 📱 ADB File Transfer 📱 ║"
|
||||
echo "║ Made with 💕 by Hikari ║"
|
||||
echo "╚═══════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
}
|
||||
|
||||
# Check if ADB is available and device connected
|
||||
check_adb_status() {
|
||||
if ! command -v adb &> /dev/null; then
|
||||
echo -e "${RED}❌ ADB not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if adb devices | grep -q "device$"; then
|
||||
local device=$(adb devices | grep "device$" | awk '{print $1}')
|
||||
local model=$(adb shell getprop ro.product.model 2>/dev/null | tr -d '\r\n')
|
||||
local android_version=$(adb shell getprop ro.build.version.release 2>/dev/null | tr -d '\r\n')
|
||||
|
||||
echo -e "${GREEN}✅ Device connected${NC}"
|
||||
echo -e "${YELLOW}📱 Model: ${model:-Unknown}${NC}"
|
||||
echo -e "${YELLOW}🤖 Android: ${android_version:-Unknown}${NC}"
|
||||
echo -e "${YELLOW}🔌 ID: ${device}${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}❌ No device connected${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Quick actions menu
|
||||
quick_actions() {
|
||||
echo -e "\n${BLUE}⚡ Quick Actions${NC}"
|
||||
echo "1) Pull all photos from camera"
|
||||
echo "2) Pull all screenshots"
|
||||
echo "3) Pull WhatsApp media"
|
||||
echo "4) Push files to Downloads"
|
||||
echo "5) Back to main menu"
|
||||
echo ""
|
||||
|
||||
read -p "Select action (1-5): " action
|
||||
|
||||
case $action in
|
||||
1)
|
||||
# Pull camera photos
|
||||
echo -e "${CYAN}📸 Pulling camera photos...${NC}"
|
||||
local backup_dir="$HOME/Pictures/android-camera-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
if adb pull /sdcard/DCIM/Camera/ "$backup_dir/"; then
|
||||
echo -e "${GREEN}✅ Photos backed up to: $backup_dir${NC}"
|
||||
echo "Files: $(find "$backup_dir" -type f | wc -l)"
|
||||
echo "Size: $(du -sh "$backup_dir" | cut -f1)"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to pull photos${NC}"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# Pull screenshots
|
||||
echo -e "${CYAN}📸 Pulling screenshots...${NC}"
|
||||
local screenshot_dir="$HOME/Pictures/android-screenshots-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$screenshot_dir"
|
||||
|
||||
# Try multiple possible screenshot locations
|
||||
local found=false
|
||||
for path in "/sdcard/Pictures/Screenshots" "/sdcard/Screenshots" "/sdcard/DCIM/Screenshots"; do
|
||||
if adb shell "test -d '$path' && echo 'exists'" 2>/dev/null | grep -q "exists"; then
|
||||
if adb pull "$path" "$screenshot_dir/"; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$found" == true ]]; then
|
||||
echo -e "${GREEN}✅ Screenshots backed up to: $screenshot_dir${NC}"
|
||||
echo "Files: $(find "$screenshot_dir" -type f | wc -l)"
|
||||
else
|
||||
echo -e "${RED}❌ No screenshots found or failed to pull${NC}"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
# Pull WhatsApp media
|
||||
echo -e "${CYAN}💬 Pulling WhatsApp media...${NC}"
|
||||
local whatsapp_dir="$HOME/Pictures/whatsapp-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$whatsapp_dir"
|
||||
|
||||
if adb shell "test -d '/sdcard/WhatsApp/Media' && echo 'exists'" 2>/dev/null | grep -q "exists"; then
|
||||
if adb pull /sdcard/WhatsApp/Media/ "$whatsapp_dir/"; then
|
||||
echo -e "${GREEN}✅ WhatsApp media backed up to: $whatsapp_dir${NC}"
|
||||
echo "Files: $(find "$whatsapp_dir" -type f | wc -l)"
|
||||
echo "Size: $(du -sh "$whatsapp_dir" | cut -f1)"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to pull WhatsApp media${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ WhatsApp media folder not found${NC}"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
# Push to Downloads
|
||||
echo -e "${CYAN}📤 Push files to Downloads folder${NC}"
|
||||
read -p "Enter file/folder path to push: " -e local_path
|
||||
local_path="${local_path/#\~/$HOME}"
|
||||
|
||||
if [[ -e "$local_path" ]]; then
|
||||
if adb push "$local_path" /sdcard/Download/; then
|
||||
echo -e "${GREEN}✅ Files pushed to Downloads folder${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to push files${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ Path not found: $local_path${NC}"
|
||||
fi
|
||||
;;
|
||||
5)
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Invalid choice${NC}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
}
|
||||
|
||||
# Device info
|
||||
device_info() {
|
||||
echo -e "\n${BLUE}📱 Device Information${NC}"
|
||||
echo "================================"
|
||||
|
||||
# Basic info
|
||||
echo -e "${YELLOW}Model:${NC} $(adb shell getprop ro.product.model 2>/dev/null | tr -d '\r\n')"
|
||||
echo -e "${YELLOW}Manufacturer:${NC} $(adb shell getprop ro.product.manufacturer 2>/dev/null | tr -d '\r\n')"
|
||||
echo -e "${YELLOW}Android Version:${NC} $(adb shell getprop ro.build.version.release 2>/dev/null | tr -d '\r\n')"
|
||||
echo -e "${YELLOW}SDK Version:${NC} $(adb shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\r\n')"
|
||||
|
||||
# Storage info
|
||||
echo -e "\n${CYAN}Storage:${NC}"
|
||||
adb shell df -h /sdcard | tail -n 1 | awk '{print " Used: " $3 " / " $2 " (" $5 ")"}'
|
||||
|
||||
# Battery info
|
||||
echo -e "\n${CYAN}Battery:${NC}"
|
||||
local battery_level=$(adb shell dumpsys battery | grep "level:" | awk '{print $2}')
|
||||
local battery_status=$(adb shell dumpsys battery | grep "status:" | awk '{print $2}')
|
||||
echo " Level: ${battery_level}%"
|
||||
echo " Status: ${battery_status}"
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
}
|
||||
|
||||
# Main menu
|
||||
main_menu() {
|
||||
while true; do
|
||||
clear
|
||||
banner
|
||||
|
||||
# Check device status
|
||||
echo -e "${MAGENTA}Device Status:${NC}"
|
||||
if ! check_adb_status; then
|
||||
echo -e "\n${YELLOW}Please connect your Android device and enable USB debugging${NC}"
|
||||
echo ""
|
||||
read -p "Press Enter to retry or Ctrl+C to exit..."
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Main Menu:${NC}"
|
||||
echo "1) Push files to Android"
|
||||
echo "2) Pull files from Android"
|
||||
echo "3) Quick actions"
|
||||
echo "4) Device information"
|
||||
echo "5) Exit"
|
||||
echo ""
|
||||
|
||||
read -p "Select option (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
echo -e "\n${CYAN}Starting push mode...${NC}\n"
|
||||
bash "$(dirname "$0")/push.sh"
|
||||
;;
|
||||
2)
|
||||
echo -e "\n${CYAN}Starting pull mode...${NC}\n"
|
||||
bash "$(dirname "$0")/pull.sh"
|
||||
;;
|
||||
3)
|
||||
quick_actions
|
||||
;;
|
||||
4)
|
||||
device_info
|
||||
;;
|
||||
5)
|
||||
echo -e "${GREEN}👋 Goodbye!${NC}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Invalid choice${NC}"
|
||||
sleep 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Run main menu
|
||||
main_menu
|
||||
Executable
+298
@@ -0,0 +1,298 @@
|
||||
#!/bin/bash
|
||||
# Pull files from Android device via ADB
|
||||
# Author: Naomi Carrigan & Hikari 💕
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Color codes for pretty output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo -e "${BLUE}Usage: $0 [android_source] [local_destination]${NC}"
|
||||
echo -e "${YELLOW}If no arguments provided, interactive mode will start${NC}"
|
||||
echo ""
|
||||
echo "Common Android paths:"
|
||||
echo " /sdcard/Download/ - Downloads folder"
|
||||
echo " /sdcard/DCIM/ - Camera folder"
|
||||
echo " /sdcard/Pictures/ - Pictures folder"
|
||||
echo " /sdcard/WhatsApp/ - WhatsApp media"
|
||||
echo " /sdcard/Screenshots/ - Screenshots"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 /sdcard/DCIM/Camera/ ~/Pictures/phone-backup/"
|
||||
echo " $0 /sdcard/Download/document.pdf ~/Downloads/"
|
||||
echo " $0"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to check if ADB is installed and device is connected
|
||||
check_adb() {
|
||||
if ! command -v adb &> /dev/null; then
|
||||
echo -e "${RED}❌ Error: ADB is not installed or not in PATH${NC}"
|
||||
echo "Please install Android Debug Bridge (ADB) first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if device is connected
|
||||
if ! adb devices | grep -q "device$"; then
|
||||
echo -e "${RED}❌ Error: No Android device connected${NC}"
|
||||
echo "Please connect your device and enable USB debugging"
|
||||
echo ""
|
||||
echo "Current devices:"
|
||||
adb devices
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to browse Android filesystem
|
||||
browse_android_fs() {
|
||||
local current_path="${1:-/sdcard/}"
|
||||
|
||||
while true; do
|
||||
echo -e "${CYAN}📱 Current path: $current_path${NC}"
|
||||
echo ""
|
||||
|
||||
# List contents
|
||||
echo -e "${YELLOW}Contents:${NC}"
|
||||
local items=$(adb shell "ls -la '$current_path' 2>/dev/null" | tail -n +2 | awk '{print $NF}' | grep -v "^\.$" | grep -v "^\.\.$")
|
||||
|
||||
local i=1
|
||||
local -a entries=()
|
||||
|
||||
# Add parent directory option if not at root
|
||||
if [[ "$current_path" != "/" ]]; then
|
||||
echo "0) .. (Go up)"
|
||||
entries[0]=".."
|
||||
fi
|
||||
|
||||
# List items
|
||||
while IFS= read -r item; do
|
||||
if [[ -n "$item" ]]; then
|
||||
# Check if directory
|
||||
if adb shell "test -d '$current_path/$item' 2>/dev/null && echo 'dir'" | grep -q "dir"; then
|
||||
echo "$i) $item/"
|
||||
else
|
||||
echo "$i) $item"
|
||||
fi
|
||||
entries[$i]="$item"
|
||||
((i++))
|
||||
fi
|
||||
done <<< "$items"
|
||||
|
||||
echo ""
|
||||
echo "s) Select this path"
|
||||
echo "q) Quit"
|
||||
echo ""
|
||||
|
||||
read -p "Enter choice: " choice
|
||||
|
||||
case $choice in
|
||||
s|S)
|
||||
echo "$current_path"
|
||||
return 0
|
||||
;;
|
||||
q|Q)
|
||||
return 1
|
||||
;;
|
||||
0)
|
||||
if [[ "$current_path" != "/" ]]; then
|
||||
current_path=$(dirname "$current_path")
|
||||
fi
|
||||
;;
|
||||
[0-9]*)
|
||||
if [[ -n "${entries[$choice]:-}" ]]; then
|
||||
local selected="${entries[$choice]}"
|
||||
local new_path="$current_path/$selected"
|
||||
# Clean up path
|
||||
new_path=$(echo "$new_path" | sed 's|//|/|g')
|
||||
|
||||
# Check if it's a directory
|
||||
if adb shell "test -d '$new_path' 2>/dev/null && echo 'dir'" | grep -q "dir"; then
|
||||
current_path="$new_path"
|
||||
else
|
||||
# It's a file, return it
|
||||
echo "$new_path"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Invalid choice${NC}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Invalid choice${NC}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
# Interactive mode
|
||||
interactive_mode() {
|
||||
echo -e "${BLUE}📲 ADB Pull - Interactive Mode${NC}"
|
||||
echo ""
|
||||
|
||||
# Choose method
|
||||
echo -e "${YELLOW}How would you like to select the source?${NC}"
|
||||
echo "1) Browse Android filesystem"
|
||||
echo "2) Enter path directly"
|
||||
echo "3) Quick access to common folders"
|
||||
echo ""
|
||||
|
||||
read -p "Select method (1-3): " method
|
||||
|
||||
case $method in
|
||||
1)
|
||||
# Browse mode
|
||||
if source_path=$(browse_android_fs); then
|
||||
echo -e "${GREEN}Selected: $source_path${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Cancelled${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# Direct path entry
|
||||
read -p "Enter Android source path: " -e source_path
|
||||
;;
|
||||
3)
|
||||
# Quick access
|
||||
echo ""
|
||||
echo -e "${YELLOW}Common locations:${NC}"
|
||||
echo "1) /sdcard/DCIM/Camera/"
|
||||
echo "2) /sdcard/Pictures/"
|
||||
echo "3) /sdcard/Download/"
|
||||
echo "4) /sdcard/WhatsApp/Media/"
|
||||
echo "5) /sdcard/Screenshots/"
|
||||
echo "6) /sdcard/Documents/"
|
||||
echo "7) /sdcard/Music/"
|
||||
echo ""
|
||||
|
||||
read -p "Select location (1-7): " quick_choice
|
||||
|
||||
case $quick_choice in
|
||||
1) source_path="/sdcard/DCIM/Camera/" ;;
|
||||
2) source_path="/sdcard/Pictures/" ;;
|
||||
3) source_path="/sdcard/Download/" ;;
|
||||
4) source_path="/sdcard/WhatsApp/Media/" ;;
|
||||
5) source_path="/sdcard/Screenshots/" ;;
|
||||
6) source_path="/sdcard/Documents/" ;;
|
||||
7) source_path="/sdcard/Music/" ;;
|
||||
*)
|
||||
echo -e "${RED}❌ Invalid choice${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Invalid choice${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get destination
|
||||
echo ""
|
||||
read -p "Enter local destination path (default: current directory): " -e dest_path
|
||||
dest_path="${dest_path:-.}"
|
||||
dest_path="${dest_path/#\~/$HOME}"
|
||||
|
||||
# Pull the file/directory
|
||||
pull_from_android "$source_path" "$dest_path"
|
||||
}
|
||||
|
||||
# Function to pull files from Android
|
||||
pull_from_android() {
|
||||
local source="$1"
|
||||
local dest="$2"
|
||||
|
||||
# Validate source exists
|
||||
if ! adb shell "test -e '$source' 2>/dev/null && echo 'exists'" | grep -q "exists"; then
|
||||
echo -e "${RED}❌ Error: Source '$source' does not exist on device${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create destination directory if needed
|
||||
if [[ ! -d "$dest" ]]; then
|
||||
mkdir -p "$dest"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📥 Pulling from Android device...${NC}"
|
||||
echo "Source: $source"
|
||||
echo "Destination: $dest"
|
||||
echo ""
|
||||
|
||||
# Check if source is directory
|
||||
if adb shell "test -d '$source' 2>/dev/null && echo 'dir'" | grep -q "dir"; then
|
||||
echo -e "${YELLOW}Pulling directory...${NC}"
|
||||
|
||||
# Count files for progress
|
||||
local file_count=$(adb shell "find '$source' -type f 2>/dev/null | wc -l" | tr -d '\r\n')
|
||||
echo "Found $file_count files to pull"
|
||||
echo ""
|
||||
|
||||
if adb pull "$source" "$dest"; then
|
||||
echo -e "${GREEN}✅ Directory pulled successfully!${NC}"
|
||||
|
||||
# Show summary
|
||||
local pulled_dir="$dest/$(basename "$source")"
|
||||
if [[ -d "$pulled_dir" ]]; then
|
||||
echo ""
|
||||
echo -e "${BLUE}📊 Summary:${NC}"
|
||||
echo "Location: $pulled_dir"
|
||||
echo "Files: $(find "$pulled_dir" -type f | wc -l)"
|
||||
echo "Total size: $(du -sh "$pulled_dir" | cut -f1)"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ Failed to pull directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Single file
|
||||
if adb pull "$source" "$dest"; then
|
||||
echo -e "${GREEN}✅ File pulled successfully!${NC}"
|
||||
|
||||
# Show file info
|
||||
local filename=$(basename "$source")
|
||||
local full_path="$dest/$filename"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📄 File info:${NC}"
|
||||
ls -lh "$full_path"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to pull file${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script
|
||||
main() {
|
||||
check_adb
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
# No arguments, run interactive mode
|
||||
interactive_mode
|
||||
elif [[ $# -eq 1 ]]; then
|
||||
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
usage
|
||||
else
|
||||
echo -e "${RED}❌ Error: Missing destination path${NC}"
|
||||
usage
|
||||
fi
|
||||
elif [[ $# -eq 2 ]]; then
|
||||
# Arguments provided
|
||||
dest_path="${2/#\~/$HOME}"
|
||||
pull_from_android "$1" "$dest_path"
|
||||
else
|
||||
echo -e "${RED}❌ Error: Too many arguments${NC}"
|
||||
usage
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+202
@@ -0,0 +1,202 @@
|
||||
#!/bin/bash
|
||||
# Push files to Android device via ADB
|
||||
# Author: Naomi Carrigan & Hikari 💕
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Color codes for pretty output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo -e "${BLUE}Usage: $0 [source_file/directory] [android_destination]${NC}"
|
||||
echo -e "${YELLOW}If no arguments provided, interactive mode will start${NC}"
|
||||
echo ""
|
||||
echo "Common Android paths:"
|
||||
echo " /sdcard/Download/ - Downloads folder"
|
||||
echo " /sdcard/DCIM/ - Camera folder"
|
||||
echo " /sdcard/Pictures/ - Pictures folder"
|
||||
echo " /sdcard/Documents/ - Documents folder"
|
||||
echo " /sdcard/Music/ - Music folder"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 photo.jpg /sdcard/Pictures/"
|
||||
echo " $0 ~/Documents/file.pdf /sdcard/Download/"
|
||||
echo " $0"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to check if ADB is installed and device is connected
|
||||
check_adb() {
|
||||
if ! command -v adb &> /dev/null; then
|
||||
echo -e "${RED}❌ Error: ADB is not installed or not in PATH${NC}"
|
||||
echo "Please install Android Debug Bridge (ADB) first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if device is connected
|
||||
if ! adb devices | grep -q "device$"; then
|
||||
echo -e "${RED}❌ Error: No Android device connected${NC}"
|
||||
echo "Please connect your device and enable USB debugging"
|
||||
echo ""
|
||||
echo "Current devices:"
|
||||
adb devices
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to validate Android path
|
||||
validate_android_path() {
|
||||
local path="$1"
|
||||
|
||||
# Check if path starts with /
|
||||
if [[ ! "$path" =~ ^/ ]]; then
|
||||
echo -e "${YELLOW}⚠️ Warning: Path doesn't start with /, prepending /sdcard/${NC}"
|
||||
path="/sdcard/$path"
|
||||
fi
|
||||
|
||||
# Check if destination exists (create if it's a directory)
|
||||
if [[ "$path" =~ /$ ]]; then
|
||||
adb shell "mkdir -p '$path' 2>/dev/null || true"
|
||||
else
|
||||
# Check if parent directory exists
|
||||
local parent_dir=$(dirname "$path")
|
||||
adb shell "mkdir -p '$parent_dir' 2>/dev/null || true"
|
||||
fi
|
||||
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
# Interactive mode
|
||||
interactive_mode() {
|
||||
echo -e "${BLUE}🚀 ADB Push - Interactive Mode${NC}"
|
||||
echo ""
|
||||
|
||||
# Get source file/directory
|
||||
read -p "Enter source file/directory path: " -e source_path
|
||||
|
||||
# Expand tilde and validate source
|
||||
source_path="${source_path/#\~/$HOME}"
|
||||
|
||||
if [[ ! -e "$source_path" ]]; then
|
||||
echo -e "${RED}❌ Error: Source '$source_path' does not exist${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show common destinations
|
||||
echo ""
|
||||
echo -e "${YELLOW}Common Android destinations:${NC}"
|
||||
echo "1) /sdcard/Download/"
|
||||
echo "2) /sdcard/Pictures/"
|
||||
echo "3) /sdcard/DCIM/"
|
||||
echo "4) /sdcard/Documents/"
|
||||
echo "5) /sdcard/Music/"
|
||||
echo "6) /sdcard/Movies/"
|
||||
echo "7) Custom path"
|
||||
echo ""
|
||||
|
||||
read -p "Select destination (1-7): " choice
|
||||
|
||||
case $choice in
|
||||
1) dest_path="/sdcard/Download/" ;;
|
||||
2) dest_path="/sdcard/Pictures/" ;;
|
||||
3) dest_path="/sdcard/DCIM/" ;;
|
||||
4) dest_path="/sdcard/Documents/" ;;
|
||||
5) dest_path="/sdcard/Music/" ;;
|
||||
6) dest_path="/sdcard/Movies/" ;;
|
||||
7)
|
||||
read -p "Enter custom destination path: " -e dest_path
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Invalid choice${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Push the file/directory
|
||||
push_to_android "$source_path" "$dest_path"
|
||||
}
|
||||
|
||||
# Function to push files to Android
|
||||
push_to_android() {
|
||||
local source="$1"
|
||||
local dest="$2"
|
||||
|
||||
# Validate destination path
|
||||
dest=$(validate_android_path "$dest")
|
||||
|
||||
echo -e "${BLUE}📦 Pushing to Android device...${NC}"
|
||||
echo "Source: $source"
|
||||
echo "Destination: $dest"
|
||||
echo ""
|
||||
|
||||
# Check if source is directory
|
||||
if [[ -d "$source" ]]; then
|
||||
echo -e "${YELLOW}Pushing directory...${NC}"
|
||||
# For directories, adb push handles recursion automatically
|
||||
if adb push "$source" "$dest"; then
|
||||
echo -e "${GREEN}✅ Directory pushed successfully!${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to push directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Single file
|
||||
if adb push "$source" "$dest"; then
|
||||
echo -e "${GREEN}✅ File pushed successfully!${NC}"
|
||||
|
||||
# Show file info on device
|
||||
if [[ "$dest" =~ /$ ]]; then
|
||||
# Destination is a directory
|
||||
filename=$(basename "$source")
|
||||
full_path="${dest}${filename}"
|
||||
else
|
||||
# Destination is a file
|
||||
full_path="$dest"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📱 File on device:${NC}"
|
||||
adb shell "ls -lh '$full_path'" 2>/dev/null || true
|
||||
else
|
||||
echo -e "${RED}❌ Failed to push file${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script
|
||||
main() {
|
||||
check_adb
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
# No arguments, run interactive mode
|
||||
interactive_mode
|
||||
elif [[ $# -eq 1 ]]; then
|
||||
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
usage
|
||||
else
|
||||
echo -e "${RED}❌ Error: Missing destination path${NC}"
|
||||
usage
|
||||
fi
|
||||
elif [[ $# -eq 2 ]]; then
|
||||
# Arguments provided
|
||||
source_path="${1/#\~/$HOME}"
|
||||
|
||||
if [[ ! -e "$source_path" ]]; then
|
||||
echo -e "${RED}❌ Error: Source '$source_path' does not exist${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
push_to_android "$source_path" "$2"
|
||||
else
|
||||
echo -e "${RED}❌ Error: Too many arguments${NC}"
|
||||
usage
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -13,9 +13,9 @@ DISCORD_CLIENT_SECRET="op://Private/Guild Counter/client secret"
|
||||
DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Amari/bot token"
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID="op://Private/Hetzner/S3 Access Key ID"
|
||||
AWS_SECRET_ACCESS_KEY="op://Private/Hetzner/S3 Secret Access Key"
|
||||
S3_ENDPOINT="op://Private/Hetzner/S3 Endpoint"
|
||||
AWS_ACCESS_KEY_ID="op://Private/S3/S3 Access Key ID"
|
||||
AWS_SECRET_ACCESS_KEY="op://Private/S3/S3 Secret Access Key"
|
||||
S3_ENDPOINT="op://Private/S3/S3 Endpoint"
|
||||
|
||||
# Gitea
|
||||
GITEA_TOKEN="op://Private/Gitea/token"
|
||||
|
||||
@@ -92,3 +92,8 @@ quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_functions = ["test_*"]
|
||||
@@ -1,5 +1,9 @@
|
||||
# Development dependencies
|
||||
ruff==0.14.14
|
||||
pytest==8.3.5
|
||||
pytest-mock==3.14.0
|
||||
responses==0.25.3
|
||||
pytest-asyncio==0.24.0
|
||||
|
||||
# Runtime dependencies
|
||||
requests==2.32.3
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Test package for ephemere Python scripts
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Tests for analyse_availability functions.
|
||||
|
||||
@copyright NHCarrigan
|
||||
@license Naomi's Public License
|
||||
@author Naomi Carrigan
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add the cohort directory to the path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "cohort"))
|
||||
|
||||
from analyse_availability import (
|
||||
analyze_applicant_availability,
|
||||
get_utc_blocks_for_hour,
|
||||
local_hour_to_utc,
|
||||
parse_time_slots,
|
||||
parse_utc_offset,
|
||||
)
|
||||
|
||||
|
||||
class TestParseUtcOffset:
|
||||
"""Tests for parse_utc_offset function."""
|
||||
|
||||
def test_positive_offset(self):
|
||||
"""Should parse positive UTC offset."""
|
||||
assert parse_utc_offset("Europe/London (UTC+0)") == 0
|
||||
assert parse_utc_offset("Europe/Paris (UTC+1)") == 1
|
||||
assert parse_utc_offset("Asia/Tokyo (UTC+9)") == 9
|
||||
|
||||
def test_negative_offset(self):
|
||||
"""Should parse negative UTC offset."""
|
||||
assert parse_utc_offset("America/New_York (UTC-5)") == -5
|
||||
assert parse_utc_offset("America/Los_Angeles (UTC-8)") == -8
|
||||
|
||||
def test_offset_with_minutes(self):
|
||||
"""Should parse offset with minutes component."""
|
||||
assert parse_utc_offset("Asia/Kolkata (UTC+5:30)") == 5.5
|
||||
assert parse_utc_offset("Asia/Kathmandu (UTC+5:45)") == 5.75
|
||||
|
||||
def test_negative_offset_with_minutes(self):
|
||||
"""Should parse negative offset with minutes."""
|
||||
assert parse_utc_offset("Canada/Newfoundland (UTC-3:30)") == -3.5
|
||||
|
||||
def test_no_match_returns_zero(self):
|
||||
"""Should return 0 when no UTC offset found."""
|
||||
assert parse_utc_offset("Unknown/Timezone") == 0
|
||||
assert parse_utc_offset("") == 0
|
||||
|
||||
|
||||
class TestParseTimeSlots:
|
||||
"""Tests for parse_time_slots function."""
|
||||
|
||||
def test_single_slot(self):
|
||||
"""Should parse a single time slot."""
|
||||
result = parse_time_slots("17:00-18:00")
|
||||
assert result == [(17, 18)]
|
||||
|
||||
def test_multiple_slots(self):
|
||||
"""Should parse multiple time slots separated by semicolon."""
|
||||
result = parse_time_slots("07:00-08:00; 19:00-20:00")
|
||||
assert result == [(7, 8), (19, 20)]
|
||||
|
||||
def test_na_values(self):
|
||||
"""Should return empty list for N/A values."""
|
||||
assert parse_time_slots("N/A") == []
|
||||
assert parse_time_slots("na") == []
|
||||
assert parse_time_slots("") == []
|
||||
|
||||
def test_wider_time_range(self):
|
||||
"""Should parse wider time ranges."""
|
||||
result = parse_time_slots("09:00-17:00")
|
||||
assert result == [(9, 17)]
|
||||
|
||||
def test_midnight_crossing_slots(self):
|
||||
"""Should parse time slots that approach midnight."""
|
||||
result = parse_time_slots("22:00-23:00")
|
||||
assert result == [(22, 23)]
|
||||
|
||||
|
||||
class TestLocalHourToUtc:
|
||||
"""Tests for local_hour_to_utc function."""
|
||||
|
||||
def test_positive_offset(self):
|
||||
"""Should convert local hour with positive UTC offset."""
|
||||
assert local_hour_to_utc(12, 1) == 11 # Noon in UTC+1 -> 11:00 UTC
|
||||
assert local_hour_to_utc(0, 9) == 15 # Midnight in UTC+9 -> 15:00 UTC
|
||||
|
||||
def test_negative_offset(self):
|
||||
"""Should convert local hour with negative UTC offset."""
|
||||
assert local_hour_to_utc(12, -5) == 17 # Noon in UTC-5 -> 17:00 UTC
|
||||
assert local_hour_to_utc(20, -8) == 4 # 8pm in UTC-8 -> 4:00 UTC (next day)
|
||||
|
||||
def test_wrapping_around_midnight(self):
|
||||
"""Should wrap around correctly at midnight."""
|
||||
assert local_hour_to_utc(1, 5) == 20 # 1am in UTC+5 -> 20:00 UTC (prev day)
|
||||
assert local_hour_to_utc(23, -3) == 2 # 11pm in UTC-3 -> 2:00 UTC (next day)
|
||||
|
||||
def test_zero_offset(self):
|
||||
"""Should return same hour for zero offset."""
|
||||
assert local_hour_to_utc(15, 0) == 15
|
||||
|
||||
|
||||
class TestGetUtcBlocksForHour:
|
||||
"""Tests for get_utc_blocks_for_hour function."""
|
||||
|
||||
def test_mornings_block(self):
|
||||
"""Should return mornings for 6-11 UTC."""
|
||||
assert "mornings" in get_utc_blocks_for_hour(6)
|
||||
assert "mornings" in get_utc_blocks_for_hour(9)
|
||||
assert "mornings" in get_utc_blocks_for_hour(11)
|
||||
assert "mornings" not in get_utc_blocks_for_hour(12)
|
||||
|
||||
def test_afternoons_block(self):
|
||||
"""Should return afternoons for 12-17 UTC."""
|
||||
assert "afternoons" in get_utc_blocks_for_hour(12)
|
||||
assert "afternoons" in get_utc_blocks_for_hour(15)
|
||||
assert "afternoons" in get_utc_blocks_for_hour(17)
|
||||
assert "afternoons" not in get_utc_blocks_for_hour(18)
|
||||
|
||||
def test_evenings_block(self):
|
||||
"""Should return evenings for 18-23 UTC."""
|
||||
assert "evenings" in get_utc_blocks_for_hour(18)
|
||||
assert "evenings" in get_utc_blocks_for_hour(21)
|
||||
assert "evenings" in get_utc_blocks_for_hour(23)
|
||||
assert "evenings" not in get_utc_blocks_for_hour(0)
|
||||
|
||||
def test_nights_block(self):
|
||||
"""Should return nights for 0-5 UTC."""
|
||||
assert "nights" in get_utc_blocks_for_hour(0)
|
||||
assert "nights" in get_utc_blocks_for_hour(3)
|
||||
assert "nights" in get_utc_blocks_for_hour(5)
|
||||
assert "nights" not in get_utc_blocks_for_hour(6)
|
||||
|
||||
|
||||
class TestAnalyzeApplicantAvailability:
|
||||
"""Tests for analyze_applicant_availability function."""
|
||||
|
||||
def test_basic_availability(self):
|
||||
"""Should analyze basic availability correctly."""
|
||||
day_slots = {
|
||||
"Monday": [(9, 12)],
|
||||
"Tuesday": [(9, 12)],
|
||||
"Wednesday": [(9, 12)],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
assert result["utc_offset"] == 0
|
||||
assert "mornings" in result["available_blocks"]
|
||||
|
||||
def test_no_availability(self):
|
||||
"""Should return empty blocks when no availability."""
|
||||
day_slots = {
|
||||
"Monday": [],
|
||||
"Tuesday": [],
|
||||
"Wednesday": [],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
assert result["available_blocks"] == []
|
||||
assert result["total_unique_utc_hours"] == 0
|
||||
|
||||
def test_timezone_conversion(self):
|
||||
"""Should correctly convert timezones."""
|
||||
day_slots = {
|
||||
"Monday": [(17, 20)], # 5pm-8pm local
|
||||
"Tuesday": [(17, 20)],
|
||||
"Wednesday": [(17, 20)],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability(
|
||||
"America/New_York (UTC-5)", day_slots
|
||||
)
|
||||
|
||||
assert result["utc_offset"] == -5
|
||||
# 5pm-8pm in UTC-5 = 22:00-01:00 UTC -> evenings/nights
|
||||
assert "evenings" in result["available_blocks"]
|
||||
|
||||
def test_block_count_threshold(self):
|
||||
"""Should only include blocks with 3+ occurrences."""
|
||||
day_slots = {
|
||||
"Monday": [(9, 10)], # Only 1 hour
|
||||
"Tuesday": [(9, 10)], # Only 1 hour
|
||||
"Wednesday": [],
|
||||
"Thursday": [],
|
||||
"Friday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
}
|
||||
result = analyze_applicant_availability("UTC (UTC+0)", day_slots)
|
||||
|
||||
# Only 2 hours, threshold is 3
|
||||
assert "mornings" not in result["available_blocks"]
|
||||
@@ -96,7 +96,7 @@ select_option() {
|
||||
|
||||
# Step 1: Select Language
|
||||
echo ""
|
||||
languages=("TypeScript" "Python")
|
||||
languages=("TypeScript" "Python" "Bash")
|
||||
select_option "Select a language:" "${languages[@]}"
|
||||
lang_index=$?
|
||||
language="${languages[$lang_index]}"
|
||||
@@ -109,7 +109,7 @@ if [ "$language" == "TypeScript" ]; then
|
||||
runner="pnpm tsx"
|
||||
# Get subdirectories as categories (excluding utils and interfaces)
|
||||
mapfile -t categories < <(find "$script_dir" -mindepth 1 -maxdepth 1 -type d ! -name 'utils' ! -name 'interfaces' -exec basename {} \; | sort)
|
||||
else
|
||||
elif [ "$language" == "Python" ]; then
|
||||
script_dir="python"
|
||||
runner="uv run python"
|
||||
# Get subdirectories as categories (excluding __pycache__ and .venv)
|
||||
@@ -118,6 +118,12 @@ else
|
||||
if ls "$script_dir"/*.py &>/dev/null 2>&1; then
|
||||
categories=("Root Scripts" "${categories[@]}")
|
||||
fi
|
||||
else # Bash
|
||||
script_dir="bash"
|
||||
runner="bash"
|
||||
# Get subdirectories as categories
|
||||
mapfile -t categories < <(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
|
||||
# Add "Root Scripts" option for bash files in root
|
||||
fi
|
||||
|
||||
if [ ${#categories[@]} -eq 0 ]; then
|
||||
@@ -134,13 +140,20 @@ echo -e "\n ${GREEN}$STAR Selected: ${WHITE}$category${RESET}\n"
|
||||
# Step 3: Get scripts in category
|
||||
if [ "$category" == "Root Scripts" ]; then
|
||||
search_dir="$script_dir"
|
||||
mapfile -t scripts < <(find "$search_dir" -maxdepth 1 -name "*.py" -exec basename {} \; | sort)
|
||||
if [ "$language" == "Python" ]; then
|
||||
mapfile -t scripts < <(find "$search_dir" -maxdepth 1 -name "*.py" -exec basename {} \; | sort)
|
||||
elif [ "$language" == "Bash" ]; then
|
||||
mapfile -t scripts < <(find "$search_dir" -maxdepth 1 -name "*.sh" -exec basename {} \; | sort)
|
||||
fi
|
||||
elif [ "$language" == "TypeScript" ]; then
|
||||
search_dir="$script_dir/$category"
|
||||
mapfile -t scripts < <(find "$search_dir" -name "*.ts" -exec basename {} \; | sort)
|
||||
else
|
||||
elif [ "$language" == "Python" ]; then
|
||||
search_dir="$script_dir/$category"
|
||||
mapfile -t scripts < <(find "$search_dir" -name "*.py" ! -name "__init__.py" -exec basename {} \; | sort)
|
||||
else # Bash
|
||||
search_dir="$script_dir/$category"
|
||||
mapfile -t scripts < <(find "$search_dir" -name "*.sh" -exec basename {} \; | sort)
|
||||
fi
|
||||
|
||||
if [ ${#scripts[@]} -eq 0 ]; then
|
||||
@@ -159,8 +172,10 @@ if [ "$category" == "Root Scripts" ]; then
|
||||
script_path="$script"
|
||||
elif [ "$language" == "TypeScript" ]; then
|
||||
script_path="src/$category/$script"
|
||||
else
|
||||
elif [ "$language" == "Python" ]; then
|
||||
script_path="$category/$script"
|
||||
else # Bash
|
||||
script_path="bash/$category/$script"
|
||||
fi
|
||||
|
||||
# Show what we're about to run
|
||||
@@ -178,10 +193,13 @@ if [ "$language" == "TypeScript" ]; then
|
||||
cd typescript
|
||||
echo -e " ${DIM}$ op run --env-file=../prod.env -- $runner $script_path${RESET}\n"
|
||||
op run --env-file=../prod.env --no-masking -- $runner "$script_path"
|
||||
else
|
||||
elif [ "$language" == "Python" ]; then
|
||||
cd python
|
||||
echo -e " ${DIM}$ op run --env-file=../prod.env -- $runner $script_path${RESET}\n"
|
||||
op run --env-file=../prod.env --no-masking -- $runner "$script_path"
|
||||
else # Bash
|
||||
echo -e " ${DIM}$ op run --env-file=prod.env -- $runner $script_path${RESET}\n"
|
||||
op run --env-file=prod.env --no-masking -- $runner "$script_path"
|
||||
fi
|
||||
|
||||
exit_code=$?
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "op run --env-file=prod.env --no-masking -- tsx"
|
||||
"start": "op run --env-file=prod.env --no-masking -- tsx",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -24,5 +27,9 @@
|
||||
"open": "11.0.0",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+332
@@ -44,9 +44,20 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
devDependencies:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))
|
||||
vitest:
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4(@types/node@24.3.0)(tsx@4.20.5)
|
||||
|
||||
packages:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -210,10 +221,31 @@ packages:
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1':
|
||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@es-joy/jsdoccomment@0.49.0':
|
||||
resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -589,9 +621,27 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@istanbuljs/schema@0.1.3':
|
||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@nhcarrigan/eslint-config@5.2.0':
|
||||
resolution: {integrity: sha512-YpTTqhviKMlRwKF+RC/GYiA5i2jTCmg8uftuiufldneNV5HMbGpTfBbV7tpa8++5mpYJc4+eZaf40QbDiz84dQ==}
|
||||
engines: {node: '>=22', pnpm: '>=9'}
|
||||
@@ -672,6 +722,10 @@ packages:
|
||||
'@octokit/types@14.1.0':
|
||||
resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@pkgr/core@0.1.2':
|
||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
@@ -1136,6 +1190,15 @@ packages:
|
||||
resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitest/coverage-v8@3.2.4':
|
||||
resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 3.2.4
|
||||
vitest: 3.2.4
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/eslint-plugin@1.1.24':
|
||||
resolution: {integrity: sha512-7IaENe4NNy33g0iuuy5bHY69JYYRjpv4lMx6H5Wp30W7ez2baLHwxsXF5TM4wa8JDYZt8ut99Ytoj7GiDO01hw==}
|
||||
peerDependencies:
|
||||
@@ -1200,10 +1263,18 @@ packages:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
are-docs-informative@0.0.2:
|
||||
resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1251,6 +1322,9 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-v8-to-istanbul@0.3.11:
|
||||
resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==}
|
||||
|
||||
async-function@1.0.0:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1434,12 +1508,18 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
electron-to-chromium@1.5.210:
|
||||
resolution: {integrity: sha512-20kSVv1tyNBN2VFsjCIJZfyvxqo7ylHPrJLME040f/030lzNMA7uQNpxtqJjWSNpccD8/2sqe53EAjrFPvQmjw==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
error-ex@1.3.2:
|
||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||
|
||||
@@ -1679,6 +1759,10 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1722,6 +1806,10 @@ packages:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
globals@13.24.0:
|
||||
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1779,6 +1867,9 @@ packages:
|
||||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1938,10 +2029,32 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
iterator.prototype@1.1.5:
|
||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
js-tokens@10.0.0:
|
||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -2013,9 +2126,19 @@ packages:
|
||||
loupe@3.2.1:
|
||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
magic-string@0.30.18:
|
||||
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
||||
|
||||
magicast@0.3.5:
|
||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||
|
||||
make-dir@4.0.0:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2042,6 +2165,10 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2127,6 +2254,9 @@ packages:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2150,6 +2280,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2379,6 +2513,9 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
std-env@3.9.0:
|
||||
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
|
||||
|
||||
@@ -2390,6 +2527,10 @@ packages:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string.prototype.matchall@4.0.12:
|
||||
resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2413,6 +2554,10 @@ packages:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-bom@3.0.0:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2443,6 +2588,10 @@ packages:
|
||||
resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -2658,6 +2807,14 @@ packages:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wsl-utils@0.3.0:
|
||||
resolution: {integrity: sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2672,6 +2829,11 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@@ -3157,8 +3319,23 @@ snapshots:
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@es-joy/jsdoccomment@0.49.0':
|
||||
dependencies:
|
||||
comment-parser: 1.4.1
|
||||
@@ -3451,8 +3628,31 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.0
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(playwright@1.55.0)(react@19.1.1)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.34.0)
|
||||
@@ -3560,6 +3760,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 25.1.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@pkgr/core@0.1.2': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.49.0':
|
||||
@@ -4163,6 +4366,25 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.41.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
ast-v8-to-istanbul: 0.3.11
|
||||
debug: 4.4.1
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6
|
||||
istanbul-reports: 3.2.0
|
||||
magic-string: 0.30.18
|
||||
magicast: 0.3.5
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/node@24.3.0)(tsx@4.20.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(tsx@4.20.5))':
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.9.2)
|
||||
@@ -4234,10 +4456,14 @@ snapshots:
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
are-docs-informative@0.0.2: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
@@ -4313,6 +4539,12 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-v8-to-istanbul@0.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
estree-walker: 3.0.3
|
||||
js-tokens: 10.0.0
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
@@ -4488,10 +4720,14 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
electron-to-chromium@1.5.210: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
error-ex@1.3.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
@@ -4900,6 +5136,11 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
@@ -4955,6 +5196,15 @@ snapshots:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
globals@13.24.0:
|
||||
dependencies:
|
||||
type-fest: 0.20.2
|
||||
@@ -5005,6 +5255,8 @@ snapshots:
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -5157,6 +5409,27 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
dependencies:
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
make-dir: 4.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
debug: 4.4.1
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
dependencies:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
iterator.prototype@1.1.5:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -5166,6 +5439,14 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
js-tokens@10.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
@@ -5226,10 +5507,22 @@ snapshots:
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
magic-string@0.30.18:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.3.5:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@@ -5251,6 +5544,8 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
@@ -5352,6 +5647,8 @@ snapshots:
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -5374,6 +5671,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -5627,6 +5929,8 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
std-env@3.9.0: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
@@ -5640,6 +5944,12 @@ snapshots:
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
string.prototype.matchall@4.0.12:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -5688,6 +5998,10 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
strip-bom@3.0.0: {}
|
||||
|
||||
strip-indent@3.0.0:
|
||||
@@ -5713,6 +6027,12 @@ snapshots:
|
||||
'@pkgr/core': 0.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.5.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -5960,6 +6280,18 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
wsl-utils@0.3.0:
|
||||
dependencies:
|
||||
is-wsl: 3.1.0
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getFiles } from "../getFiles.js";
|
||||
|
||||
describe("getFiles", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch files from a project", async() => {
|
||||
const mockFiles = [
|
||||
{ data: { id: 1, name: "file1.json", path: "/file1.json" } },
|
||||
{ data: { id: 2, name: "file2.json", path: "/file2.json" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockFiles });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockFiles);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination when there are 500+ files", async() => {
|
||||
const files500 = Array.from({ length: 500 }, (_, index) => {
|
||||
return { data: { id: index, name: `file${String(index)}.json` } };
|
||||
});
|
||||
const filesRemaining = [
|
||||
{ data: { id: 500, name: "file500.json" } },
|
||||
{ data: { id: 501, name: "file501.json" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: files500 });
|
||||
},
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: filesRemaining });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(502);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://api.crowdin.com/api/v2/projects/123/files?limit=500&offset=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no files exist", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: [] });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getFiles(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getLanguages } from "../getLanguages.js";
|
||||
|
||||
describe("getLanguages", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch project and return target language IDs", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 1,
|
||||
name: "Test Project",
|
||||
targetLanguageIds: [ "de", "fr", "es", "ja" ],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getLanguages(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([ "de", "fr", "es", "ja" ]);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no target languages", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: 2,
|
||||
name: "Empty Project",
|
||||
targetLanguageIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getLanguages(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should use the correct API URL", async() => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
targetLanguageIds: [ "en" ],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockResponse);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
await getLanguages(
|
||||
"789",
|
||||
"https://custom.crowdin.com/v2",
|
||||
"my-token",
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://custom.crowdin.com/v2/projects/789",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getStrings } from "../getStrings.js";
|
||||
|
||||
describe("getStrings", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch strings from a project", async() => {
|
||||
const mockStrings = [
|
||||
{ data: { id: 1, text: "Hello", identifier: "greeting" } },
|
||||
{ data: { id: 2, text: "Goodbye", identifier: "farewell" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockStrings });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"test-token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, text: "Hello", identifier: "greeting" },
|
||||
{ id: 2, text: "Goodbye", identifier: "farewell" },
|
||||
]);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://api.crowdin.com/api/v2/projects/123/strings?limit=500",
|
||||
{
|
||||
headers: {
|
||||
authorization: "Bearer test-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination when there are 500+ strings", async() => {
|
||||
const strings500 = Array.from({ length: 500 }, (_, index) => {
|
||||
return { data: { id: index, text: `String ${String(index)}` } };
|
||||
});
|
||||
const stringsRemaining = [
|
||||
{ data: { id: 500, text: "String 500" } },
|
||||
{ data: { id: 501, text: "String 501" } },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: strings500 });
|
||||
},
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: stringsRemaining });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"123",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(502);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://api.crowdin.com/api/v2/projects/123/strings?limit=500&offset=500",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array when no strings exist", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: [] });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"456",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should unwrap the data from each string object", async() => {
|
||||
const mockStrings = [
|
||||
{
|
||||
data: {
|
||||
context: "greeting context",
|
||||
id: 1,
|
||||
identifier: "hello",
|
||||
isHidden: false,
|
||||
text: "Hello World",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: mockStrings });
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const result = await getStrings(
|
||||
"789",
|
||||
"https://api.crowdin.com/api/v2",
|
||||
"token",
|
||||
);
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
context: "greeting context",
|
||||
id: 1,
|
||||
identifier: "hello",
|
||||
isHidden: false,
|
||||
text: "Hello World",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { backoffAndRetry } from "../backoffAndRetry.js";
|
||||
|
||||
describe("backoffAndRetry", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "error").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return JSON data on successful response", async() => {
|
||||
const mockData = { success: true, data: "test" };
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass options to fetch", async() => {
|
||||
const mockData = { result: "success" };
|
||||
const options: RequestInit = {
|
||||
headers: { authorization: "Bearer token" },
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
await backoffAndRetry("https://example.com/api", options);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
it("should retry after 5 seconds on 429 response", async() => {
|
||||
const mockData = { result: "success" };
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const resultPromise = backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should handle multiple 429 responses with backoff", async() => {
|
||||
const mockData = { result: "finally" };
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const resultPromise = backoffAndRetry<typeof mockData>(
|
||||
"https://example.com/api",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should return null on non-429 error response", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return null on fetch error", async() => {
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return null on JSON parse error", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.reject(new Error("Invalid JSON"));
|
||||
},
|
||||
ok: true,
|
||||
} as Response);
|
||||
|
||||
const result = await backoffAndRetry("https://example.com/api");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getMimeType } from "../mimeType.js";
|
||||
|
||||
describe("getMimeType", () => {
|
||||
describe("image types", () => {
|
||||
it("should return image/png for .png files", () => {
|
||||
expect(getMimeType("image.png")).toBe("image/png");
|
||||
expect(getMimeType("/path/to/image.png")).toBe("image/png");
|
||||
});
|
||||
|
||||
it("should return image/jpeg for .jpg and .jpeg files", () => {
|
||||
expect(getMimeType("photo.jpg")).toBe("image/jpeg");
|
||||
expect(getMimeType("photo.jpeg")).toBe("image/jpeg");
|
||||
});
|
||||
|
||||
it("should return image/gif for .gif files", () => {
|
||||
expect(getMimeType("animation.gif")).toBe("image/gif");
|
||||
});
|
||||
|
||||
it("should return image/webp for .webp files", () => {
|
||||
expect(getMimeType("image.webp")).toBe("image/webp");
|
||||
});
|
||||
|
||||
it("should return image/svg+xml for .svg files", () => {
|
||||
expect(getMimeType("icon.svg")).toBe("image/svg+xml");
|
||||
});
|
||||
|
||||
it("should return image/bmp for .bmp files", () => {
|
||||
expect(getMimeType("bitmap.bmp")).toBe("image/bmp");
|
||||
});
|
||||
|
||||
it("should return image/tiff for .tif and .tiff files", () => {
|
||||
expect(getMimeType("scan.tif")).toBe("image/tiff");
|
||||
expect(getMimeType("scan.tiff")).toBe("image/tiff");
|
||||
});
|
||||
|
||||
it("should return image/x-icon for .ico files", () => {
|
||||
expect(getMimeType("favicon.ico")).toBe("image/x-icon");
|
||||
});
|
||||
});
|
||||
|
||||
describe("video types", () => {
|
||||
it("should return video/mp4 for .mp4 files", () => {
|
||||
expect(getMimeType("video.mp4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should return video/webm for .webm files", () => {
|
||||
expect(getMimeType("video.webm")).toBe("video/webm");
|
||||
});
|
||||
|
||||
it("should return video/x-msvideo for .avi files", () => {
|
||||
expect(getMimeType("video.avi")).toBe("video/x-msvideo");
|
||||
});
|
||||
|
||||
it("should return video/quicktime for .mov files", () => {
|
||||
expect(getMimeType("video.mov")).toBe("video/quicktime");
|
||||
});
|
||||
|
||||
it("should return video/x-matroska for .mkv files", () => {
|
||||
expect(getMimeType("video.mkv")).toBe("video/x-matroska");
|
||||
});
|
||||
});
|
||||
|
||||
describe("audio types", () => {
|
||||
it("should return audio/mpeg for .mp3 files", () => {
|
||||
expect(getMimeType("song.mp3")).toBe("audio/mpeg");
|
||||
});
|
||||
|
||||
it("should return audio/wav for .wav files", () => {
|
||||
expect(getMimeType("sound.wav")).toBe("audio/wav");
|
||||
});
|
||||
|
||||
it("should return audio/ogg for .ogg files", () => {
|
||||
expect(getMimeType("audio.ogg")).toBe("audio/ogg");
|
||||
});
|
||||
|
||||
it("should return audio/aac for .aac files", () => {
|
||||
expect(getMimeType("audio.aac")).toBe("audio/aac");
|
||||
});
|
||||
|
||||
it("should return audio/flac for .flac files", () => {
|
||||
expect(getMimeType("music.flac")).toBe("audio/flac");
|
||||
});
|
||||
});
|
||||
|
||||
describe("document types", () => {
|
||||
it("should return application/pdf for .pdf files", () => {
|
||||
expect(getMimeType("document.pdf")).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("should return application/json for .json files", () => {
|
||||
expect(getMimeType("data.json")).toBe("application/json");
|
||||
});
|
||||
|
||||
it("should return text/plain for .txt files", () => {
|
||||
expect(getMimeType("readme.txt")).toBe("text/plain");
|
||||
});
|
||||
|
||||
it("should return text/markdown for .md files", () => {
|
||||
expect(getMimeType("README.md")).toBe("text/markdown");
|
||||
});
|
||||
|
||||
it("should return text/html for .html and .htm files", () => {
|
||||
expect(getMimeType("page.html")).toBe("text/html");
|
||||
expect(getMimeType("page.htm")).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should return text/css for .css files", () => {
|
||||
expect(getMimeType("styles.css")).toBe("text/css");
|
||||
});
|
||||
|
||||
it("should return text/javascript for .js files", () => {
|
||||
expect(getMimeType("script.js")).toBe("text/javascript");
|
||||
});
|
||||
|
||||
it("should return text/csv for .csv files", () => {
|
||||
expect(getMimeType("data.csv")).toBe("text/csv");
|
||||
});
|
||||
|
||||
it("should return application/xml for .xml files", () => {
|
||||
expect(getMimeType("data.xml")).toBe("application/xml");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Microsoft Office types", () => {
|
||||
it("should return correct MIME type for .doc files", () => {
|
||||
expect(getMimeType("document.doc")).toBe("application/msword");
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .docx files", () => {
|
||||
expect(getMimeType("document.docx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .xls files", () => {
|
||||
expect(getMimeType("spreadsheet.xls")).toBe("application/vnd.ms-excel");
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .xlsx files", () => {
|
||||
expect(getMimeType("spreadsheet.xlsx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .ppt files", () => {
|
||||
expect(getMimeType("presentation.ppt")).toBe(
|
||||
"application/vnd.ms-powerpoint",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return correct MIME type for .pptx files", () => {
|
||||
expect(getMimeType("presentation.pptx")).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("font types", () => {
|
||||
it("should return font/ttf for .ttf files", () => {
|
||||
expect(getMimeType("font.ttf")).toBe("font/ttf");
|
||||
});
|
||||
|
||||
it("should return font/otf for .otf files", () => {
|
||||
expect(getMimeType("font.otf")).toBe("font/otf");
|
||||
});
|
||||
|
||||
it("should return font/woff for .woff files", () => {
|
||||
expect(getMimeType("font.woff")).toBe("font/woff");
|
||||
});
|
||||
|
||||
it("should return font/woff2 for .woff2 files", () => {
|
||||
expect(getMimeType("font.woff2")).toBe("font/woff2");
|
||||
});
|
||||
|
||||
it("should return application/vnd.ms-fontobject for .eot files", () => {
|
||||
expect(getMimeType("font.eot")).toBe("application/vnd.ms-fontobject");
|
||||
});
|
||||
});
|
||||
|
||||
describe("archive types", () => {
|
||||
it("should return application/zip for .zip files", () => {
|
||||
expect(getMimeType("archive.zip")).toBe("application/zip");
|
||||
});
|
||||
|
||||
it("should return application/gzip for .gz files", () => {
|
||||
expect(getMimeType("archive.gz")).toBe("application/gzip");
|
||||
});
|
||||
|
||||
it("should return application/x-tar for .tar files", () => {
|
||||
expect(getMimeType("archive.tar")).toBe("application/x-tar");
|
||||
});
|
||||
|
||||
it("should return application/x-rar-compressed for .rar files", () => {
|
||||
expect(getMimeType("archive.rar")).toBe("application/x-rar-compressed");
|
||||
});
|
||||
|
||||
it("should return application/x-7z-compressed for .7z files", () => {
|
||||
expect(getMimeType("archive.7z")).toBe("application/x-7z-compressed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("case insensitivity", () => {
|
||||
it("should handle uppercase extensions", () => {
|
||||
expect(getMimeType("IMAGE.PNG")).toBe("image/png");
|
||||
expect(getMimeType("VIDEO.MP4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should handle mixed case extensions", () => {
|
||||
expect(getMimeType("file.JpG")).toBe("image/jpeg");
|
||||
expect(getMimeType("file.Mp3")).toBe("audio/mpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown types", () => {
|
||||
it("should return undefined for unknown extensions", () => {
|
||||
expect(getMimeType("file.xyz")).toBeUndefined();
|
||||
expect(getMimeType("file.unknown")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for files without extensions", () => {
|
||||
expect(getMimeType("filename")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("path handling", () => {
|
||||
it("should handle full paths", () => {
|
||||
expect(getMimeType("/home/user/documents/image.png")).toBe("image/png");
|
||||
expect(getMimeType("./relative/path/video.mp4")).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("should handle filenames with multiple dots", () => {
|
||||
expect(getMimeType("file.name.with.dots.png")).toBe("image/png");
|
||||
expect(getMimeType("archive.tar.gz")).toBe("application/gzip");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { paginatedFetch } from "../paginatedFetch.js";
|
||||
|
||||
type TestItem = Record<string, unknown>;
|
||||
|
||||
describe("paginatedFetch", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(global, "fetch");
|
||||
vi.spyOn(console, "log").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch a single page when results fit in one request", async() => {
|
||||
const mockData = [
|
||||
{ id: 1, name: "item1" },
|
||||
{ id: 2, name: "item2" },
|
||||
];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(mockData);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/api?limit=10&page=1&offset=0",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle multiple pages", async() => {
|
||||
const page1 = [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
];
|
||||
const page2 = [
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
];
|
||||
const page3: Array<TestItem> = [];
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page1);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page2);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve(page3);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([ ...page1, ...page2 ]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(3);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://example.com/api?limit=2&page=1&offset=0",
|
||||
{},
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://example.com/api?limit=2&page=2&offset=2",
|
||||
{},
|
||||
);
|
||||
expect(global.fetch).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"https://example.com/api?limit=2&page=3&offset=4",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass options to fetch", async() => {
|
||||
const options: RequestInit = {
|
||||
headers: { authorization: "Bearer token" },
|
||||
};
|
||||
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
options,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on first page error", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
} as Response);
|
||||
|
||||
await expect(
|
||||
paginatedFetch<Array<TestItem>>("https://example.com/api", 10),
|
||||
).rejects.toThrow(
|
||||
"Failed to fetch https://example.com/api?limit=10&page=1&offset=0: 500 Internal Server Error",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if first page is not an array", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ data: "not an array" });
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
await expect(
|
||||
paginatedFetch<Array<TestItem>>("https://example.com/api", 10),
|
||||
).rejects.toThrow("Expected array response but got object");
|
||||
});
|
||||
|
||||
it("should stop pagination on subsequent page error", async() => {
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }]);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should stop pagination if subsequent page is not an array", async() => {
|
||||
vi.mocked(global.fetch).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response).
|
||||
mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve({ notAnArray: true });
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }]);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle empty first page", async() => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
json: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
} as Response);
|
||||
|
||||
const result = await paginatedFetch<Array<TestItem>>(
|
||||
"https://example.com/api",
|
||||
10,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { serialiseJsonOrError } from "../serialiseJsonOrError.js";
|
||||
|
||||
describe("serialiseJsonOrError", () => {
|
||||
describe("valid JSON parsing", () => {
|
||||
it("should parse a simple object", () => {
|
||||
const result = serialiseJsonOrError('{"key": "value"}');
|
||||
expect(result).toEqual({ key: "value" });
|
||||
});
|
||||
|
||||
it("should parse an object with multiple properties", () => {
|
||||
const result = serialiseJsonOrError(
|
||||
'{"name": "test", "count": 42, "active": true}',
|
||||
);
|
||||
expect(result).toEqual({ name: "test", count: 42, active: true });
|
||||
});
|
||||
|
||||
it("should parse nested objects", () => {
|
||||
const result = serialiseJsonOrError(
|
||||
'{"outer": {"inner": {"deep": "value"}}}',
|
||||
);
|
||||
expect(result).toEqual({ outer: { inner: { deep: "value" } } });
|
||||
});
|
||||
|
||||
it("should parse objects with arrays", () => {
|
||||
const result = serialiseJsonOrError('{"items": [1, 2, 3]}');
|
||||
expect(result).toEqual({ items: [ 1, 2, 3 ] });
|
||||
});
|
||||
|
||||
it("should parse objects with null values", () => {
|
||||
const result = serialiseJsonOrError('{"value": null}');
|
||||
expect(result).toEqual({ value: null });
|
||||
});
|
||||
|
||||
it("should parse an empty object", () => {
|
||||
const result = serialiseJsonOrError("{}");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should parse objects with special characters in strings", () => {
|
||||
const result = serialiseJsonOrError('{"text": "hello\\nworld"}');
|
||||
expect(result).toEqual({ text: "hello\nworld" });
|
||||
});
|
||||
|
||||
it("should parse objects with unicode characters", () => {
|
||||
const result = serialiseJsonOrError('{"emoji": "🎉", "text": "日本語"}');
|
||||
expect(result).toEqual({ emoji: "🎉", text: "日本語" });
|
||||
});
|
||||
|
||||
it("should parse objects with numeric keys (as strings)", () => {
|
||||
const result = serialiseJsonOrError('{"123": "numeric key"}');
|
||||
expect(result).toEqual({ "123": "numeric key" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid JSON handling", () => {
|
||||
it("should return null for invalid JSON syntax", () => {
|
||||
expect(serialiseJsonOrError("{invalid}")).toBeNull();
|
||||
expect(serialiseJsonOrError("not json")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unclosed braces", () => {
|
||||
expect(serialiseJsonOrError('{"key": "value"')).toBeNull();
|
||||
expect(serialiseJsonOrError('{"key": {')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for trailing commas", () => {
|
||||
expect(serialiseJsonOrError('{"key": "value",}')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for single quotes", () => {
|
||||
expect(serialiseJsonOrError("{'key': 'value'}")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unquoted keys", () => {
|
||||
expect(serialiseJsonOrError('{key: "value"}')).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for empty string", () => {
|
||||
expect(serialiseJsonOrError("")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for whitespace only", () => {
|
||||
expect(serialiseJsonOrError(" ")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should parse a plain array (returns as object)", () => {
|
||||
const result = serialiseJsonOrError("[1, 2, 3]");
|
||||
expect(result).toEqual([ 1, 2, 3 ]);
|
||||
});
|
||||
|
||||
it("should parse primitive values", () => {
|
||||
expect(serialiseJsonOrError("42")).toBe(42);
|
||||
expect(serialiseJsonOrError('"string"')).toBe("string");
|
||||
expect(serialiseJsonOrError("true")).toBe(true);
|
||||
expect(serialiseJsonOrError("false")).toBe(false);
|
||||
expect(serialiseJsonOrError("null")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle very large numbers", () => {
|
||||
const result = serialiseJsonOrError('{"big": 9007199254740991}');
|
||||
expect(result).toEqual({ big: 9007199254740991 });
|
||||
});
|
||||
|
||||
it("should handle scientific notation", () => {
|
||||
const result = serialiseJsonOrError('{"sci": 1.23e10}');
|
||||
expect(result).toEqual({ sci: 1.23e10 });
|
||||
});
|
||||
|
||||
it("should handle deeply nested structures", () => {
|
||||
const deepJson = '{"a":{"b":{"c":{"d":{"e":"deep"}}}}}';
|
||||
const result = serialiseJsonOrError(deepJson);
|
||||
expect(result).toEqual({ a: { b: { c: { d: { e: "deep" } } } } });
|
||||
});
|
||||
|
||||
it("should handle JSON with whitespace", () => {
|
||||
const result = serialiseJsonOrError(' { "key" : "value" } ');
|
||||
expect(result).toEqual({ key: "value" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { sleep } from "../sleep.js";
|
||||
|
||||
describe("sleep", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should resolve after the specified time", async() => {
|
||||
const sleepPromise = sleep(1000);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not resolve before the specified time", async() => {
|
||||
let resolved = false;
|
||||
const sleepPromise = sleep(1000).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(999);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
await sleepPromise;
|
||||
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle zero milliseconds", async() => {
|
||||
const sleepPromise = sleep(0);
|
||||
|
||||
vi.advanceTimersByTime(0);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle small delays", async() => {
|
||||
const sleepPromise = sleep(10);
|
||||
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle large delays", async() => {
|
||||
const sleepPromise = sleep(60000);
|
||||
|
||||
vi.advanceTimersByTime(60000);
|
||||
|
||||
await expect(sleepPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("real timer tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should actually wait with real timers (short delay)", async() => {
|
||||
const start = Date.now();
|
||||
await sleep(50);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(elapsed).toBeGreaterThanOrEqual(45);
|
||||
expect(elapsed).toBeLessThan(150);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
include: ["src/**/__tests__/**/*.test.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "html"],
|
||||
exclude: ["node_modules/", "**/__tests__/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user