diff --git a/internal/cmd/ai/cmd_agent.go b/cmd/ai/cmd_agent.go similarity index 100% rename from internal/cmd/ai/cmd_agent.go rename to cmd/ai/cmd_agent.go diff --git a/internal/cmd/ai/cmd_ai.go b/cmd/ai/cmd_ai.go similarity index 100% rename from internal/cmd/ai/cmd_ai.go rename to cmd/ai/cmd_ai.go diff --git a/internal/cmd/ai/cmd_commands.go b/cmd/ai/cmd_commands.go similarity index 97% rename from internal/cmd/ai/cmd_commands.go rename to cmd/ai/cmd_commands.go index a106e343..55e3ff65 100644 --- a/internal/cmd/ai/cmd_commands.go +++ b/cmd/ai/cmd_commands.go @@ -13,7 +13,7 @@ package ai import ( - ragcmd "forge.lthn.ai/core/cli/internal/cmd/rag" + ragcmd "forge.lthn.ai/core/cli/cmd/rag" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" ) diff --git a/internal/cmd/ai/cmd_dispatch.go b/cmd/ai/cmd_dispatch.go similarity index 100% rename from internal/cmd/ai/cmd_dispatch.go rename to cmd/ai/cmd_dispatch.go diff --git a/internal/cmd/ai/cmd_git.go b/cmd/ai/cmd_git.go similarity index 100% rename from internal/cmd/ai/cmd_git.go rename to cmd/ai/cmd_git.go diff --git a/internal/cmd/ai/cmd_metrics.go b/cmd/ai/cmd_metrics.go similarity index 100% rename from internal/cmd/ai/cmd_metrics.go rename to cmd/ai/cmd_metrics.go diff --git a/internal/cmd/ai/cmd_ratelimits.go b/cmd/ai/cmd_ratelimits.go similarity index 100% rename from internal/cmd/ai/cmd_ratelimits.go rename to cmd/ai/cmd_ratelimits.go diff --git a/internal/cmd/ai/cmd_tasks.go b/cmd/ai/cmd_tasks.go similarity index 100% rename from internal/cmd/ai/cmd_tasks.go rename to cmd/ai/cmd_tasks.go diff --git a/internal/cmd/ai/cmd_updates.go b/cmd/ai/cmd_updates.go similarity index 100% rename from internal/cmd/ai/cmd_updates.go rename to cmd/ai/cmd_updates.go diff --git a/internal/cmd/ai/ratelimit_dispatch.go b/cmd/ai/ratelimit_dispatch.go similarity index 100% rename from internal/cmd/ai/ratelimit_dispatch.go rename to cmd/ai/ratelimit_dispatch.go diff --git a/cmd/bugseti/.gitignore b/cmd/bugseti/.gitignore deleted file mode 100644 index 94f214e3..00000000 --- a/cmd/bugseti/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Build output -bin/ -frontend/dist/ -frontend/node_modules/ -frontend/.angular/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Go -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test -*.test -*.out -coverage/ - -# Wails -wails.json diff --git a/cmd/bugseti/README.md b/cmd/bugseti/README.md deleted file mode 100644 index 3739b5c5..00000000 --- a/cmd/bugseti/README.md +++ /dev/null @@ -1,186 +0,0 @@ -# BugSETI - -**Distributed Bug Fixing - like SETI@home but for code** - -BugSETI is a system tray application that helps developers contribute to open source by fixing bugs in their spare CPU cycles. It fetches issues from GitHub repositories, prepares context using AI, and guides you through the fix-and-submit workflow. - -## Features - -- **System Tray Integration**: Runs quietly in the background, ready when you are -- **Issue Queue**: Automatically fetches and queues issues from configured repositories -- **AI Context Seeding**: Prepares relevant code context for each issue using pattern matching -- **Workbench UI**: Full-featured interface for reviewing issues and submitting fixes -- **Automated PR Submission**: Streamlined workflow from fix to pull request -- **Stats & Leaderboard**: Track your contributions and compete with the community - -## Installation - -### From Source - -```bash -# Clone the repository -git clone https://forge.lthn.ai/core/go.git -cd core - -# Build BugSETI -task bugseti:build - -# The binary will be in build/bin/bugseti -``` - -### Prerequisites - -- Go 1.25 or later -- Node.js 18+ and npm (for frontend) -- GitHub CLI (`gh`) authenticated -- Chrome/Chromium (optional, for webview features) - -## Configuration - -On first launch, BugSETI will show an onboarding wizard to configure: - -1. **GitHub Token**: For fetching issues and submitting PRs -2. **Repositories**: Which repos to fetch issues from -3. **Filters**: Issue labels, difficulty levels, languages -4. **Notifications**: How to alert you about new issues - -### Configuration File - -Settings are stored in `~/.config/bugseti/config.json`: - -```json -{ - "github_token": "ghp_...", - "repositories": [ - "host-uk/core", - "example/repo" - ], - "filters": { - "labels": ["good first issue", "help wanted", "bug"], - "languages": ["go", "typescript"], - "max_age_days": 30 - }, - "notifications": { - "enabled": true, - "sound": true - }, - "fetch_interval_minutes": 30 -} -``` - -## Usage - -### Starting BugSETI - -```bash -# Run the application -./bugseti - -# Or use task runner -task bugseti:run -``` - -The app will appear in your system tray. Click the icon to see the quick menu or open the workbench. - -### Workflow - -1. **Browse Issues**: Click the tray icon to see available issues -2. **Select an Issue**: Choose one to work on from the queue -3. **Review Context**: BugSETI shows relevant files and patterns -4. **Fix the Bug**: Make your changes in your preferred editor -5. **Submit PR**: Use the workbench to create and submit your pull request - -### Keyboard Shortcuts - -| Shortcut | Action | -|----------|--------| -| `Ctrl+Shift+B` | Open workbench | -| `Ctrl+Shift+N` | Next issue | -| `Ctrl+Shift+S` | Submit PR | - -## Architecture - -``` -cmd/bugseti/ - main.go # Application entry point - tray.go # System tray service - icons/ # Tray icons (light/dark/template) - frontend/ # Angular frontend - src/ - app/ - tray/ # Tray panel component - workbench/ # Main workbench - settings/ # Settings panel - onboarding/ # First-run wizard - -internal/bugseti/ - config.go # Configuration service - fetcher.go # GitHub issue fetcher - queue.go # Issue queue management - seeder.go # Context seeding via AI - submit.go # PR submission - notify.go # Notification service - stats.go # Statistics tracking -``` - -## Contributing - -We welcome contributions! Here's how to get involved: - -### Development Setup - -```bash -# Install dependencies -cd cmd/bugseti/frontend -npm install - -# Run in development mode -task bugseti:dev -``` - -### Running Tests - -```bash -# Go tests -go test ./cmd/bugseti/... ./internal/bugseti/... - -# Frontend tests -cd cmd/bugseti/frontend -npm test -``` - -### Submitting Changes - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/my-feature` -3. Make your changes and add tests -4. Run the test suite: `task test` -5. Submit a pull request - -### Code Style - -- Go: Follow standard Go conventions, run `go fmt` -- TypeScript/Angular: Follow Angular style guide -- Commits: Use conventional commit messages - -## Roadmap - -- [ ] Auto-update mechanism -- [ ] Team/organization support -- [ ] Integration with more issue trackers (GitLab, Jira) -- [ ] AI-assisted code review -- [ ] Mobile companion app - -## License - -MIT License - see [LICENSE](../../LICENSE) for details. - -## Acknowledgments - -- Inspired by SETI@home and distributed computing projects -- Built with [Wails v3](https://wails.io/) for native desktop integration -- Uses [Angular](https://angular.io/) for the frontend - ---- - -**Happy Bug Hunting!** diff --git a/cmd/bugseti/Taskfile.yml b/cmd/bugseti/Taskfile.yml deleted file mode 100644 index b19deeff..00000000 --- a/cmd/bugseti/Taskfile.yml +++ /dev/null @@ -1,134 +0,0 @@ -version: '3' - -includes: - common: ./build/Taskfile.yml - windows: ./build/windows/Taskfile.yml - darwin: ./build/darwin/Taskfile.yml - linux: ./build/linux/Taskfile.yml - -vars: - APP_NAME: "bugseti" - BIN_DIR: "bin" - VITE_PORT: '{{.WAILS_VITE_PORT | default 9246}}' - -tasks: - build: - summary: Builds the application - cmds: - - task: "{{OS}}:build" - - package: - summary: Packages a production build of the application - cmds: - - task: "{{OS}}:package" - - run: - summary: Runs the application - cmds: - - task: "{{OS}}:run" - - dev: - summary: Runs the application in development mode - cmds: - - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} - - build:all: - summary: Builds for all platforms - cmds: - - task: darwin:build - vars: - PRODUCTION: "true" - - task: linux:build - vars: - PRODUCTION: "true" - - task: windows:build - vars: - PRODUCTION: "true" - - package:all: - summary: Packages for all platforms - cmds: - - task: darwin:package - - task: linux:package - - task: windows:package - - clean: - summary: Cleans build artifacts - cmds: - - rm -rf bin/ - - rm -rf frontend/dist/ - - rm -rf frontend/node_modules/ - - # Release targets - release:stable: - summary: Creates a stable release tag - desc: | - Creates a stable release tag (bugseti-vX.Y.Z). - Usage: task release:stable VERSION=1.0.0 - preconditions: - - sh: '[ -n "{{.VERSION}}" ]' - msg: "VERSION is required. Usage: task release:stable VERSION=1.0.0" - cmds: - - git tag -a "bugseti-v{{.VERSION}}" -m "BugSETI v{{.VERSION}} stable release" - - echo "Created tag bugseti-v{{.VERSION}}" - - echo "To push: git push origin bugseti-v{{.VERSION}}" - - release:beta: - summary: Creates a beta release tag - desc: | - Creates a beta release tag (bugseti-vX.Y.Z-beta.N). - Usage: task release:beta VERSION=1.0.0 BETA=1 - preconditions: - - sh: '[ -n "{{.VERSION}}" ]' - msg: "VERSION is required. Usage: task release:beta VERSION=1.0.0 BETA=1" - - sh: '[ -n "{{.BETA}}" ]' - msg: "BETA number is required. Usage: task release:beta VERSION=1.0.0 BETA=1" - cmds: - - git tag -a "bugseti-v{{.VERSION}}-beta.{{.BETA}}" -m "BugSETI v{{.VERSION}} beta {{.BETA}}" - - echo "Created tag bugseti-v{{.VERSION}}-beta.{{.BETA}}" - - echo "To push: git push origin bugseti-v{{.VERSION}}-beta.{{.BETA}}" - - release:nightly: - summary: Creates a nightly release tag - desc: Creates a nightly release tag (bugseti-nightly-YYYYMMDD) - vars: - DATE: - sh: date -u +%Y%m%d - cmds: - - git tag -a "bugseti-nightly-{{.DATE}}" -m "BugSETI nightly build {{.DATE}}" - - echo "Created tag bugseti-nightly-{{.DATE}}" - - echo "To push: git push origin bugseti-nightly-{{.DATE}}" - - release:push: - summary: Pushes the latest release tag - desc: | - Pushes the most recent bugseti-* tag to origin. - Usage: task release:push - vars: - TAG: - sh: git tag -l 'bugseti-*' | sort -V | tail -1 - preconditions: - - sh: '[ -n "{{.TAG}}" ]' - msg: "No bugseti-* tags found" - cmds: - - echo "Pushing tag {{.TAG}}..." - - git push origin {{.TAG}} - - echo "Tag {{.TAG}} pushed. GitHub Actions will build and release." - - release:list: - summary: Lists all BugSETI release tags - cmds: - - echo "=== BugSETI Release Tags ===" - - git tag -l 'bugseti-*' | sort -V - - version: - summary: Shows current version info - cmds: - - | - echo "=== BugSETI Version Info ===" - echo "Latest stable tag:" - git tag -l 'bugseti-v*' | grep -v beta | sort -V | tail -1 || echo " (none)" - echo "Latest beta tag:" - git tag -l 'bugseti-v*-beta.*' | sort -V | tail -1 || echo " (none)" - echo "Latest nightly tag:" - git tag -l 'bugseti-nightly-*' | sort -V | tail -1 || echo " (none)" diff --git a/cmd/bugseti/build/Taskfile.yml b/cmd/bugseti/build/Taskfile.yml deleted file mode 100644 index 96e71339..00000000 --- a/cmd/bugseti/build/Taskfile.yml +++ /dev/null @@ -1,90 +0,0 @@ -version: '3' - -tasks: - go:mod:tidy: - summary: Runs `go mod tidy` - internal: true - cmds: - - go mod tidy - - install:frontend:deps: - summary: Install frontend dependencies - dir: frontend - sources: - - package.json - - package-lock.json - generates: - - node_modules/* - preconditions: - - sh: npm version - msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" - cmds: - - npm install - - build:frontend: - label: build:frontend (PRODUCTION={{.PRODUCTION}}) - summary: Build the frontend project - dir: frontend - sources: - - "**/*" - generates: - - dist/**/* - deps: - - task: install:frontend:deps - - task: generate:bindings - vars: - BUILD_FLAGS: - ref: .BUILD_FLAGS - cmds: - - npm run {{.BUILD_COMMAND}} -q - env: - PRODUCTION: '{{.PRODUCTION | default "false"}}' - vars: - BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' - - generate:bindings: - label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) - summary: Generates bindings for the frontend - deps: - - task: go:mod:tidy - sources: - - "**/*.[jt]s" - - exclude: frontend/**/* - - frontend/bindings/**/* - - "**/*.go" - - go.mod - - go.sum - generates: - - frontend/bindings/**/* - cmds: - - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=false -ts -i - - generate:icons: - summary: Generates Windows `.ico` and Mac `.icns` files from an image - dir: build - sources: - - "appicon.png" - generates: - - "darwin/icons.icns" - - "windows/icon.ico" - cmds: - - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico - - dev:frontend: - summary: Runs the frontend in development mode - dir: frontend - deps: - - task: install:frontend:deps - cmds: - - npm run dev -- --port {{.VITE_PORT}} - vars: - VITE_PORT: '{{.VITE_PORT | default "5173"}}' - - update:build-assets: - summary: Updates the build assets - dir: build - preconditions: - - sh: '[ -n "{{.APP_NAME}}" ]' - msg: "APP_NAME variable is required" - cmds: - - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/cmd/bugseti/build/config.yml b/cmd/bugseti/build/config.yml deleted file mode 100644 index b55fb12e..00000000 --- a/cmd/bugseti/build/config.yml +++ /dev/null @@ -1,38 +0,0 @@ -# BugSETI Wails v3 Build Configuration -version: '3' - -# Build metadata -info: - companyName: "Lethean" - productName: "BugSETI" - productIdentifier: "io.lethean.bugseti" - description: "Distributed Bug Fixing - like SETI@home but for code" - copyright: "Copyright 2026 Lethean" - comments: "Distributed OSS bug fixing application" - version: "0.1.0" - -# Dev mode configuration -dev_mode: - root_path: . - log_level: warn - debounce: 1000 - ignore: - dir: - - .git - - node_modules - - frontend - - bin - file: - - .DS_Store - - .gitignore - - .gitkeep - watched_extension: - - "*.go" - git_ignore: true - executes: - - cmd: go build -buildvcs=false -gcflags=all=-l -o bin/bugseti . - type: blocking - - cmd: cd frontend && npx ng serve --port ${WAILS_FRONTEND_PORT:-9246} - type: background - - cmd: bin/bugseti - type: primary diff --git a/cmd/bugseti/build/darwin/Info.dev.plist b/cmd/bugseti/build/darwin/Info.dev.plist deleted file mode 100644 index af4bd2c0..00000000 --- a/cmd/bugseti/build/darwin/Info.dev.plist +++ /dev/null @@ -1,37 +0,0 @@ - - - - - CFBundlePackageType - APPL - CFBundleName - BugSETI (Dev) - CFBundleExecutable - bugseti - CFBundleIdentifier - io.lethean.bugseti.dev - CFBundleVersion - 0.1.0-dev - CFBundleGetInfoString - Distributed Bug Fixing - like SETI@home but for code (Development) - CFBundleShortVersionString - 0.1.0-dev - CFBundleIconFile - icons.icns - LSMinimumSystemVersion - 10.15.0 - NSHighResolutionCapable - - LSUIElement - - LSApplicationCategoryType - public.app-category.developer-tools - NSAppTransportSecurity - - NSAllowsLocalNetworking - - NSAllowsArbitraryLoads - - - - diff --git a/cmd/bugseti/build/darwin/Info.plist b/cmd/bugseti/build/darwin/Info.plist deleted file mode 100644 index 061b7b48..00000000 --- a/cmd/bugseti/build/darwin/Info.plist +++ /dev/null @@ -1,35 +0,0 @@ - - - - - CFBundlePackageType - APPL - CFBundleName - BugSETI - CFBundleExecutable - bugseti - CFBundleIdentifier - io.lethean.bugseti - CFBundleVersion - 0.1.0 - CFBundleGetInfoString - Distributed Bug Fixing - like SETI@home but for code - CFBundleShortVersionString - 0.1.0 - CFBundleIconFile - icons.icns - LSMinimumSystemVersion - 10.15.0 - NSHighResolutionCapable - - LSUIElement - - LSApplicationCategoryType - public.app-category.developer-tools - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - - diff --git a/cmd/bugseti/build/darwin/Taskfile.yml b/cmd/bugseti/build/darwin/Taskfile.yml deleted file mode 100644 index bf49fbe9..00000000 --- a/cmd/bugseti/build/darwin/Taskfile.yml +++ /dev/null @@ -1,84 +0,0 @@ -version: '3' - -includes: - common: ../Taskfile.yml - -tasks: - build: - summary: Creates a production build of the application - deps: - - task: common:go:mod:tidy - - task: common:build:frontend - vars: - BUILD_FLAGS: - ref: .BUILD_FLAGS - PRODUCTION: - ref: .PRODUCTION - - task: common:generate:icons - cmds: - - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} - vars: - BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' - DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' - OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' - env: - GOOS: darwin - CGO_ENABLED: 1 - GOARCH: '{{.ARCH | default ARCH}}' - CGO_CFLAGS: "-mmacosx-version-min=10.15" - CGO_LDFLAGS: "-mmacosx-version-min=10.15" - MACOSX_DEPLOYMENT_TARGET: "10.15" - PRODUCTION: '{{.PRODUCTION | default "false"}}' - - build:universal: - summary: Builds darwin universal binary (arm64 + amd64) - deps: - - task: build - vars: - ARCH: amd64 - OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" - PRODUCTION: '{{.PRODUCTION | default "true"}}' - - task: build - vars: - ARCH: arm64 - OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" - PRODUCTION: '{{.PRODUCTION | default "true"}}' - cmds: - - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" - - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" - - package: - summary: Packages a production build of the application into a `.app` bundle - deps: - - task: build - vars: - PRODUCTION: "true" - cmds: - - task: create:app:bundle - - package:universal: - summary: Packages darwin universal binary (arm64 + amd64) - deps: - - task: build:universal - cmds: - - task: create:app:bundle - - create:app:bundle: - summary: Creates an `.app` bundle - cmds: - - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} - - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources - - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS - - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents - - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app - - run: - deps: - - task: build - cmds: - - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} - - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources - - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS - - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist - - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app - - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/cmd/bugseti/build/linux/Taskfile.yml b/cmd/bugseti/build/linux/Taskfile.yml deleted file mode 100644 index 7fd20f73..00000000 --- a/cmd/bugseti/build/linux/Taskfile.yml +++ /dev/null @@ -1,103 +0,0 @@ -version: '3' - -includes: - common: ../Taskfile.yml - -tasks: - build: - summary: Builds the application for Linux - deps: - - task: common:go:mod:tidy - - task: common:build:frontend - vars: - BUILD_FLAGS: - ref: .BUILD_FLAGS - PRODUCTION: - ref: .PRODUCTION - - task: common:generate:icons - cmds: - - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} - vars: - BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' - env: - GOOS: linux - CGO_ENABLED: 1 - GOARCH: '{{.ARCH | default ARCH}}' - PRODUCTION: '{{.PRODUCTION | default "false"}}' - - package: - summary: Packages a production build of the application for Linux - deps: - - task: build - vars: - PRODUCTION: "true" - cmds: - - task: create:appimage - - task: create:deb - - task: create:rpm - - create:appimage: - summary: Creates an AppImage - dir: build/linux/appimage - deps: - - task: build - vars: - PRODUCTION: "true" - - task: generate:dotdesktop - cmds: - - cp {{.APP_BINARY}} {{.APP_NAME}} - - cp ../../appicon.png {{.APP_NAME}}.png - - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build - vars: - APP_NAME: '{{.APP_NAME}}' - APP_BINARY: '../../../bin/{{.APP_NAME}}' - ICON: '{{.APP_NAME}}.png' - DESKTOP_FILE: '../{{.APP_NAME}}.desktop' - OUTPUT_DIR: '../../../bin' - - create:deb: - summary: Creates a deb package - deps: - - task: build - vars: - PRODUCTION: "true" - cmds: - - task: generate:dotdesktop - - task: generate:deb - - create:rpm: - summary: Creates a rpm package - deps: - - task: build - vars: - PRODUCTION: "true" - cmds: - - task: generate:dotdesktop - - task: generate:rpm - - generate:deb: - summary: Creates a deb package - cmds: - - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin - - generate:rpm: - summary: Creates a rpm package - cmds: - - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin - - generate:dotdesktop: - summary: Generates a `.desktop` file - dir: build - cmds: - - mkdir -p {{.ROOT_DIR}}/build/linux/appimage - - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" - vars: - APP_NAME: 'BugSETI' - EXEC: '{{.APP_NAME}}' - ICON: 'bugseti' - CATEGORIES: 'Development;' - OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' - - run: - cmds: - - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/cmd/bugseti/build/linux/nfpm/nfpm.yaml b/cmd/bugseti/build/linux/nfpm/nfpm.yaml deleted file mode 100644 index 23acab04..00000000 --- a/cmd/bugseti/build/linux/nfpm/nfpm.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# nfpm configuration for BugSETI -name: "bugseti" -arch: "${GOARCH}" -platform: "linux" -version: "0.1.0" -section: "devel" -priority: "optional" -maintainer: "Lethean " -description: | - BugSETI - Distributed Bug Fixing - Like SETI@home but for code. Install the system tray app, - it pulls OSS issues from GitHub, AI prepares context, - you fix bugs, and it auto-submits PRs. -vendor: "Lethean" -homepage: "https://forge.lthn.ai/core/go" -license: "MIT" - -contents: - - src: ./bin/bugseti - dst: /usr/bin/bugseti - - src: ./build/linux/bugseti.desktop - dst: /usr/share/applications/bugseti.desktop - - src: ./build/appicon.png - dst: /usr/share/icons/hicolor/256x256/apps/bugseti.png - -overrides: - deb: - dependencies: - - libwebkit2gtk-4.1-0 - - libgtk-3-0 - rpm: - dependencies: - - webkit2gtk4.1 - - gtk3 diff --git a/cmd/bugseti/build/windows/Taskfile.yml b/cmd/bugseti/build/windows/Taskfile.yml deleted file mode 100644 index ac1d2d91..00000000 --- a/cmd/bugseti/build/windows/Taskfile.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '3' - -includes: - common: ../Taskfile.yml - -tasks: - build: - summary: Builds the application for Windows - deps: - - task: common:go:mod:tidy - - task: common:build:frontend - vars: - BUILD_FLAGS: - ref: .BUILD_FLAGS - PRODUCTION: - ref: .PRODUCTION - - task: common:generate:icons - cmds: - - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe - vars: - BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' - env: - GOOS: windows - CGO_ENABLED: 1 - GOARCH: '{{.ARCH | default ARCH}}' - PRODUCTION: '{{.PRODUCTION | default "false"}}' - - package: - summary: Packages a production build of the application for Windows - deps: - - task: build - vars: - PRODUCTION: "true" - cmds: - - task: create:nsis - - create:nsis: - summary: Creates an NSIS installer - cmds: - - wails3 tool package -name {{.APP_NAME}} -format nsis -config ./build/windows/nsis/installer.nsi -out {{.ROOT_DIR}}/bin - - create:msi: - summary: Creates an MSI installer - cmds: - - wails3 tool package -name {{.APP_NAME}} -format msi -config ./build/windows/wix/main.wxs -out {{.ROOT_DIR}}/bin - - run: - cmds: - - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/cmd/bugseti/frontend/angular.json b/cmd/bugseti/frontend/angular.json deleted file mode 100644 index 97d1fe6a..00000000 --- a/cmd/bugseti/frontend/angular.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "bugseti": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss", - "standalone": true - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:application", - "options": { - "outputPath": "dist/bugseti", - "index": "src/index.html", - "browser": "src/main.ts", - "polyfills": ["zone.js"], - "tsConfig": "tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" - } - ], - "outputHashing": "all" - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "buildTarget": "bugseti:build:production" - }, - "development": { - "buildTarget": "bugseti:build:development" - } - }, - "defaultConfiguration": "development" - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "polyfills": ["zone.js", "zone.js/testing"], - "tsConfig": "tsconfig.spec.json", - "inlineStyleLanguage": "scss", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - } - } - } - } - }, - "cli": { - "analytics": false - } -} diff --git a/cmd/bugseti/frontend/package-lock.json b/cmd/bugseti/frontend/package-lock.json deleted file mode 100644 index 0904b6fa..00000000 --- a/cmd/bugseti/frontend/package-lock.json +++ /dev/null @@ -1,15012 +0,0 @@ -{ - "name": "bugseti", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "bugseti", - "version": "0.1.0", - "dependencies": { - "@angular/animations": "^19.1.0", - "@angular/common": "^19.1.0", - "@angular/compiler": "^19.1.0", - "@angular/core": "^19.1.0", - "@angular/forms": "^19.1.0", - "@angular/platform-browser": "^19.1.0", - "@angular/platform-browser-dynamic": "^19.1.0", - "@angular/router": "^19.1.0", - "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" - }, - "devDependencies": { - "@angular-devkit/build-angular": "^19.1.0", - "@angular/cli": "^21.1.2", - "@angular/compiler-cli": "^19.1.0", - "@types/jasmine": "~5.1.0", - "jasmine-core": "~5.1.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.5.2" - } - }, - "node_modules/@algolia/abtesting": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", - "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-abtesting": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", - "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-analytics": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", - "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-common": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", - "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-insights": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", - "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-personalization": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", - "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-query-suggestions": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", - "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-search": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", - "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/ingestion": { - "version": "1.46.2", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", - "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/monitoring": { - "version": "1.46.2", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", - "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/recommend": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", - "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-browser-xhr": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", - "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-fetch": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", - "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-node-http": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", - "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@angular-devkit/architect": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.19.tgz", - "integrity": "sha512-iexYDIYpGAeAU7T60bGcfrGwtq1bxpZixYxWuHYiaD1b5baQgNSfd1isGEOh37GgDNsf4In9i2LOLPm0wBdtgQ==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "19.2.19", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/build-angular": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.19.tgz", - "integrity": "sha512-uIxi6Vzss6+ycljVhkyPUPWa20w8qxJL9lEn0h6+sX/fhM8Djt0FHIuTQjoX58EoMaQ/1jrXaRaGimkbaFcG9A==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", - "@angular-devkit/build-webpack": "0.1902.19", - "@angular-devkit/core": "19.2.19", - "@angular/build": "19.2.19", - "@babel/core": "7.26.10", - "@babel/generator": "7.26.10", - "@babel/helper-annotate-as-pure": "7.25.9", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-transform-async-generator-functions": "7.26.8", - "@babel/plugin-transform-async-to-generator": "7.25.9", - "@babel/plugin-transform-runtime": "7.26.10", - "@babel/preset-env": "7.26.9", - "@babel/runtime": "7.26.10", - "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.2.19", - "@vitejs/plugin-basic-ssl": "1.2.0", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.20", - "babel-loader": "9.2.1", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "12.0.2", - "css-loader": "7.1.2", - "esbuild-wasm": "0.25.4", - "fast-glob": "3.3.3", - "http-proxy-middleware": "3.0.5", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "karma-source-map-support": "1.4.0", - "less": "4.2.2", - "less-loader": "12.2.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.3.1", - "mini-css-extract-plugin": "2.9.2", - "open": "10.1.0", - "ora": "5.4.1", - "picomatch": "4.0.2", - "piscina": "4.8.0", - "postcss": "8.5.2", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.1", - "sass": "1.85.0", - "sass-loader": "16.0.5", - "semver": "7.7.1", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.39.0", - "tree-kill": "1.2.2", - "tslib": "2.8.1", - "webpack": "5.98.0", - "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.2.2", - "webpack-merge": "6.0.1", - "webpack-subresource-integrity": "5.1.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.25.4" - }, - "peerDependencies": { - "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", - "@angular/localize": "^19.0.0 || ^19.2.0-next.0", - "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", - "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", - "@web/test-runner": "^0.20.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "karma": "^6.3.0", - "ng-packagr": "^19.0.0 || ^19.2.0-next.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "typescript": ">=5.5 <5.9" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.19.tgz", - "integrity": "sha512-x2tlGg5CsUveFzuRuqeHknSbGirSAoRynEh+KqPRGK0G3WpMViW/M8SuVurecasegfIrDWtYZ4FnVxKqNbKwXQ==", - "dev": true, - "dependencies": { - "@angular-devkit/architect": "0.1902.19", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" - } - }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", - "dev": true, - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^4.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/schematics": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.2.tgz", - "integrity": "sha512-PA3gkiFhHUuXd2XuP7yzKg/9N++bjw+uOl473KwIsMuZwMPhncKa4+mUYBaffDoPqaujZvjfo6mjtCBuiBv05w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.1.2", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.21", - "ora": "9.0.0", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.2.tgz", - "integrity": "sha512-0wl5nJlFWsbwfUB2CQeTSmnVQ8AtqqwM3bYPYtXSc+vA8+hzsOAjjDuRnBxZS9zTnqtXKXB1e7M3Iy7KUwh7LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/animations": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.18.tgz", - "integrity": "sha512-c76x1t+OiSstPsvJdHmV8Q4taF+8SxWKqiY750fOjpd01it4jJbU6YQqIroC6Xie7154zZIxOTHH2uTj+nm5qA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18" - } - }, - "node_modules/@angular/build": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.19.tgz", - "integrity": "sha512-SFzQ1bRkNFiOVu+aaz+9INmts7tDUrsHLEr9HmARXr9qk5UmR8prlw39p2u+Bvi6/lCiJ18TZMQQl9mGyr63lg==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", - "@babel/core": "7.26.10", - "@babel/helper-annotate-as-pure": "7.25.9", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.26.0", - "@inquirer/confirm": "5.1.6", - "@vitejs/plugin-basic-ssl": "1.2.0", - "beasties": "0.3.2", - "browserslist": "^4.23.0", - "esbuild": "0.25.4", - "fast-glob": "3.3.3", - "https-proxy-agent": "7.0.6", - "istanbul-lib-instrument": "6.0.3", - "listr2": "8.2.5", - "magic-string": "0.30.17", - "mrmime": "2.0.1", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.8.0", - "rollup": "4.34.8", - "sass": "1.85.0", - "semver": "7.7.1", - "source-map-support": "0.5.21", - "vite": "6.4.1", - "watchpack": "2.4.2" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "lmdb": "3.2.6" - }, - "peerDependencies": { - "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", - "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", - "@angular/localize": "^19.0.0 || ^19.2.0-next.0", - "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", - "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", - "karma": "^6.4.0", - "less": "^4.2.0", - "ng-packagr": "^19.0.0 || ^19.2.0-next.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "typescript": ">=5.5 <5.9" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "karma": { - "optional": true - }, - "less": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular/cli": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.2.tgz", - "integrity": "sha512-AHjXCBl2PEilMJct6DX3ih5Fl5PiKpNDIj0ViTyVh1YcfpYjt6NzhVlV2o++8VNPNH/vMcmf2551LZIDProXXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.2101.2", - "@angular-devkit/core": "21.1.2", - "@angular-devkit/schematics": "21.1.2", - "@inquirer/prompts": "7.10.1", - "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.25.2", - "@schematics/angular": "21.1.2", - "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.46.2", - "ini": "6.0.0", - "jsonc-parser": "3.3.1", - "listr2": "9.0.5", - "npm-package-arg": "13.0.2", - "pacote": "21.0.4", - "parse5-html-rewriting-stream": "8.0.0", - "resolve": "1.22.11", - "semver": "7.7.3", - "yargs": "18.0.0", - "zod": "4.3.5" - }, - "bin": { - "ng": "bin/ng.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.2101.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.2.tgz", - "integrity": "sha512-pV2onJgp16xO0vAqEfRWVynRPPLVHydYLANNa3UX3l5T39JcYdMIoOHSIIl8tWrxVeOwiWd1ajub0VsFTUok4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.1.2", - "rxjs": "7.8.2" - }, - "bin": { - "architect": "bin/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.2.tgz", - "integrity": "sha512-0wl5nJlFWsbwfUB2CQeTSmnVQ8AtqqwM3bYPYtXSc+vA8+hzsOAjjDuRnBxZS9zTnqtXKXB1e7M3Iy7KUwh7LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/@listr2/prompt-adapter-inquirer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", - "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/type": "^3.0.8" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8", - "listr2": "9.0.5" - } - }, - "node_modules/@angular/cli/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@angular/cli/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular/cli/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@angular/cli/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/parse5-html-rewriting-stream": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", - "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0", - "parse5": "^8.0.0", - "parse5-sax-parser": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/parse5-sax-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", - "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@angular/cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular/cli/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular/common": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.18.tgz", - "integrity": "sha512-CrV02Omzw/QtfjlEVXVPJVXipdx83NuA+qSASZYrxrhKFusUZyK3P/Zznqg+wiAeNDbedQwMUVqoAARHf0xQrw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "19.2.18", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.18.tgz", - "integrity": "sha512-3MscvODxRVxc3Cs0ZlHI5Pk5rEvE80otfvxZTMksOZuPlv1B+S8MjWfc3X3jk9SbyUEzODBEH55iCaBHD48V3g==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - } - }, - "node_modules/@angular/compiler-cli": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.18.tgz", - "integrity": "sha512-N4TMtLfImJIoMaRL6mx7885UBeQidywptHH6ACZj71Ar6++DBc1mMlcwuvbeJCd3r3y8MQ5nLv5PZSN/tHr13w==", - "dev": true, - "dependencies": { - "@babel/core": "7.26.9", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^4.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^17.2.1" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/index.js" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/compiler": "19.2.18", - "typescript": ">=5.5 <5.9" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular/core": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.18.tgz", - "integrity": "sha512-+QRrf0Igt8ccUWXHA+7doK5W6ODyhHdqVyblSlcQ8OciwkzIIGGEYNZom5OZyWMh+oI54lcSeyV2O3xaDepSrQ==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" - } - }, - "node_modules/@angular/forms": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.18.tgz", - "integrity": "sha512-pe40934jWhoS7DyGl7jyZdoj1gvBgur2t1zrJD+csEkTitYnW14+La2Pv6SW1pNX5nIzFsgsS9Nex1KcH5S6Tw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/platform-browser": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.18.tgz", - "integrity": "sha512-eahtsHPyXTYLARs9YOlXhnXGgzw0wcyOcDkBvNWK/3lA0NHIgIHmQgXAmBo+cJ+g9skiEQTD2OmSrrwbFKWJkw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/animations": "19.2.18", - "@angular/common": "19.2.18", - "@angular/core": "19.2.18" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.18.tgz", - "integrity": "sha512-wqDtK2yVN5VDqVeOSOfqELdu40fyoIDknBGSxA27CEXzFVdMWJyIpuvUi+GMa+9eGjlS+1uVVBaRwxmnuvHj+A==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/compiler": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18" - } - }, - "node_modules/@angular/router": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.18.tgz", - "integrity": "sha512-7cimxtPODSwokFQ0TRYzX0ad8Yjrl0MJfzaDCJejd1n/q7RZ7KZmHd0DS/LkDNXVMEh4swr00fK+3YWG/Szsrg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/common": "19.2.18", - "@angular/core": "19.2.18", - "@angular/platform-browser": "19.2.18", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", - "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.26.5", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", - "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.26.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", - "dev": true, - "engines": { - "node": ">=14.17.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", - "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", - "dev": true, - "dependencies": { - "@inquirer/core": "^10.1.7", - "@inquirer/type": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts/node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.65.0.tgz", - "integrity": "sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", - "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", - "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", - "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", - "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", - "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", - "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", - "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.56.10", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", - "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", - "dev": true, - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.65.0.tgz", - "integrity": "sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.65.0.tgz", - "integrity": "sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.65.0.tgz", - "integrity": "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==", - "dev": true, - "dependencies": { - "@jsonjoy.com/base64": "17.65.0", - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0", - "@jsonjoy.com/json-pointer": "17.65.0", - "@jsonjoy.com/util": "17.65.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.65.0.tgz", - "integrity": "sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==", - "dev": true, - "dependencies": { - "@jsonjoy.com/util": "17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.65.0.tgz", - "integrity": "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==", - "dev": true, - "dependencies": { - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true - }, - "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", - "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", - "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", - "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", - "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", - "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", - "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.7", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@napi-rs/nice": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", - "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", - "dev": true, - "optional": true, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/nice-android-arm-eabi": "1.1.1", - "@napi-rs/nice-android-arm64": "1.1.1", - "@napi-rs/nice-darwin-arm64": "1.1.1", - "@napi-rs/nice-darwin-x64": "1.1.1", - "@napi-rs/nice-freebsd-x64": "1.1.1", - "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", - "@napi-rs/nice-linux-arm64-gnu": "1.1.1", - "@napi-rs/nice-linux-arm64-musl": "1.1.1", - "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", - "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", - "@napi-rs/nice-linux-s390x-gnu": "1.1.1", - "@napi-rs/nice-linux-x64-gnu": "1.1.1", - "@napi-rs/nice-linux-x64-musl": "1.1.1", - "@napi-rs/nice-openharmony-arm64": "1.1.1", - "@napi-rs/nice-win32-arm64-msvc": "1.1.1", - "@napi-rs/nice-win32-ia32-msvc": "1.1.1", - "@napi-rs/nice-win32-x64-msvc": "1.1.1" - } - }, - "node_modules/@napi-rs/nice-android-arm-eabi": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", - "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-android-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", - "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", - "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-x64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", - "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-freebsd-x64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", - "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", - "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", - "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", - "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-ppc64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", - "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-riscv64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", - "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-s390x-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", - "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-gnu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", - "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-x64-musl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", - "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-openharmony-arm64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", - "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-arm64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", - "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-ia32-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", - "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-x64-msvc": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", - "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@ngtools/webpack": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.19.tgz", - "integrity": "sha512-R9aeTrOBiRVl8I698JWPniUAAEpSvzc8SUGWSM5UXWMcHnWqd92cOnJJ1aXDGJZKXrbhMhCBx9Dglmcks5IDpg==", - "dev": true, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", - "typescript": ">=5.5 <5.9", - "webpack": "^5.54.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/fs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", - "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^9.0.0", - "ini": "^6.0.0", - "lru-cache": "^11.2.1", - "npm-pick-manifest": "^11.0.1", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/installed-package-contents": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", - "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^5.0.0", - "npm-normalize-package-bin": "^5.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/node-gyp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", - "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/package-json": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", - "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "glob": "^13.0.0", - "hosted-git-info": "^9.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/redact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", - "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "optional": true - }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@schematics/angular": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.2.tgz", - "integrity": "sha512-kxwxhCIUrj7DfzEtDSs/pi/w+aII/WQLpPfLgoQCWE8/95v60WnTfd1afmsXsFoxikKPxkwoPWtU2YbhSoX9MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.1.2", - "@angular-devkit/schematics": "21.1.2", - "jsonc-parser": "3.3.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "21.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.2.tgz", - "integrity": "sha512-0wl5nJlFWsbwfUB2CQeTSmnVQ8AtqqwM3bYPYtXSc+vA8+hzsOAjjDuRnBxZS9zTnqtXKXB1e7M3Iy7KUwh7LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@schematics/angular/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@schematics/angular/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@sigstore/bundle": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", - "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", - "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", - "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@sigstore/sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", - "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.3", - "proc-log": "^6.1.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/tuf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", - "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sigstore/verify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", - "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true - }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", - "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^10.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/jasmine": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", - "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", - "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", - "dev": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", - "dev": true, - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/algoliasearch": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", - "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.12.2", - "@algolia/client-abtesting": "5.46.2", - "@algolia/client-analytics": "5.46.2", - "@algolia/client-common": "5.46.2", - "@algolia/client-insights": "5.46.2", - "@algolia/client-personalization": "5.46.2", - "@algolia/client-query-suggestions": "5.46.2", - "@algolia/client-search": "5.46.2", - "@algolia/ingestion": "1.46.2", - "@algolia/monitoring": "1.46.2", - "@algolia/recommend": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "dev": true, - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true - }, - "node_modules/beasties": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz", - "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==", - "dev": true, - "dependencies": { - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "htmlparser2": "^10.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dev": true, - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", - "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^5.0.0", - "fs-minipass": "^3.0.0", - "glob": "^13.0.0", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001768", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", - "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true - }, - "node_modules/copy-anything": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", - "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, - "dependencies": { - "is-what": "^3.14.1" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", - "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", - "dev": true, - "dependencies": { - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.1", - "globby": "^14.0.0", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", - "dev": true, - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dev": true, - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true - }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", - "dev": true, - "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/engine.io": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", - "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", - "dev": true, - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", - "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "punycode": "^1.4.1", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "optional": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/esbuild-wasm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.4.tgz", - "integrity": "sha512-2HlCS6rNvKWaSKhWaG/YIyRsTsL3gUrMP2ToZMBIjw9LM7vVcIs+rz8kE2vExvTJgvM8OKPqNpcHawY/BQc/qQ==", - "dev": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-middleware": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", - "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.15", - "debug": "^4.3.6", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.3", - "is-plain-object": "^5.0.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "engines": { - "node": ">=10.18" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", - "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, - "optional": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jasmine-core": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", - "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", - "dev": true - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", - "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/karma": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", - "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", - "dev": true, - "dependencies": { - "@colors/colors": "1.5.0", - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.5.1", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.7", - "graceful-fs": "^4.2.6", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.8", - "lodash": "^4.17.21", - "log4js": "^6.4.1", - "mime": "^2.5.2", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.5", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^4.7.2", - "source-map": "^0.6.1", - "tmp": "^0.2.1", - "ua-parser-js": "^0.7.30", - "yargs": "^16.1.1" - }, - "bin": { - "karma": "bin/karma" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/karma-chrome-launcher": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", - "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", - "dev": true, - "dependencies": { - "which": "^1.2.1" - } - }, - "node_modules/karma-coverage": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", - "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.0.5", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma-coverage/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/karma-jasmine": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", - "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", - "dev": true, - "dependencies": { - "jasmine-core": "^4.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "karma": "^6.0.0" - } - }, - "node_modules/karma-jasmine-html-reporter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", - "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", - "dev": true, - "peerDependencies": { - "jasmine-core": "^4.0.0 || ^5.0.0", - "karma": "^6.0.0", - "karma-jasmine": "^5.0.0" - } - }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", - "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", - "dev": true - }, - "node_modules/karma-source-map-support": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", - "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", - "dev": true, - "dependencies": { - "source-map-support": "^0.5.5" - } - }, - "node_modules/karma/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/karma/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/karma/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/karma/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/karma/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/karma/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/karma/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/karma/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", - "dev": true, - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/less": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", - "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", - "dev": true, - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/less-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", - "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", - "dev": true, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "optional": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/license-webpack-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", - "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", - "dev": true, - "dependencies": { - "webpack-sources": "^3.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", - "dev": true, - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lmdb": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", - "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "msgpackr": "^1.11.2", - "node-addon-api": "^6.1.0", - "node-gyp-build-optional-packages": "5.2.2", - "ordered-binary": "^1.5.3", - "weak-lru-cache": "^1.2.2" - }, - "bin": { - "download-lmdb-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.2.6", - "@lmdb/lmdb-darwin-x64": "3.2.6", - "@lmdb/lmdb-linux-arm": "3.2.6", - "@lmdb/lmdb-linux-arm64": "3.2.6", - "@lmdb/lmdb-linux-x64": "3.2.6", - "@lmdb/lmdb-win32-x64": "3.2.6" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "dev": true, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "dev": true, - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", - "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", - "dev": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-to-fsa": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", - "dev": true, - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", - "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^2.0.0", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", - "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/msgpackr": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", - "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", - "dev": true, - "optional": true, - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, - "optional": true - }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "dev": true, - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-bundled": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", - "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-install-checks": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", - "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-package-arg": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", - "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", - "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "npm-package-arg": "^13.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-registry-fetch": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", - "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^4.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^15.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^13.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ordered-binary": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", - "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", - "dev": true, - "optional": true - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/pacote": { - "version": "21.0.4", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", - "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "@npmcli/run-script": "^10.0.0", - "cacache": "^20.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^13.0.0", - "npm-packlist": "^10.0.1", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", - "ssri": "^13.0.0", - "tar": "^7.4.3" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-html-rewriting-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", - "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", - "dev": true, - "dependencies": { - "entities": "^4.3.0", - "parse5": "^7.0.0", - "parse5-sax-parser": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-sax-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", - "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", - "dev": true, - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/piscina": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", - "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", - "dev": true, - "optionalDependencies": { - "@napi-rs/nice": "^1.0.1" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", - "dev": true, - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "optional": true - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - }, - "node_modules/qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true, - "engines": { - "node": ">=0.9" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true - }, - "node_modules/regex-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", - "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", - "dev": true - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "dev": true, - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", - "dev": true, - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", - "dev": true, - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass-loader": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", - "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", - "dev": true, - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.8.0", - "mime-types": "~2.1.35", - "parseurl": "~1.3.3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sigstore": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", - "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", - "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socket.io": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", - "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", - "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", - "dev": true, - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.18.3" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-loader/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "dev": true, - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", - "dev": true, - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/tuf-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", - "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "4.1.0", - "debug": "^4.4.3", - "make-fetch-happen": "^15.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-assert": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", - "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ua-parser-js": { - "version": "0.7.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", - "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-filename": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", - "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/unique-slug": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", - "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/vite/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/vite/node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/weak-lru-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", - "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", - "dev": true, - "optional": true - }, - "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", - "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/express-serve-static-core": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-subresource-integrity": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", - "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", - "dev": true, - "dependencies": { - "typed-assert": "^1.0.8" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", - "webpack": "^5.12.0" - }, - "peerDependenciesMeta": { - "html-webpack-plugin": { - "optional": true - } - } - }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - }, - "node_modules/zone.js": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", - "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==" - } - } -} diff --git a/cmd/bugseti/frontend/package.json b/cmd/bugseti/frontend/package.json deleted file mode 100644 index d5cdb888..00000000 --- a/cmd/bugseti/frontend/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "bugseti", - "version": "0.1.0", - "private": true, - "scripts": { - "ng": "ng", - "start": "ng serve", - "dev": "ng serve --configuration development", - "build": "ng build --configuration production", - "build:dev": "ng build --configuration development", - "watch": "ng build --watch --configuration development", - "test": "ng test", - "lint": "ng lint" - }, - "dependencies": { - "@angular/animations": "^19.1.0", - "@angular/common": "^19.1.0", - "@angular/compiler": "^19.1.0", - "@angular/core": "^19.1.0", - "@angular/forms": "^19.1.0", - "@angular/platform-browser": "^19.1.0", - "@angular/platform-browser-dynamic": "^19.1.0", - "@angular/router": "^19.1.0", - "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" - }, - "devDependencies": { - "@angular-devkit/build-angular": "^19.1.0", - "@angular/cli": "^21.1.2", - "@angular/compiler-cli": "^19.1.0", - "@types/jasmine": "~5.1.0", - "jasmine-core": "~5.1.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.5.2" - } -} diff --git a/cmd/bugseti/frontend/src/app/app.component.ts b/cmd/bugseti/frontend/src/app/app.component.ts deleted file mode 100644 index 48d645c3..00000000 --- a/cmd/bugseti/frontend/src/app/app.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; - -@Component({ - selector: 'app-root', - standalone: true, - imports: [RouterOutlet], - template: '', - styles: [` - :host { - display: block; - height: 100%; - } - `] -}) -export class AppComponent { - title = 'BugSETI'; -} diff --git a/cmd/bugseti/frontend/src/app/app.config.ts b/cmd/bugseti/frontend/src/app/app.config.ts deleted file mode 100644 index 628370af..00000000 --- a/cmd/bugseti/frontend/src/app/app.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApplicationConfig } from '@angular/core'; -import { provideRouter, withHashLocation } from '@angular/router'; -import { routes } from './app.routes'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideRouter(routes, withHashLocation()) - ] -}; diff --git a/cmd/bugseti/frontend/src/app/app.routes.ts b/cmd/bugseti/frontend/src/app/app.routes.ts deleted file mode 100644 index 76725edb..00000000 --- a/cmd/bugseti/frontend/src/app/app.routes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - redirectTo: 'tray', - pathMatch: 'full' - }, - { - path: 'tray', - loadComponent: () => import('./tray/tray.component').then(m => m.TrayComponent) - }, - { - path: 'workbench', - loadComponent: () => import('./workbench/workbench.component').then(m => m.WorkbenchComponent) - }, - { - path: 'settings', - loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent) - }, - { - path: 'onboarding', - loadComponent: () => import('./onboarding/onboarding.component').then(m => m.OnboardingComponent) - }, - { - path: 'jellyfin', - loadComponent: () => import('./jellyfin/jellyfin.component').then(m => m.JellyfinComponent) - } -]; diff --git a/cmd/bugseti/frontend/src/app/jellyfin/jellyfin.component.ts b/cmd/bugseti/frontend/src/app/jellyfin/jellyfin.component.ts deleted file mode 100644 index 95801067..00000000 --- a/cmd/bugseti/frontend/src/app/jellyfin/jellyfin.component.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; - -type Mode = 'web' | 'stream'; - -@Component({ - selector: 'app-jellyfin', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
-
-

Jellyfin Player

-

Quick embed for media.lthn.ai or any Jellyfin host.

-
-
- - -
-
- -
-
- - -
- -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
- -
- -
- -
- -

Set Item ID and API key to build stream URL.

-
-
- `, - styles: [` - .jellyfin { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - padding: var(--spacing-md); - height: 100%; - overflow: auto; - background: var(--bg-secondary); - } - - .jellyfin__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-md); - } - - .jellyfin__header h1 { - margin-bottom: var(--spacing-xs); - } - - .mode-switch { - display: flex; - gap: var(--spacing-xs); - } - - .mode-switch .btn.is-active { - border-color: var(--accent-primary); - color: var(--accent-primary); - } - - .jellyfin__config { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - } - - .stream-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-sm); - } - - .actions { - display: flex; - gap: var(--spacing-sm); - } - - .jellyfin__viewer { - flex: 1; - min-height: 420px; - padding: 0; - overflow: hidden; - } - - .jellyfin-frame, - .jellyfin-video { - border: 0; - width: 100%; - height: 100%; - min-height: 420px; - background: #000; - } - - .stream-hint { - padding: var(--spacing-md); - margin: 0; - } - `] -}) -export class JellyfinComponent { - mode: Mode = 'web'; - loaded = false; - - serverUrl = 'https://media.lthn.ai'; - itemId = ''; - apiKey = ''; - mediaSourceId = ''; - - safeWebUrl!: SafeResourceUrl; - streamUrl = ''; - - constructor(private sanitizer: DomSanitizer) { - this.safeWebUrl = this.sanitizer.bypassSecurityTrustResourceUrl('https://media.lthn.ai/web/index.html'); - } - - load(): void { - const base = this.normalizeBase(this.serverUrl); - this.safeWebUrl = this.sanitizer.bypassSecurityTrustResourceUrl(`${base}/web/index.html`); - this.streamUrl = this.buildStreamUrl(base); - this.loaded = true; - } - - reset(): void { - this.loaded = false; - this.itemId = ''; - this.apiKey = ''; - this.mediaSourceId = ''; - this.streamUrl = ''; - } - - private normalizeBase(value: string): string { - const raw = value.trim() || 'https://media.lthn.ai'; - const withProtocol = raw.startsWith('http://') || raw.startsWith('https://') ? raw : `https://${raw}`; - return withProtocol.replace(/\/+$/, ''); - } - - private buildStreamUrl(base: string): string { - if (!this.itemId.trim() || !this.apiKey.trim()) { - return ''; - } - - const url = new URL(`${base}/Videos/${encodeURIComponent(this.itemId.trim())}/stream`); - url.searchParams.set('api_key', this.apiKey.trim()); - url.searchParams.set('static', 'true'); - if (this.mediaSourceId.trim()) { - url.searchParams.set('MediaSourceId', this.mediaSourceId.trim()); - } - return url.toString(); - } -} diff --git a/cmd/bugseti/frontend/src/app/onboarding/onboarding.component.ts b/cmd/bugseti/frontend/src/app/onboarding/onboarding.component.ts deleted file mode 100644 index 7d95d7be..00000000 --- a/cmd/bugseti/frontend/src/app/onboarding/onboarding.component.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -@Component({ - selector: 'app-onboarding', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
- -
-
B
-

Welcome to BugSETI

-

Distributed Bug Fixing - like SETI@home but for code

- -
-
- [1] -
- Find Issues -

We pull beginner-friendly issues from OSS projects you care about.

-
-
-
- [2] -
- Get Context -

AI prepares relevant context to help you understand each issue.

-
-
-
- [3] -
- Submit PRs -

Fix bugs and submit PRs with minimal friction.

-
-
-
- - -
- - -
-

Connect GitHub

-

BugSETI uses the GitHub CLI (gh) to interact with repositories.

- -
- {{ ghAuthenticated ? '[OK]' : '[!]' }} - {{ ghAuthenticated ? 'GitHub CLI authenticated' : 'GitHub CLI not detected' }} -
- -
-

To authenticate with GitHub CLI, run:

- gh auth login -

After authenticating, click "Check Again".

-
- -
- - -
-
- - -
-

Choose Repositories

-

Add repositories you want to contribute to.

- -
- - -
- -
-

Selected Repositories

-
- {{ repo }} - -
-
- -
-

Suggested Repositories

-
- -
-
- -
- - -
-
- - -
-
[OK]
-

You're All Set!

-

BugSETI is ready to help you contribute to open source.

- -
-

{{ selectedRepos.length }} repositories selected

-

Looking for issues with these labels:

-
- good first issue - help wanted - beginner-friendly -
-
- - -
-
- -
- - - - -
-
- `, - styles: [` - .onboarding { - display: flex; - flex-direction: column; - height: 100%; - background-color: var(--bg-primary); - } - - .onboarding-content { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-xl); - } - - .step { - max-width: 500px; - text-align: center; - } - - .step-icon, .complete-icon { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto var(--spacing-lg); - background: linear-gradient(135deg, var(--accent-primary), var(--accent-success)); - border-radius: var(--radius-lg); - font-size: 32px; - font-weight: bold; - color: white; - } - - .complete-icon { - background: var(--accent-success); - } - - h1 { - font-size: 28px; - margin-bottom: var(--spacing-sm); - } - - h2 { - font-size: 24px; - margin-bottom: var(--spacing-sm); - } - - .subtitle { - color: var(--text-secondary); - margin-bottom: var(--spacing-xl); - } - - .feature-list { - text-align: left; - margin-bottom: var(--spacing-xl); - } - - .feature { - display: flex; - gap: var(--spacing-md); - margin-bottom: var(--spacing-md); - padding: var(--spacing-md); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - } - - .feature-icon { - font-family: var(--font-mono); - color: var(--accent-primary); - font-weight: bold; - } - - .feature strong { - display: block; - margin-bottom: var(--spacing-xs); - } - - .feature p { - color: var(--text-secondary); - font-size: 13px; - margin: 0; - } - - .auth-status { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); - padding: var(--spacing-md); - background-color: var(--bg-tertiary); - border-radius: var(--radius-md); - margin: var(--spacing-lg) 0; - } - - .auth-status.auth-success { - background-color: rgba(63, 185, 80, 0.15); - color: var(--accent-success); - } - - .status-icon { - font-family: var(--font-mono); - font-weight: bold; - } - - .auth-instructions { - text-align: left; - padding: var(--spacing-md); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - } - - .auth-instructions code { - display: block; - margin: var(--spacing-md) 0; - padding: var(--spacing-md); - background-color: var(--bg-tertiary); - } - - .auth-instructions .note { - color: var(--text-muted); - font-size: 13px; - margin: 0; - } - - .step-actions { - display: flex; - gap: var(--spacing-md); - justify-content: center; - margin-top: var(--spacing-xl); - } - - .repo-input { - display: flex; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-lg); - } - - .repo-input .form-input { - flex: 1; - } - - .selected-repos, .suggested-repos { - text-align: left; - margin-bottom: var(--spacing-lg); - } - - .selected-repos h3, .suggested-repos h3 { - font-size: 12px; - text-transform: uppercase; - color: var(--text-muted); - margin-bottom: var(--spacing-sm); - } - - .repo-chip { - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-xs) var(--spacing-sm); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - margin-right: var(--spacing-xs); - margin-bottom: var(--spacing-xs); - } - - .repo-remove { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - padding: 0; - } - - .suggested-list { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - } - - .suggestion { - padding: var(--spacing-xs) var(--spacing-sm); - background-color: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - color: var(--text-secondary); - cursor: pointer; - font-size: 13px; - } - - .suggestion:hover { - background-color: var(--bg-secondary); - border-color: var(--accent-primary); - } - - .summary { - padding: var(--spacing-lg); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - margin-bottom: var(--spacing-xl); - } - - .summary p { - margin-bottom: var(--spacing-sm); - } - - .label-list { - display: flex; - gap: var(--spacing-xs); - justify-content: center; - flex-wrap: wrap; - } - - .step-indicators { - display: flex; - justify-content: center; - gap: var(--spacing-sm); - padding: var(--spacing-lg); - } - - .indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--border-color); - } - - .indicator.active { - background-color: var(--accent-primary); - } - - .indicator.current { - width: 24px; - border-radius: 4px; - } - - .btn--lg { - padding: var(--spacing-md) var(--spacing-xl); - font-size: 16px; - } - `] -}) -export class OnboardingComponent { - step = 1; - ghAuthenticated = false; - newRepo = ''; - selectedRepos: string[] = []; - suggestedRepos = [ - 'facebook/react', - 'microsoft/vscode', - 'golang/go', - 'kubernetes/kubernetes', - 'rust-lang/rust', - 'angular/angular', - 'nodejs/node', - 'python/cpython' - ]; - - ngOnInit() { - this.checkGhAuth(); - } - - nextStep() { - if (this.step < 4) { - this.step++; - } - } - - prevStep() { - if (this.step > 1) { - this.step--; - } - } - - async checkGhAuth() { - try { - // Check if gh CLI is authenticated - // In a real implementation, this would call the backend - this.ghAuthenticated = true; // Assume authenticated for demo - } catch (err) { - this.ghAuthenticated = false; - } - } - - addRepo() { - if (this.newRepo && !this.selectedRepos.includes(this.newRepo)) { - this.selectedRepos.push(this.newRepo); - this.newRepo = ''; - } - } - - removeRepo(index: number) { - this.selectedRepos.splice(index, 1); - } - - addSuggested(repo: string) { - if (!this.selectedRepos.includes(repo)) { - this.selectedRepos.push(repo); - } - } - - async complete() { - try { - // Save repos to config - if ((window as any).go?.main?.ConfigService?.SetConfig) { - const config = await (window as any).go.main.ConfigService.GetConfig() || {}; - config.watchedRepos = this.selectedRepos; - await (window as any).go.main.ConfigService.SetConfig(config); - } - - // Mark onboarding as complete - if ((window as any).go?.main?.TrayService?.CompleteOnboarding) { - await (window as any).go.main.TrayService.CompleteOnboarding(); - } - - // Close onboarding window and start fetching - if ((window as any).wails?.Window) { - (window as any).wails.Window.GetByName('onboarding').then((w: any) => w.Hide()); - } - - // Start fetching - if ((window as any).go?.main?.TrayService?.StartFetching) { - await (window as any).go.main.TrayService.StartFetching(); - } - } catch (err) { - console.error('Failed to complete onboarding:', err); - } - } -} diff --git a/cmd/bugseti/frontend/src/app/settings/settings.component.ts b/cmd/bugseti/frontend/src/app/settings/settings.component.ts deleted file mode 100644 index 7447d3fa..00000000 --- a/cmd/bugseti/frontend/src/app/settings/settings.component.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -interface Config { - watchedRepos: string[]; - labels: string[]; - fetchIntervalMinutes: number; - notificationsEnabled: boolean; - notificationSound: boolean; - workspaceDir: string; - marketplaceMcpRoot: string; - theme: string; - autoSeedContext: boolean; - workHours?: { - enabled: boolean; - startHour: number; - endHour: number; - days: number[]; - timezone: string; - }; -} - -@Component({ - selector: 'app-settings', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
-

Settings

- -
- -
-
-

Repositories

-

Add GitHub repositories to watch for issues.

- -
-
- {{ repo }} - -
-
- -
- - -
-
- -
-

Issue Labels

-

Filter issues by these labels.

- -
- - {{ label }} - - -
- -
- - -
-
- -
-

Fetch Settings

- -
- - -
- -
- -
-
- -
-

Work Hours

-

Only fetch issues during these hours.

- -
- -
- -
-
- - -
- -
- - -
- -
- -
- -
-
-
-
- -
-

Notifications

- -
- -
- -
- -
-
- -
-

Appearance

- -
- - -
-
- -
-

Storage

- -
- - -
- -
- - -

Override the marketplace MCP root. Leave empty to auto-detect.

-
-
-
-
- `, - styles: [` - .settings { - display: flex; - flex-direction: column; - height: 100%; - background-color: var(--bg-secondary); - } - - .settings-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--spacing-md) var(--spacing-lg); - background-color: var(--bg-primary); - border-bottom: 1px solid var(--border-color); - } - - .settings-header h1 { - font-size: 18px; - margin: 0; - } - - .settings-content { - flex: 1; - overflow-y: auto; - padding: var(--spacing-lg); - } - - .settings-section { - background-color: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - margin-bottom: var(--spacing-lg); - } - - .settings-section h2 { - font-size: 16px; - margin-bottom: var(--spacing-xs); - } - - .section-description { - color: var(--text-muted); - font-size: 13px; - margin-bottom: var(--spacing-md); - } - - .repo-list, .label-list { - margin-bottom: var(--spacing-md); - } - - .repo-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--spacing-sm); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - margin-bottom: var(--spacing-xs); - } - - .add-repo, .add-label { - display: flex; - gap: var(--spacing-sm); - } - - .add-repo .form-input, .add-label .form-input { - flex: 1; - } - - .label-list { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - } - - .label-chip { - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-xs) var(--spacing-sm); - background-color: var(--bg-tertiary); - border-radius: 999px; - font-size: 13px; - } - - .label-remove { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - padding: 0; - font-size: 14px; - line-height: 1; - } - - .label-remove:hover { - color: var(--accent-danger); - } - - .checkbox-label { - display: flex; - align-items: center; - gap: var(--spacing-sm); - cursor: pointer; - } - - .checkbox-label input[type="checkbox"] { - width: 16px; - height: 16px; - } - - .work-hours-config { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-md); - margin-top: var(--spacing-md); - } - - .day-checkboxes { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-sm); - } - - .day-checkboxes .checkbox-label { - width: auto; - } - - .btn--sm { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: 12px; - } - `] -}) -export class SettingsComponent implements OnInit { - config: Config = { - watchedRepos: [], - labels: ['good first issue', 'help wanted'], - fetchIntervalMinutes: 15, - notificationsEnabled: true, - notificationSound: true, - workspaceDir: '', - marketplaceMcpRoot: '', - theme: 'dark', - autoSeedContext: true, - workHours: { - enabled: false, - startHour: 9, - endHour: 17, - days: [1, 2, 3, 4, 5], - timezone: '' - } - }; - - newRepo = ''; - newLabel = ''; - hours = Array.from({ length: 24 }, (_, i) => i); - days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - - ngOnInit() { - this.loadConfig(); - } - - async loadConfig() { - try { - if ((window as any).go?.main?.ConfigService?.GetConfig) { - this.config = await (window as any).go.main.ConfigService.GetConfig(); - if (!this.config.workHours) { - this.config.workHours = { - enabled: false, - startHour: 9, - endHour: 17, - days: [1, 2, 3, 4, 5], - timezone: '' - }; - } - } - } catch (err) { - console.error('Failed to load config:', err); - } - } - - async saveSettings() { - try { - if ((window as any).go?.main?.ConfigService?.SetConfig) { - await (window as any).go.main.ConfigService.SetConfig(this.config); - alert('Settings saved!'); - } - } catch (err) { - console.error('Failed to save config:', err); - alert('Failed to save settings.'); - } - } - - addRepo() { - if (this.newRepo && !this.config.watchedRepos.includes(this.newRepo)) { - this.config.watchedRepos.push(this.newRepo); - this.newRepo = ''; - } - } - - removeRepo(index: number) { - this.config.watchedRepos.splice(index, 1); - } - - addLabel() { - if (this.newLabel && !this.config.labels.includes(this.newLabel)) { - this.config.labels.push(this.newLabel); - this.newLabel = ''; - } - } - - removeLabel(index: number) { - this.config.labels.splice(index, 1); - } - - isDaySelected(day: number): boolean { - return this.config.workHours?.days.includes(day) || false; - } - - toggleDay(day: number) { - if (!this.config.workHours) return; - - const index = this.config.workHours.days.indexOf(day); - if (index === -1) { - this.config.workHours.days.push(day); - } else { - this.config.workHours.days.splice(index, 1); - } - } -} diff --git a/cmd/bugseti/frontend/src/app/settings/updates.component.ts b/cmd/bugseti/frontend/src/app/settings/updates.component.ts deleted file mode 100644 index fb4edf94..00000000 --- a/cmd/bugseti/frontend/src/app/settings/updates.component.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -interface UpdateSettings { - channel: string; - autoUpdate: boolean; - checkInterval: number; - lastCheck: string; -} - -interface VersionInfo { - version: string; - channel: string; - commit: string; - buildTime: string; - goVersion: string; - os: string; - arch: string; -} - -interface ChannelInfo { - id: string; - name: string; - description: string; -} - -interface UpdateCheckResult { - available: boolean; - currentVersion: string; - latestVersion: string; - release?: { - version: string; - channel: string; - tag: string; - name: string; - body: string; - publishedAt: string; - htmlUrl: string; - }; - error?: string; - checkedAt: string; -} - -@Component({ - selector: 'app-updates-settings', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
-
- {{ versionInfo?.version || 'Unknown' }} - - {{ versionInfo?.channel || 'dev' }} - -
-

- Built {{ versionInfo.buildTime | date:'medium' }} ({{ versionInfo.commit?.substring(0, 7) }}) -

-
- -
-
-
!
-
-

Update Available

-

Version {{ checkResult.latestVersion }} is available

- - View Release Notes - -
- -
- -
-
OK
-
-

Up to Date

-

You're running the latest version

- - Last checked: {{ checkResult.checkedAt | date:'short' }} - -
-
- -
-
X
-
-

Check Failed

-

{{ checkResult.error }}

-
-
-
- -
- -
- -
-

Update Channel

-

Choose which release channel to follow for updates.

- -
- -
-
- -
-

Automatic Updates

- -
- -

When enabled, updates will be installed automatically on app restart.

-
- -
- - -
-
- -
- {{ saveMessage }} -
-
- `, - styles: [` - .updates-settings { - padding: var(--spacing-md); - } - - .current-version { - background: var(--bg-tertiary); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - margin-bottom: var(--spacing-lg); - text-align: center; - } - - .version-badge { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-xs); - } - - .version-number { - font-size: 24px; - font-weight: 600; - } - - .channel-badge { - padding: 2px 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - } - - .channel-stable { background: var(--accent-success); color: white; } - .channel-beta { background: var(--accent-warning); color: black; } - .channel-nightly { background: var(--accent-purple, #8b5cf6); color: white; } - .channel-dev { background: var(--text-muted); color: var(--bg-primary); } - - .build-info { - color: var(--text-muted); - font-size: 12px; - margin: 0; - } - - .update-check { - margin-bottom: var(--spacing-lg); - } - - .update-available, .up-to-date, .check-error { - display: flex; - align-items: center; - gap: var(--spacing-md); - padding: var(--spacing-md); - border-radius: var(--radius-md); - } - - .update-available { - background: var(--accent-warning-bg, rgba(245, 158, 11, 0.1)); - border: 1px solid var(--accent-warning); - } - - .up-to-date { - background: var(--accent-success-bg, rgba(34, 197, 94, 0.1)); - border: 1px solid var(--accent-success); - } - - .check-error { - background: var(--accent-danger-bg, rgba(239, 68, 68, 0.1)); - border: 1px solid var(--accent-danger); - } - - .update-icon, .check-icon, .error-icon { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - flex-shrink: 0; - } - - .update-icon { background: var(--accent-warning); color: black; } - .check-icon { background: var(--accent-success); color: white; } - .error-icon { background: var(--accent-danger); color: white; } - - .update-info, .check-info, .error-info { - flex: 1; - } - - .update-info h4, .check-info h4, .error-info h4 { - margin: 0 0 var(--spacing-xs) 0; - font-size: 14px; - } - - .update-info p, .check-info p, .error-info p { - margin: 0; - font-size: 13px; - color: var(--text-muted); - } - - .release-link { - color: var(--accent-primary); - font-size: 12px; - } - - .last-check { - font-size: 11px; - color: var(--text-muted); - } - - .check-button-row { - margin-bottom: var(--spacing-lg); - } - - .settings-section { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - margin-bottom: var(--spacing-lg); - } - - .settings-section h3 { - font-size: 14px; - margin: 0 0 var(--spacing-xs) 0; - } - - .section-description { - color: var(--text-muted); - font-size: 12px; - margin-bottom: var(--spacing-md); - } - - .channel-options { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - } - - .channel-option { - display: flex; - align-items: flex-start; - gap: var(--spacing-sm); - padding: var(--spacing-md); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.15s ease; - } - - .channel-option:hover { - border-color: var(--accent-primary); - } - - .channel-option.selected { - border-color: var(--accent-primary); - background: var(--accent-primary-bg, rgba(59, 130, 246, 0.1)); - } - - .channel-option input[type="radio"] { - margin-top: 2px; - } - - .channel-content { - display: flex; - flex-direction: column; - gap: 2px; - } - - .channel-name { - font-weight: 500; - font-size: 14px; - } - - .channel-desc { - font-size: 12px; - color: var(--text-muted); - } - - .form-group { - margin-bottom: var(--spacing-md); - } - - .form-group:last-child { - margin-bottom: 0; - } - - .checkbox-label { - display: flex; - align-items: center; - gap: var(--spacing-sm); - cursor: pointer; - } - - .setting-hint { - color: var(--text-muted); - font-size: 12px; - margin: var(--spacing-xs) 0 0 24px; - } - - .form-label { - display: block; - font-size: 13px; - margin-bottom: var(--spacing-xs); - } - - .form-select { - width: 100%; - padding: var(--spacing-sm); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-secondary); - color: var(--text-primary); - font-size: 14px; - } - - .save-status { - text-align: center; - font-size: 13px; - color: var(--accent-success); - } - - .save-status .error { - color: var(--accent-danger); - } - - .btn { - padding: var(--spacing-sm) var(--spacing-md); - border: none; - border-radius: var(--radius-md); - font-size: 14px; - cursor: pointer; - transition: all 0.15s ease; - } - - .btn:disabled { - opacity: 0.6; - cursor: not-allowed; - } - - .btn--primary { - background: var(--accent-primary); - color: white; - } - - .btn--primary:hover:not(:disabled) { - background: var(--accent-primary-hover, #2563eb); - } - - .btn--secondary { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border-color); - } - - .btn--secondary:hover:not(:disabled) { - background: var(--bg-secondary); - } - `] -}) -export class UpdatesComponent implements OnInit, OnDestroy { - settings: UpdateSettings = { - channel: 'stable', - autoUpdate: false, - checkInterval: 6, - lastCheck: '' - }; - - versionInfo: VersionInfo | null = null; - checkResult: UpdateCheckResult | null = null; - - channels: ChannelInfo[] = [ - { id: 'stable', name: 'Stable', description: 'Production releases - most stable, recommended for most users' }, - { id: 'beta', name: 'Beta', description: 'Pre-release builds - new features being tested before stable release' }, - { id: 'nightly', name: 'Nightly', description: 'Latest development builds - bleeding edge, may be unstable' } - ]; - - isChecking = false; - isInstalling = false; - saveMessage = ''; - saveError = false; - - private saveTimeout: ReturnType | null = null; - - ngOnInit() { - this.loadSettings(); - this.loadVersionInfo(); - } - - ngOnDestroy() { - if (this.saveTimeout) { - clearTimeout(this.saveTimeout); - } - } - - async loadSettings() { - try { - const wails = (window as any).go?.main; - if (wails?.UpdateService?.GetSettings) { - this.settings = await wails.UpdateService.GetSettings(); - } else if (wails?.ConfigService?.GetUpdateSettings) { - this.settings = await wails.ConfigService.GetUpdateSettings(); - } - } catch (err) { - console.error('Failed to load update settings:', err); - } - } - - async loadVersionInfo() { - try { - const wails = (window as any).go?.main; - if (wails?.VersionService?.GetVersionInfo) { - this.versionInfo = await wails.VersionService.GetVersionInfo(); - } else if (wails?.UpdateService?.GetVersionInfo) { - this.versionInfo = await wails.UpdateService.GetVersionInfo(); - } - } catch (err) { - console.error('Failed to load version info:', err); - } - } - - async checkForUpdates() { - this.isChecking = true; - this.checkResult = null; - - try { - const wails = (window as any).go?.main; - if (wails?.UpdateService?.CheckForUpdate) { - this.checkResult = await wails.UpdateService.CheckForUpdate(); - } - } catch (err) { - console.error('Failed to check for updates:', err); - this.checkResult = { - available: false, - currentVersion: this.versionInfo?.version || 'unknown', - latestVersion: '', - error: 'Failed to check for updates', - checkedAt: new Date().toISOString() - }; - } finally { - this.isChecking = false; - } - } - - async installUpdate() { - if (!this.checkResult?.available || !this.checkResult.release) { - return; - } - - this.isInstalling = true; - - try { - const wails = (window as any).go?.main; - if (wails?.UpdateService?.InstallUpdate) { - await wails.UpdateService.InstallUpdate(); - } - } catch (err) { - console.error('Failed to install update:', err); - alert('Failed to install update. Please try again or download manually.'); - } finally { - this.isInstalling = false; - } - } - - async onSettingsChange() { - // Debounce save - if (this.saveTimeout) { - clearTimeout(this.saveTimeout); - } - - this.saveTimeout = setTimeout(() => this.saveSettings(), 500); - } - - async saveSettings() { - try { - const wails = (window as any).go?.main; - if (wails?.UpdateService?.SetSettings) { - await wails.UpdateService.SetSettings(this.settings); - } else if (wails?.ConfigService?.SetUpdateSettings) { - await wails.ConfigService.SetUpdateSettings(this.settings); - } - this.saveMessage = 'Settings saved'; - this.saveError = false; - } catch (err) { - console.error('Failed to save update settings:', err); - this.saveMessage = 'Failed to save settings'; - this.saveError = true; - } - - // Clear message after 2 seconds - setTimeout(() => { - this.saveMessage = ''; - }, 2000); - } -} diff --git a/cmd/bugseti/frontend/src/app/tray/tray.component.ts b/cmd/bugseti/frontend/src/app/tray/tray.component.ts deleted file mode 100644 index f6232e90..00000000 --- a/cmd/bugseti/frontend/src/app/tray/tray.component.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -interface TrayStatus { - running: boolean; - currentIssue: string; - queueSize: number; - issuesFixed: number; - prsMerged: number; -} - -@Component({ - selector: 'app-tray', - standalone: true, - imports: [CommonModule], - template: ` -
-
- - - {{ status.running ? 'Running' : 'Paused' }} - -
- -
-
- {{ status.queueSize }} - In Queue -
-
- {{ status.issuesFixed }} - Fixed -
-
- {{ status.prsMerged }} - Merged -
-
- -
-

Current Issue

-
-

{{ status.currentIssue }}

-
- - -
-
-
- -
-
- [ ] -

No issue in progress

- -
-
- -
- - - -
-
- `, - styles: [` - .tray-panel { - display: flex; - flex-direction: column; - height: 100%; - padding: var(--spacing-md); - background-color: var(--bg-primary); - } - - .tray-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--spacing-md); - } - - .logo { - display: flex; - align-items: center; - gap: var(--spacing-sm); - } - - .logo-icon { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, var(--accent-primary), var(--accent-success)); - border-radius: var(--radius-md); - font-weight: bold; - color: white; - } - - .logo-text { - font-weight: 600; - font-size: 16px; - } - - .stats-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-sm); - margin-bottom: var(--spacing-md); - } - - .stat-card { - display: flex; - flex-direction: column; - align-items: center; - padding: var(--spacing-sm); - background-color: var(--bg-secondary); - border-radius: var(--radius-md); - } - - .stat-value { - font-size: 24px; - font-weight: bold; - color: var(--accent-primary); - } - - .stat-label { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - } - - .current-issue { - flex: 1; - margin-bottom: var(--spacing-md); - } - - .current-issue h3 { - font-size: 12px; - color: var(--text-muted); - text-transform: uppercase; - margin-bottom: var(--spacing-sm); - } - - .issue-card { - background-color: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: var(--spacing-md); - } - - .issue-title { - font-size: 13px; - line-height: 1.4; - margin-bottom: var(--spacing-sm); - } - - .issue-actions { - display: flex; - gap: var(--spacing-sm); - } - - .empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--spacing-xl); - text-align: center; - } - - .empty-icon { - font-size: 32px; - color: var(--text-muted); - margin-bottom: var(--spacing-sm); - } - - .empty-state p { - color: var(--text-muted); - margin-bottom: var(--spacing-md); - } - - .tray-footer { - display: flex; - gap: var(--spacing-sm); - justify-content: center; - } - - .btn--sm { - padding: var(--spacing-xs) var(--spacing-sm); - font-size: 12px; - } - `] -}) -export class TrayComponent implements OnInit, OnDestroy { - status: TrayStatus = { - running: false, - currentIssue: '', - queueSize: 0, - issuesFixed: 0, - prsMerged: 0 - }; - - private refreshInterval?: ReturnType; - - ngOnInit() { - this.loadStatus(); - this.refreshInterval = setInterval(() => this.loadStatus(), 5000); - } - - ngOnDestroy() { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - } - } - - async loadStatus() { - try { - // Call Wails binding when available - if ((window as any).go?.main?.TrayService?.GetStatus) { - this.status = await (window as any).go.main.TrayService.GetStatus(); - } - } catch (err) { - console.error('Failed to load status:', err); - } - } - - async toggleRunning() { - try { - if (this.status.running) { - if ((window as any).go?.main?.TrayService?.PauseFetching) { - await (window as any).go.main.TrayService.PauseFetching(); - } - } else { - if ((window as any).go?.main?.TrayService?.StartFetching) { - await (window as any).go.main.TrayService.StartFetching(); - } - } - this.loadStatus(); - } catch (err) { - console.error('Failed to toggle running:', err); - } - } - - async nextIssue() { - try { - if ((window as any).go?.main?.TrayService?.NextIssue) { - await (window as any).go.main.TrayService.NextIssue(); - } - this.loadStatus(); - } catch (err) { - console.error('Failed to get next issue:', err); - } - } - - async skipIssue() { - try { - if ((window as any).go?.main?.TrayService?.SkipIssue) { - await (window as any).go.main.TrayService.SkipIssue(); - } - this.loadStatus(); - } catch (err) { - console.error('Failed to skip issue:', err); - } - } - - openWorkbench() { - if ((window as any).wails?.Window) { - (window as any).wails.Window.GetByName('workbench').then((w: any) => { - w.Show(); - w.Focus(); - }); - } - } - - openSettings() { - if ((window as any).wails?.Window) { - (window as any).wails.Window.GetByName('settings').then((w: any) => { - w.Show(); - w.Focus(); - }); - } - } - - openJellyfin() { - window.location.assign('/jellyfin'); - } -} diff --git a/cmd/bugseti/frontend/src/app/workbench/workbench.component.ts b/cmd/bugseti/frontend/src/app/workbench/workbench.component.ts deleted file mode 100644 index c8d4014d..00000000 --- a/cmd/bugseti/frontend/src/app/workbench/workbench.component.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -interface Issue { - id: string; - number: number; - repo: string; - title: string; - body: string; - url: string; - labels: string[]; - author: string; - context?: IssueContext; -} - -interface IssueContext { - summary: string; - relevantFiles: string[]; - suggestedFix: string; - complexity: string; - estimatedTime: string; -} - -@Component({ - selector: 'app-workbench', - standalone: true, - imports: [CommonModule, FormsModule], - template: ` -
-
-

BugSETI Workbench

-
- - -
-
- -
- - -
-
-
-

PR Details

-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
-
- -
-

No Issue Selected

-

Get an issue from the queue to start working.

- -
-
- `, - styles: [` - .workbench { - display: flex; - flex-direction: column; - height: 100%; - background-color: var(--bg-secondary); - } - - .workbench-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--spacing-md) var(--spacing-lg); - background-color: var(--bg-primary); - border-bottom: 1px solid var(--border-color); - } - - .workbench-header h1 { - font-size: 18px; - margin: 0; - } - - .header-actions { - display: flex; - gap: var(--spacing-sm); - } - - .workbench-content { - display: grid; - grid-template-columns: 400px 1fr; - flex: 1; - overflow: hidden; - } - - .issue-panel { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - padding: var(--spacing-md); - overflow-y: auto; - border-right: 1px solid var(--border-color); - } - - .editor-panel { - padding: var(--spacing-md); - overflow-y: auto; - } - - .labels { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin: var(--spacing-sm) 0; - } - - .issue-meta { - display: flex; - gap: var(--spacing-md); - font-size: 12px; - color: var(--text-muted); - margin-bottom: var(--spacing-md); - } - - .issue-body { - padding: var(--spacing-md); - background-color: var(--bg-tertiary); - border-radius: var(--radius-md); - max-height: 200px; - overflow-y: auto; - } - - .issue-body pre { - white-space: pre-wrap; - word-wrap: break-word; - font-size: 13px; - line-height: 1.5; - margin: 0; - } - - .context-summary { - color: var(--text-secondary); - margin-bottom: var(--spacing-md); - } - - .context-section { - margin-bottom: var(--spacing-md); - } - - .context-section h4 { - font-size: 12px; - text-transform: uppercase; - color: var(--text-muted); - margin-bottom: var(--spacing-xs); - } - - .file-list { - list-style: none; - padding: 0; - margin: 0; - } - - .file-list li { - padding: var(--spacing-xs) 0; - } - - .context-meta { - font-size: 12px; - color: var(--text-muted); - } - - .empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - flex: 1; - text-align: center; - } - - .empty-state h2 { - color: var(--text-secondary); - } - - .empty-state p { - color: var(--text-muted); - } - `] -}) -export class WorkbenchComponent implements OnInit { - currentIssue: Issue | null = null; - prTitle = ''; - prBody = ''; - branchName = ''; - commitMessage = ''; - - get canSubmit(): boolean { - return !!this.currentIssue && !!this.prTitle; - } - - ngOnInit() { - this.loadCurrentIssue(); - } - - async loadCurrentIssue() { - try { - if ((window as any).go?.main?.TrayService?.GetCurrentIssue) { - this.currentIssue = await (window as any).go.main.TrayService.GetCurrentIssue(); - if (this.currentIssue) { - this.initDefaults(); - } - } - } catch (err) { - console.error('Failed to load current issue:', err); - } - } - - initDefaults() { - if (!this.currentIssue) return; - - this.prTitle = `Fix #${this.currentIssue.number}: ${this.currentIssue.title}`; - this.branchName = `bugseti/issue-${this.currentIssue.number}`; - this.commitMessage = `fix: resolve issue #${this.currentIssue.number}\n\n${this.currentIssue.title}`; - } - - async nextIssue() { - try { - if ((window as any).go?.main?.TrayService?.NextIssue) { - this.currentIssue = await (window as any).go.main.TrayService.NextIssue(); - if (this.currentIssue) { - this.initDefaults(); - } - } - } catch (err) { - console.error('Failed to get next issue:', err); - } - } - - async skipIssue() { - try { - if ((window as any).go?.main?.TrayService?.SkipIssue) { - await (window as any).go.main.TrayService.SkipIssue(); - this.currentIssue = null; - this.prTitle = ''; - this.prBody = ''; - this.branchName = ''; - this.commitMessage = ''; - } - } catch (err) { - console.error('Failed to skip issue:', err); - } - } - - async submitPR() { - if (!this.currentIssue || !this.canSubmit) return; - - try { - if ((window as any).go?.main?.SubmitService?.Submit) { - const result = await (window as any).go.main.SubmitService.Submit({ - issue: this.currentIssue, - title: this.prTitle, - body: this.prBody, - branch: this.branchName, - commitMsg: this.commitMessage - }); - - if (result.success) { - alert(`PR submitted successfully!\n\n${result.prUrl}`); - this.currentIssue = null; - } else { - alert(`Failed to submit PR: ${result.error}`); - } - } - } catch (err) { - console.error('Failed to submit PR:', err); - alert('Failed to submit PR. Check console for details.'); - } - } -} diff --git a/cmd/bugseti/frontend/src/favicon.ico b/cmd/bugseti/frontend/src/favicon.ico deleted file mode 100644 index e69de29b..00000000 diff --git a/cmd/bugseti/frontend/src/index.html b/cmd/bugseti/frontend/src/index.html deleted file mode 100644 index c05ac318..00000000 --- a/cmd/bugseti/frontend/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - BugSETI - - - - - - - - diff --git a/cmd/bugseti/frontend/src/main.ts b/cmd/bugseti/frontend/src/main.ts deleted file mode 100644 index 35b00f34..00000000 --- a/cmd/bugseti/frontend/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { AppComponent } from './app/app.component'; - -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); diff --git a/cmd/bugseti/frontend/src/styles.scss b/cmd/bugseti/frontend/src/styles.scss deleted file mode 100644 index e28d79c3..00000000 --- a/cmd/bugseti/frontend/src/styles.scss +++ /dev/null @@ -1,268 +0,0 @@ -// BugSETI Global Styles - -// CSS Variables for theming -:root { - // Dark theme (default) - --bg-primary: #161b22; - --bg-secondary: #0d1117; - --bg-tertiary: #21262d; - --text-primary: #c9d1d9; - --text-secondary: #8b949e; - --text-muted: #6e7681; - --border-color: #30363d; - --accent-primary: #58a6ff; - --accent-success: #3fb950; - --accent-warning: #d29922; - --accent-danger: #f85149; - - // Spacing - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --spacing-xl: 32px; - - // Border radius - --radius-sm: 4px; - --radius-md: 6px; - --radius-lg: 12px; - - // Font - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; - --font-mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; -} - -// Light theme -[data-theme="light"] { - --bg-primary: #ffffff; - --bg-secondary: #f6f8fa; - --bg-tertiary: #f0f3f6; - --text-primary: #24292f; - --text-secondary: #57606a; - --text-muted: #8b949e; - --border-color: #d0d7de; - --accent-primary: #0969da; - --accent-success: #1a7f37; - --accent-warning: #9a6700; - --accent-danger: #cf222e; -} - -// Reset -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html, body { - height: 100%; - width: 100%; -} - -body { - font-family: var(--font-family); - font-size: 14px; - line-height: 1.5; - color: var(--text-primary); - background-color: var(--bg-primary); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -// Typography -h1, h2, h3, h4, h5, h6 { - font-weight: 600; - line-height: 1.25; - margin-bottom: var(--spacing-sm); -} - -h1 { font-size: 24px; } -h2 { font-size: 20px; } -h3 { font-size: 16px; } -h4 { font-size: 14px; } - -p { - margin-bottom: var(--spacing-md); -} - -a { - color: var(--accent-primary); - text-decoration: none; - - &:hover { - text-decoration: underline; - } -} - -code { - font-family: var(--font-mono); - font-size: 12px; - padding: 2px 6px; - background-color: var(--bg-tertiary); - border-radius: var(--radius-sm); -} - -// Buttons -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - padding: var(--spacing-sm) var(--spacing-md); - font-size: 14px; - font-weight: 500; - line-height: 1; - border: 1px solid transparent; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &--primary { - background-color: var(--accent-primary); - color: white; - - &:hover:not(:disabled) { - opacity: 0.9; - } - } - - &--secondary { - background-color: var(--bg-tertiary); - border-color: var(--border-color); - color: var(--text-primary); - - &:hover:not(:disabled) { - background-color: var(--bg-secondary); - } - } - - &--success { - background-color: var(--accent-success); - color: white; - } - - &--danger { - background-color: var(--accent-danger); - color: white; - } -} - -// Forms -.form-group { - margin-bottom: var(--spacing-md); -} - -.form-label { - display: block; - margin-bottom: var(--spacing-xs); - font-weight: 500; - color: var(--text-primary); -} - -.form-input, -.form-select, -.form-textarea { - width: 100%; - padding: var(--spacing-sm) var(--spacing-md); - font-size: 14px; - background-color: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - color: var(--text-primary); - - &:focus { - outline: none; - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2); - } - - &::placeholder { - color: var(--text-muted); - } -} - -.form-textarea { - resize: vertical; - min-height: 100px; -} - -// Cards -.card { - background-color: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--spacing-md); - - &__header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--spacing-md); - padding-bottom: var(--spacing-sm); - border-bottom: 1px solid var(--border-color); - } - - &__title { - font-size: 16px; - font-weight: 600; - } -} - -// Badges -.badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - font-size: 12px; - font-weight: 500; - border-radius: 999px; - - &--primary { - background-color: rgba(88, 166, 255, 0.15); - color: var(--accent-primary); - } - - &--success { - background-color: rgba(63, 185, 80, 0.15); - color: var(--accent-success); - } - - &--warning { - background-color: rgba(210, 153, 34, 0.15); - color: var(--accent-warning); - } - - &--danger { - background-color: rgba(248, 81, 73, 0.15); - color: var(--accent-danger); - } -} - -// Utility classes -.text-center { text-align: center; } -.text-right { text-align: right; } -.text-muted { color: var(--text-muted); } -.text-success { color: var(--accent-success); } -.text-danger { color: var(--accent-danger); } -.text-warning { color: var(--accent-warning); } - -.flex { display: flex; } -.flex-col { flex-direction: column; } -.items-center { align-items: center; } -.justify-between { justify-content: space-between; } -.gap-sm { gap: var(--spacing-sm); } -.gap-md { gap: var(--spacing-md); } - -.mt-sm { margin-top: var(--spacing-sm); } -.mt-md { margin-top: var(--spacing-md); } -.mb-sm { margin-bottom: var(--spacing-sm); } -.mb-md { margin-bottom: var(--spacing-md); } - -.hidden { display: none; } diff --git a/cmd/bugseti/frontend/tsconfig.app.json b/cmd/bugseti/frontend/tsconfig.app.json deleted file mode 100644 index 7d7c716d..00000000 --- a/cmd/bugseti/frontend/tsconfig.app.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] - }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] -} diff --git a/cmd/bugseti/frontend/tsconfig.json b/cmd/bugseti/frontend/tsconfig.json deleted file mode 100644 index 62eaf438..00000000 --- a/cmd/bugseti/frontend/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "sourceMap": true, - "declaration": false, - "experimentalDecorators": true, - "moduleResolution": "bundler", - "importHelpers": true, - "target": "ES2022", - "module": "ES2022", - "lib": [ - "ES2022", - "dom" - ], - "paths": { - "@app/*": ["src/app/*"], - "@shared/*": ["src/app/shared/*"] - } - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} diff --git a/cmd/bugseti/frontend/tsconfig.spec.json b/cmd/bugseti/frontend/tsconfig.spec.json deleted file mode 100644 index b18619fd..00000000 --- a/cmd/bugseti/frontend/tsconfig.spec.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] - }, - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/cmd/bugseti/go.mod b/cmd/bugseti/go.mod deleted file mode 100644 index 7bdc52fc..00000000 --- a/cmd/bugseti/go.mod +++ /dev/null @@ -1,88 +0,0 @@ -module forge.lthn.ai/core/go/cmd/bugseti - -go 1.25.5 - -require ( - forge.lthn.ai/core/go v0.0.0 - forge.lthn.ai/core/go/internal/bugseti v0.0.0 - forge.lthn.ai/core/go/internal/bugseti/updater v0.0.0 - github.com/Snider/Borg v0.2.0 - forge.lthn.ai/core/go v0.0.0 - forge.lthn.ai/core/go/internal/bugseti v0.0.0 - forge.lthn.ai/core/go/internal/bugseti/updater v0.0.0 - github.com/wailsapp/wails/v3 v3.0.0-alpha.64 -) - -replace forge.lthn.ai/core/go => ../.. - -replace forge.lthn.ai/core/go/internal/bugseti => ../../internal/bugseti - -replace forge.lthn.ai/core/go/internal/bugseti/updater => ../../internal/bugseti/updater - -require ( - codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect - dario.cat/mergo v1.0.2 // indirect - github.com/42wim/httpsig v1.2.3 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/Snider/Enchantrix v0.0.2 // indirect - github.com/adrg/xdg v0.5.3 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/bep/debounce v1.2.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/cloudflare/circl v1.6.3 // indirect - github.com/coder/websocket v1.8.14 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/ebitengine/purego v0.9.1 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-fed/httpsig v1.1.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.4 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leaanthony/go-ansi-parser v1.6.1 // indirect - github.com/leaanthony/u v1.1.1 // indirect - github.com/lmittmann/tint v1.1.2 // indirect - github.com/mailru/easyjson v0.9.1 // indirect - github.com/mark3labs/mcp-go v0.43.2 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pjbgf/sha1cd v0.5.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/samber/lo v1.52.0 // indirect - github.com/sergi/go-diff v1.4.0 // indirect - github.com/skeema/knownhosts v1.3.2 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/wailsapp/go-webview2 v1.0.23 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/cmd/bugseti/go.sum b/cmd/bugseti/go.sum deleted file mode 100644 index 479e6650..00000000 --- a/cmd/bugseti/go.sum +++ /dev/null @@ -1,181 +0,0 @@ -codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/Snider/Borg v0.2.0 h1:iCyDhY4WTXi39+FexRwXbn2YpZ2U9FUXVXDZk9xRCXQ= -github.com/Snider/Borg v0.2.0/go.mod h1:TqlKnfRo9okioHbgrZPfWjQsztBV0Nfskz4Om1/vdMY= -github.com/Snider/Enchantrix v0.0.2 h1:ExZQiBhfS/p/AHFTKhY80TOd+BXZjK95EzByAEgwvjs= -github.com/Snider/Enchantrix v0.0.2/go.mod h1:CtFcLAvnDT1KcuF1JBb/DJj0KplY8jHryO06KzQ1hsQ= -github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= -github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= -github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= -github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= -github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= -github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= -github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= -github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= -github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= -github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= -github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= -github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= -github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= -github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= -github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= -github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/wails/v3 v3.0.0-alpha.64 h1:xAhLFVfdbg7XdZQ5mMQmBv2BglWu8hMqe50Z+3UJvBs= -github.com/wailsapp/wails/v3 v3.0.0-alpha.64/go.mod h1:zvgNL/mlFcX8aRGu6KOz9AHrMmTBD+4hJRQIONqF/Yw= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/bugseti/icons/appicon.png b/cmd/bugseti/icons/appicon.png deleted file mode 100644 index 5131e1cd..00000000 Binary files a/cmd/bugseti/icons/appicon.png and /dev/null differ diff --git a/cmd/bugseti/icons/icons.go b/cmd/bugseti/icons/icons.go deleted file mode 100644 index 083f6b38..00000000 --- a/cmd/bugseti/icons/icons.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package icons provides embedded icon assets for the BugSETI application. -package icons - -import _ "embed" - -// TrayTemplate is the template icon for macOS systray (22x22 PNG, black on transparent). -// Template icons automatically adapt to light/dark mode on macOS. -// -//go:embed tray-template.png -var TrayTemplate []byte - -// TrayLight is the light mode icon for Windows/Linux systray. -// -//go:embed tray-light.png -var TrayLight []byte - -// TrayDark is the dark mode icon for Windows/Linux systray. -// -//go:embed tray-dark.png -var TrayDark []byte - -// AppIcon is the main application icon. -// -//go:embed appicon.png -var AppIcon []byte diff --git a/cmd/bugseti/icons/tray-dark.png b/cmd/bugseti/icons/tray-dark.png deleted file mode 100644 index e8295b79..00000000 Binary files a/cmd/bugseti/icons/tray-dark.png and /dev/null differ diff --git a/cmd/bugseti/icons/tray-light.png b/cmd/bugseti/icons/tray-light.png deleted file mode 100644 index e4457ce6..00000000 Binary files a/cmd/bugseti/icons/tray-light.png and /dev/null differ diff --git a/cmd/bugseti/icons/tray-template.png b/cmd/bugseti/icons/tray-template.png deleted file mode 100644 index f6c03ff8..00000000 Binary files a/cmd/bugseti/icons/tray-template.png and /dev/null differ diff --git a/cmd/bugseti/main.go b/cmd/bugseti/main.go deleted file mode 100644 index 3f4777dd..00000000 --- a/cmd/bugseti/main.go +++ /dev/null @@ -1,290 +0,0 @@ -// Package main provides the BugSETI system tray application. -// BugSETI - "Distributed Bug Fixing like SETI@home but for code" -// -// The application runs as a system tray app that: -// - Pulls OSS issues from Forgejo -// - Uses AI to prepare context for each issue -// - Presents issues to users for fixing -// - Automates PR submission -package main - -import ( - "embed" - "io/fs" - "log" - "net/http" - "runtime" - "strings" - - "forge.lthn.ai/core/go/cmd/bugseti/icons" - "forge.lthn.ai/core/cli/internal/bugseti" - "forge.lthn.ai/core/cli/internal/bugseti/updater" - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/events" -) - -//go:embed all:frontend/dist/bugseti/browser -var assets embed.FS - -func main() { - // Strip the embed path prefix so files are served from root - staticAssets, err := fs.Sub(assets, "frontend/dist/bugseti/browser") - if err != nil { - log.Fatal(err) - } - - // Initialize the config service - configService := bugseti.NewConfigService() - if err := configService.Load(); err != nil { - log.Printf("Warning: Could not load config: %v", err) - } - - // Check Forgejo API availability - forgeClient, err := bugseti.CheckForge() - if err != nil { - log.Fatalf("Forgejo check failed: %v\n\nConfigure with: core forge config --url URL --token TOKEN", err) - } - - // Initialize core services - notifyService := bugseti.NewNotifyService(configService) - statsService := bugseti.NewStatsService(configService) - fetcherService := bugseti.NewFetcherService(configService, notifyService, forgeClient) - queueService := bugseti.NewQueueService(configService) - seederService := bugseti.NewSeederService(configService, forgeClient.URL(), forgeClient.Token()) - submitService := bugseti.NewSubmitService(configService, notifyService, statsService, forgeClient) - hubService := bugseti.NewHubService(configService) - versionService := bugseti.NewVersionService() - workspaceService := NewWorkspaceService(configService) - - // Initialize update service - updateService, err := updater.NewService(configService) - if err != nil { - log.Printf("Warning: Could not initialize update service: %v", err) - } - - // Create the tray service (we'll set the app reference later) - trayService := NewTrayService(nil) - - // Build services list - services := []application.Service{ - application.NewService(configService), - application.NewService(notifyService), - application.NewService(statsService), - application.NewService(fetcherService), - application.NewService(queueService), - application.NewService(seederService), - application.NewService(submitService), - application.NewService(versionService), - application.NewService(workspaceService), - application.NewService(hubService), - application.NewService(trayService), - } - - // Add update service if available - if updateService != nil { - services = append(services, application.NewService(updateService)) - } - - // Create the application - app := application.New(application.Options{ - Name: "BugSETI", - Description: "Distributed Bug Fixing - like SETI@home but for code", - Services: services, - Assets: application.AssetOptions{ - Handler: spaHandler(staticAssets), - }, - Mac: application.MacOptions{ - ActivationPolicy: application.ActivationPolicyAccessory, - }, - }) - - // Set the app reference and services in tray service - trayService.app = app - trayService.SetServices(fetcherService, queueService, configService, statsService) - - // Set up system tray - setupSystemTray(app, fetcherService, queueService, configService) - - // Start update service background checker - if updateService != nil { - updateService.Start() - } - - log.Println("Starting BugSETI...") - log.Println(" - System tray active") - log.Println(" - Waiting for issues...") - log.Printf(" - Version: %s (%s)", bugseti.GetVersion(), bugseti.GetChannel()) - - // Attempt hub registration (non-blocking) - if hubURL := configService.GetHubURL(); hubURL != "" { - if err := hubService.AutoRegister(); err != nil { - log.Printf(" - Hub: auto-register skipped: %v", err) - } else if err := hubService.Register(); err != nil { - log.Printf(" - Hub: registration failed: %v", err) - } else { - log.Println(" - Hub: registered with portal") - } - } else { - log.Println(" - Hub: not configured (set hubUrl in config)") - } - - if err := app.Run(); err != nil { - log.Fatal(err) - } - - // Stop update service on exit - if updateService != nil { - updateService.Stop() - } -} - -// setupSystemTray configures the system tray icon and menu -func setupSystemTray(app *application.App, fetcher *bugseti.FetcherService, queue *bugseti.QueueService, config *bugseti.ConfigService) { - systray := app.SystemTray.New() - systray.SetTooltip("BugSETI - Distributed Bug Fixing") - - // Set tray icon based on OS - if runtime.GOOS == "darwin" { - systray.SetTemplateIcon(icons.TrayTemplate) - } else { - systray.SetDarkModeIcon(icons.TrayDark) - systray.SetIcon(icons.TrayLight) - } - - // Create tray panel window (workbench preview) - trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "tray-panel", - Title: "BugSETI", - Width: 420, - Height: 520, - URL: "/tray", - Hidden: true, - Frameless: true, - BackgroundColour: application.NewRGB(22, 27, 34), - }) - systray.AttachWindow(trayWindow).WindowOffset(5) - - // Create main workbench window - workbenchWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "workbench", - Title: "BugSETI Workbench", - Width: 1200, - Height: 800, - URL: "/workbench", - Hidden: true, - BackgroundColour: application.NewRGB(22, 27, 34), - }) - - // Create settings window - settingsWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "settings", - Title: "BugSETI Settings", - Width: 600, - Height: 500, - URL: "/settings", - Hidden: true, - BackgroundColour: application.NewRGB(22, 27, 34), - }) - - // Create onboarding window - onboardingWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "onboarding", - Title: "Welcome to BugSETI", - Width: 700, - Height: 600, - URL: "/onboarding", - Hidden: true, - BackgroundColour: application.NewRGB(22, 27, 34), - }) - - // Build tray menu - trayMenu := app.Menu.New() - - // Status item (dynamic) - statusItem := trayMenu.Add("Status: Idle") - statusItem.SetEnabled(false) - - trayMenu.AddSeparator() - - // Start/Pause toggle - startPauseItem := trayMenu.Add("Start Fetching") - startPauseItem.OnClick(func(ctx *application.Context) { - if fetcher.IsRunning() { - fetcher.Pause() - startPauseItem.SetLabel("Start Fetching") - statusItem.SetLabel("Status: Paused") - } else { - fetcher.Start() - startPauseItem.SetLabel("Pause") - statusItem.SetLabel("Status: Running") - } - }) - - trayMenu.AddSeparator() - - // Current Issue - currentIssueItem := trayMenu.Add("Current Issue: None") - currentIssueItem.OnClick(func(ctx *application.Context) { - if issue := queue.CurrentIssue(); issue != nil { - workbenchWindow.Show() - workbenchWindow.Focus() - } - }) - - // Open Workbench - trayMenu.Add("Open Workbench").OnClick(func(ctx *application.Context) { - workbenchWindow.Show() - workbenchWindow.Focus() - }) - - trayMenu.AddSeparator() - - // Settings - trayMenu.Add("Settings...").OnClick(func(ctx *application.Context) { - settingsWindow.Show() - settingsWindow.Focus() - }) - - // Stats submenu - statsMenu := trayMenu.AddSubmenu("Stats") - statsMenu.Add("Issues Fixed: 0").SetEnabled(false) - statsMenu.Add("PRs Merged: 0").SetEnabled(false) - statsMenu.Add("Repos Contributed: 0").SetEnabled(false) - - trayMenu.AddSeparator() - - // Quit - trayMenu.Add("Quit BugSETI").OnClick(func(ctx *application.Context) { - app.Quit() - }) - - systray.SetMenu(trayMenu) - - // Check if onboarding needed (deferred until app is running) - app.Event.RegisterApplicationEventHook(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { - if !config.IsOnboarded() { - onboardingWindow.Show() - onboardingWindow.Focus() - } - }) -} - -// spaHandler wraps an fs.FS to serve static files with SPA fallback. -// If the requested path doesn't match a real file, it serves index.html -// so Angular's client-side router can handle the route. -func spaHandler(fsys fs.FS) http.Handler { - fileServer := http.FileServer(http.FS(fsys)) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := strings.TrimPrefix(r.URL.Path, "/") - if path == "" { - path = "index.html" - } - - // Check if the file exists - if _, err := fs.Stat(fsys, path); err != nil { - // File doesn't exist — serve index.html for SPA routing - r.URL.Path = "/" - } - fileServer.ServeHTTP(w, r) - }) -} diff --git a/cmd/bugseti/tray.go b/cmd/bugseti/tray.go deleted file mode 100644 index 3610fecc..00000000 --- a/cmd/bugseti/tray.go +++ /dev/null @@ -1,158 +0,0 @@ -// Package main provides the BugSETI system tray application. -package main - -import ( - "context" - "log" - - "forge.lthn.ai/core/cli/internal/bugseti" - "github.com/wailsapp/wails/v3/pkg/application" -) - -// TrayService provides system tray bindings for the frontend. -type TrayService struct { - app *application.App - fetcher *bugseti.FetcherService - queue *bugseti.QueueService - config *bugseti.ConfigService - stats *bugseti.StatsService -} - -// NewTrayService creates a new TrayService instance. -func NewTrayService(app *application.App) *TrayService { - return &TrayService{ - app: app, - } -} - -// SetServices sets the service references after initialization. -func (t *TrayService) SetServices(fetcher *bugseti.FetcherService, queue *bugseti.QueueService, config *bugseti.ConfigService, stats *bugseti.StatsService) { - t.fetcher = fetcher - t.queue = queue - t.config = config - t.stats = stats -} - -// ServiceName returns the service name for Wails. -func (t *TrayService) ServiceName() string { - return "TrayService" -} - -// ServiceStartup is called when the Wails application starts. -func (t *TrayService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - log.Println("TrayService started") - return nil -} - -// ServiceShutdown is called when the Wails application shuts down. -func (t *TrayService) ServiceShutdown() error { - log.Println("TrayService shutdown") - return nil -} - -// TrayStatus represents the current status of the tray. -type TrayStatus struct { - Running bool `json:"running"` - CurrentIssue string `json:"currentIssue"` - QueueSize int `json:"queueSize"` - IssuesFixed int `json:"issuesFixed"` - PRsMerged int `json:"prsMerged"` -} - -// GetStatus returns the current tray status. -func (t *TrayService) GetStatus() TrayStatus { - var currentIssue string - if t.queue != nil { - if issue := t.queue.CurrentIssue(); issue != nil { - currentIssue = issue.Title - } - } - - var queueSize int - if t.queue != nil { - queueSize = t.queue.Size() - } - - var running bool - if t.fetcher != nil { - running = t.fetcher.IsRunning() - } - - var issuesFixed, prsMerged int - if t.stats != nil { - stats := t.stats.GetStats() - issuesFixed = stats.IssuesAttempted - prsMerged = stats.PRsMerged - } - - return TrayStatus{ - Running: running, - CurrentIssue: currentIssue, - QueueSize: queueSize, - IssuesFixed: issuesFixed, - PRsMerged: prsMerged, - } -} - -// StartFetching starts the issue fetcher. -func (t *TrayService) StartFetching() error { - if t.fetcher == nil { - return nil - } - return t.fetcher.Start() -} - -// PauseFetching pauses the issue fetcher. -func (t *TrayService) PauseFetching() { - if t.fetcher != nil { - t.fetcher.Pause() - } -} - -// GetCurrentIssue returns the current issue being worked on. -func (t *TrayService) GetCurrentIssue() *bugseti.Issue { - if t.queue == nil { - return nil - } - return t.queue.CurrentIssue() -} - -// NextIssue moves to the next issue in the queue. -func (t *TrayService) NextIssue() *bugseti.Issue { - if t.queue == nil { - return nil - } - return t.queue.Next() -} - -// SkipIssue skips the current issue. -func (t *TrayService) SkipIssue() { - if t.queue == nil { - return - } - t.queue.Skip() -} - -// ShowWindow shows a specific window by name. -func (t *TrayService) ShowWindow(name string) { - if t.app == nil { - return - } - // Window will be shown by the frontend via Wails runtime -} - -// IsOnboarded returns whether the user has completed onboarding. -func (t *TrayService) IsOnboarded() bool { - if t.config == nil { - return false - } - return t.config.IsOnboarded() -} - -// CompleteOnboarding marks onboarding as complete. -func (t *TrayService) CompleteOnboarding() error { - if t.config == nil { - return nil - } - return t.config.CompleteOnboarding() -} diff --git a/cmd/bugseti/workspace.go b/cmd/bugseti/workspace.go deleted file mode 100644 index 69b53495..00000000 --- a/cmd/bugseti/workspace.go +++ /dev/null @@ -1,374 +0,0 @@ -// Package main provides the BugSETI system tray application. -package main - -import ( - "fmt" - "io/fs" - "log" - "os" - "path/filepath" - "sort" - "sync" - "time" - - "forge.lthn.ai/core/cli/internal/bugseti" - "forge.lthn.ai/core/go/pkg/io/datanode" - "github.com/Snider/Borg/pkg/tim" -) - -const ( - // defaultMaxWorkspaces is the fallback upper bound when config is unavailable. - defaultMaxWorkspaces = 100 - // defaultWorkspaceTTL is the fallback TTL when config is unavailable. - defaultWorkspaceTTL = 24 * time.Hour - // sweepInterval is how often the background sweeper runs. - sweepInterval = 5 * time.Minute -) - -// WorkspaceService manages DataNode-backed workspaces for issues. -// Each issue gets a sandboxed in-memory filesystem that can be -// snapshotted, packaged as a TIM container, or shipped as a crash report. -type WorkspaceService struct { - config *bugseti.ConfigService - workspaces map[string]*Workspace // issue ID -> workspace - mu sync.RWMutex - done chan struct{} // signals the background sweeper to stop - stopped chan struct{} // closed when the sweeper goroutine exits -} - -// Workspace tracks a DataNode-backed workspace for an issue. -type Workspace struct { - Issue *bugseti.Issue `json:"issue"` - Medium *datanode.Medium - DiskPath string `json:"diskPath"` - CreatedAt time.Time `json:"createdAt"` - Snapshots int `json:"snapshots"` -} - -// CrashReport contains a packaged workspace state for debugging. -type CrashReport struct { - IssueID string `json:"issueId"` - Repo string `json:"repo"` - Number int `json:"number"` - Title string `json:"title"` - Error string `json:"error"` - Timestamp time.Time `json:"timestamp"` - Data []byte `json:"data"` // tar snapshot - Files int `json:"files"` - Size int64 `json:"size"` -} - -// NewWorkspaceService creates a new WorkspaceService. -// Call Start() to begin the background TTL sweeper. -func NewWorkspaceService(config *bugseti.ConfigService) *WorkspaceService { - return &WorkspaceService{ - config: config, - workspaces: make(map[string]*Workspace), - done: make(chan struct{}), - stopped: make(chan struct{}), - } -} - -// ServiceName returns the service name for Wails. -func (w *WorkspaceService) ServiceName() string { - return "WorkspaceService" -} - -// Start launches the background sweeper goroutine that periodically -// evicts expired workspaces. This prevents unbounded map growth even -// when no new Capture calls arrive. -func (w *WorkspaceService) Start() { - go func() { - defer close(w.stopped) - ticker := time.NewTicker(sweepInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - w.mu.Lock() - evicted := w.cleanup() - w.mu.Unlock() - if evicted > 0 { - log.Printf("Workspace sweeper: evicted %d stale entries, %d remaining", evicted, w.ActiveWorkspaces()) - } - case <-w.done: - return - } - } - }() - log.Printf("Workspace sweeper started (interval=%s, ttl=%s, max=%d)", - sweepInterval, w.ttl(), w.maxCap()) -} - -// Stop signals the background sweeper to exit and waits for it to finish. -func (w *WorkspaceService) Stop() { - close(w.done) - <-w.stopped - log.Printf("Workspace sweeper stopped") -} - -// ttl returns the configured workspace TTL, falling back to the default. -func (w *WorkspaceService) ttl() time.Duration { - if w.config != nil { - return w.config.GetWorkspaceTTL() - } - return defaultWorkspaceTTL -} - -// maxCap returns the configured max workspace count, falling back to the default. -func (w *WorkspaceService) maxCap() int { - if w.config != nil { - return w.config.GetMaxWorkspaces() - } - return defaultMaxWorkspaces -} - -// Capture loads a filesystem workspace into a DataNode Medium. -// Call this after git clone to create the in-memory snapshot. -func (w *WorkspaceService) Capture(issue *bugseti.Issue, diskPath string) error { - if issue == nil { - return fmt.Errorf("issue is nil") - } - - m := datanode.New() - - // Walk the filesystem and load all files into the DataNode - err := filepath.WalkDir(diskPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return nil // skip errors - } - - // Get relative path - rel, err := filepath.Rel(diskPath, path) - if err != nil { - return nil - } - if rel == "." { - return nil - } - - // Skip .git internals (keep .git marker but not the pack files) - if rel == ".git" { - return fs.SkipDir - } - - if d.IsDir() { - return m.EnsureDir(rel) - } - - // Skip large files (>1MB) to keep DataNode lightweight - info, err := d.Info() - if err != nil || info.Size() > 1<<20 { - return nil - } - - content, err := os.ReadFile(path) - if err != nil { - return nil - } - return m.Write(rel, string(content)) - }) - if err != nil { - return fmt.Errorf("failed to capture workspace: %w", err) - } - - w.mu.Lock() - w.cleanup() - w.workspaces[issue.ID] = &Workspace{ - Issue: issue, - Medium: m, - DiskPath: diskPath, - CreatedAt: time.Now(), - } - w.mu.Unlock() - - log.Printf("Captured workspace for issue #%d (%s)", issue.Number, issue.Repo) - return nil -} - -// GetMedium returns the DataNode Medium for an issue's workspace. -func (w *WorkspaceService) GetMedium(issueID string) *datanode.Medium { - w.mu.RLock() - defer w.mu.RUnlock() - - ws := w.workspaces[issueID] - if ws == nil { - return nil - } - return ws.Medium -} - -// Snapshot takes a tar snapshot of the workspace. -func (w *WorkspaceService) Snapshot(issueID string) ([]byte, error) { - w.mu.Lock() - defer w.mu.Unlock() - - ws := w.workspaces[issueID] - if ws == nil { - return nil, fmt.Errorf("workspace not found: %s", issueID) - } - - data, err := ws.Medium.Snapshot() - if err != nil { - return nil, fmt.Errorf("snapshot failed: %w", err) - } - - ws.Snapshots++ - return data, nil -} - -// PackageCrashReport captures the current workspace state as a crash report. -// Re-reads from disk to get the latest state (including git changes). -func (w *WorkspaceService) PackageCrashReport(issue *bugseti.Issue, errMsg string) (*CrashReport, error) { - if issue == nil { - return nil, fmt.Errorf("issue is nil") - } - - w.mu.RLock() - ws := w.workspaces[issue.ID] - w.mu.RUnlock() - - var diskPath string - if ws != nil { - diskPath = ws.DiskPath - } else { - // Try to find the workspace on disk - baseDir := w.config.GetWorkspaceDir() - if baseDir == "" { - baseDir = filepath.Join(os.TempDir(), "bugseti") - } - diskPath = filepath.Join(baseDir, sanitizeForPath(issue.Repo), fmt.Sprintf("issue-%d", issue.Number)) - } - - // Re-capture from disk to get latest state - if err := w.Capture(issue, diskPath); err != nil { - return nil, fmt.Errorf("capture failed: %w", err) - } - - // Snapshot the captured workspace - data, err := w.Snapshot(issue.ID) - if err != nil { - return nil, fmt.Errorf("snapshot failed: %w", err) - } - - return &CrashReport{ - IssueID: issue.ID, - Repo: issue.Repo, - Number: issue.Number, - Title: issue.Title, - Error: errMsg, - Timestamp: time.Now(), - Data: data, - Size: int64(len(data)), - }, nil -} - -// PackageTIM wraps the workspace as a TIM container (runc-compatible bundle). -// The resulting TIM can be executed via runc or encrypted to .stim for transit. -func (w *WorkspaceService) PackageTIM(issueID string) (*tim.TerminalIsolationMatrix, error) { - w.mu.RLock() - ws := w.workspaces[issueID] - w.mu.RUnlock() - - if ws == nil { - return nil, fmt.Errorf("workspace not found: %s", issueID) - } - - dn := ws.Medium.DataNode() - return tim.FromDataNode(dn) -} - -// SaveCrashReport writes a crash report to the data directory. -func (w *WorkspaceService) SaveCrashReport(report *CrashReport) (string, error) { - dataDir := w.config.GetDataDir() - if dataDir == "" { - dataDir = filepath.Join(os.TempDir(), "bugseti") - } - - crashDir := filepath.Join(dataDir, "crash-reports") - if err := os.MkdirAll(crashDir, 0755); err != nil { - return "", fmt.Errorf("failed to create crash dir: %w", err) - } - - filename := fmt.Sprintf("crash-%s-issue-%d-%s.tar", - sanitizeForPath(report.Repo), - report.Number, - report.Timestamp.Format("20060102-150405"), - ) - path := filepath.Join(crashDir, filename) - - if err := os.WriteFile(path, report.Data, 0644); err != nil { - return "", fmt.Errorf("failed to write crash report: %w", err) - } - - log.Printf("Crash report saved: %s (%d bytes)", path, report.Size) - return path, nil -} - -// cleanup evicts expired workspaces and enforces the max size cap. -// Must be called with w.mu held for writing. -// Returns the number of evicted entries. -func (w *WorkspaceService) cleanup() int { - now := time.Now() - ttl := w.ttl() - cap := w.maxCap() - evicted := 0 - - // First pass: evict entries older than TTL. - for id, ws := range w.workspaces { - if now.Sub(ws.CreatedAt) > ttl { - delete(w.workspaces, id) - evicted++ - } - } - - // Second pass: if still over cap, evict oldest entries. - if len(w.workspaces) > cap { - type entry struct { - id string - createdAt time.Time - } - entries := make([]entry, 0, len(w.workspaces)) - for id, ws := range w.workspaces { - entries = append(entries, entry{id, ws.CreatedAt}) - } - sort.Slice(entries, func(i, j int) bool { - return entries[i].createdAt.Before(entries[j].createdAt) - }) - toEvict := len(w.workspaces) - cap - for i := 0; i < toEvict; i++ { - delete(w.workspaces, entries[i].id) - evicted++ - } - } - - return evicted -} - -// Release removes a workspace from memory. -func (w *WorkspaceService) Release(issueID string) { - w.mu.Lock() - delete(w.workspaces, issueID) - w.mu.Unlock() -} - -// ActiveWorkspaces returns the count of active workspaces. -func (w *WorkspaceService) ActiveWorkspaces() int { - w.mu.RLock() - defer w.mu.RUnlock() - return len(w.workspaces) -} - -// sanitizeForPath converts owner/repo to a safe directory name. -func sanitizeForPath(s string) string { - result := make([]byte, 0, len(s)) - for _, c := range s { - if c == '/' || c == '\\' || c == ':' { - result = append(result, '-') - } else { - result = append(result, byte(c)) - } - } - return string(result) -} diff --git a/cmd/bugseti/workspace_test.go b/cmd/bugseti/workspace_test.go deleted file mode 100644 index d1711216..00000000 --- a/cmd/bugseti/workspace_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" - - "forge.lthn.ai/core/cli/internal/bugseti" -) - -func TestCleanup_TTL(t *testing.T) { - svc := NewWorkspaceService(bugseti.NewConfigService()) - - // Seed with entries that are older than TTL. - svc.mu.Lock() - for i := 0; i < 5; i++ { - svc.workspaces[fmt.Sprintf("old-%d", i)] = &Workspace{ - CreatedAt: time.Now().Add(-25 * time.Hour), - } - } - // Add one fresh entry. - svc.workspaces["fresh"] = &Workspace{ - CreatedAt: time.Now(), - } - svc.cleanup() - svc.mu.Unlock() - - if got := svc.ActiveWorkspaces(); got != 1 { - t.Errorf("expected 1 workspace after TTL cleanup, got %d", got) - } -} - -func TestCleanup_MaxSize(t *testing.T) { - svc := NewWorkspaceService(bugseti.NewConfigService()) - - maxCap := svc.maxCap() - - // Fill beyond the cap with fresh entries. - svc.mu.Lock() - for i := 0; i < maxCap+20; i++ { - svc.workspaces[fmt.Sprintf("ws-%d", i)] = &Workspace{ - CreatedAt: time.Now().Add(-time.Duration(i) * time.Minute), - } - } - svc.cleanup() - svc.mu.Unlock() - - if got := svc.ActiveWorkspaces(); got != maxCap { - t.Errorf("expected %d workspaces after cap cleanup, got %d", maxCap, got) - } -} - -func TestCleanup_EvictsOldestWhenOverCap(t *testing.T) { - svc := NewWorkspaceService(bugseti.NewConfigService()) - - maxCap := svc.maxCap() - - // Create maxCap+1 entries; the newest should survive. - svc.mu.Lock() - for i := 0; i <= maxCap; i++ { - svc.workspaces[fmt.Sprintf("ws-%d", i)] = &Workspace{ - CreatedAt: time.Now().Add(-time.Duration(maxCap-i) * time.Minute), - } - } - svc.cleanup() - svc.mu.Unlock() - - // The newest entry (ws-) should still exist. - newest := fmt.Sprintf("ws-%d", maxCap) - - svc.mu.RLock() - _, exists := svc.workspaces[newest] - svc.mu.RUnlock() - if !exists { - t.Error("expected newest workspace to survive eviction") - } - - // The oldest entry (ws-0) should have been evicted. - svc.mu.RLock() - _, exists = svc.workspaces["ws-0"] - svc.mu.RUnlock() - if exists { - t.Error("expected oldest workspace to be evicted") - } -} - -func TestCleanup_ReturnsEvictedCount(t *testing.T) { - svc := NewWorkspaceService(bugseti.NewConfigService()) - - svc.mu.Lock() - for i := 0; i < 3; i++ { - svc.workspaces[fmt.Sprintf("old-%d", i)] = &Workspace{ - CreatedAt: time.Now().Add(-25 * time.Hour), - } - } - svc.workspaces["fresh"] = &Workspace{ - CreatedAt: time.Now(), - } - evicted := svc.cleanup() - svc.mu.Unlock() - - if evicted != 3 { - t.Errorf("expected 3 evicted entries, got %d", evicted) - } -} - -func TestStartStop(t *testing.T) { - svc := NewWorkspaceService(bugseti.NewConfigService()) - svc.Start() - - // Add a stale entry while the sweeper is running. - svc.mu.Lock() - svc.workspaces["stale"] = &Workspace{ - CreatedAt: time.Now().Add(-25 * time.Hour), - } - svc.mu.Unlock() - - // Stop should return without hanging. - svc.Stop() -} - -func TestConfigurableTTL(t *testing.T) { - cfg := bugseti.NewConfigService() - svc := NewWorkspaceService(cfg) - - // Default TTL should be 24h (1440 minutes). - if got := svc.ttl(); got != 24*time.Hour { - t.Errorf("expected default TTL of 24h, got %s", got) - } - - // Default max cap should be 100. - if got := svc.maxCap(); got != 100 { - t.Errorf("expected default max cap of 100, got %d", got) - } -} - -func TestNilConfigFallback(t *testing.T) { - svc := &WorkspaceService{ - config: nil, - workspaces: make(map[string]*Workspace), - done: make(chan struct{}), - stopped: make(chan struct{}), - } - - if got := svc.ttl(); got != defaultWorkspaceTTL { - t.Errorf("expected fallback TTL %s, got %s", defaultWorkspaceTTL, got) - } - if got := svc.maxCap(); got != defaultMaxWorkspaces { - t.Errorf("expected fallback max cap %d, got %d", defaultMaxWorkspaces, got) - } -} diff --git a/internal/cmd/collect/cmd.go b/cmd/collect/cmd.go similarity index 100% rename from internal/cmd/collect/cmd.go rename to cmd/collect/cmd.go diff --git a/internal/cmd/collect/cmd_bitcointalk.go b/cmd/collect/cmd_bitcointalk.go similarity index 100% rename from internal/cmd/collect/cmd_bitcointalk.go rename to cmd/collect/cmd_bitcointalk.go diff --git a/internal/cmd/collect/cmd_dispatch.go b/cmd/collect/cmd_dispatch.go similarity index 100% rename from internal/cmd/collect/cmd_dispatch.go rename to cmd/collect/cmd_dispatch.go diff --git a/internal/cmd/collect/cmd_excavate.go b/cmd/collect/cmd_excavate.go similarity index 100% rename from internal/cmd/collect/cmd_excavate.go rename to cmd/collect/cmd_excavate.go diff --git a/internal/cmd/collect/cmd_github.go b/cmd/collect/cmd_github.go similarity index 100% rename from internal/cmd/collect/cmd_github.go rename to cmd/collect/cmd_github.go diff --git a/internal/cmd/collect/cmd_market.go b/cmd/collect/cmd_market.go similarity index 100% rename from internal/cmd/collect/cmd_market.go rename to cmd/collect/cmd_market.go diff --git a/internal/cmd/collect/cmd_papers.go b/cmd/collect/cmd_papers.go similarity index 100% rename from internal/cmd/collect/cmd_papers.go rename to cmd/collect/cmd_papers.go diff --git a/internal/cmd/collect/cmd_process.go b/cmd/collect/cmd_process.go similarity index 100% rename from internal/cmd/collect/cmd_process.go rename to cmd/collect/cmd_process.go diff --git a/internal/cmd/config/cmd.go b/cmd/config/cmd.go similarity index 100% rename from internal/cmd/config/cmd.go rename to cmd/config/cmd.go diff --git a/internal/cmd/config/cmd_get.go b/cmd/config/cmd_get.go similarity index 100% rename from internal/cmd/config/cmd_get.go rename to cmd/config/cmd_get.go diff --git a/internal/cmd/config/cmd_list.go b/cmd/config/cmd_list.go similarity index 100% rename from internal/cmd/config/cmd_list.go rename to cmd/config/cmd_list.go diff --git a/internal/cmd/config/cmd_path.go b/cmd/config/cmd_path.go similarity index 100% rename from internal/cmd/config/cmd_path.go rename to cmd/config/cmd_path.go diff --git a/internal/cmd/config/cmd_set.go b/cmd/config/cmd_set.go similarity index 100% rename from internal/cmd/config/cmd_set.go rename to cmd/config/cmd_set.go diff --git a/internal/cmd/crypt/cmd.go b/cmd/crypt/cmd.go similarity index 100% rename from internal/cmd/crypt/cmd.go rename to cmd/crypt/cmd.go diff --git a/internal/cmd/crypt/cmd_checksum.go b/cmd/crypt/cmd_checksum.go similarity index 100% rename from internal/cmd/crypt/cmd_checksum.go rename to cmd/crypt/cmd_checksum.go diff --git a/internal/cmd/crypt/cmd_encrypt.go b/cmd/crypt/cmd_encrypt.go similarity index 100% rename from internal/cmd/crypt/cmd_encrypt.go rename to cmd/crypt/cmd_encrypt.go diff --git a/internal/cmd/crypt/cmd_hash.go b/cmd/crypt/cmd_hash.go similarity index 100% rename from internal/cmd/crypt/cmd_hash.go rename to cmd/crypt/cmd_hash.go diff --git a/internal/cmd/crypt/cmd_keygen.go b/cmd/crypt/cmd_keygen.go similarity index 100% rename from internal/cmd/crypt/cmd_keygen.go rename to cmd/crypt/cmd_keygen.go diff --git a/internal/cmd/daemon/cmd.go b/cmd/daemon/cmd.go similarity index 100% rename from internal/cmd/daemon/cmd.go rename to cmd/daemon/cmd.go diff --git a/internal/cmd/deploy/cmd_ansible.go b/cmd/deploy/cmd_ansible.go similarity index 100% rename from internal/cmd/deploy/cmd_ansible.go rename to cmd/deploy/cmd_ansible.go diff --git a/internal/cmd/deploy/cmd_commands.go b/cmd/deploy/cmd_commands.go similarity index 100% rename from internal/cmd/deploy/cmd_commands.go rename to cmd/deploy/cmd_commands.go diff --git a/internal/cmd/deploy/cmd_deploy.go b/cmd/deploy/cmd_deploy.go similarity index 100% rename from internal/cmd/deploy/cmd_deploy.go rename to cmd/deploy/cmd_deploy.go diff --git a/internal/cmd/dev/cmd_api.go b/cmd/dev/cmd_api.go similarity index 100% rename from internal/cmd/dev/cmd_api.go rename to cmd/dev/cmd_api.go diff --git a/internal/cmd/dev/cmd_apply.go b/cmd/dev/cmd_apply.go similarity index 100% rename from internal/cmd/dev/cmd_apply.go rename to cmd/dev/cmd_apply.go diff --git a/internal/cmd/dev/cmd_bundles.go b/cmd/dev/cmd_bundles.go similarity index 100% rename from internal/cmd/dev/cmd_bundles.go rename to cmd/dev/cmd_bundles.go diff --git a/internal/cmd/dev/cmd_ci.go b/cmd/dev/cmd_ci.go similarity index 100% rename from internal/cmd/dev/cmd_ci.go rename to cmd/dev/cmd_ci.go diff --git a/internal/cmd/dev/cmd_commit.go b/cmd/dev/cmd_commit.go similarity index 100% rename from internal/cmd/dev/cmd_commit.go rename to cmd/dev/cmd_commit.go diff --git a/internal/cmd/dev/cmd_dev.go b/cmd/dev/cmd_dev.go similarity index 100% rename from internal/cmd/dev/cmd_dev.go rename to cmd/dev/cmd_dev.go diff --git a/internal/cmd/dev/cmd_file_sync.go b/cmd/dev/cmd_file_sync.go similarity index 100% rename from internal/cmd/dev/cmd_file_sync.go rename to cmd/dev/cmd_file_sync.go diff --git a/internal/cmd/dev/cmd_health.go b/cmd/dev/cmd_health.go similarity index 100% rename from internal/cmd/dev/cmd_health.go rename to cmd/dev/cmd_health.go diff --git a/internal/cmd/dev/cmd_impact.go b/cmd/dev/cmd_impact.go similarity index 100% rename from internal/cmd/dev/cmd_impact.go rename to cmd/dev/cmd_impact.go diff --git a/internal/cmd/dev/cmd_issues.go b/cmd/dev/cmd_issues.go similarity index 100% rename from internal/cmd/dev/cmd_issues.go rename to cmd/dev/cmd_issues.go diff --git a/internal/cmd/dev/cmd_pull.go b/cmd/dev/cmd_pull.go similarity index 100% rename from internal/cmd/dev/cmd_pull.go rename to cmd/dev/cmd_pull.go diff --git a/internal/cmd/dev/cmd_push.go b/cmd/dev/cmd_push.go similarity index 100% rename from internal/cmd/dev/cmd_push.go rename to cmd/dev/cmd_push.go diff --git a/internal/cmd/dev/cmd_reviews.go b/cmd/dev/cmd_reviews.go similarity index 100% rename from internal/cmd/dev/cmd_reviews.go rename to cmd/dev/cmd_reviews.go diff --git a/internal/cmd/dev/cmd_sync.go b/cmd/dev/cmd_sync.go similarity index 100% rename from internal/cmd/dev/cmd_sync.go rename to cmd/dev/cmd_sync.go diff --git a/internal/cmd/dev/cmd_vm.go b/cmd/dev/cmd_vm.go similarity index 100% rename from internal/cmd/dev/cmd_vm.go rename to cmd/dev/cmd_vm.go diff --git a/internal/cmd/dev/cmd_work.go b/cmd/dev/cmd_work.go similarity index 100% rename from internal/cmd/dev/cmd_work.go rename to cmd/dev/cmd_work.go diff --git a/internal/cmd/dev/cmd_workflow.go b/cmd/dev/cmd_workflow.go similarity index 100% rename from internal/cmd/dev/cmd_workflow.go rename to cmd/dev/cmd_workflow.go diff --git a/internal/cmd/dev/cmd_workflow_test.go b/cmd/dev/cmd_workflow_test.go similarity index 100% rename from internal/cmd/dev/cmd_workflow_test.go rename to cmd/dev/cmd_workflow_test.go diff --git a/internal/cmd/dev/registry.go b/cmd/dev/registry.go similarity index 97% rename from internal/cmd/dev/registry.go rename to cmd/dev/registry.go index 3ce5ee12..119d0b73 100644 --- a/internal/cmd/dev/registry.go +++ b/cmd/dev/registry.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "forge.lthn.ai/core/cli/internal/cmd/workspace" + "forge.lthn.ai/core/cli/cmd/workspace" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" "forge.lthn.ai/core/go/pkg/io" diff --git a/internal/cmd/dev/service.go b/cmd/dev/service.go similarity index 100% rename from internal/cmd/dev/service.go rename to cmd/dev/service.go diff --git a/internal/cmd/docs/cmd_commands.go b/cmd/docs/cmd_commands.go similarity index 100% rename from internal/cmd/docs/cmd_commands.go rename to cmd/docs/cmd_commands.go diff --git a/internal/cmd/docs/cmd_docs.go b/cmd/docs/cmd_docs.go similarity index 100% rename from internal/cmd/docs/cmd_docs.go rename to cmd/docs/cmd_docs.go diff --git a/internal/cmd/docs/cmd_list.go b/cmd/docs/cmd_list.go similarity index 100% rename from internal/cmd/docs/cmd_list.go rename to cmd/docs/cmd_list.go diff --git a/internal/cmd/docs/cmd_scan.go b/cmd/docs/cmd_scan.go similarity index 98% rename from internal/cmd/docs/cmd_scan.go rename to cmd/docs/cmd_scan.go index a897b04d..d4f8fa73 100644 --- a/internal/cmd/docs/cmd_scan.go +++ b/cmd/docs/cmd_scan.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "forge.lthn.ai/core/cli/internal/cmd/workspace" + "forge.lthn.ai/core/cli/cmd/workspace" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" "forge.lthn.ai/core/go/pkg/io" diff --git a/internal/cmd/docs/cmd_sync.go b/cmd/docs/cmd_sync.go similarity index 100% rename from internal/cmd/docs/cmd_sync.go rename to cmd/docs/cmd_sync.go diff --git a/internal/cmd/doctor/cmd_checks.go b/cmd/doctor/cmd_checks.go similarity index 100% rename from internal/cmd/doctor/cmd_checks.go rename to cmd/doctor/cmd_checks.go diff --git a/internal/cmd/doctor/cmd_commands.go b/cmd/doctor/cmd_commands.go similarity index 100% rename from internal/cmd/doctor/cmd_commands.go rename to cmd/doctor/cmd_commands.go diff --git a/internal/cmd/doctor/cmd_doctor.go b/cmd/doctor/cmd_doctor.go similarity index 100% rename from internal/cmd/doctor/cmd_doctor.go rename to cmd/doctor/cmd_doctor.go diff --git a/internal/cmd/doctor/cmd_environment.go b/cmd/doctor/cmd_environment.go similarity index 100% rename from internal/cmd/doctor/cmd_environment.go rename to cmd/doctor/cmd_environment.go diff --git a/internal/cmd/doctor/cmd_install.go b/cmd/doctor/cmd_install.go similarity index 100% rename from internal/cmd/doctor/cmd_install.go rename to cmd/doctor/cmd_install.go diff --git a/internal/cmd/forge/cmd_auth.go b/cmd/forge/cmd_auth.go similarity index 100% rename from internal/cmd/forge/cmd_auth.go rename to cmd/forge/cmd_auth.go diff --git a/internal/cmd/forge/cmd_config.go b/cmd/forge/cmd_config.go similarity index 100% rename from internal/cmd/forge/cmd_config.go rename to cmd/forge/cmd_config.go diff --git a/internal/cmd/forge/cmd_forge.go b/cmd/forge/cmd_forge.go similarity index 100% rename from internal/cmd/forge/cmd_forge.go rename to cmd/forge/cmd_forge.go diff --git a/internal/cmd/forge/cmd_issues.go b/cmd/forge/cmd_issues.go similarity index 100% rename from internal/cmd/forge/cmd_issues.go rename to cmd/forge/cmd_issues.go diff --git a/internal/cmd/forge/cmd_labels.go b/cmd/forge/cmd_labels.go similarity index 100% rename from internal/cmd/forge/cmd_labels.go rename to cmd/forge/cmd_labels.go diff --git a/internal/cmd/forge/cmd_migrate.go b/cmd/forge/cmd_migrate.go similarity index 100% rename from internal/cmd/forge/cmd_migrate.go rename to cmd/forge/cmd_migrate.go diff --git a/internal/cmd/forge/cmd_orgs.go b/cmd/forge/cmd_orgs.go similarity index 100% rename from internal/cmd/forge/cmd_orgs.go rename to cmd/forge/cmd_orgs.go diff --git a/internal/cmd/forge/cmd_prs.go b/cmd/forge/cmd_prs.go similarity index 100% rename from internal/cmd/forge/cmd_prs.go rename to cmd/forge/cmd_prs.go diff --git a/internal/cmd/forge/cmd_repos.go b/cmd/forge/cmd_repos.go similarity index 100% rename from internal/cmd/forge/cmd_repos.go rename to cmd/forge/cmd_repos.go diff --git a/internal/cmd/forge/cmd_status.go b/cmd/forge/cmd_status.go similarity index 100% rename from internal/cmd/forge/cmd_status.go rename to cmd/forge/cmd_status.go diff --git a/internal/cmd/forge/cmd_sync.go b/cmd/forge/cmd_sync.go similarity index 100% rename from internal/cmd/forge/cmd_sync.go rename to cmd/forge/cmd_sync.go diff --git a/internal/cmd/forge/helpers.go b/cmd/forge/helpers.go similarity index 100% rename from internal/cmd/forge/helpers.go rename to cmd/forge/helpers.go diff --git a/internal/cmd/gitcmd/cmd_git.go b/cmd/gitcmd/cmd_git.go similarity index 96% rename from internal/cmd/gitcmd/cmd_git.go rename to cmd/gitcmd/cmd_git.go index 2326752f..0024ecda 100644 --- a/internal/cmd/gitcmd/cmd_git.go +++ b/cmd/gitcmd/cmd_git.go @@ -13,7 +13,7 @@ package gitcmd import ( - "forge.lthn.ai/core/cli/internal/cmd/dev" + "forge.lthn.ai/core/cli/cmd/dev" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" ) diff --git a/internal/cmd/gitea/cmd_config.go b/cmd/gitea/cmd_config.go similarity index 100% rename from internal/cmd/gitea/cmd_config.go rename to cmd/gitea/cmd_config.go diff --git a/internal/cmd/gitea/cmd_gitea.go b/cmd/gitea/cmd_gitea.go similarity index 100% rename from internal/cmd/gitea/cmd_gitea.go rename to cmd/gitea/cmd_gitea.go diff --git a/internal/cmd/gitea/cmd_issues.go b/cmd/gitea/cmd_issues.go similarity index 100% rename from internal/cmd/gitea/cmd_issues.go rename to cmd/gitea/cmd_issues.go diff --git a/internal/cmd/gitea/cmd_mirror.go b/cmd/gitea/cmd_mirror.go similarity index 100% rename from internal/cmd/gitea/cmd_mirror.go rename to cmd/gitea/cmd_mirror.go diff --git a/internal/cmd/gitea/cmd_prs.go b/cmd/gitea/cmd_prs.go similarity index 100% rename from internal/cmd/gitea/cmd_prs.go rename to cmd/gitea/cmd_prs.go diff --git a/internal/cmd/gitea/cmd_repos.go b/cmd/gitea/cmd_repos.go similarity index 100% rename from internal/cmd/gitea/cmd_repos.go rename to cmd/gitea/cmd_repos.go diff --git a/internal/cmd/gitea/cmd_sync.go b/cmd/gitea/cmd_sync.go similarity index 100% rename from internal/cmd/gitea/cmd_sync.go rename to cmd/gitea/cmd_sync.go diff --git a/internal/cmd/go/cmd_commands.go b/cmd/go/cmd_commands.go similarity index 100% rename from internal/cmd/go/cmd_commands.go rename to cmd/go/cmd_commands.go diff --git a/internal/cmd/go/cmd_format.go b/cmd/go/cmd_format.go similarity index 100% rename from internal/cmd/go/cmd_format.go rename to cmd/go/cmd_format.go diff --git a/internal/cmd/go/cmd_fuzz.go b/cmd/go/cmd_fuzz.go similarity index 100% rename from internal/cmd/go/cmd_fuzz.go rename to cmd/go/cmd_fuzz.go diff --git a/internal/cmd/go/cmd_go.go b/cmd/go/cmd_go.go similarity index 100% rename from internal/cmd/go/cmd_go.go rename to cmd/go/cmd_go.go diff --git a/internal/cmd/go/cmd_gotest.go b/cmd/go/cmd_gotest.go similarity index 100% rename from internal/cmd/go/cmd_gotest.go rename to cmd/go/cmd_gotest.go diff --git a/internal/cmd/go/cmd_qa.go b/cmd/go/cmd_qa.go similarity index 99% rename from internal/cmd/go/cmd_qa.go rename to cmd/go/cmd_qa.go index ed318651..62d4439b 100644 --- a/internal/cmd/go/cmd_qa.go +++ b/cmd/go/cmd_qa.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "forge.lthn.ai/core/cli/internal/cmd/qa" + "forge.lthn.ai/core/cli/cmd/qa" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" ) diff --git a/internal/cmd/go/cmd_tools.go b/cmd/go/cmd_tools.go similarity index 100% rename from internal/cmd/go/cmd_tools.go rename to cmd/go/cmd_tools.go diff --git a/internal/cmd/go/coverage_test.go b/cmd/go/coverage_test.go similarity index 100% rename from internal/cmd/go/coverage_test.go rename to cmd/go/coverage_test.go diff --git a/internal/cmd/help/cmd.go b/cmd/help/cmd.go similarity index 100% rename from internal/cmd/help/cmd.go rename to cmd/help/cmd.go diff --git a/internal/cmd/lab/cmd_lab.go b/cmd/lab/cmd_lab.go similarity index 100% rename from internal/cmd/lab/cmd_lab.go rename to cmd/lab/cmd_lab.go diff --git a/internal/cmd/mcpcmd/cmd_mcp.go b/cmd/mcpcmd/cmd_mcp.go similarity index 100% rename from internal/cmd/mcpcmd/cmd_mcp.go rename to cmd/mcpcmd/cmd_mcp.go diff --git a/internal/cmd/ml/cmd_agent.go b/cmd/ml/cmd_agent.go similarity index 100% rename from internal/cmd/ml/cmd_agent.go rename to cmd/ml/cmd_agent.go diff --git a/internal/cmd/ml/cmd_approve.go b/cmd/ml/cmd_approve.go similarity index 100% rename from internal/cmd/ml/cmd_approve.go rename to cmd/ml/cmd_approve.go diff --git a/internal/cmd/ml/cmd_consolidate.go b/cmd/ml/cmd_consolidate.go similarity index 100% rename from internal/cmd/ml/cmd_consolidate.go rename to cmd/ml/cmd_consolidate.go diff --git a/internal/cmd/ml/cmd_convert.go b/cmd/ml/cmd_convert.go similarity index 100% rename from internal/cmd/ml/cmd_convert.go rename to cmd/ml/cmd_convert.go diff --git a/internal/cmd/ml/cmd_coverage.go b/cmd/ml/cmd_coverage.go similarity index 100% rename from internal/cmd/ml/cmd_coverage.go rename to cmd/ml/cmd_coverage.go diff --git a/internal/cmd/ml/cmd_expand.go b/cmd/ml/cmd_expand.go similarity index 100% rename from internal/cmd/ml/cmd_expand.go rename to cmd/ml/cmd_expand.go diff --git a/internal/cmd/ml/cmd_export.go b/cmd/ml/cmd_export.go similarity index 100% rename from internal/cmd/ml/cmd_export.go rename to cmd/ml/cmd_export.go diff --git a/internal/cmd/ml/cmd_gguf.go b/cmd/ml/cmd_gguf.go similarity index 100% rename from internal/cmd/ml/cmd_gguf.go rename to cmd/ml/cmd_gguf.go diff --git a/internal/cmd/ml/cmd_import.go b/cmd/ml/cmd_import.go similarity index 100% rename from internal/cmd/ml/cmd_import.go rename to cmd/ml/cmd_import.go diff --git a/internal/cmd/ml/cmd_ingest.go b/cmd/ml/cmd_ingest.go similarity index 100% rename from internal/cmd/ml/cmd_ingest.go rename to cmd/ml/cmd_ingest.go diff --git a/internal/cmd/ml/cmd_inventory.go b/cmd/ml/cmd_inventory.go similarity index 100% rename from internal/cmd/ml/cmd_inventory.go rename to cmd/ml/cmd_inventory.go diff --git a/internal/cmd/ml/cmd_metrics.go b/cmd/ml/cmd_metrics.go similarity index 100% rename from internal/cmd/ml/cmd_metrics.go rename to cmd/ml/cmd_metrics.go diff --git a/internal/cmd/ml/cmd_ml.go b/cmd/ml/cmd_ml.go similarity index 100% rename from internal/cmd/ml/cmd_ml.go rename to cmd/ml/cmd_ml.go diff --git a/internal/cmd/ml/cmd_normalize.go b/cmd/ml/cmd_normalize.go similarity index 100% rename from internal/cmd/ml/cmd_normalize.go rename to cmd/ml/cmd_normalize.go diff --git a/internal/cmd/ml/cmd_probe.go b/cmd/ml/cmd_probe.go similarity index 100% rename from internal/cmd/ml/cmd_probe.go rename to cmd/ml/cmd_probe.go diff --git a/internal/cmd/ml/cmd_publish.go b/cmd/ml/cmd_publish.go similarity index 100% rename from internal/cmd/ml/cmd_publish.go rename to cmd/ml/cmd_publish.go diff --git a/internal/cmd/ml/cmd_query.go b/cmd/ml/cmd_query.go similarity index 100% rename from internal/cmd/ml/cmd_query.go rename to cmd/ml/cmd_query.go diff --git a/internal/cmd/ml/cmd_score.go b/cmd/ml/cmd_score.go similarity index 100% rename from internal/cmd/ml/cmd_score.go rename to cmd/ml/cmd_score.go diff --git a/internal/cmd/ml/cmd_seed_influx.go b/cmd/ml/cmd_seed_influx.go similarity index 100% rename from internal/cmd/ml/cmd_seed_influx.go rename to cmd/ml/cmd_seed_influx.go diff --git a/internal/cmd/ml/cmd_serve.go b/cmd/ml/cmd_serve.go similarity index 100% rename from internal/cmd/ml/cmd_serve.go rename to cmd/ml/cmd_serve.go diff --git a/internal/cmd/ml/cmd_status.go b/cmd/ml/cmd_status.go similarity index 100% rename from internal/cmd/ml/cmd_status.go rename to cmd/ml/cmd_status.go diff --git a/internal/cmd/ml/cmd_worker.go b/cmd/ml/cmd_worker.go similarity index 100% rename from internal/cmd/ml/cmd_worker.go rename to cmd/ml/cmd_worker.go diff --git a/internal/cmd/ml/serve_backend_default.go b/cmd/ml/serve_backend_default.go similarity index 100% rename from internal/cmd/ml/serve_backend_default.go rename to cmd/ml/serve_backend_default.go diff --git a/internal/cmd/ml/serve_backend_mlx.go b/cmd/ml/serve_backend_mlx.go similarity index 100% rename from internal/cmd/ml/serve_backend_mlx.go rename to cmd/ml/serve_backend_mlx.go diff --git a/internal/cmd/monitor/cmd_commands.go b/cmd/monitor/cmd_commands.go similarity index 100% rename from internal/cmd/monitor/cmd_commands.go rename to cmd/monitor/cmd_commands.go diff --git a/internal/cmd/monitor/cmd_monitor.go b/cmd/monitor/cmd_monitor.go similarity index 100% rename from internal/cmd/monitor/cmd_monitor.go rename to cmd/monitor/cmd_monitor.go diff --git a/internal/cmd/pkgcmd/cmd_commands.go b/cmd/pkgcmd/cmd_commands.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_commands.go rename to cmd/pkgcmd/cmd_commands.go diff --git a/internal/cmd/pkgcmd/cmd_install.go b/cmd/pkgcmd/cmd_install.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_install.go rename to cmd/pkgcmd/cmd_install.go diff --git a/internal/cmd/pkgcmd/cmd_manage.go b/cmd/pkgcmd/cmd_manage.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_manage.go rename to cmd/pkgcmd/cmd_manage.go diff --git a/internal/cmd/pkgcmd/cmd_pkg.go b/cmd/pkgcmd/cmd_pkg.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_pkg.go rename to cmd/pkgcmd/cmd_pkg.go diff --git a/internal/cmd/pkgcmd/cmd_remove.go b/cmd/pkgcmd/cmd_remove.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_remove.go rename to cmd/pkgcmd/cmd_remove.go diff --git a/internal/cmd/pkgcmd/cmd_remove_test.go b/cmd/pkgcmd/cmd_remove_test.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_remove_test.go rename to cmd/pkgcmd/cmd_remove_test.go diff --git a/internal/cmd/pkgcmd/cmd_search.go b/cmd/pkgcmd/cmd_search.go similarity index 100% rename from internal/cmd/pkgcmd/cmd_search.go rename to cmd/pkgcmd/cmd_search.go diff --git a/internal/cmd/plugin/cmd.go b/cmd/plugin/cmd.go similarity index 100% rename from internal/cmd/plugin/cmd.go rename to cmd/plugin/cmd.go diff --git a/internal/cmd/plugin/cmd_info.go b/cmd/plugin/cmd_info.go similarity index 100% rename from internal/cmd/plugin/cmd_info.go rename to cmd/plugin/cmd_info.go diff --git a/internal/cmd/plugin/cmd_install.go b/cmd/plugin/cmd_install.go similarity index 100% rename from internal/cmd/plugin/cmd_install.go rename to cmd/plugin/cmd_install.go diff --git a/internal/cmd/plugin/cmd_list.go b/cmd/plugin/cmd_list.go similarity index 100% rename from internal/cmd/plugin/cmd_list.go rename to cmd/plugin/cmd_list.go diff --git a/internal/cmd/plugin/cmd_remove.go b/cmd/plugin/cmd_remove.go similarity index 100% rename from internal/cmd/plugin/cmd_remove.go rename to cmd/plugin/cmd_remove.go diff --git a/internal/cmd/plugin/cmd_update.go b/cmd/plugin/cmd_update.go similarity index 100% rename from internal/cmd/plugin/cmd_update.go rename to cmd/plugin/cmd_update.go diff --git a/internal/cmd/prod/cmd_commands.go b/cmd/prod/cmd_commands.go similarity index 100% rename from internal/cmd/prod/cmd_commands.go rename to cmd/prod/cmd_commands.go diff --git a/internal/cmd/prod/cmd_dns.go b/cmd/prod/cmd_dns.go similarity index 100% rename from internal/cmd/prod/cmd_dns.go rename to cmd/prod/cmd_dns.go diff --git a/internal/cmd/prod/cmd_lb.go b/cmd/prod/cmd_lb.go similarity index 100% rename from internal/cmd/prod/cmd_lb.go rename to cmd/prod/cmd_lb.go diff --git a/internal/cmd/prod/cmd_prod.go b/cmd/prod/cmd_prod.go similarity index 100% rename from internal/cmd/prod/cmd_prod.go rename to cmd/prod/cmd_prod.go diff --git a/internal/cmd/prod/cmd_setup.go b/cmd/prod/cmd_setup.go similarity index 100% rename from internal/cmd/prod/cmd_setup.go rename to cmd/prod/cmd_setup.go diff --git a/internal/cmd/prod/cmd_ssh.go b/cmd/prod/cmd_ssh.go similarity index 100% rename from internal/cmd/prod/cmd_ssh.go rename to cmd/prod/cmd_ssh.go diff --git a/internal/cmd/prod/cmd_status.go b/cmd/prod/cmd_status.go similarity index 100% rename from internal/cmd/prod/cmd_status.go rename to cmd/prod/cmd_status.go diff --git a/internal/cmd/qa/cmd_docblock.go b/cmd/qa/cmd_docblock.go similarity index 100% rename from internal/cmd/qa/cmd_docblock.go rename to cmd/qa/cmd_docblock.go diff --git a/internal/cmd/qa/cmd_health.go b/cmd/qa/cmd_health.go similarity index 100% rename from internal/cmd/qa/cmd_health.go rename to cmd/qa/cmd_health.go diff --git a/internal/cmd/qa/cmd_issues.go b/cmd/qa/cmd_issues.go similarity index 100% rename from internal/cmd/qa/cmd_issues.go rename to cmd/qa/cmd_issues.go diff --git a/internal/cmd/qa/cmd_qa.go b/cmd/qa/cmd_qa.go similarity index 100% rename from internal/cmd/qa/cmd_qa.go rename to cmd/qa/cmd_qa.go diff --git a/internal/cmd/qa/cmd_review.go b/cmd/qa/cmd_review.go similarity index 100% rename from internal/cmd/qa/cmd_review.go rename to cmd/qa/cmd_review.go diff --git a/internal/cmd/qa/cmd_watch.go b/cmd/qa/cmd_watch.go similarity index 100% rename from internal/cmd/qa/cmd_watch.go rename to cmd/qa/cmd_watch.go diff --git a/internal/cmd/rag/cmd_collections.go b/cmd/rag/cmd_collections.go similarity index 100% rename from internal/cmd/rag/cmd_collections.go rename to cmd/rag/cmd_collections.go diff --git a/internal/cmd/rag/cmd_commands.go b/cmd/rag/cmd_commands.go similarity index 100% rename from internal/cmd/rag/cmd_commands.go rename to cmd/rag/cmd_commands.go diff --git a/internal/cmd/rag/cmd_ingest.go b/cmd/rag/cmd_ingest.go similarity index 100% rename from internal/cmd/rag/cmd_ingest.go rename to cmd/rag/cmd_ingest.go diff --git a/internal/cmd/rag/cmd_query.go b/cmd/rag/cmd_query.go similarity index 100% rename from internal/cmd/rag/cmd_query.go rename to cmd/rag/cmd_query.go diff --git a/internal/cmd/rag/cmd_rag.go b/cmd/rag/cmd_rag.go similarity index 100% rename from internal/cmd/rag/cmd_rag.go rename to cmd/rag/cmd_rag.go diff --git a/internal/cmd/security/cmd.go b/cmd/security/cmd.go similarity index 100% rename from internal/cmd/security/cmd.go rename to cmd/security/cmd.go diff --git a/internal/cmd/security/cmd_alerts.go b/cmd/security/cmd_alerts.go similarity index 100% rename from internal/cmd/security/cmd_alerts.go rename to cmd/security/cmd_alerts.go diff --git a/internal/cmd/security/cmd_deps.go b/cmd/security/cmd_deps.go similarity index 100% rename from internal/cmd/security/cmd_deps.go rename to cmd/security/cmd_deps.go diff --git a/internal/cmd/security/cmd_jobs.go b/cmd/security/cmd_jobs.go similarity index 100% rename from internal/cmd/security/cmd_jobs.go rename to cmd/security/cmd_jobs.go diff --git a/internal/cmd/security/cmd_scan.go b/cmd/security/cmd_scan.go similarity index 100% rename from internal/cmd/security/cmd_scan.go rename to cmd/security/cmd_scan.go diff --git a/internal/cmd/security/cmd_secrets.go b/cmd/security/cmd_secrets.go similarity index 100% rename from internal/cmd/security/cmd_secrets.go rename to cmd/security/cmd_secrets.go diff --git a/internal/cmd/security/cmd_security.go b/cmd/security/cmd_security.go similarity index 100% rename from internal/cmd/security/cmd_security.go rename to cmd/security/cmd_security.go diff --git a/internal/cmd/session/cmd_session.go b/cmd/session/cmd_session.go similarity index 100% rename from internal/cmd/session/cmd_session.go rename to cmd/session/cmd_session.go diff --git a/internal/cmd/setup/cmd_bootstrap.go b/cmd/setup/cmd_bootstrap.go similarity index 99% rename from internal/cmd/setup/cmd_bootstrap.go rename to cmd/setup/cmd_bootstrap.go index 81562ad3..3473d6f2 100644 --- a/internal/cmd/setup/cmd_bootstrap.go +++ b/cmd/setup/cmd_bootstrap.go @@ -13,7 +13,7 @@ import ( "path/filepath" "strings" - "forge.lthn.ai/core/cli/internal/cmd/workspace" + "forge.lthn.ai/core/cli/cmd/workspace" "forge.lthn.ai/core/go/pkg/i18n" coreio "forge.lthn.ai/core/go/pkg/io" "forge.lthn.ai/core/go/pkg/repos" diff --git a/internal/cmd/setup/cmd_ci.go b/cmd/setup/cmd_ci.go similarity index 100% rename from internal/cmd/setup/cmd_ci.go rename to cmd/setup/cmd_ci.go diff --git a/internal/cmd/setup/cmd_commands.go b/cmd/setup/cmd_commands.go similarity index 100% rename from internal/cmd/setup/cmd_commands.go rename to cmd/setup/cmd_commands.go diff --git a/internal/cmd/setup/cmd_github.go b/cmd/setup/cmd_github.go similarity index 100% rename from internal/cmd/setup/cmd_github.go rename to cmd/setup/cmd_github.go diff --git a/internal/cmd/setup/cmd_registry.go b/cmd/setup/cmd_registry.go similarity index 99% rename from internal/cmd/setup/cmd_registry.go rename to cmd/setup/cmd_registry.go index c1dd152d..6af20401 100644 --- a/internal/cmd/setup/cmd_registry.go +++ b/cmd/setup/cmd_registry.go @@ -13,7 +13,7 @@ import ( "path/filepath" "strings" - "forge.lthn.ai/core/cli/internal/cmd/workspace" + "forge.lthn.ai/core/cli/cmd/workspace" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" coreio "forge.lthn.ai/core/go/pkg/io" diff --git a/internal/cmd/setup/cmd_repo.go b/cmd/setup/cmd_repo.go similarity index 100% rename from internal/cmd/setup/cmd_repo.go rename to cmd/setup/cmd_repo.go diff --git a/internal/cmd/setup/cmd_setup.go b/cmd/setup/cmd_setup.go similarity index 100% rename from internal/cmd/setup/cmd_setup.go rename to cmd/setup/cmd_setup.go diff --git a/internal/cmd/setup/cmd_wizard.go b/cmd/setup/cmd_wizard.go similarity index 100% rename from internal/cmd/setup/cmd_wizard.go rename to cmd/setup/cmd_wizard.go diff --git a/internal/cmd/setup/github_config.go b/cmd/setup/github_config.go similarity index 100% rename from internal/cmd/setup/github_config.go rename to cmd/setup/github_config.go diff --git a/internal/cmd/setup/github_diff.go b/cmd/setup/github_diff.go similarity index 100% rename from internal/cmd/setup/github_diff.go rename to cmd/setup/github_diff.go diff --git a/internal/cmd/setup/github_labels.go b/cmd/setup/github_labels.go similarity index 100% rename from internal/cmd/setup/github_labels.go rename to cmd/setup/github_labels.go diff --git a/internal/cmd/setup/github_protection.go b/cmd/setup/github_protection.go similarity index 100% rename from internal/cmd/setup/github_protection.go rename to cmd/setup/github_protection.go diff --git a/internal/cmd/setup/github_security.go b/cmd/setup/github_security.go similarity index 100% rename from internal/cmd/setup/github_security.go rename to cmd/setup/github_security.go diff --git a/internal/cmd/setup/github_webhooks.go b/cmd/setup/github_webhooks.go similarity index 100% rename from internal/cmd/setup/github_webhooks.go rename to cmd/setup/github_webhooks.go diff --git a/internal/cmd/test/cmd_commands.go b/cmd/test/cmd_commands.go similarity index 100% rename from internal/cmd/test/cmd_commands.go rename to cmd/test/cmd_commands.go diff --git a/internal/cmd/test/cmd_main.go b/cmd/test/cmd_main.go similarity index 100% rename from internal/cmd/test/cmd_main.go rename to cmd/test/cmd_main.go diff --git a/internal/cmd/test/cmd_output.go b/cmd/test/cmd_output.go similarity index 100% rename from internal/cmd/test/cmd_output.go rename to cmd/test/cmd_output.go diff --git a/internal/cmd/test/cmd_runner.go b/cmd/test/cmd_runner.go similarity index 100% rename from internal/cmd/test/cmd_runner.go rename to cmd/test/cmd_runner.go diff --git a/internal/cmd/test/output_test.go b/cmd/test/output_test.go similarity index 100% rename from internal/cmd/test/output_test.go rename to cmd/test/output_test.go diff --git a/internal/cmd/unifi/cmd_clients.go b/cmd/unifi/cmd_clients.go similarity index 100% rename from internal/cmd/unifi/cmd_clients.go rename to cmd/unifi/cmd_clients.go diff --git a/internal/cmd/unifi/cmd_config.go b/cmd/unifi/cmd_config.go similarity index 100% rename from internal/cmd/unifi/cmd_config.go rename to cmd/unifi/cmd_config.go diff --git a/internal/cmd/unifi/cmd_devices.go b/cmd/unifi/cmd_devices.go similarity index 100% rename from internal/cmd/unifi/cmd_devices.go rename to cmd/unifi/cmd_devices.go diff --git a/internal/cmd/unifi/cmd_networks.go b/cmd/unifi/cmd_networks.go similarity index 100% rename from internal/cmd/unifi/cmd_networks.go rename to cmd/unifi/cmd_networks.go diff --git a/internal/cmd/unifi/cmd_routes.go b/cmd/unifi/cmd_routes.go similarity index 100% rename from internal/cmd/unifi/cmd_routes.go rename to cmd/unifi/cmd_routes.go diff --git a/internal/cmd/unifi/cmd_sites.go b/cmd/unifi/cmd_sites.go similarity index 100% rename from internal/cmd/unifi/cmd_sites.go rename to cmd/unifi/cmd_sites.go diff --git a/internal/cmd/unifi/cmd_unifi.go b/cmd/unifi/cmd_unifi.go similarity index 100% rename from internal/cmd/unifi/cmd_unifi.go rename to cmd/unifi/cmd_unifi.go diff --git a/internal/cmd/updater/.github/workflows/ci.yml b/cmd/updater/.github/workflows/ci.yml similarity index 100% rename from internal/cmd/updater/.github/workflows/ci.yml rename to cmd/updater/.github/workflows/ci.yml diff --git a/internal/cmd/updater/.github/workflows/release.yml b/cmd/updater/.github/workflows/release.yml similarity index 100% rename from internal/cmd/updater/.github/workflows/release.yml rename to cmd/updater/.github/workflows/release.yml diff --git a/internal/cmd/updater/.gitignore b/cmd/updater/.gitignore similarity index 100% rename from internal/cmd/updater/.gitignore rename to cmd/updater/.gitignore diff --git a/internal/cmd/updater/LICENSE b/cmd/updater/LICENSE similarity index 100% rename from internal/cmd/updater/LICENSE rename to cmd/updater/LICENSE diff --git a/internal/cmd/updater/Makefile b/cmd/updater/Makefile similarity index 100% rename from internal/cmd/updater/Makefile rename to cmd/updater/Makefile diff --git a/internal/cmd/updater/README.md b/cmd/updater/README.md similarity index 100% rename from internal/cmd/updater/README.md rename to cmd/updater/README.md diff --git a/internal/cmd/updater/build/main.go b/cmd/updater/build/main.go similarity index 100% rename from internal/cmd/updater/build/main.go rename to cmd/updater/build/main.go diff --git a/internal/cmd/updater/cmd.go b/cmd/updater/cmd.go similarity index 100% rename from internal/cmd/updater/cmd.go rename to cmd/updater/cmd.go diff --git a/internal/cmd/updater/cmd_unix.go b/cmd/updater/cmd_unix.go similarity index 100% rename from internal/cmd/updater/cmd_unix.go rename to cmd/updater/cmd_unix.go diff --git a/internal/cmd/updater/cmd_windows.go b/cmd/updater/cmd_windows.go similarity index 100% rename from internal/cmd/updater/cmd_windows.go rename to cmd/updater/cmd_windows.go diff --git a/internal/cmd/updater/docs/README.md b/cmd/updater/docs/README.md similarity index 100% rename from internal/cmd/updater/docs/README.md rename to cmd/updater/docs/README.md diff --git a/internal/cmd/updater/docs/architecture.md b/cmd/updater/docs/architecture.md similarity index 100% rename from internal/cmd/updater/docs/architecture.md rename to cmd/updater/docs/architecture.md diff --git a/internal/cmd/updater/docs/configuration.md b/cmd/updater/docs/configuration.md similarity index 100% rename from internal/cmd/updater/docs/configuration.md rename to cmd/updater/docs/configuration.md diff --git a/internal/cmd/updater/docs/getting-started.md b/cmd/updater/docs/getting-started.md similarity index 100% rename from internal/cmd/updater/docs/getting-started.md rename to cmd/updater/docs/getting-started.md diff --git a/internal/cmd/updater/generic_http.go b/cmd/updater/generic_http.go similarity index 100% rename from internal/cmd/updater/generic_http.go rename to cmd/updater/generic_http.go diff --git a/internal/cmd/updater/generic_http_test.go b/cmd/updater/generic_http_test.go similarity index 100% rename from internal/cmd/updater/generic_http_test.go rename to cmd/updater/generic_http_test.go diff --git a/internal/cmd/updater/github.go b/cmd/updater/github.go similarity index 100% rename from internal/cmd/updater/github.go rename to cmd/updater/github.go diff --git a/internal/cmd/updater/github_test.go b/cmd/updater/github_test.go similarity index 100% rename from internal/cmd/updater/github_test.go rename to cmd/updater/github_test.go diff --git a/internal/cmd/updater/mock_github_client_test.go b/cmd/updater/mock_github_client_test.go similarity index 100% rename from internal/cmd/updater/mock_github_client_test.go rename to cmd/updater/mock_github_client_test.go diff --git a/internal/cmd/updater/package.json b/cmd/updater/package.json similarity index 100% rename from internal/cmd/updater/package.json rename to cmd/updater/package.json diff --git a/internal/cmd/updater/service.go b/cmd/updater/service.go similarity index 98% rename from internal/cmd/updater/service.go rename to cmd/updater/service.go index bebc5d6e..213f5b2a 100644 --- a/internal/cmd/updater/service.go +++ b/cmd/updater/service.go @@ -1,4 +1,4 @@ -//go:generate go run forge.lthn.ai/core/cli/internal/cmd/updater/build +//go:generate go run forge.lthn.ai/core/cli/cmd/updater/build // Package updater provides functionality for self-updating Go applications. // It supports updates from GitHub releases and generic HTTP endpoints. diff --git a/internal/cmd/updater/service_examples_test.go b/cmd/updater/service_examples_test.go similarity index 95% rename from internal/cmd/updater/service_examples_test.go rename to cmd/updater/service_examples_test.go index 8a07910f..5d6b9366 100644 --- a/internal/cmd/updater/service_examples_test.go +++ b/cmd/updater/service_examples_test.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "forge.lthn.ai/core/cli/internal/cmd/updater" + "forge.lthn.ai/core/cli/cmd/updater" ) func ExampleNewUpdateService() { diff --git a/internal/cmd/updater/service_test.go b/cmd/updater/service_test.go similarity index 100% rename from internal/cmd/updater/service_test.go rename to cmd/updater/service_test.go diff --git a/internal/cmd/updater/tests.patch b/cmd/updater/tests.patch similarity index 100% rename from internal/cmd/updater/tests.patch rename to cmd/updater/tests.patch diff --git a/internal/cmd/updater/ui/.editorconfig b/cmd/updater/ui/.editorconfig similarity index 100% rename from internal/cmd/updater/ui/.editorconfig rename to cmd/updater/ui/.editorconfig diff --git a/internal/cmd/updater/ui/.gitignore b/cmd/updater/ui/.gitignore similarity index 100% rename from internal/cmd/updater/ui/.gitignore rename to cmd/updater/ui/.gitignore diff --git a/internal/cmd/updater/ui/.vscode/extensions.json b/cmd/updater/ui/.vscode/extensions.json similarity index 100% rename from internal/cmd/updater/ui/.vscode/extensions.json rename to cmd/updater/ui/.vscode/extensions.json diff --git a/internal/cmd/updater/ui/.vscode/launch.json b/cmd/updater/ui/.vscode/launch.json similarity index 100% rename from internal/cmd/updater/ui/.vscode/launch.json rename to cmd/updater/ui/.vscode/launch.json diff --git a/internal/cmd/updater/ui/.vscode/tasks.json b/cmd/updater/ui/.vscode/tasks.json similarity index 100% rename from internal/cmd/updater/ui/.vscode/tasks.json rename to cmd/updater/ui/.vscode/tasks.json diff --git a/internal/cmd/updater/ui/README.md b/cmd/updater/ui/README.md similarity index 100% rename from internal/cmd/updater/ui/README.md rename to cmd/updater/ui/README.md diff --git a/internal/cmd/updater/ui/angular.json b/cmd/updater/ui/angular.json similarity index 100% rename from internal/cmd/updater/ui/angular.json rename to cmd/updater/ui/angular.json diff --git a/internal/cmd/updater/ui/package-lock.json b/cmd/updater/ui/package-lock.json similarity index 100% rename from internal/cmd/updater/ui/package-lock.json rename to cmd/updater/ui/package-lock.json diff --git a/internal/cmd/updater/ui/package.json b/cmd/updater/ui/package.json similarity index 100% rename from internal/cmd/updater/ui/package.json rename to cmd/updater/ui/package.json diff --git a/internal/cmd/updater/ui/public/favicon.ico b/cmd/updater/ui/public/favicon.ico similarity index 100% rename from internal/cmd/updater/ui/public/favicon.ico rename to cmd/updater/ui/public/favicon.ico diff --git a/internal/cmd/updater/ui/src/app/app-module.ts b/cmd/updater/ui/src/app/app-module.ts similarity index 100% rename from internal/cmd/updater/ui/src/app/app-module.ts rename to cmd/updater/ui/src/app/app-module.ts diff --git a/internal/cmd/updater/ui/src/app/app.html b/cmd/updater/ui/src/app/app.html similarity index 100% rename from internal/cmd/updater/ui/src/app/app.html rename to cmd/updater/ui/src/app/app.html diff --git a/internal/cmd/updater/ui/src/app/app.ts b/cmd/updater/ui/src/app/app.ts similarity index 100% rename from internal/cmd/updater/ui/src/app/app.ts rename to cmd/updater/ui/src/app/app.ts diff --git a/internal/cmd/updater/ui/src/index.html b/cmd/updater/ui/src/index.html similarity index 100% rename from internal/cmd/updater/ui/src/index.html rename to cmd/updater/ui/src/index.html diff --git a/internal/cmd/updater/ui/src/main.ts b/cmd/updater/ui/src/main.ts similarity index 100% rename from internal/cmd/updater/ui/src/main.ts rename to cmd/updater/ui/src/main.ts diff --git a/internal/cmd/updater/ui/src/styles.css b/cmd/updater/ui/src/styles.css similarity index 100% rename from internal/cmd/updater/ui/src/styles.css rename to cmd/updater/ui/src/styles.css diff --git a/internal/cmd/updater/ui/tsconfig.app.json b/cmd/updater/ui/tsconfig.app.json similarity index 100% rename from internal/cmd/updater/ui/tsconfig.app.json rename to cmd/updater/ui/tsconfig.app.json diff --git a/internal/cmd/updater/ui/tsconfig.json b/cmd/updater/ui/tsconfig.json similarity index 100% rename from internal/cmd/updater/ui/tsconfig.json rename to cmd/updater/ui/tsconfig.json diff --git a/internal/cmd/updater/ui/tsconfig.spec.json b/cmd/updater/ui/tsconfig.spec.json similarity index 100% rename from internal/cmd/updater/ui/tsconfig.spec.json rename to cmd/updater/ui/tsconfig.spec.json diff --git a/internal/cmd/updater/updater.go b/cmd/updater/updater.go similarity index 100% rename from internal/cmd/updater/updater.go rename to cmd/updater/updater.go diff --git a/internal/cmd/updater/updater_test.go b/cmd/updater/updater_test.go similarity index 100% rename from internal/cmd/updater/updater_test.go rename to cmd/updater/updater_test.go diff --git a/internal/cmd/updater/version.go b/cmd/updater/version.go similarity index 100% rename from internal/cmd/updater/version.go rename to cmd/updater/version.go diff --git a/internal/cmd/vm/cmd_commands.go b/cmd/vm/cmd_commands.go similarity index 100% rename from internal/cmd/vm/cmd_commands.go rename to cmd/vm/cmd_commands.go diff --git a/internal/cmd/vm/cmd_container.go b/cmd/vm/cmd_container.go similarity index 100% rename from internal/cmd/vm/cmd_container.go rename to cmd/vm/cmd_container.go diff --git a/internal/cmd/vm/cmd_templates.go b/cmd/vm/cmd_templates.go similarity index 100% rename from internal/cmd/vm/cmd_templates.go rename to cmd/vm/cmd_templates.go diff --git a/internal/cmd/vm/cmd_vm.go b/cmd/vm/cmd_vm.go similarity index 100% rename from internal/cmd/vm/cmd_vm.go rename to cmd/vm/cmd_vm.go diff --git a/internal/cmd/workspace/cmd.go b/cmd/workspace/cmd.go similarity index 100% rename from internal/cmd/workspace/cmd.go rename to cmd/workspace/cmd.go diff --git a/internal/cmd/workspace/cmd_agent.go b/cmd/workspace/cmd_agent.go similarity index 100% rename from internal/cmd/workspace/cmd_agent.go rename to cmd/workspace/cmd_agent.go diff --git a/internal/cmd/workspace/cmd_agent_test.go b/cmd/workspace/cmd_agent_test.go similarity index 100% rename from internal/cmd/workspace/cmd_agent_test.go rename to cmd/workspace/cmd_agent_test.go diff --git a/internal/cmd/workspace/cmd_task.go b/cmd/workspace/cmd_task.go similarity index 100% rename from internal/cmd/workspace/cmd_task.go rename to cmd/workspace/cmd_task.go diff --git a/internal/cmd/workspace/cmd_task_test.go b/cmd/workspace/cmd_task_test.go similarity index 100% rename from internal/cmd/workspace/cmd_task_test.go rename to cmd/workspace/cmd_task_test.go diff --git a/internal/cmd/workspace/cmd_workspace.go b/cmd/workspace/cmd_workspace.go similarity index 100% rename from internal/cmd/workspace/cmd_workspace.go rename to cmd/workspace/cmd_workspace.go diff --git a/internal/cmd/workspace/config.go b/cmd/workspace/config.go similarity index 100% rename from internal/cmd/workspace/config.go rename to cmd/workspace/config.go diff --git a/internal/bugseti/config.go b/internal/bugseti/config.go deleted file mode 100644 index fe8776ed..00000000 --- a/internal/bugseti/config.go +++ /dev/null @@ -1,646 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "encoding/json" - "log" - "os" - "path/filepath" - "sync" - "time" -) - -// ConfigService manages application configuration and persistence. -type ConfigService struct { - config *Config - path string - mu sync.RWMutex -} - -// Config holds all BugSETI configuration. -type Config struct { - // Authentication — Forgejo API (resolved via pkg/forge config if empty) - ForgeURL string `json:"forgeUrl,omitempty"` - ForgeToken string `json:"forgeToken,omitempty"` - - // Hub coordination (agentic portal) - HubURL string `json:"hubUrl,omitempty"` - HubToken string `json:"hubToken,omitempty"` - ClientID string `json:"clientId,omitempty"` - ClientName string `json:"clientName,omitempty"` - - // Deprecated: use ForgeToken. Kept for migration. - GitHubToken string `json:"githubToken,omitempty"` - - // Repositories - WatchedRepos []string `json:"watchedRepos"` - Labels []string `json:"labels"` - - // Scheduling - WorkHours *WorkHours `json:"workHours,omitempty"` - FetchInterval int `json:"fetchIntervalMinutes"` - - // Notifications - NotificationsEnabled bool `json:"notificationsEnabled"` - NotificationSound bool `json:"notificationSound"` - - // Workspace - WorkspaceDir string `json:"workspaceDir,omitempty"` - DataDir string `json:"dataDir,omitempty"` - // Marketplace MCP - MarketplaceMCPRoot string `json:"marketplaceMcpRoot,omitempty"` - - // Onboarding - Onboarded bool `json:"onboarded"` - OnboardedAt time.Time `json:"onboardedAt,omitempty"` - - // UI Preferences - Theme string `json:"theme"` - ShowTrayPanel bool `json:"showTrayPanel"` - - // Advanced - MaxConcurrentIssues int `json:"maxConcurrentIssues"` - AutoSeedContext bool `json:"autoSeedContext"` - - // Workspace cache - MaxWorkspaces int `json:"maxWorkspaces"` // Upper bound on cached workspace entries (0 = default 100) - WorkspaceTTLMinutes int `json:"workspaceTtlMinutes"` // TTL for workspace entries in minutes (0 = default 1440 = 24h) - - // Updates - UpdateChannel string `json:"updateChannel"` // stable, beta, nightly - AutoUpdate bool `json:"autoUpdate"` // Automatically install updates - UpdateCheckInterval int `json:"updateCheckInterval"` // Check interval in hours (0 = disabled) - LastUpdateCheck time.Time `json:"lastUpdateCheck,omitempty"` -} - -// WorkHours defines when BugSETI should actively fetch issues. -type WorkHours struct { - Enabled bool `json:"enabled"` - StartHour int `json:"startHour"` // 0-23 - EndHour int `json:"endHour"` // 0-23 - Days []int `json:"days"` // 0=Sunday, 6=Saturday - Timezone string `json:"timezone"` -} - -// NewConfigService creates a new ConfigService with default values. -func NewConfigService() *ConfigService { - // Determine config path - configDir, err := os.UserConfigDir() - if err != nil { - configDir = filepath.Join(os.Getenv("HOME"), ".config") - } - - bugsetiDir := filepath.Join(configDir, "bugseti") - if err := os.MkdirAll(bugsetiDir, 0755); err != nil { - log.Printf("Warning: could not create config directory: %v", err) - } - - return &ConfigService{ - path: filepath.Join(bugsetiDir, "config.json"), - config: &Config{ - WatchedRepos: []string{}, - Labels: []string{ - "good first issue", - "help wanted", - "beginner-friendly", - }, - FetchInterval: 15, - NotificationsEnabled: true, - NotificationSound: true, - Theme: "dark", - ShowTrayPanel: true, - MaxConcurrentIssues: 1, - AutoSeedContext: true, - DataDir: bugsetiDir, - MarketplaceMCPRoot: "", - MaxWorkspaces: 100, - WorkspaceTTLMinutes: 1440, // 24 hours - UpdateChannel: "stable", - AutoUpdate: false, - UpdateCheckInterval: 6, // Check every 6 hours - }, - } -} - -// ServiceName returns the service name for Wails. -func (c *ConfigService) ServiceName() string { - return "ConfigService" -} - -// Load reads the configuration from disk. -func (c *ConfigService) Load() error { - c.mu.Lock() - defer c.mu.Unlock() - - data, err := os.ReadFile(c.path) - if err != nil { - if os.IsNotExist(err) { - // No config file yet, use defaults - return c.saveUnsafe() - } - return err - } - - var config Config - if err := json.Unmarshal(data, &config); err != nil { - return err - } - - // Merge with defaults for any new fields - c.mergeDefaults(&config) - c.config = &config - return nil -} - -// Save persists the configuration to disk. -func (c *ConfigService) Save() error { - c.mu.Lock() - defer c.mu.Unlock() - return c.saveUnsafe() -} - -// saveUnsafe writes config without acquiring lock. -func (c *ConfigService) saveUnsafe() error { - data, err := json.MarshalIndent(c.config, "", " ") - if err != nil { - return err - } - return os.WriteFile(c.path, data, 0600) -} - -// mergeDefaults fills in default values for any unset fields. -func (c *ConfigService) mergeDefaults(config *Config) { - if config.Labels == nil || len(config.Labels) == 0 { - config.Labels = c.config.Labels - } - if config.FetchInterval == 0 { - config.FetchInterval = 15 - } - if config.Theme == "" { - config.Theme = "dark" - } - if config.MaxConcurrentIssues == 0 { - config.MaxConcurrentIssues = 1 - } - if config.DataDir == "" { - config.DataDir = c.config.DataDir - } - if config.MaxWorkspaces == 0 { - config.MaxWorkspaces = 100 - } - if config.WorkspaceTTLMinutes == 0 { - config.WorkspaceTTLMinutes = 1440 - } - if config.UpdateChannel == "" { - config.UpdateChannel = "stable" - } - if config.UpdateCheckInterval == 0 { - config.UpdateCheckInterval = 6 - } -} - -// GetConfig returns a copy of the current configuration. -func (c *ConfigService) GetConfig() Config { - c.mu.RLock() - defer c.mu.RUnlock() - return *c.config -} - -// GetMarketplaceMCPRoot returns the configured marketplace MCP root path. -func (c *ConfigService) GetMarketplaceMCPRoot() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.MarketplaceMCPRoot -} - -// SetConfig updates the configuration and saves it. -func (c *ConfigService) SetConfig(config Config) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config = &config - return c.saveUnsafe() -} - -// GetWatchedRepos returns the list of watched repositories. -func (c *ConfigService) GetWatchedRepos() []string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.WatchedRepos -} - -// AddWatchedRepo adds a repository to the watch list. -func (c *ConfigService) AddWatchedRepo(repo string) error { - c.mu.Lock() - defer c.mu.Unlock() - - for _, r := range c.config.WatchedRepos { - if r == repo { - return nil // Already watching - } - } - - c.config.WatchedRepos = append(c.config.WatchedRepos, repo) - return c.saveUnsafe() -} - -// RemoveWatchedRepo removes a repository from the watch list. -func (c *ConfigService) RemoveWatchedRepo(repo string) error { - c.mu.Lock() - defer c.mu.Unlock() - - for i, r := range c.config.WatchedRepos { - if r == repo { - c.config.WatchedRepos = append(c.config.WatchedRepos[:i], c.config.WatchedRepos[i+1:]...) - return c.saveUnsafe() - } - } - - return nil -} - -// GetLabels returns the issue labels to filter by. -func (c *ConfigService) GetLabels() []string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.Labels -} - -// SetLabels updates the issue labels. -func (c *ConfigService) SetLabels(labels []string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.Labels = labels - return c.saveUnsafe() -} - -// GetFetchInterval returns the fetch interval as a duration. -func (c *ConfigService) GetFetchInterval() time.Duration { - c.mu.RLock() - defer c.mu.RUnlock() - return time.Duration(c.config.FetchInterval) * time.Minute -} - -// SetFetchInterval sets the fetch interval in minutes. -func (c *ConfigService) SetFetchInterval(minutes int) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.FetchInterval = minutes - return c.saveUnsafe() -} - -// IsWithinWorkHours checks if the current time is within configured work hours. -func (c *ConfigService) IsWithinWorkHours() bool { - c.mu.RLock() - defer c.mu.RUnlock() - - if c.config.WorkHours == nil || !c.config.WorkHours.Enabled { - return true // No work hours restriction - } - - wh := c.config.WorkHours - now := time.Now() - - // Check timezone - if wh.Timezone != "" { - loc, err := time.LoadLocation(wh.Timezone) - if err == nil { - now = now.In(loc) - } - } - - // Check day - day := int(now.Weekday()) - dayAllowed := false - for _, d := range wh.Days { - if d == day { - dayAllowed = true - break - } - } - if !dayAllowed { - return false - } - - // Check hour - hour := now.Hour() - if wh.StartHour <= wh.EndHour { - return hour >= wh.StartHour && hour < wh.EndHour - } - // Handle overnight (e.g., 22:00 - 06:00) - return hour >= wh.StartHour || hour < wh.EndHour -} - -// GetWorkHours returns the work hours configuration. -func (c *ConfigService) GetWorkHours() *WorkHours { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.WorkHours -} - -// SetWorkHours updates the work hours configuration. -func (c *ConfigService) SetWorkHours(wh *WorkHours) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.WorkHours = wh - return c.saveUnsafe() -} - -// IsNotificationsEnabled returns whether notifications are enabled. -func (c *ConfigService) IsNotificationsEnabled() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.NotificationsEnabled -} - -// SetNotificationsEnabled enables or disables notifications. -func (c *ConfigService) SetNotificationsEnabled(enabled bool) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.NotificationsEnabled = enabled - return c.saveUnsafe() -} - -// GetWorkspaceDir returns the workspace directory. -func (c *ConfigService) GetWorkspaceDir() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.WorkspaceDir -} - -// SetWorkspaceDir sets the workspace directory. -func (c *ConfigService) SetWorkspaceDir(dir string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.WorkspaceDir = dir - return c.saveUnsafe() -} - -// GetDataDir returns the data directory. -func (c *ConfigService) GetDataDir() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.DataDir -} - -// IsOnboarded returns whether the user has completed onboarding. -func (c *ConfigService) IsOnboarded() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.Onboarded -} - -// CompleteOnboarding marks onboarding as complete. -func (c *ConfigService) CompleteOnboarding() error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.Onboarded = true - c.config.OnboardedAt = time.Now() - return c.saveUnsafe() -} - -// GetTheme returns the current theme. -func (c *ConfigService) GetTheme() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.Theme -} - -// SetTheme sets the theme. -func (c *ConfigService) SetTheme(theme string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.Theme = theme - return c.saveUnsafe() -} - -// IsAutoSeedEnabled returns whether automatic context seeding is enabled. -func (c *ConfigService) IsAutoSeedEnabled() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.AutoSeedContext -} - -// SetAutoSeedEnabled enables or disables automatic context seeding. -func (c *ConfigService) SetAutoSeedEnabled(enabled bool) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.AutoSeedContext = enabled - return c.saveUnsafe() -} - -// GetMaxWorkspaces returns the maximum number of cached workspaces. -func (c *ConfigService) GetMaxWorkspaces() int { - c.mu.RLock() - defer c.mu.RUnlock() - if c.config.MaxWorkspaces <= 0 { - return 100 - } - return c.config.MaxWorkspaces -} - -// GetWorkspaceTTL returns the workspace TTL as a time.Duration. -func (c *ConfigService) GetWorkspaceTTL() time.Duration { - c.mu.RLock() - defer c.mu.RUnlock() - if c.config.WorkspaceTTLMinutes <= 0 { - return 24 * time.Hour - } - return time.Duration(c.config.WorkspaceTTLMinutes) * time.Minute -} - -// UpdateSettings holds update-related configuration. -type UpdateSettings struct { - Channel string `json:"channel"` - AutoUpdate bool `json:"autoUpdate"` - CheckInterval int `json:"checkInterval"` // Hours - LastCheck time.Time `json:"lastCheck"` -} - -// GetUpdateSettings returns the update settings. -func (c *ConfigService) GetUpdateSettings() UpdateSettings { - c.mu.RLock() - defer c.mu.RUnlock() - return UpdateSettings{ - Channel: c.config.UpdateChannel, - AutoUpdate: c.config.AutoUpdate, - CheckInterval: c.config.UpdateCheckInterval, - LastCheck: c.config.LastUpdateCheck, - } -} - -// SetUpdateSettings updates the update settings. -func (c *ConfigService) SetUpdateSettings(settings UpdateSettings) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.UpdateChannel = settings.Channel - c.config.AutoUpdate = settings.AutoUpdate - c.config.UpdateCheckInterval = settings.CheckInterval - return c.saveUnsafe() -} - -// GetUpdateChannel returns the update channel. -func (c *ConfigService) GetUpdateChannel() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.UpdateChannel -} - -// SetUpdateChannel sets the update channel. -func (c *ConfigService) SetUpdateChannel(channel string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.UpdateChannel = channel - return c.saveUnsafe() -} - -// IsAutoUpdateEnabled returns whether automatic updates are enabled. -func (c *ConfigService) IsAutoUpdateEnabled() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.AutoUpdate -} - -// SetAutoUpdateEnabled enables or disables automatic updates. -func (c *ConfigService) SetAutoUpdateEnabled(enabled bool) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.AutoUpdate = enabled - return c.saveUnsafe() -} - -// GetUpdateCheckInterval returns the update check interval in hours. -func (c *ConfigService) GetUpdateCheckInterval() int { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.UpdateCheckInterval -} - -// SetUpdateCheckInterval sets the update check interval in hours. -func (c *ConfigService) SetUpdateCheckInterval(hours int) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.UpdateCheckInterval = hours - return c.saveUnsafe() -} - -// GetLastUpdateCheck returns the last update check time. -func (c *ConfigService) GetLastUpdateCheck() time.Time { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.LastUpdateCheck -} - -// SetLastUpdateCheck sets the last update check time. -func (c *ConfigService) SetLastUpdateCheck(t time.Time) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.LastUpdateCheck = t - return c.saveUnsafe() -} - -// GetForgeURL returns the configured Forge URL (may be empty to use pkg/forge defaults). -func (c *ConfigService) GetForgeURL() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.ForgeURL -} - -// GetForgeToken returns the configured Forge token (may be empty to use pkg/forge defaults). -func (c *ConfigService) GetForgeToken() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.ForgeToken -} - -// SetForgeURL sets the Forge URL. -func (c *ConfigService) SetForgeURL(url string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.ForgeURL = url - return c.saveUnsafe() -} - -// SetForgeToken sets the Forge token. -func (c *ConfigService) SetForgeToken(token string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.ForgeToken = token - return c.saveUnsafe() -} - -// GetHubURL returns the configured Hub URL. -func (c *ConfigService) GetHubURL() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.HubURL -} - -// SetHubURL sets the Hub URL. -func (c *ConfigService) SetHubURL(url string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.HubURL = url - return c.saveUnsafe() -} - -// GetHubToken returns the configured Hub token. -func (c *ConfigService) GetHubToken() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.HubToken -} - -// SetHubToken sets the Hub token. -func (c *ConfigService) SetHubToken(token string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.HubToken = token - return c.saveUnsafe() -} - -// GetClientID returns the configured client ID. -func (c *ConfigService) GetClientID() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.ClientID -} - -// SetClientID sets the client ID. -func (c *ConfigService) SetClientID(id string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.ClientID = id - return c.saveUnsafe() -} - -// GetClientName returns the configured client name. -func (c *ConfigService) GetClientName() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.config.ClientName -} - -// SetClientName sets the client name. -func (c *ConfigService) SetClientName(name string) error { - c.mu.Lock() - defer c.mu.Unlock() - c.config.ClientName = name - return c.saveUnsafe() -} - -// ShouldCheckForUpdates returns true if it's time to check for updates. -func (c *ConfigService) ShouldCheckForUpdates() bool { - c.mu.RLock() - defer c.mu.RUnlock() - - if c.config.UpdateCheckInterval <= 0 { - return false // Updates disabled - } - - if c.config.LastUpdateCheck.IsZero() { - return true // Never checked - } - - interval := time.Duration(c.config.UpdateCheckInterval) * time.Hour - return time.Since(c.config.LastUpdateCheck) >= interval -} diff --git a/internal/bugseti/config_test.go b/internal/bugseti/config_test.go deleted file mode 100644 index 19ed143a..00000000 --- a/internal/bugseti/config_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package bugseti - -import ( - "os" - "testing" -) - -func TestConfigPermissions(t *testing.T) { - // Get a temporary file path - f, err := os.CreateTemp("", "bugseti-config-*.json") - if err != nil { - t.Fatal(err) - } - name := f.Name() - f.Close() - os.Remove(name) // Ensure it doesn't exist - defer os.Remove(name) - - c := &ConfigService{ - path: name, - config: &Config{}, - } - - if err := c.Save(); err != nil { - t.Fatalf("Save failed: %v", err) - } - - info, err := os.Stat(name) - if err != nil { - t.Fatal(err) - } - - mode := info.Mode().Perm() - if mode != 0600 { - t.Errorf("expected file permissions 0600, got %04o", mode) - } -} diff --git a/internal/bugseti/ethics_guard.go b/internal/bugseti/ethics_guard.go deleted file mode 100644 index 555ea138..00000000 --- a/internal/bugseti/ethics_guard.go +++ /dev/null @@ -1,252 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "bytes" - "context" - "encoding/xml" - "strings" - "sync" - "time" -) - -const ( - maxEnvRunes = 512 - maxTitleRunes = 160 - maxNotificationRunes = 200 - maxSummaryRunes = 4000 - maxBodyRunes = 8000 - maxFileRunes = 260 -) - -type EthicsGuard struct { - Modal string - Axioms map[string]any - Loaded bool -} - -var ( - ethicsGuardMu sync.Mutex - ethicsGuard *EthicsGuard - ethicsGuardRoot string -) - -func getEthicsGuard(ctx context.Context) *EthicsGuard { - return getEthicsGuardWithRoot(ctx, "") -} - -func getEthicsGuardWithRoot(ctx context.Context, rootHint string) *EthicsGuard { - rootHint = strings.TrimSpace(rootHint) - - ethicsGuardMu.Lock() - defer ethicsGuardMu.Unlock() - - if ethicsGuard != nil && ethicsGuardRoot == rootHint { - return ethicsGuard - } - - guard := loadEthicsGuard(ctx, rootHint) - if guard == nil { - guard = &EthicsGuard{} - } - - ethicsGuard = guard - ethicsGuardRoot = rootHint - if ethicsGuard == nil { - return &EthicsGuard{} - } - return ethicsGuard -} - -func guardFromMarketplace(ctx context.Context, client marketplaceClient) *EthicsGuard { - if client == nil { - return &EthicsGuard{} - } - if ctx == nil { - ctx = context.Background() - } - - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - ethics, err := client.EthicsCheck(ctx) - if err != nil || ethics == nil { - return &EthicsGuard{} - } - - return &EthicsGuard{ - Modal: ethics.Modal, - Axioms: ethics.Axioms, - Loaded: true, - } -} - -func loadEthicsGuard(ctx context.Context, rootHint string) *EthicsGuard { - if ctx == nil { - ctx = context.Background() - } - - ctx, cancel := context.WithTimeout(ctx, 2*time.Second) - defer cancel() - client, err := newMarketplaceClient(ctx, rootHint) - if err != nil { - return &EthicsGuard{} - } - defer client.Close() - - ethics, err := client.EthicsCheck(ctx) - if err != nil || ethics == nil { - return &EthicsGuard{} - } - - return &EthicsGuard{ - Modal: ethics.Modal, - Axioms: ethics.Axioms, - Loaded: true, - } -} - -func (g *EthicsGuard) SanitizeEnv(value string) string { - return stripShellMeta(sanitizeInline(value, maxEnvRunes)) -} - -// stripShellMeta removes shell metacharacters that could allow command -// injection when a value is interpolated inside a shell environment variable. -func stripShellMeta(s string) string { - var b strings.Builder - b.Grow(len(s)) - for _, r := range s { - switch r { - case '`', '$', ';', '|', '&', '(', ')', '{', '}', '<', '>', '!', '\\', '\'', '"', '\n', '\r': - continue - default: - b.WriteRune(r) - } - } - return strings.TrimSpace(b.String()) -} - -func (g *EthicsGuard) SanitizeTitle(value string) string { - return sanitizeInline(value, maxTitleRunes) -} - -func (g *EthicsGuard) SanitizeNotification(value string) string { - return sanitizeInline(value, maxNotificationRunes) -} - -func (g *EthicsGuard) SanitizeSummary(value string) string { - return sanitizeMultiline(value, maxSummaryRunes) -} - -func (g *EthicsGuard) SanitizeBody(value string) string { - return sanitizeMultiline(value, maxBodyRunes) -} - -func (g *EthicsGuard) SanitizeFiles(values []string) []string { - if len(values) == 0 { - return nil - } - - seen := make(map[string]bool) - clean := make([]string, 0, len(values)) - for _, value := range values { - trimmed := sanitizeInline(value, maxFileRunes) - if trimmed == "" { - continue - } - if strings.Contains(trimmed, "..") { - continue - } - if seen[trimmed] { - continue - } - seen[trimmed] = true - clean = append(clean, trimmed) - } - return clean -} - -func (g *EthicsGuard) SanitizeList(values []string, maxRunes int) []string { - if len(values) == 0 { - return nil - } - if maxRunes <= 0 { - maxRunes = maxTitleRunes - } - clean := make([]string, 0, len(values)) - for _, value := range values { - trimmed := sanitizeInline(value, maxRunes) - if trimmed == "" { - continue - } - clean = append(clean, trimmed) - } - return clean -} - -func sanitizeInline(input string, maxRunes int) string { - return sanitizeText(input, maxRunes, false) -} - -func sanitizeMultiline(input string, maxRunes int) string { - return sanitizeText(input, maxRunes, true) -} - -func sanitizeText(input string, maxRunes int, allowNewlines bool) string { - if input == "" { - return "" - } - if maxRunes <= 0 { - maxRunes = maxSummaryRunes - } - - var b strings.Builder - count := 0 - for _, r := range input { - if r == '\r' { - continue - } - if r == '\n' { - if allowNewlines { - b.WriteRune(r) - count++ - } else { - b.WriteRune(' ') - count++ - } - if count >= maxRunes { - break - } - continue - } - if r == '\t' { - b.WriteRune(' ') - count++ - if count >= maxRunes { - break - } - continue - } - if r < 0x20 || r == 0x7f { - continue - } - b.WriteRune(r) - count++ - if count >= maxRunes { - break - } - } - - return strings.TrimSpace(b.String()) -} - -func escapeAppleScript(value string) string { - value = strings.ReplaceAll(value, "\\", "\\\\") - value = strings.ReplaceAll(value, "\"", "\\\"") - return value -} - -func escapePowerShellXML(value string) string { - var buffer bytes.Buffer - _ = xml.EscapeText(&buffer, []byte(value)) - return buffer.String() -} diff --git a/internal/bugseti/ethics_guard_test.go b/internal/bugseti/ethics_guard_test.go deleted file mode 100644 index 4784160a..00000000 --- a/internal/bugseti/ethics_guard_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package bugseti - -import ( - "testing" -) - -func TestSanitizeInline_Good(t *testing.T) { - input := "Hello world" - output := sanitizeInline(input, 50) - if output != input { - t.Fatalf("expected %q, got %q", input, output) - } -} - -func TestSanitizeInline_Bad(t *testing.T) { - input := "Hello\nworld\t\x00" - expected := "Hello world" - output := sanitizeInline(input, 50) - if output != expected { - t.Fatalf("expected %q, got %q", expected, output) - } -} - -func TestSanitizeMultiline_Ugly(t *testing.T) { - input := "ab\ncd\tef\x00" - output := sanitizeMultiline(input, 5) - if output != "ab\ncd" { - t.Fatalf("expected %q, got %q", "ab\ncd", output) - } -} - -func TestSanitizeEnv_Good(t *testing.T) { - g := &EthicsGuard{} - input := "owner/repo-name" - output := g.SanitizeEnv(input) - if output != input { - t.Fatalf("expected %q, got %q", input, output) - } -} - -func TestSanitizeEnv_Bad(t *testing.T) { - g := &EthicsGuard{} - - tests := []struct { - name string - input string - expected string - }{ - {"backtick", "owner/repo`whoami`", "owner/repowhoami"}, - {"dollar", "owner/repo$(id)", "owner/repoid"}, - {"semicolon", "owner/repo;rm -rf /", "owner/reporm -rf /"}, - {"pipe", "owner/repo|cat /etc/passwd", "owner/repocat /etc/passwd"}, - {"ampersand", "owner/repo&&echo pwned", "owner/repoecho pwned"}, - {"mixed", "`$;|&(){}<>!\\'\"\n\r", ""}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - output := g.SanitizeEnv(tc.input) - if output != tc.expected { - t.Fatalf("expected %q, got %q", tc.expected, output) - } - }) - } -} - -func TestStripShellMeta_Ugly(t *testing.T) { - // All metacharacters should be stripped, leaving empty string - input := "`$;|&(){}<>!\\'\"" - output := stripShellMeta(input) - if output != "" { - t.Fatalf("expected empty string, got %q", output) - } -} diff --git a/internal/bugseti/fetcher.go b/internal/bugseti/fetcher.go deleted file mode 100644 index 4565b3dc..00000000 --- a/internal/bugseti/fetcher.go +++ /dev/null @@ -1,276 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "fmt" - "log" - "strings" - "sync" - "time" - - "forge.lthn.ai/core/go/pkg/forge" -) - -// FetcherService fetches issues from configured OSS repositories. -type FetcherService struct { - config *ConfigService - notify *NotifyService - forge *forge.Client - running bool - mu sync.RWMutex - stopCh chan struct{} - issuesCh chan []*Issue -} - -// NewFetcherService creates a new FetcherService. -func NewFetcherService(config *ConfigService, notify *NotifyService, forgeClient *forge.Client) *FetcherService { - return &FetcherService{ - config: config, - notify: notify, - forge: forgeClient, - issuesCh: make(chan []*Issue, 10), - } -} - -// ServiceName returns the service name for Wails. -func (f *FetcherService) ServiceName() string { - return "FetcherService" -} - -// Start begins fetching issues from configured repositories. -func (f *FetcherService) Start() error { - f.mu.Lock() - defer f.mu.Unlock() - - if f.running { - return nil - } - - f.running = true - f.stopCh = make(chan struct{}) - - go f.fetchLoop() - log.Println("FetcherService started") - return nil -} - -// Pause stops fetching issues. -func (f *FetcherService) Pause() { - f.mu.Lock() - defer f.mu.Unlock() - - if !f.running { - return - } - - f.running = false - close(f.stopCh) - log.Println("FetcherService paused") -} - -// IsRunning returns whether the fetcher is actively running. -func (f *FetcherService) IsRunning() bool { - f.mu.RLock() - defer f.mu.RUnlock() - return f.running -} - -// Issues returns a channel that receives batches of fetched issues. -func (f *FetcherService) Issues() <-chan []*Issue { - return f.issuesCh -} - -// fetchLoop periodically fetches issues from all configured repositories. -func (f *FetcherService) fetchLoop() { - // Initial fetch - f.fetchAll() - - // Set up ticker for periodic fetching - interval := f.config.GetFetchInterval() - if interval < time.Minute { - interval = 15 * time.Minute - } - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-f.stopCh: - return - case <-ticker.C: - // Check if within work hours - if f.config.IsWithinWorkHours() { - f.fetchAll() - } - } - } -} - -// fetchAll fetches issues from all configured repositories. -func (f *FetcherService) fetchAll() { - repos := f.config.GetWatchedRepos() - if len(repos) == 0 { - log.Println("No repositories configured") - return - } - - var allIssues []*Issue - for _, repo := range repos { - issues, err := f.fetchFromRepo(repo) - if err != nil { - log.Printf("Error fetching from %s: %v", repo, err) - continue - } - allIssues = append(allIssues, issues...) - } - - if len(allIssues) > 0 { - select { - case f.issuesCh <- allIssues: - f.notify.Notify("BugSETI", fmt.Sprintf("Found %d new issues", len(allIssues))) - default: - // Channel full, skip - } - } -} - -// fetchFromRepo fetches issues from a single repository using the Forgejo API. -func (f *FetcherService) fetchFromRepo(repo string) ([]*Issue, error) { - owner, repoName, err := splitRepo(repo) - if err != nil { - return nil, err - } - - labels := f.config.GetLabels() - if len(labels) == 0 { - labels = []string{"good first issue", "help wanted", "beginner-friendly"} - } - - forgeIssues, err := f.forge.ListIssues(owner, repoName, forge.ListIssuesOpts{ - State: "open", - Labels: labels, - Limit: 20, - }) - if err != nil { - return nil, fmt.Errorf("forge list issues failed: %w", err) - } - - issues := make([]*Issue, 0, len(forgeIssues)) - for _, fi := range forgeIssues { - labelNames := make([]string, len(fi.Labels)) - for i, l := range fi.Labels { - labelNames[i] = l.Name - } - - author := "" - if fi.Poster != nil { - author = fi.Poster.UserName - } - - issues = append(issues, &Issue{ - ID: fmt.Sprintf("%s#%d", repo, fi.Index), - Number: int(fi.Index), - Repo: repo, - Title: fi.Title, - Body: fi.Body, - URL: fi.HTMLURL, - Labels: labelNames, - Author: author, - CreatedAt: fi.Created, - Priority: calculatePriority(labelNames), - }) - } - - return issues, nil -} - -// FetchIssue fetches a single issue by repo and number. -func (f *FetcherService) FetchIssue(repo string, number int) (*Issue, error) { - owner, repoName, err := splitRepo(repo) - if err != nil { - return nil, err - } - - fi, err := f.forge.GetIssue(owner, repoName, int64(number)) - if err != nil { - return nil, fmt.Errorf("forge get issue failed: %w", err) - } - - labelNames := make([]string, len(fi.Labels)) - for i, l := range fi.Labels { - labelNames[i] = l.Name - } - - author := "" - if fi.Poster != nil { - author = fi.Poster.UserName - } - - // Fetch comments - forgeComments, err := f.forge.ListIssueComments(owner, repoName, int64(number)) - if err != nil { - log.Printf("Warning: could not fetch comments for %s#%d: %v", repo, number, err) - } - - comments := make([]Comment, 0, len(forgeComments)) - for _, c := range forgeComments { - commentAuthor := "" - if c.Poster != nil { - commentAuthor = c.Poster.UserName - } - comments = append(comments, Comment{ - Author: commentAuthor, - Body: c.Body, - }) - } - - return &Issue{ - ID: fmt.Sprintf("%s#%d", repo, fi.Index), - Number: int(fi.Index), - Repo: repo, - Title: fi.Title, - Body: fi.Body, - URL: fi.HTMLURL, - Labels: labelNames, - Author: author, - CreatedAt: fi.Created, - Priority: calculatePriority(labelNames), - Comments: comments, - }, nil -} - -// splitRepo splits "owner/repo" into owner and repo parts. -func splitRepo(repo string) (string, string, error) { - parts := strings.SplitN(repo, "/", 2) - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid repo format %q, expected owner/repo", repo) - } - return parts[0], parts[1], nil -} - -// calculatePriority assigns a priority score based on labels. -func calculatePriority(labels []string) int { - priority := 50 // Default priority - - for _, label := range labels { - lower := strings.ToLower(label) - switch { - case strings.Contains(lower, "good first issue"): - priority += 30 - case strings.Contains(lower, "help wanted"): - priority += 20 - case strings.Contains(lower, "beginner"): - priority += 25 - case strings.Contains(lower, "easy"): - priority += 20 - case strings.Contains(lower, "bug"): - priority += 10 - case strings.Contains(lower, "documentation"): - priority += 5 - case strings.Contains(lower, "priority"): - priority += 15 - } - } - - return priority -} diff --git a/internal/bugseti/fetcher_test.go b/internal/bugseti/fetcher_test.go deleted file mode 100644 index 2fdc1982..00000000 --- a/internal/bugseti/fetcher_test.go +++ /dev/null @@ -1,407 +0,0 @@ -package bugseti - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// testConfigService creates a ConfigService with in-memory config for testing. -func testConfigService(t *testing.T, repos []string, labels []string) *ConfigService { - t.Helper() - dir := t.TempDir() - cs := &ConfigService{ - path: dir + "/config.json", - config: &Config{ - WatchedRepos: repos, - Labels: labels, - FetchInterval: 15, - DataDir: dir, - }, - } - return cs -} - -// TestHelperProcess is invoked by the test binary when GO_TEST_HELPER_PROCESS -// is set. It prints the value of GO_TEST_HELPER_OUTPUT and optionally exits -// with a non-zero code. Kept for future exec.Command mocking. -func TestHelperProcess(t *testing.T) { - if os.Getenv("GO_TEST_HELPER_PROCESS") != "1" { - return - } - fmt.Fprint(os.Stdout, os.Getenv("GO_TEST_HELPER_OUTPUT")) - if os.Getenv("GO_TEST_HELPER_EXIT_ERROR") == "1" { - os.Exit(1) - } - os.Exit(0) -} - -// ---- NewFetcherService ---- - -func TestNewFetcherService_Good(t *testing.T) { - cfg := testConfigService(t, nil, nil) - notify := NewNotifyService(cfg) - f := NewFetcherService(cfg, notify, nil) - - require.NotNil(t, f) - assert.Equal(t, "FetcherService", f.ServiceName()) - assert.False(t, f.IsRunning()) - assert.NotNil(t, f.Issues()) -} - -// ---- Start / Pause / IsRunning lifecycle ---- - -func TestStartPause_Good(t *testing.T) { - cfg := testConfigService(t, nil, nil) - notify := NewNotifyService(cfg) - f := NewFetcherService(cfg, notify, nil) - - require.NoError(t, f.Start()) - assert.True(t, f.IsRunning()) - - // Starting again is a no-op. - require.NoError(t, f.Start()) - assert.True(t, f.IsRunning()) - - f.Pause() - assert.False(t, f.IsRunning()) - - // Pausing again is a no-op. - f.Pause() - assert.False(t, f.IsRunning()) -} - -// ---- calculatePriority ---- - -func TestCalculatePriority_Good(t *testing.T) { - tests := []struct { - name string - labels []string - expected int - }{ - {"no labels", nil, 50}, - {"good first issue", []string{"good first issue"}, 80}, - {"help wanted", []string{"Help Wanted"}, 70}, - {"beginner", []string{"beginner-friendly"}, 75}, - {"easy", []string{"Easy"}, 70}, - {"bug", []string{"bug"}, 60}, - {"documentation", []string{"Documentation"}, 55}, - {"priority", []string{"high-priority"}, 65}, - {"combined", []string{"good first issue", "bug"}, 90}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, calculatePriority(tt.labels)) - }) - } -} - -func TestCalculatePriority_Bad(t *testing.T) { - // Unknown labels should not change priority from default. - assert.Equal(t, 50, calculatePriority([]string{"unknown-label", "something-else"})) -} - -// ---- Label query construction ---- - -func TestLabelQuery_Good(t *testing.T) { - // When config has custom labels, fetchFromRepo should use them. - cfg := testConfigService(t, []string{"owner/repo"}, []string{"custom-label", "another"}) - labels := cfg.GetLabels() - labelQuery := strings.Join(labels, ",") - assert.Equal(t, "custom-label,another", labelQuery) -} - -func TestLabelQuery_Bad(t *testing.T) { - // When config has empty labels, fetchFromRepo falls back to defaults. - cfg := testConfigService(t, []string{"owner/repo"}, nil) - labels := cfg.GetLabels() - if len(labels) == 0 { - labels = []string{"good first issue", "help wanted", "beginner-friendly"} - } - labelQuery := strings.Join(labels, ",") - assert.Equal(t, "good first issue,help wanted,beginner-friendly", labelQuery) -} - -// ---- fetchFromRepo with mocked gh CLI output ---- - -func TestFetchFromRepo_Good(t *testing.T) { - ghIssues := []struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body"` - URL string `json:"url"` - CreatedAt time.Time `json:"createdAt"` - Author struct { - Login string `json:"login"` - } `json:"author"` - Labels []struct { - Name string `json:"name"` - } `json:"labels"` - }{ - { - Number: 42, - Title: "Fix login bug", - Body: "The login page crashes", - URL: "https://github.com/test/repo/issues/42", - CreatedAt: time.Date(2026, 1, 15, 10, 0, 0, 0, time.UTC), - }, - } - ghIssues[0].Author.Login = "octocat" - ghIssues[0].Labels = []struct { - Name string `json:"name"` - }{ - {Name: "good first issue"}, - {Name: "bug"}, - } - - output, err := json.Marshal(ghIssues) - require.NoError(t, err) - - // We can't easily intercept exec.CommandContext in the production code - // without refactoring, so we test the JSON parsing path by directly - // calling json.Unmarshal the same way fetchFromRepo does. - var parsed []struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body"` - URL string `json:"url"` - CreatedAt time.Time `json:"createdAt"` - Author struct { - Login string `json:"login"` - } `json:"author"` - Labels []struct { - Name string `json:"name"` - } `json:"labels"` - } - require.NoError(t, json.Unmarshal(output, &parsed)) - require.Len(t, parsed, 1) - - gi := parsed[0] - labels := make([]string, len(gi.Labels)) - for i, l := range gi.Labels { - labels[i] = l.Name - } - - issue := &Issue{ - ID: fmt.Sprintf("%s#%d", "test/repo", gi.Number), - Number: gi.Number, - Repo: "test/repo", - Title: gi.Title, - Body: gi.Body, - URL: gi.URL, - Labels: labels, - Author: gi.Author.Login, - CreatedAt: gi.CreatedAt, - Priority: calculatePriority(labels), - } - - assert.Equal(t, "test/repo#42", issue.ID) - assert.Equal(t, 42, issue.Number) - assert.Equal(t, "Fix login bug", issue.Title) - assert.Equal(t, "octocat", issue.Author) - assert.Equal(t, []string{"good first issue", "bug"}, issue.Labels) - assert.Equal(t, 90, issue.Priority) // 50 + 30 (good first issue) + 10 (bug) -} - -func TestFetchFromRepo_Bad_InvalidJSON(t *testing.T) { - // Simulate gh returning invalid JSON. - var ghIssues []struct { - Number int `json:"number"` - } - err := json.Unmarshal([]byte(`not json at all`), &ghIssues) - assert.Error(t, err, "invalid JSON should produce an error") -} - -func TestFetchFromRepo_Bad_GhNotInstalled(t *testing.T) { - // Verify that a missing executable produces an exec error. - cmd := exec.Command("gh-nonexistent-binary-12345") - _, err := cmd.Output() - assert.Error(t, err, "missing binary should produce an error") -} - -// ---- fetchAll: no repos configured ---- - -func TestFetchAll_Bad_NoRepos(t *testing.T) { - cfg := testConfigService(t, nil, nil) - notify := NewNotifyService(cfg) - f := NewFetcherService(cfg, notify, nil) - - // fetchAll with no repos should not panic and should not send to channel. - f.fetchAll() - - // Channel should be empty. - select { - case <-f.issuesCh: - t.Fatal("expected no issues on channel when no repos configured") - default: - // expected - } -} - -// ---- Channel backpressure ---- - -func TestChannelBackpressure_Ugly(t *testing.T) { - cfg := testConfigService(t, nil, nil) - notify := NewNotifyService(cfg) - f := NewFetcherService(cfg, notify, nil) - - // Fill the channel to capacity (buffer size is 10). - for i := 0; i < 10; i++ { - f.issuesCh <- []*Issue{{ID: fmt.Sprintf("test#%d", i)}} - } - - // Now try to send via the select path (same logic as fetchAll). - // This should be a non-blocking drop, not a deadlock. - done := make(chan struct{}) - go func() { - defer close(done) - issues := []*Issue{{ID: "overflow#1"}} - select { - case f.issuesCh <- issues: - // Shouldn't happen — channel is full. - t.Error("expected channel send to be skipped due to backpressure") - default: - // This is the expected path — channel full, message dropped. - } - }() - - select { - case <-done: - // success — did not deadlock - case <-time.After(time.Second): - t.Fatal("backpressure test timed out — possible deadlock") - } -} - -// ---- FetchIssue single-issue parsing ---- - -func TestFetchIssue_Good_Parse(t *testing.T) { - // Test the JSON parsing and Issue construction for FetchIssue. - ghIssue := struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body"` - URL string `json:"url"` - CreatedAt time.Time `json:"createdAt"` - Author struct { - Login string `json:"login"` - } `json:"author"` - Labels []struct { - Name string `json:"name"` - } `json:"labels"` - Comments []struct { - Body string `json:"body"` - Author struct { - Login string `json:"login"` - } `json:"author"` - } `json:"comments"` - }{ - Number: 99, - Title: "Add dark mode", - Body: "Please add dark mode support", - URL: "https://github.com/test/repo/issues/99", - CreatedAt: time.Date(2026, 2, 1, 12, 0, 0, 0, time.UTC), - } - ghIssue.Author.Login = "contributor" - ghIssue.Labels = []struct { - Name string `json:"name"` - }{ - {Name: "help wanted"}, - } - ghIssue.Comments = []struct { - Body string `json:"body"` - Author struct { - Login string `json:"login"` - } `json:"author"` - }{ - {Body: "I can work on this"}, - } - ghIssue.Comments[0].Author.Login = "volunteer" - - data, err := json.Marshal(ghIssue) - require.NoError(t, err) - - // Re-parse as the function would. - var parsed struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body"` - URL string `json:"url"` - CreatedAt time.Time `json:"createdAt"` - Author struct { - Login string `json:"login"` - } `json:"author"` - Labels []struct { - Name string `json:"name"` - } `json:"labels"` - Comments []struct { - Body string `json:"body"` - Author struct { - Login string `json:"login"` - } `json:"author"` - } `json:"comments"` - } - require.NoError(t, json.Unmarshal(data, &parsed)) - - labels := make([]string, len(parsed.Labels)) - for i, l := range parsed.Labels { - labels[i] = l.Name - } - comments := make([]Comment, len(parsed.Comments)) - for i, c := range parsed.Comments { - comments[i] = Comment{Author: c.Author.Login, Body: c.Body} - } - - issue := &Issue{ - ID: fmt.Sprintf("%s#%d", "test/repo", parsed.Number), - Number: parsed.Number, - Repo: "test/repo", - Title: parsed.Title, - Body: parsed.Body, - URL: parsed.URL, - Labels: labels, - Author: parsed.Author.Login, - CreatedAt: parsed.CreatedAt, - Priority: calculatePriority(labels), - Comments: comments, - } - - assert.Equal(t, "test/repo#99", issue.ID) - assert.Equal(t, "contributor", issue.Author) - assert.Equal(t, 70, issue.Priority) // 50 + 20 (help wanted) - require.Len(t, issue.Comments, 1) - assert.Equal(t, "volunteer", issue.Comments[0].Author) - assert.Equal(t, "I can work on this", issue.Comments[0].Body) -} - -// ---- Issues() channel accessor ---- - -func TestIssuesChannel_Good(t *testing.T) { - cfg := testConfigService(t, nil, nil) - notify := NewNotifyService(cfg) - f := NewFetcherService(cfg, notify, nil) - - ch := f.Issues() - require.NotNil(t, ch) - - // Send and receive through the channel. - go func() { - f.issuesCh <- []*Issue{{ID: "test#1", Title: "Test issue"}} - }() - - select { - case issues := <-ch: - require.Len(t, issues, 1) - assert.Equal(t, "test#1", issues[0].ID) - case <-time.After(time.Second): - t.Fatal("timed out waiting for issues on channel") - } -} diff --git a/internal/bugseti/ghcheck.go b/internal/bugseti/ghcheck.go deleted file mode 100644 index bf33f3e9..00000000 --- a/internal/bugseti/ghcheck.go +++ /dev/null @@ -1,22 +0,0 @@ -package bugseti - -import ( - "forge.lthn.ai/core/go/pkg/forge" -) - -// CheckForge verifies that the Forgejo API is configured and reachable. -// Returns nil if a token is configured and the API responds, or an error -// with actionable instructions for the user. -func CheckForge() (*forge.Client, error) { - client, err := forge.NewFromConfig("", "") - if err != nil { - return nil, err - } - - // Verify the token works by fetching the current user - if _, err := client.GetCurrentUser(); err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/bugseti/ghcheck_test.go b/internal/bugseti/ghcheck_test.go deleted file mode 100644 index b2fc10dc..00000000 --- a/internal/bugseti/ghcheck_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package bugseti - -import ( - "os" - "testing" -) - -func TestCheckForge_Bad_MissingConfig(t *testing.T) { - // Clear any env-based forge config to ensure CheckForge fails - t.Setenv("FORGE_TOKEN", "") - t.Setenv("FORGE_URL", "") - - // Point HOME to a temp dir so no config file is found - t.Setenv("HOME", t.TempDir()) - if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { - t.Setenv("XDG_CONFIG_HOME", t.TempDir()) - } - - _, err := CheckForge() - if err == nil { - t.Fatal("expected error when forge is not configured") - } -} diff --git a/internal/bugseti/go.mod b/internal/bugseti/go.mod deleted file mode 100644 index 62eaa655..00000000 --- a/internal/bugseti/go.mod +++ /dev/null @@ -1,32 +0,0 @@ -module forge.lthn.ai/core/cli/internal/bugseti - -go 1.25.5 - -require ( - codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 - github.com/mark3labs/mcp-go v0.43.2 - github.com/stretchr/testify v1.11.1 -) - -require ( - github.com/42wim/httpsig v1.2.3 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/go-fed/httpsig v1.1.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/internal/bugseti/go.sum b/internal/bugseti/go.sum deleted file mode 100644 index 1c248236..00000000 --- a/internal/bugseti/go.sum +++ /dev/null @@ -1,39 +0,0 @@ -codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= -github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bugseti/hub.go b/internal/bugseti/hub.go deleted file mode 100644 index 664c1189..00000000 --- a/internal/bugseti/hub.go +++ /dev/null @@ -1,576 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "bytes" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - "path/filepath" - "runtime" - "sync" - "time" - - "forge.lthn.ai/core/go/pkg/forge" -) - -// HubService coordinates with the agentic portal for issue assignment and leaderboard. -type HubService struct { - config *ConfigService - client *http.Client - connected bool - pending []PendingOp - mu sync.RWMutex -} - -// PendingOp represents an operation queued for retry when the hub is unreachable. -type PendingOp struct { - Method string `json:"method"` - Path string `json:"path"` - Body json.RawMessage `json:"body,omitempty"` - CreatedAt time.Time `json:"createdAt"` -} - -// HubClaim represents a claimed issue from the hub. -type HubClaim struct { - ID string `json:"id"` - IssueURL string `json:"issueUrl"` - ClientID string `json:"clientId"` - ClaimedAt time.Time `json:"claimedAt"` - ExpiresAt time.Time `json:"expiresAt"` - Status string `json:"status"` -} - -// LeaderboardEntry represents a single entry on the leaderboard. -type LeaderboardEntry struct { - ClientID string `json:"clientId"` - ClientName string `json:"clientName"` - Score int `json:"score"` - PRsMerged int `json:"prsMerged"` - Rank int `json:"rank"` -} - -// GlobalStats holds aggregate statistics from the hub. -type GlobalStats struct { - TotalClients int `json:"totalClients"` - TotalClaims int `json:"totalClaims"` - TotalPRsMerged int `json:"totalPrsMerged"` - ActiveClaims int `json:"activeClaims"` - IssuesAvailable int `json:"issuesAvailable"` -} - -// ConflictError indicates a 409 response from the hub (e.g. issue already claimed). -type ConflictError struct { - StatusCode int -} - -func (e *ConflictError) Error() string { - return fmt.Sprintf("conflict: status %d", e.StatusCode) -} - -// NotFoundError indicates a 404 response from the hub. -type NotFoundError struct { - StatusCode int -} - -func (e *NotFoundError) Error() string { - return fmt.Sprintf("not found: status %d", e.StatusCode) -} - -// NewHubService creates a new HubService with the given config. -// If the config has no ClientID, one is generated and persisted. -func NewHubService(config *ConfigService) *HubService { - h := &HubService{ - config: config, - client: &http.Client{ - Timeout: 10 * time.Second, - }, - pending: make([]PendingOp, 0), - } - - // Generate client ID if not set. - if config.GetClientID() == "" { - id := generateClientID() - _ = config.SetClientID(id) - } - - h.loadPendingOps() - - return h -} - -// ServiceName returns the service name for Wails. -func (h *HubService) ServiceName() string { - return "HubService" -} - -// GetClientID returns the client ID from config. -func (h *HubService) GetClientID() string { - return h.config.GetClientID() -} - -// IsConnected returns whether the hub was reachable on the last request. -func (h *HubService) IsConnected() bool { - h.mu.RLock() - defer h.mu.RUnlock() - return h.connected -} - -// generateClientID creates a random hex string (16 bytes = 32 hex chars). -func generateClientID() string { - b := make([]byte, 16) - if _, err := rand.Read(b); err != nil { - // Fallback: this should never happen with crypto/rand. - return fmt.Sprintf("fallback-%d", time.Now().UnixNano()) - } - return hex.EncodeToString(b) -} - -// doRequest builds and executes an HTTP request against the hub API. -// It returns the raw *http.Response and any transport-level error. -func (h *HubService) doRequest(method, path string, body interface{}) (*http.Response, error) { - hubURL := h.config.GetHubURL() - if hubURL == "" { - return nil, fmt.Errorf("hub URL not configured") - } - - fullURL := hubURL + "/api/bugseti" + path - - var bodyReader io.Reader - if body != nil { - data, err := json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("marshal request body: %w", err) - } - bodyReader = bytes.NewReader(data) - } - - req, err := http.NewRequest(method, fullURL, bodyReader) - if err != nil { - return nil, fmt.Errorf("build request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - token := h.config.GetHubToken() - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - - resp, err := h.client.Do(req) - if err != nil { - h.mu.Lock() - h.connected = false - h.mu.Unlock() - return nil, err - } - - h.mu.Lock() - h.connected = true - h.mu.Unlock() - - return resp, nil -} - -// doJSON executes an HTTP request and decodes the JSON response into dest. -// It handles common error status codes with typed errors. -func (h *HubService) doJSON(method, path string, body, dest interface{}) error { - resp, err := h.doRequest(method, path, body) - if err != nil { - return err - } - defer resp.Body.Close() - - switch { - case resp.StatusCode == http.StatusUnauthorized: - return fmt.Errorf("unauthorised") - case resp.StatusCode == http.StatusConflict: - return &ConflictError{StatusCode: resp.StatusCode} - case resp.StatusCode == http.StatusNotFound: - return &NotFoundError{StatusCode: resp.StatusCode} - case resp.StatusCode >= 400: - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("hub error %d: %s", resp.StatusCode, string(respBody)) - } - - if dest != nil { - if err := json.NewDecoder(resp.Body).Decode(dest); err != nil { - return fmt.Errorf("decode response: %w", err) - } - } - - return nil -} - -// queueOp marshals body to JSON and appends a PendingOp to the queue. -func (h *HubService) queueOp(method, path string, body interface{}) { - var raw json.RawMessage - if body != nil { - data, err := json.Marshal(body) - if err != nil { - log.Printf("BugSETI: queueOp marshal error: %v", err) - return - } - raw = data - } - - h.mu.Lock() - h.pending = append(h.pending, PendingOp{ - Method: method, - Path: path, - Body: raw, - CreatedAt: time.Now(), - }) - h.mu.Unlock() - - h.savePendingOps() -} - -// drainPendingOps replays queued operations against the hub. -// 5xx/transport errors are kept for retry; 4xx responses are dropped (stale). -func (h *HubService) drainPendingOps() { - h.mu.Lock() - ops := h.pending - h.pending = make([]PendingOp, 0) - h.mu.Unlock() - - if len(ops) == 0 { - return - } - - var failed []PendingOp - for _, op := range ops { - var body interface{} - if len(op.Body) > 0 { - body = json.RawMessage(op.Body) - } - - resp, err := h.doRequest(op.Method, op.Path, body) - if err != nil { - // Transport error — keep for retry. - failed = append(failed, op) - continue - } - resp.Body.Close() - - if resp.StatusCode >= 500 { - // Server error — keep for retry. - failed = append(failed, op) - } // 4xx are dropped (stale). - } - - if len(failed) > 0 { - h.mu.Lock() - h.pending = append(failed, h.pending...) - h.mu.Unlock() - } - - h.savePendingOps() -} - -// savePendingOps persists the pending operations queue to disk. -func (h *HubService) savePendingOps() { - dataDir := h.config.GetDataDir() - if dataDir == "" { - return - } - - h.mu.RLock() - data, err := json.Marshal(h.pending) - h.mu.RUnlock() - if err != nil { - log.Printf("BugSETI: savePendingOps marshal error: %v", err) - return - } - - path := filepath.Join(dataDir, "hub_pending.json") - if err := os.WriteFile(path, data, 0600); err != nil { - log.Printf("BugSETI: savePendingOps write error: %v", err) - } -} - -// loadPendingOps loads the pending operations queue from disk. -// Errors are silently ignored (the file may not exist yet). -func (h *HubService) loadPendingOps() { - dataDir := h.config.GetDataDir() - if dataDir == "" { - return - } - - path := filepath.Join(dataDir, "hub_pending.json") - data, err := os.ReadFile(path) - if err != nil { - return - } - - var ops []PendingOp - if err := json.Unmarshal(data, &ops); err != nil { - return - } - h.pending = ops -} - -// PendingCount returns the number of queued pending operations. -func (h *HubService) PendingCount() int { - h.mu.RLock() - defer h.mu.RUnlock() - return len(h.pending) -} - -// ---- Task 4: Auto-Register via Forge Token ---- - -// AutoRegister exchanges a Forge API token for a hub API key. -// If a hub token is already configured, this is a no-op. -func (h *HubService) AutoRegister() error { - // Skip if already registered. - if h.config.GetHubToken() != "" { - return nil - } - - hubURL := h.config.GetHubURL() - if hubURL == "" { - return fmt.Errorf("hub URL not configured") - } - - // Resolve forge credentials from config/env. - forgeURL := h.config.GetForgeURL() - forgeToken := h.config.GetForgeToken() - if forgeToken == "" { - resolvedURL, resolvedToken, err := forge.ResolveConfig(forgeURL, "") - if err != nil { - return fmt.Errorf("resolve forge config: %w", err) - } - forgeURL = resolvedURL - forgeToken = resolvedToken - } - - if forgeToken == "" { - return fmt.Errorf("no forge token available (set FORGE_TOKEN or run: core forge config --token TOKEN)") - } - - // Build request body. - payload := map[string]string{ - "forge_url": forgeURL, - "forge_token": forgeToken, - "client_id": h.config.GetClientID(), - } - data, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("marshal auto-register body: %w", err) - } - - // POST directly (no bearer token yet). - resp, err := h.client.Post(hubURL+"/api/bugseti/auth/forge", "application/json", bytes.NewReader(data)) - if err != nil { - h.mu.Lock() - h.connected = false - h.mu.Unlock() - return fmt.Errorf("auto-register request: %w", err) - } - defer resp.Body.Close() - - h.mu.Lock() - h.connected = true - h.mu.Unlock() - - if resp.StatusCode >= 400 { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("auto-register failed %d: %s", resp.StatusCode, string(respBody)) - } - - var result struct { - APIKey string `json:"api_key"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return fmt.Errorf("decode auto-register response: %w", err) - } - - if err := h.config.SetHubToken(result.APIKey); err != nil { - return fmt.Errorf("cache hub token: %w", err) - } - - log.Printf("BugSETI: auto-registered with hub, token cached") - return nil -} - -// ---- Task 5: Write Operations ---- - -// Register registers this client with the hub. -func (h *HubService) Register() error { - h.drainPendingOps() - - name := h.config.GetClientName() - clientID := h.config.GetClientID() - if name == "" { - if len(clientID) >= 8 { - name = "BugSETI-" + clientID[:8] - } else { - name = "BugSETI-" + clientID - } - } - - body := map[string]string{ - "client_id": clientID, - "name": name, - "version": GetVersion(), - "os": runtime.GOOS, - "arch": runtime.GOARCH, - } - - return h.doJSON("POST", "/register", body, nil) -} - -// Heartbeat sends a heartbeat to the hub. -func (h *HubService) Heartbeat() error { - body := map[string]string{ - "client_id": h.config.GetClientID(), - } - return h.doJSON("POST", "/heartbeat", body, nil) -} - -// ClaimIssue claims an issue on the hub, returning the claim details. -// Returns a ConflictError if the issue is already claimed by another client. -func (h *HubService) ClaimIssue(issue *Issue) (*HubClaim, error) { - h.drainPendingOps() - - body := map[string]interface{}{ - "client_id": h.config.GetClientID(), - "issue_id": issue.ID, - "repo": issue.Repo, - "issue_number": issue.Number, - "title": issue.Title, - "url": issue.URL, - } - - var claim HubClaim - if err := h.doJSON("POST", "/issues/claim", body, &claim); err != nil { - return nil, err - } - return &claim, nil -} - -// UpdateStatus updates the status of a claimed issue on the hub. -func (h *HubService) UpdateStatus(issueID, status, prURL string, prNumber int) error { - body := map[string]interface{}{ - "client_id": h.config.GetClientID(), - "status": status, - } - if prURL != "" { - body["pr_url"] = prURL - } - if prNumber > 0 { - body["pr_number"] = prNumber - } - - path := "/issues/" + url.PathEscape(issueID) + "/status" - return h.doJSON("PATCH", path, body, nil) -} - -// ReleaseClaim releases a previously claimed issue back to the pool. -func (h *HubService) ReleaseClaim(issueID string) error { - body := map[string]string{ - "client_id": h.config.GetClientID(), - } - - path := "/issues/" + url.PathEscape(issueID) + "/claim" - return h.doJSON("DELETE", path, body, nil) -} - -// SyncStats uploads local statistics to the hub. -func (h *HubService) SyncStats(stats *Stats) error { - // Build repos_contributed as a flat string slice from the map keys. - repos := make([]string, 0, len(stats.ReposContributed)) - for k := range stats.ReposContributed { - repos = append(repos, k) - } - - body := map[string]interface{}{ - "client_id": h.config.GetClientID(), - "stats": map[string]interface{}{ - "issues_attempted": stats.IssuesAttempted, - "issues_completed": stats.IssuesCompleted, - "issues_skipped": stats.IssuesSkipped, - "prs_submitted": stats.PRsSubmitted, - "prs_merged": stats.PRsMerged, - "prs_rejected": stats.PRsRejected, - "current_streak": stats.CurrentStreak, - "longest_streak": stats.LongestStreak, - "total_time_minutes": int(stats.TotalTimeSpent.Minutes()), - "repos_contributed": repos, - }, - } - - return h.doJSON("POST", "/stats/sync", body, nil) -} - -// ---- Task 6: Read Operations ---- - -// IsIssueClaimed checks whether an issue is currently claimed on the hub. -// Returns the claim if it exists, or (nil, nil) if the issue is not claimed (404). -func (h *HubService) IsIssueClaimed(issueID string) (*HubClaim, error) { - path := "/issues/" + url.PathEscape(issueID) - - var claim HubClaim - if err := h.doJSON("GET", path, nil, &claim); err != nil { - if _, ok := err.(*NotFoundError); ok { - return nil, nil - } - return nil, err - } - return &claim, nil -} - -// ListClaims returns claimed issues, optionally filtered by status and/or repo. -func (h *HubService) ListClaims(status, repo string) ([]*HubClaim, error) { - params := url.Values{} - if status != "" { - params.Set("status", status) - } - if repo != "" { - params.Set("repo", repo) - } - - path := "/issues/claimed" - if encoded := params.Encode(); encoded != "" { - path += "?" + encoded - } - - var claims []*HubClaim - if err := h.doJSON("GET", path, nil, &claims); err != nil { - return nil, err - } - return claims, nil -} - -// leaderboardResponse wraps the hub leaderboard JSON envelope. -type leaderboardResponse struct { - Entries []LeaderboardEntry `json:"entries"` - TotalParticipants int `json:"totalParticipants"` -} - -// GetLeaderboard fetches the top N leaderboard entries from the hub. -func (h *HubService) GetLeaderboard(limit int) ([]LeaderboardEntry, int, error) { - path := fmt.Sprintf("/leaderboard?limit=%d", limit) - - var resp leaderboardResponse - if err := h.doJSON("GET", path, nil, &resp); err != nil { - return nil, 0, err - } - return resp.Entries, resp.TotalParticipants, nil -} - -// GetGlobalStats fetches aggregate statistics from the hub. -func (h *HubService) GetGlobalStats() (*GlobalStats, error) { - var stats GlobalStats - if err := h.doJSON("GET", "/stats", nil, &stats); err != nil { - return nil, err - } - return &stats, nil -} diff --git a/internal/bugseti/hub_test.go b/internal/bugseti/hub_test.go deleted file mode 100644 index e5236daf..00000000 --- a/internal/bugseti/hub_test.go +++ /dev/null @@ -1,558 +0,0 @@ -package bugseti - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func testHubService(t *testing.T, serverURL string) *HubService { - t.Helper() - cfg := testConfigService(t, nil, nil) - if serverURL != "" { - cfg.config.HubURL = serverURL - } - return NewHubService(cfg) -} - -// ---- NewHubService ---- - -func TestNewHubService_Good(t *testing.T) { - h := testHubService(t, "") - require.NotNil(t, h) - assert.NotNil(t, h.config) - assert.NotNil(t, h.client) - assert.False(t, h.IsConnected()) -} - -func TestHubServiceName_Good(t *testing.T) { - h := testHubService(t, "") - assert.Equal(t, "HubService", h.ServiceName()) -} - -func TestNewHubService_Good_GeneratesClientID(t *testing.T) { - h := testHubService(t, "") - id := h.GetClientID() - assert.NotEmpty(t, id) - // 16 bytes = 32 hex characters - assert.Len(t, id, 32) -} - -func TestNewHubService_Good_ReusesClientID(t *testing.T) { - cfg := testConfigService(t, nil, nil) - cfg.config.ClientID = "existing-client-id" - - h := NewHubService(cfg) - assert.Equal(t, "existing-client-id", h.GetClientID()) -} - -// ---- doRequest ---- - -func TestDoRequest_Good(t *testing.T) { - var gotAuth string - var gotContentType string - var gotAccept string - var gotBody map[string]string - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotAuth = r.Header.Get("Authorization") - gotContentType = r.Header.Get("Content-Type") - gotAccept = r.Header.Get("Accept") - - if r.Body != nil { - _ = json.NewDecoder(r.Body).Decode(&gotBody) - } - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"ok":true}`)) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "test-token-123" - h := NewHubService(cfg) - - body := map[string]string{"key": "value"} - resp, err := h.doRequest("POST", "/test", body) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "Bearer test-token-123", gotAuth) - assert.Equal(t, "application/json", gotContentType) - assert.Equal(t, "application/json", gotAccept) - assert.Equal(t, "value", gotBody["key"]) - assert.True(t, h.IsConnected()) -} - -func TestDoRequest_Bad_NoHubURL(t *testing.T) { - h := testHubService(t, "") - - resp, err := h.doRequest("GET", "/test", nil) - assert.Nil(t, resp) - assert.Error(t, err) - assert.Contains(t, err.Error(), "hub URL not configured") -} - -func TestDoRequest_Bad_NetworkError(t *testing.T) { - // Point to a port where nothing is listening. - h := testHubService(t, "http://127.0.0.1:1") - - resp, err := h.doRequest("GET", "/test", nil) - assert.Nil(t, resp) - assert.Error(t, err) - assert.False(t, h.IsConnected()) -} - -// ---- Task 4: AutoRegister ---- - -func TestAutoRegister_Good(t *testing.T) { - var gotBody map[string]string - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/auth/forge", r.URL.Path) - assert.Equal(t, "POST", r.Method) - - _ = json.NewDecoder(r.Body).Decode(&gotBody) - - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte(`{"api_key":"ak_test_12345"}`)) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.ForgeURL = "https://forge.example.com" - cfg.config.ForgeToken = "forge-tok-abc" - h := NewHubService(cfg) - - err := h.AutoRegister() - require.NoError(t, err) - - // Verify token was cached. - assert.Equal(t, "ak_test_12345", h.config.GetHubToken()) - - // Verify request body. - assert.Equal(t, "https://forge.example.com", gotBody["forge_url"]) - assert.Equal(t, "forge-tok-abc", gotBody["forge_token"]) - assert.NotEmpty(t, gotBody["client_id"]) -} - -func TestAutoRegister_Bad_NoForgeToken(t *testing.T) { - // Isolate from user's real ~/.core/config.yaml and env vars. - origHome := os.Getenv("HOME") - t.Setenv("HOME", t.TempDir()) - t.Setenv("FORGE_TOKEN", "") - t.Setenv("FORGE_URL", "") - defer os.Setenv("HOME", origHome) - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = "https://hub.example.com" - // No forge token set, and env/config are empty in test. - h := NewHubService(cfg) - - err := h.AutoRegister() - require.Error(t, err) - assert.Contains(t, err.Error(), "no forge token available") -} - -func TestAutoRegister_Good_SkipsIfAlreadyRegistered(t *testing.T) { - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = "https://hub.example.com" - cfg.config.HubToken = "existing-token" - h := NewHubService(cfg) - - err := h.AutoRegister() - require.NoError(t, err) - - // Token should remain unchanged. - assert.Equal(t, "existing-token", h.config.GetHubToken()) -} - -// ---- Task 5: Write Operations ---- - -func TestRegister_Good(t *testing.T) { - var gotPath string - var gotMethod string - var gotBody map[string]string - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath = r.URL.Path - gotMethod = r.Method - _ = json.NewDecoder(r.Body).Decode(&gotBody) - w.WriteHeader(http.StatusOK) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - cfg.config.ClientName = "MyBugSETI" - h := NewHubService(cfg) - - err := h.Register() - require.NoError(t, err) - assert.Equal(t, "/api/bugseti/register", gotPath) - assert.Equal(t, "POST", gotMethod) - assert.Equal(t, "MyBugSETI", gotBody["name"]) - assert.NotEmpty(t, gotBody["client_id"]) - assert.NotEmpty(t, gotBody["version"]) - assert.NotEmpty(t, gotBody["os"]) - assert.NotEmpty(t, gotBody["arch"]) -} - -func TestHeartbeat_Good(t *testing.T) { - var gotPath string - var gotMethod string - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath = r.URL.Path - gotMethod = r.Method - w.WriteHeader(http.StatusOK) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - err := h.Heartbeat() - require.NoError(t, err) - assert.Equal(t, "/api/bugseti/heartbeat", gotPath) - assert.Equal(t, "POST", gotMethod) -} - -func TestClaimIssue_Good(t *testing.T) { - now := time.Now().Truncate(time.Second) - expires := now.Add(30 * time.Minute) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/issues/claim", r.URL.Path) - assert.Equal(t, "POST", r.Method) - - var body map[string]interface{} - _ = json.NewDecoder(r.Body).Decode(&body) - assert.Equal(t, "issue-42", body["issue_id"]) - assert.Equal(t, "org/repo", body["repo"]) - assert.Equal(t, float64(42), body["issue_number"]) - assert.Equal(t, "Fix the bug", body["title"]) - - w.WriteHeader(http.StatusOK) - resp := HubClaim{ - ID: "claim-1", - IssueURL: "https://github.com/org/repo/issues/42", - ClientID: "test", - ClaimedAt: now, - ExpiresAt: expires, - Status: "claimed", - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - issue := &Issue{ - ID: "issue-42", - Number: 42, - Repo: "org/repo", - Title: "Fix the bug", - URL: "https://github.com/org/repo/issues/42", - } - - claim, err := h.ClaimIssue(issue) - require.NoError(t, err) - require.NotNil(t, claim) - assert.Equal(t, "claim-1", claim.ID) - assert.Equal(t, "claimed", claim.Status) -} - -func TestClaimIssue_Bad_Conflict(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusConflict) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - issue := &Issue{ID: "issue-99", Number: 99, Repo: "org/repo", Title: "Already claimed"} - - claim, err := h.ClaimIssue(issue) - assert.Nil(t, claim) - require.Error(t, err) - - var conflictErr *ConflictError - assert.ErrorAs(t, err, &conflictErr) -} - -func TestUpdateStatus_Good(t *testing.T) { - var gotPath string - var gotMethod string - var gotBody map[string]interface{} - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath = r.URL.Path - gotMethod = r.Method - _ = json.NewDecoder(r.Body).Decode(&gotBody) - w.WriteHeader(http.StatusOK) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - err := h.UpdateStatus("issue-42", "completed", "https://github.com/org/repo/pull/10", 10) - require.NoError(t, err) - assert.Equal(t, "PATCH", gotMethod) - assert.Equal(t, "/api/bugseti/issues/issue-42/status", gotPath) - assert.Equal(t, "completed", gotBody["status"]) - assert.Equal(t, "https://github.com/org/repo/pull/10", gotBody["pr_url"]) - assert.Equal(t, float64(10), gotBody["pr_number"]) -} - -func TestSyncStats_Good(t *testing.T) { - var gotBody map[string]interface{} - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/stats/sync", r.URL.Path) - assert.Equal(t, "POST", r.Method) - _ = json.NewDecoder(r.Body).Decode(&gotBody) - w.WriteHeader(http.StatusOK) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - stats := &Stats{ - IssuesAttempted: 10, - IssuesCompleted: 7, - IssuesSkipped: 3, - PRsSubmitted: 6, - PRsMerged: 5, - PRsRejected: 1, - CurrentStreak: 3, - LongestStreak: 5, - TotalTimeSpent: 90 * time.Minute, - ReposContributed: map[string]*RepoStats{ - "org/repo-a": {Name: "org/repo-a"}, - "org/repo-b": {Name: "org/repo-b"}, - }, - } - - err := h.SyncStats(stats) - require.NoError(t, err) - - assert.NotEmpty(t, gotBody["client_id"]) - statsMap, ok := gotBody["stats"].(map[string]interface{}) - require.True(t, ok) - assert.Equal(t, float64(10), statsMap["issues_attempted"]) - assert.Equal(t, float64(7), statsMap["issues_completed"]) - assert.Equal(t, float64(3), statsMap["issues_skipped"]) - assert.Equal(t, float64(6), statsMap["prs_submitted"]) - assert.Equal(t, float64(5), statsMap["prs_merged"]) - assert.Equal(t, float64(1), statsMap["prs_rejected"]) - assert.Equal(t, float64(3), statsMap["current_streak"]) - assert.Equal(t, float64(5), statsMap["longest_streak"]) - assert.Equal(t, float64(90), statsMap["total_time_minutes"]) - - reposRaw, ok := statsMap["repos_contributed"].([]interface{}) - require.True(t, ok) - assert.Len(t, reposRaw, 2) -} - -// ---- Task 6: Read Operations ---- - -func TestIsIssueClaimed_Good_Claimed(t *testing.T) { - now := time.Now().Truncate(time.Second) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/issues/issue-42", r.URL.Path) - assert.Equal(t, "GET", r.Method) - - w.WriteHeader(http.StatusOK) - claim := HubClaim{ - ID: "claim-1", - IssueURL: "https://github.com/org/repo/issues/42", - ClientID: "client-abc", - ClaimedAt: now, - Status: "claimed", - } - _ = json.NewEncoder(w).Encode(claim) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - claim, err := h.IsIssueClaimed("issue-42") - require.NoError(t, err) - require.NotNil(t, claim) - assert.Equal(t, "claim-1", claim.ID) - assert.Equal(t, "claimed", claim.Status) -} - -func TestIsIssueClaimed_Good_NotClaimed(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - claim, err := h.IsIssueClaimed("issue-999") - assert.NoError(t, err) - assert.Nil(t, claim) -} - -func TestGetLeaderboard_Good(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/leaderboard", r.URL.Path) - assert.Equal(t, "GET", r.Method) - assert.Equal(t, "10", r.URL.Query().Get("limit")) - - resp := leaderboardResponse{ - Entries: []LeaderboardEntry{ - {ClientID: "a", ClientName: "Alice", Score: 100, PRsMerged: 10, Rank: 1}, - {ClientID: "b", ClientName: "Bob", Score: 80, PRsMerged: 8, Rank: 2}, - }, - TotalParticipants: 42, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(resp) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - entries, total, err := h.GetLeaderboard(10) - require.NoError(t, err) - assert.Equal(t, 42, total) - require.Len(t, entries, 2) - assert.Equal(t, "Alice", entries[0].ClientName) - assert.Equal(t, 1, entries[0].Rank) - assert.Equal(t, "Bob", entries[1].ClientName) -} - -func TestGetGlobalStats_Good(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/bugseti/stats", r.URL.Path) - assert.Equal(t, "GET", r.Method) - - stats := GlobalStats{ - TotalClients: 100, - TotalClaims: 500, - TotalPRsMerged: 300, - ActiveClaims: 25, - IssuesAvailable: 150, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(stats) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - stats, err := h.GetGlobalStats() - require.NoError(t, err) - require.NotNil(t, stats) - assert.Equal(t, 100, stats.TotalClients) - assert.Equal(t, 500, stats.TotalClaims) - assert.Equal(t, 300, stats.TotalPRsMerged) - assert.Equal(t, 25, stats.ActiveClaims) - assert.Equal(t, 150, stats.IssuesAvailable) -} - -// ---- Task 7: Pending Operations Queue ---- - -func TestPendingOps_Good_QueueAndDrain(t *testing.T) { - var callCount int32 - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - callCount++ - w.WriteHeader(http.StatusOK) - })) - defer srv.Close() - - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = srv.URL - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - // Manually queue a pending op (simulates a previous failed request). - h.queueOp("POST", "/heartbeat", map[string]string{"client_id": "test"}) - assert.Equal(t, 1, h.PendingCount()) - - // Register() calls drainPendingOps() first, then sends its own request. - err := h.Register() - require.NoError(t, err) - - // At least 2 calls: 1 from drain (the queued heartbeat) + 1 from Register itself. - assert.GreaterOrEqual(t, callCount, int32(2)) - assert.Equal(t, 0, h.PendingCount()) -} - -func TestPendingOps_Good_PersistAndLoad(t *testing.T) { - cfg1 := testConfigService(t, nil, nil) - cfg1.config.HubURL = "https://hub.example.com" - cfg1.config.HubToken = "tok" - h1 := NewHubService(cfg1) - - // Queue an op — this also calls savePendingOps. - h1.queueOp("POST", "/heartbeat", map[string]string{"client_id": "test"}) - assert.Equal(t, 1, h1.PendingCount()) - - // Create a second HubService with the same data dir. - // NewHubService calls loadPendingOps() in its constructor. - cfg2 := testConfigService(t, nil, nil) - cfg2.config.DataDir = cfg1.config.DataDir // Share the same data dir. - cfg2.config.HubURL = "https://hub.example.com" - cfg2.config.HubToken = "tok" - h2 := NewHubService(cfg2) - - assert.Equal(t, 1, h2.PendingCount()) -} - -func TestPendingCount_Good(t *testing.T) { - cfg := testConfigService(t, nil, nil) - cfg.config.HubURL = "https://hub.example.com" - cfg.config.HubToken = "tok" - h := NewHubService(cfg) - - assert.Equal(t, 0, h.PendingCount()) - - h.queueOp("POST", "/test1", nil) - assert.Equal(t, 1, h.PendingCount()) - - h.queueOp("POST", "/test2", map[string]string{"key": "val"}) - assert.Equal(t, 2, h.PendingCount()) -} diff --git a/internal/bugseti/mcp_marketplace.go b/internal/bugseti/mcp_marketplace.go deleted file mode 100644 index 9f379dfd..00000000 --- a/internal/bugseti/mcp_marketplace.go +++ /dev/null @@ -1,246 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" -) - -type Marketplace struct { - Schema string `json:"$schema,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - Owner MarketplaceOwner `json:"owner"` - Plugins []MarketplacePlugin `json:"plugins"` -} - -type MarketplaceOwner struct { - Name string `json:"name"` - Email string `json:"email"` -} - -type MarketplacePlugin struct { - Name string `json:"name"` - Description string `json:"description"` - Version string `json:"version"` - Source string `json:"source"` - Category string `json:"category"` -} - -type PluginInfo struct { - Plugin MarketplacePlugin `json:"plugin"` - Path string `json:"path"` - Manifest map[string]any `json:"manifest,omitempty"` - Commands []string `json:"commands,omitempty"` - Skills []string `json:"skills,omitempty"` -} - -type EthicsContext struct { - Modal string `json:"modal"` - Axioms map[string]any `json:"axioms"` -} - -type marketplaceClient interface { - ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error) - PluginInfo(ctx context.Context, name string) (*PluginInfo, error) - EthicsCheck(ctx context.Context) (*EthicsContext, error) - Close() error -} - -type mcpMarketplaceClient struct { - client *client.Client -} - -func newMarketplaceClient(ctx context.Context, rootHint string) (marketplaceClient, error) { - if ctx == nil { - ctx = context.Background() - } - - command, args, err := resolveMarketplaceCommand(rootHint) - if err != nil { - return nil, err - } - - mcpClient, err := client.NewStdioMCPClient(command, nil, args...) - if err != nil { - return nil, fmt.Errorf("failed to start marketplace MCP client: %w", err) - } - - initRequest := mcp.InitializeRequest{} - initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION - initRequest.Params.ClientInfo = mcp.Implementation{ - Name: "bugseti", - Version: GetVersion(), - } - - initCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - if _, err := mcpClient.Initialize(initCtx, initRequest); err != nil { - _ = mcpClient.Close() - return nil, fmt.Errorf("failed to initialize marketplace MCP client: %w", err) - } - - return &mcpMarketplaceClient{client: mcpClient}, nil -} - -func (c *mcpMarketplaceClient) Close() error { - if c == nil || c.client == nil { - return nil - } - return c.client.Close() -} - -func (c *mcpMarketplaceClient) ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error) { - var marketplace Marketplace - if err := c.callToolStructured(ctx, "marketplace_list", nil, &marketplace); err != nil { - return nil, err - } - return marketplace.Plugins, nil -} - -func (c *mcpMarketplaceClient) PluginInfo(ctx context.Context, name string) (*PluginInfo, error) { - var info PluginInfo - args := map[string]any{"name": name} - if err := c.callToolStructured(ctx, "marketplace_plugin_info", args, &info); err != nil { - return nil, err - } - return &info, nil -} - -func (c *mcpMarketplaceClient) EthicsCheck(ctx context.Context) (*EthicsContext, error) { - var ethics EthicsContext - if err := c.callToolStructured(ctx, "ethics_check", nil, ðics); err != nil { - return nil, err - } - return ðics, nil -} - -func (c *mcpMarketplaceClient) callToolStructured(ctx context.Context, name string, args map[string]any, target any) error { - if c == nil || c.client == nil { - return errors.New("marketplace client is not initialized") - } - if ctx == nil { - ctx = context.Background() - } - - request := mcp.CallToolRequest{} - request.Params.Name = name - if args != nil { - request.Params.Arguments = args - } - - result, err := c.client.CallTool(ctx, request) - if err != nil { - return err - } - if result == nil { - return errors.New("marketplace tool returned no result") - } - if result.IsError { - return fmt.Errorf("marketplace tool %s error: %s", name, toolResultMessage(result)) - } - if result.StructuredContent == nil { - return fmt.Errorf("marketplace tool %s returned no structured content", name) - } - payload, err := json.Marshal(result.StructuredContent) - if err != nil { - return fmt.Errorf("failed to encode marketplace response: %w", err) - } - if err := json.Unmarshal(payload, target); err != nil { - return fmt.Errorf("failed to decode marketplace response: %w", err) - } - return nil -} - -func toolResultMessage(result *mcp.CallToolResult) string { - if result == nil { - return "unknown error" - } - for _, content := range result.Content { - switch value := content.(type) { - case mcp.TextContent: - if value.Text != "" { - return value.Text - } - case *mcp.TextContent: - if value != nil && value.Text != "" { - return value.Text - } - } - } - return "unknown error" -} - -func resolveMarketplaceCommand(rootHint string) (string, []string, error) { - if command := strings.TrimSpace(os.Getenv("BUGSETI_MCP_COMMAND")); command != "" { - args := strings.Fields(os.Getenv("BUGSETI_MCP_ARGS")) - return command, args, nil - } - - if root := strings.TrimSpace(rootHint); root != "" { - path := filepath.Join(root, "mcp") - return "go", []string{"run", path}, nil - } - - if root := strings.TrimSpace(os.Getenv("BUGSETI_MCP_ROOT")); root != "" { - path := filepath.Join(root, "mcp") - return "go", []string{"run", path}, nil - } - - if root, ok := findCoreAgentRoot(); ok { - return "go", []string{"run", filepath.Join(root, "mcp")}, nil - } - - return "", nil, fmt.Errorf("marketplace MCP server not configured (set BUGSETI_MCP_COMMAND or BUGSETI_MCP_ROOT)") -} - -func findCoreAgentRoot() (string, bool) { - var candidates []string - if cwd, err := os.Getwd(); err == nil { - candidates = append(candidates, cwd) - candidates = append(candidates, filepath.Dir(cwd)) - } - if exe, err := os.Executable(); err == nil { - exeDir := filepath.Dir(exe) - candidates = append(candidates, exeDir) - candidates = append(candidates, filepath.Dir(exeDir)) - } - - seen := make(map[string]bool) - for _, base := range candidates { - base = filepath.Clean(base) - if seen[base] { - continue - } - seen[base] = true - - root := filepath.Join(base, "core-agent") - if hasMcpDir(root) { - return root, true - } - - root = filepath.Join(base, "..", "core-agent") - if hasMcpDir(root) { - return filepath.Clean(root), true - } - } - - return "", false -} - -func hasMcpDir(root string) bool { - if root == "" { - return false - } - info, err := os.Stat(filepath.Join(root, "mcp", "main.go")) - return err == nil && !info.IsDir() -} diff --git a/internal/bugseti/notify.go b/internal/bugseti/notify.go deleted file mode 100644 index c467c1b2..00000000 --- a/internal/bugseti/notify.go +++ /dev/null @@ -1,252 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "context" - "fmt" - "log" - "os/exec" - "runtime" - "time" -) - -// NotifyService handles desktop notifications. -type NotifyService struct { - enabled bool - sound bool - config *ConfigService -} - -// NewNotifyService creates a new NotifyService. -func NewNotifyService(config *ConfigService) *NotifyService { - return &NotifyService{ - enabled: true, - sound: true, - config: config, - } -} - -// ServiceName returns the service name for Wails. -func (n *NotifyService) ServiceName() string { - return "NotifyService" -} - -// SetEnabled enables or disables notifications. -func (n *NotifyService) SetEnabled(enabled bool) { - n.enabled = enabled -} - -// SetSound enables or disables notification sounds. -func (n *NotifyService) SetSound(sound bool) { - n.sound = sound -} - -// Notify sends a desktop notification. -func (n *NotifyService) Notify(title, message string) error { - if !n.enabled { - return nil - } - - guard := getEthicsGuardWithRoot(context.Background(), n.getMarketplaceRoot()) - safeTitle := guard.SanitizeNotification(title) - safeMessage := guard.SanitizeNotification(message) - - log.Printf("Notification: %s - %s", safeTitle, safeMessage) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - var err error - switch runtime.GOOS { - case "darwin": - err = n.notifyMacOS(ctx, safeTitle, safeMessage) - case "linux": - err = n.notifyLinux(ctx, safeTitle, safeMessage) - case "windows": - err = n.notifyWindows(ctx, safeTitle, safeMessage) - default: - err = fmt.Errorf("unsupported platform: %s", runtime.GOOS) - } - - if err != nil { - log.Printf("Notification error: %v", err) - } - return err -} - -func (n *NotifyService) getMarketplaceRoot() string { - if n == nil || n.config == nil { - return "" - } - return n.config.GetMarketplaceMCPRoot() -} - -// NotifyIssue sends a notification about a new issue. -func (n *NotifyService) NotifyIssue(issue *Issue) error { - title := "New Issue Available" - message := fmt.Sprintf("%s: %s", issue.Repo, issue.Title) - return n.Notify(title, message) -} - -// NotifyPRStatus sends a notification about a PR status change. -func (n *NotifyService) NotifyPRStatus(repo string, prNumber int, status string) error { - title := "PR Status Update" - message := fmt.Sprintf("%s #%d: %s", repo, prNumber, status) - return n.Notify(title, message) -} - -// notifyMacOS sends a notification on macOS using osascript. -func (n *NotifyService) notifyMacOS(ctx context.Context, title, message string) error { - script := fmt.Sprintf(`display notification "%s" with title "%s"`, escapeAppleScript(message), escapeAppleScript(title)) - if n.sound { - script += ` sound name "Glass"` - } - cmd := exec.CommandContext(ctx, "osascript", "-e", script) - return cmd.Run() -} - -// notifyLinux sends a notification on Linux using notify-send. -func (n *NotifyService) notifyLinux(ctx context.Context, title, message string) error { - args := []string{ - "--app-name=BugSETI", - "--urgency=normal", - title, - message, - } - cmd := exec.CommandContext(ctx, "notify-send", args...) - return cmd.Run() -} - -// notifyWindows sends a notification on Windows using PowerShell. -func (n *NotifyService) notifyWindows(ctx context.Context, title, message string) error { - title = escapePowerShellXML(title) - message = escapePowerShellXML(message) - - script := fmt.Sprintf(` -[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null -[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null - -$template = @" - - - - %s - %s - - - -"@ - -$xml = New-Object Windows.Data.Xml.Dom.XmlDocument -$xml.LoadXml($template) -$toast = [Windows.UI.Notifications.ToastNotification]::new($xml) -[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("BugSETI").Show($toast) -`, title, message) - - cmd := exec.CommandContext(ctx, "powershell", "-Command", script) - return cmd.Run() -} - -// NotifyWithAction sends a notification with an action button (platform-specific). -func (n *NotifyService) NotifyWithAction(title, message, actionLabel string) error { - if !n.enabled { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - switch runtime.GOOS { - case "darwin": - // macOS: Use terminal-notifier if available for actions - if _, err := exec.LookPath("terminal-notifier"); err == nil { - cmd := exec.CommandContext(ctx, "terminal-notifier", - "-title", title, - "-message", message, - "-appIcon", "NSApplication", - "-actions", actionLabel, - "-group", "BugSETI") - return cmd.Run() - } - return n.notifyMacOS(ctx, title, message) - - case "linux": - // Linux: Use notify-send with action - args := []string{ - "--app-name=BugSETI", - "--urgency=normal", - "--action=open=" + actionLabel, - title, - message, - } - cmd := exec.CommandContext(ctx, "notify-send", args...) - return cmd.Run() - - default: - return n.Notify(title, message) - } -} - -// NotifyProgress sends a notification with a progress indicator. -func (n *NotifyService) NotifyProgress(title, message string, progress int) error { - if !n.enabled { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - switch runtime.GOOS { - case "linux": - // Linux supports progress hints - args := []string{ - "--app-name=BugSETI", - "--hint=int:value:" + fmt.Sprintf("%d", progress), - title, - message, - } - cmd := exec.CommandContext(ctx, "notify-send", args...) - return cmd.Run() - - default: - // Other platforms: include progress in message - messageWithProgress := fmt.Sprintf("%s (%d%%)", message, progress) - return n.Notify(title, messageWithProgress) - } -} - -// PlaySound plays a notification sound. -func (n *NotifyService) PlaySound() error { - if !n.sound { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - switch runtime.GOOS { - case "darwin": - cmd := exec.CommandContext(ctx, "afplay", "/System/Library/Sounds/Glass.aiff") - return cmd.Run() - - case "linux": - // Try paplay (PulseAudio), then aplay (ALSA) - if _, err := exec.LookPath("paplay"); err == nil { - cmd := exec.CommandContext(ctx, "paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga") - return cmd.Run() - } - if _, err := exec.LookPath("aplay"); err == nil { - cmd := exec.CommandContext(ctx, "aplay", "-q", "/usr/share/sounds/alsa/Front_Center.wav") - return cmd.Run() - } - return nil - - case "windows": - script := `[console]::beep(800, 200)` - cmd := exec.CommandContext(ctx, "powershell", "-Command", script) - return cmd.Run() - - default: - return nil - } -} diff --git a/internal/bugseti/queue.go b/internal/bugseti/queue.go deleted file mode 100644 index 6b06d5ca..00000000 --- a/internal/bugseti/queue.go +++ /dev/null @@ -1,314 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "container/heap" - "encoding/json" - "log" - "os" - "path/filepath" - "sync" - "time" -) - -// IssueStatus represents the status of an issue in the queue. -type IssueStatus string - -const ( - StatusPending IssueStatus = "pending" - StatusClaimed IssueStatus = "claimed" - StatusInProgress IssueStatus = "in_progress" - StatusCompleted IssueStatus = "completed" - StatusSkipped IssueStatus = "skipped" -) - -// Issue represents a GitHub issue in the queue. -type Issue struct { - ID string `json:"id"` - Number int `json:"number"` - Repo string `json:"repo"` - Title string `json:"title"` - Body string `json:"body"` - URL string `json:"url"` - Labels []string `json:"labels"` - Author string `json:"author"` - CreatedAt time.Time `json:"createdAt"` - Priority int `json:"priority"` - Status IssueStatus `json:"status"` - ClaimedAt time.Time `json:"claimedAt,omitempty"` - Context *IssueContext `json:"context,omitempty"` - Comments []Comment `json:"comments,omitempty"` - index int // For heap interface -} - -// Comment represents a comment on an issue. -type Comment struct { - Author string `json:"author"` - Body string `json:"body"` -} - -// IssueContext contains AI-prepared context for an issue. -type IssueContext struct { - Summary string `json:"summary"` - RelevantFiles []string `json:"relevantFiles"` - SuggestedFix string `json:"suggestedFix"` - RelatedIssues []string `json:"relatedIssues"` - Complexity string `json:"complexity"` - EstimatedTime string `json:"estimatedTime"` - PreparedAt time.Time `json:"preparedAt"` -} - -// QueueService manages the priority queue of issues. -type QueueService struct { - config *ConfigService - issues issueHeap - seen map[string]bool - current *Issue - mu sync.RWMutex -} - -// issueHeap implements heap.Interface for priority queue. -type issueHeap []*Issue - -func (h issueHeap) Len() int { return len(h) } -func (h issueHeap) Less(i, j int) bool { return h[i].Priority > h[j].Priority } // Higher priority first -func (h issueHeap) Swap(i, j int) { - h[i], h[j] = h[j], h[i] - h[i].index = i - h[j].index = j -} - -func (h *issueHeap) Push(x any) { - n := len(*h) - item := x.(*Issue) - item.index = n - *h = append(*h, item) -} - -func (h *issueHeap) Pop() any { - old := *h - n := len(old) - item := old[n-1] - old[n-1] = nil - item.index = -1 - *h = old[0 : n-1] - return item -} - -// NewQueueService creates a new QueueService. -func NewQueueService(config *ConfigService) *QueueService { - q := &QueueService{ - config: config, - } - - // Hold the lock for the entire initialization sequence so that all - // shared state (issues, seen, current) is fully populated before - // any concurrent caller can observe the service. - q.mu.Lock() - defer q.mu.Unlock() - - q.issues = make(issueHeap, 0) - q.seen = make(map[string]bool) - q.load() // Load persisted queue (overwrites issues/seen if file exists) - return q -} - -// ServiceName returns the service name for Wails. -func (q *QueueService) ServiceName() string { - return "QueueService" -} - -// Add adds issues to the queue, deduplicating by ID. -func (q *QueueService) Add(issues []*Issue) int { - q.mu.Lock() - defer q.mu.Unlock() - - added := 0 - for _, issue := range issues { - if q.seen[issue.ID] { - continue - } - q.seen[issue.ID] = true - issue.Status = StatusPending - heap.Push(&q.issues, issue) - added++ - } - - if added > 0 { - q.save() - } - return added -} - -// Size returns the number of issues in the queue. -func (q *QueueService) Size() int { - q.mu.RLock() - defer q.mu.RUnlock() - return len(q.issues) -} - -// CurrentIssue returns the issue currently being worked on. -func (q *QueueService) CurrentIssue() *Issue { - q.mu.RLock() - defer q.mu.RUnlock() - return q.current -} - -// Next claims and returns the next issue from the queue. -func (q *QueueService) Next() *Issue { - q.mu.Lock() - defer q.mu.Unlock() - - if len(q.issues) == 0 { - return nil - } - - // Pop the highest priority issue - issue := heap.Pop(&q.issues).(*Issue) - issue.Status = StatusClaimed - issue.ClaimedAt = time.Now() - q.current = issue - q.save() - return issue -} - -// Skip marks the current issue as skipped and moves to the next. -func (q *QueueService) Skip() { - q.mu.Lock() - defer q.mu.Unlock() - - if q.current != nil { - q.current.Status = StatusSkipped - q.current = nil - q.save() - } -} - -// Complete marks the current issue as completed. -func (q *QueueService) Complete() { - q.mu.Lock() - defer q.mu.Unlock() - - if q.current != nil { - q.current.Status = StatusCompleted - q.current = nil - q.save() - } -} - -// SetInProgress marks the current issue as in progress. -func (q *QueueService) SetInProgress() { - q.mu.Lock() - defer q.mu.Unlock() - - if q.current != nil { - q.current.Status = StatusInProgress - q.save() - } -} - -// SetContext sets the AI-prepared context for the current issue. -func (q *QueueService) SetContext(ctx *IssueContext) { - q.mu.Lock() - defer q.mu.Unlock() - - if q.current != nil { - q.current.Context = ctx - q.save() - } -} - -// GetPending returns all pending issues. -func (q *QueueService) GetPending() []*Issue { - q.mu.RLock() - defer q.mu.RUnlock() - - result := make([]*Issue, 0, len(q.issues)) - for _, issue := range q.issues { - if issue.Status == StatusPending { - result = append(result, issue) - } - } - return result -} - -// Clear removes all issues from the queue. -func (q *QueueService) Clear() { - q.mu.Lock() - defer q.mu.Unlock() - - q.issues = make(issueHeap, 0) - q.seen = make(map[string]bool) - q.current = nil - heap.Init(&q.issues) - q.save() -} - -// queueState represents the persisted queue state. -type queueState struct { - Issues []*Issue `json:"issues"` - Current *Issue `json:"current"` - Seen []string `json:"seen"` -} - -// save persists the queue to disk. Must be called with q.mu held. -func (q *QueueService) save() { - dataDir := q.config.GetDataDir() - if dataDir == "" { - return - } - - path := filepath.Join(dataDir, "queue.json") - - seen := make([]string, 0, len(q.seen)) - for id := range q.seen { - seen = append(seen, id) - } - - state := queueState{ - Issues: []*Issue(q.issues), - Current: q.current, - Seen: seen, - } - - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - log.Printf("Failed to marshal queue: %v", err) - return - } - - if err := os.WriteFile(path, data, 0644); err != nil { - log.Printf("Failed to save queue: %v", err) - } -} - -// load restores the queue from disk. Must be called with q.mu held. -func (q *QueueService) load() { - dataDir := q.config.GetDataDir() - if dataDir == "" { - return - } - - path := filepath.Join(dataDir, "queue.json") - data, err := os.ReadFile(path) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("Failed to read queue: %v", err) - } - return - } - - var state queueState - if err := json.Unmarshal(data, &state); err != nil { - log.Printf("Failed to unmarshal queue: %v", err) - return - } - - q.issues = state.Issues - heap.Init(&q.issues) - q.current = state.Current - q.seen = make(map[string]bool) - for _, id := range state.Seen { - q.seen[id] = true - } -} diff --git a/internal/bugseti/seeder.go b/internal/bugseti/seeder.go deleted file mode 100644 index 50195148..00000000 --- a/internal/bugseti/seeder.go +++ /dev/null @@ -1,383 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" -) - -// SeederService prepares context for issues using the seed-agent-developer skill. -type SeederService struct { - mu sync.Mutex - config *ConfigService - forgeURL string - forgeToken string -} - -// NewSeederService creates a new SeederService. -func NewSeederService(config *ConfigService, forgeURL, forgeToken string) *SeederService { - return &SeederService{ - config: config, - forgeURL: forgeURL, - forgeToken: forgeToken, - } -} - -// ServiceName returns the service name for Wails. -func (s *SeederService) ServiceName() string { - return "SeederService" -} - -// SeedIssue prepares context for an issue by calling the seed-agent-developer skill. -func (s *SeederService) SeedIssue(issue *Issue) (*IssueContext, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if issue == nil { - return nil, fmt.Errorf("issue is nil") - } - - // Create a temporary workspace for the issue - workDir, err := s.prepareWorkspace(issue) - if err != nil { - return nil, fmt.Errorf("failed to prepare workspace: %w", err) - } - - // Try to use the seed-agent-developer skill via plugin system - ctx, err := s.runSeedSkill(issue, workDir) - if err != nil { - log.Printf("Seed skill failed, using fallback: %v", err) - // Fallback to basic context preparation - guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot()) - ctx = s.prepareBasicContext(issue, guard) - } - - ctx.PreparedAt = time.Now() - return ctx, nil -} - -// prepareWorkspace creates a temporary workspace and clones the repo. -func (s *SeederService) prepareWorkspace(issue *Issue) (string, error) { - // Create workspace directory - baseDir := s.config.GetWorkspaceDir() - if baseDir == "" { - baseDir = filepath.Join(os.TempDir(), "bugseti") - } - - // Create issue-specific directory - workDir := filepath.Join(baseDir, sanitizeRepoName(issue.Repo), fmt.Sprintf("issue-%d", issue.Number)) - if err := os.MkdirAll(workDir, 0755); err != nil { - return "", fmt.Errorf("failed to create workspace: %w", err) - } - - // Check if repo already cloned - if _, err := os.Stat(filepath.Join(workDir, ".git")); os.IsNotExist(err) { - // Clone the repository - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - cloneURL := fmt.Sprintf("%s/%s.git", strings.TrimRight(s.forgeURL, "/"), issue.Repo) - cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", cloneURL, workDir) - cmd.Env = append(os.Environ(), - fmt.Sprintf("GIT_ASKPASS=echo"), - fmt.Sprintf("GIT_TERMINAL_PROMPT=0"), - ) - if s.forgeToken != "" { - // Use token auth via URL for HTTPS clones - cloneURL = fmt.Sprintf("%s/%s.git", strings.TrimRight(s.forgeURL, "/"), issue.Repo) - cloneURL = strings.Replace(cloneURL, "://", fmt.Sprintf("://bugseti:%s@", s.forgeToken), 1) - cmd = exec.CommandContext(ctx, "git", "clone", "--depth=1", cloneURL, workDir) - } - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to clone repo: %s: %w", stderr.String(), err) - } - } - - return workDir, nil -} - -// runSeedSkill executes the seed-agent-developer skill to prepare context. -func (s *SeederService) runSeedSkill(issue *Issue, workDir string) (*IssueContext, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - mcpCtx, mcpCancel := context.WithTimeout(ctx, 20*time.Second) - defer mcpCancel() - - marketplace, err := newMarketplaceClient(mcpCtx, s.config.GetMarketplaceMCPRoot()) - if err != nil { - return nil, err - } - defer marketplace.Close() - - guard := guardFromMarketplace(mcpCtx, marketplace) - - scriptPath, err := findSeedSkillScript(mcpCtx, marketplace) - if err != nil { - return nil, err - } - - // Run the analyze-issue script - cmd := exec.CommandContext(ctx, "bash", scriptPath) - cmd.Dir = workDir - cmd.Env = append(os.Environ(), - fmt.Sprintf("ISSUE_NUMBER=%d", issue.Number), - fmt.Sprintf("ISSUE_REPO=%s", guard.SanitizeEnv(issue.Repo)), - fmt.Sprintf("ISSUE_TITLE=%s", guard.SanitizeEnv(issue.Title)), - fmt.Sprintf("ISSUE_URL=%s", guard.SanitizeEnv(issue.URL)), - ) - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("seed skill failed: %s: %w", stderr.String(), err) - } - - // Parse the output as JSON - var result struct { - Summary string `json:"summary"` - RelevantFiles []string `json:"relevant_files"` - SuggestedFix string `json:"suggested_fix"` - RelatedIssues []string `json:"related_issues"` - Complexity string `json:"complexity"` - EstimatedTime string `json:"estimated_time"` - } - - if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { - // If not JSON, treat as plain text summary - return sanitizeIssueContext(&IssueContext{ - Summary: stdout.String(), - Complexity: "unknown", - }, guard), nil - } - - return sanitizeIssueContext(&IssueContext{ - Summary: result.Summary, - RelevantFiles: result.RelevantFiles, - SuggestedFix: result.SuggestedFix, - RelatedIssues: result.RelatedIssues, - Complexity: result.Complexity, - EstimatedTime: result.EstimatedTime, - }, guard), nil -} - -// prepareBasicContext creates a basic context without the seed skill. -func (s *SeederService) prepareBasicContext(issue *Issue, guard *EthicsGuard) *IssueContext { - // Extract potential file references from issue body - files := extractFileReferences(issue.Body) - - // Estimate complexity based on labels and body length - complexity := estimateComplexity(issue) - - return sanitizeIssueContext(&IssueContext{ - Summary: fmt.Sprintf("Issue #%d in %s: %s", issue.Number, issue.Repo, issue.Title), - RelevantFiles: files, - Complexity: complexity, - EstimatedTime: estimateTime(complexity), - }, guard) -} - -// sanitizeRepoName converts owner/repo to a safe directory name. -func sanitizeRepoName(repo string) string { - return strings.ReplaceAll(repo, "/", "-") -} - -// extractFileReferences finds file paths mentioned in text. -func extractFileReferences(text string) []string { - var files []string - seen := make(map[string]bool) - - // Common file patterns - patterns := []string{ - `.go`, `.js`, `.ts`, `.py`, `.rs`, `.java`, `.cpp`, `.c`, `.h`, - `.json`, `.yaml`, `.yml`, `.toml`, `.xml`, `.md`, - } - - words := strings.Fields(text) - for _, word := range words { - // Clean up the word - word = strings.Trim(word, "`,\"'()[]{}:") - - // Check if it looks like a file path - for _, ext := range patterns { - if strings.HasSuffix(word, ext) && !seen[word] { - files = append(files, word) - seen[word] = true - break - } - } - } - - return files -} - -// estimateComplexity guesses issue complexity from content. -func estimateComplexity(issue *Issue) string { - bodyLen := len(issue.Body) - labelScore := 0 - - for _, label := range issue.Labels { - lower := strings.ToLower(label) - switch { - case strings.Contains(lower, "good first issue"), strings.Contains(lower, "beginner"): - labelScore -= 2 - case strings.Contains(lower, "easy"): - labelScore -= 1 - case strings.Contains(lower, "complex"), strings.Contains(lower, "hard"): - labelScore += 2 - case strings.Contains(lower, "refactor"): - labelScore += 1 - } - } - - // Combine body length and label score - score := labelScore - if bodyLen > 2000 { - score += 2 - } else if bodyLen > 500 { - score += 1 - } - - switch { - case score <= -1: - return "easy" - case score <= 1: - return "medium" - default: - return "hard" - } -} - -// estimateTime suggests time based on complexity. -func estimateTime(complexity string) string { - switch complexity { - case "easy": - return "15-30 minutes" - case "medium": - return "1-2 hours" - case "hard": - return "2-4 hours" - default: - return "unknown" - } -} - -const seedSkillName = "seed-agent-developer" - -func findSeedSkillScript(ctx context.Context, marketplace marketplaceClient) (string, error) { - if marketplace == nil { - return "", fmt.Errorf("marketplace client is nil") - } - - plugins, err := marketplace.ListMarketplace(ctx) - if err != nil { - return "", err - } - - for _, plugin := range plugins { - info, err := marketplace.PluginInfo(ctx, plugin.Name) - if err != nil || info == nil { - continue - } - - if !containsSkill(info.Skills, seedSkillName) { - continue - } - - scriptPath, err := safeJoinUnder(info.Path, "skills", seedSkillName, "scripts", "analyze-issue.sh") - if err != nil { - continue - } - if stat, err := os.Stat(scriptPath); err == nil && !stat.IsDir() { - return scriptPath, nil - } - } - - return "", fmt.Errorf("seed-agent-developer skill not found in marketplace") -} - -func containsSkill(skills []string, name string) bool { - for _, skill := range skills { - if skill == name { - return true - } - } - return false -} - -func safeJoinUnder(base string, elems ...string) (string, error) { - if base == "" { - return "", fmt.Errorf("base path is empty") - } - baseAbs, err := filepath.Abs(base) - if err != nil { - return "", fmt.Errorf("failed to resolve base path: %w", err) - } - - joined := filepath.Join(append([]string{baseAbs}, elems...)...) - rel, err := filepath.Rel(baseAbs, joined) - if err != nil { - return "", fmt.Errorf("failed to resolve relative path: %w", err) - } - if strings.HasPrefix(rel, "..") { - return "", fmt.Errorf("resolved path escapes base: %s", rel) - } - - return joined, nil -} - -func sanitizeIssueContext(ctx *IssueContext, guard *EthicsGuard) *IssueContext { - if ctx == nil { - return nil - } - if guard == nil { - guard = &EthicsGuard{} - } - - ctx.Summary = guard.SanitizeSummary(ctx.Summary) - ctx.SuggestedFix = guard.SanitizeSummary(ctx.SuggestedFix) - ctx.Complexity = guard.SanitizeTitle(ctx.Complexity) - ctx.EstimatedTime = guard.SanitizeTitle(ctx.EstimatedTime) - ctx.RelatedIssues = guard.SanitizeList(ctx.RelatedIssues, maxTitleRunes) - ctx.RelevantFiles = guard.SanitizeFiles(ctx.RelevantFiles) - return ctx -} - -// GetWorkspaceDir returns the workspace directory for an issue. -func (s *SeederService) GetWorkspaceDir(issue *Issue) string { - s.mu.Lock() - defer s.mu.Unlock() - - return s.getWorkspaceDir(issue) -} - -// getWorkspaceDir is the lock-free implementation; caller must hold s.mu. -func (s *SeederService) getWorkspaceDir(issue *Issue) string { - baseDir := s.config.GetWorkspaceDir() - if baseDir == "" { - baseDir = filepath.Join(os.TempDir(), "bugseti") - } - return filepath.Join(baseDir, sanitizeRepoName(issue.Repo), fmt.Sprintf("issue-%d", issue.Number)) -} - -// CleanupWorkspace removes the workspace for an issue. -func (s *SeederService) CleanupWorkspace(issue *Issue) error { - s.mu.Lock() - defer s.mu.Unlock() - - workDir := s.getWorkspaceDir(issue) - return os.RemoveAll(workDir) -} diff --git a/internal/bugseti/seeder_test.go b/internal/bugseti/seeder_test.go deleted file mode 100644 index daef6591..00000000 --- a/internal/bugseti/seeder_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package bugseti - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" -) - -type fakeMarketplaceClient struct { - plugins []MarketplacePlugin - infos map[string]*PluginInfo - listErr error - infoErr map[string]error -} - -func (f *fakeMarketplaceClient) ListMarketplace(ctx context.Context) ([]MarketplacePlugin, error) { - if f.listErr != nil { - return nil, f.listErr - } - return f.plugins, nil -} - -func (f *fakeMarketplaceClient) PluginInfo(ctx context.Context, name string) (*PluginInfo, error) { - if err, ok := f.infoErr[name]; ok { - return nil, err - } - info, ok := f.infos[name] - if !ok { - return nil, fmt.Errorf("plugin not found") - } - return info, nil -} - -func (f *fakeMarketplaceClient) EthicsCheck(ctx context.Context) (*EthicsContext, error) { - return nil, fmt.Errorf("not implemented") -} - -func (f *fakeMarketplaceClient) Close() error { - return nil -} - -func TestFindSeedSkillScript_Good(t *testing.T) { - root := t.TempDir() - scriptPath := filepath.Join(root, "skills", seedSkillName, "scripts", "analyze-issue.sh") - if err := os.MkdirAll(filepath.Dir(scriptPath), 0755); err != nil { - t.Fatalf("failed to create script directory: %v", err) - } - if err := os.WriteFile(scriptPath, []byte("#!/bin/bash\n"), 0755); err != nil { - t.Fatalf("failed to write script: %v", err) - } - - plugin := MarketplacePlugin{Name: "seed-plugin"} - client := &fakeMarketplaceClient{ - plugins: []MarketplacePlugin{plugin}, - infos: map[string]*PluginInfo{ - plugin.Name: { - Plugin: plugin, - Path: root, - Skills: []string{seedSkillName}, - }, - }, - } - - found, err := findSeedSkillScript(context.Background(), client) - if err != nil { - t.Fatalf("expected script path, got error: %v", err) - } - if found != scriptPath { - t.Fatalf("expected %q, got %q", scriptPath, found) - } -} - -func TestFindSeedSkillScript_Bad(t *testing.T) { - plugin := MarketplacePlugin{Name: "empty-plugin"} - client := &fakeMarketplaceClient{ - plugins: []MarketplacePlugin{plugin}, - infos: map[string]*PluginInfo{ - plugin.Name: { - Plugin: plugin, - Path: t.TempDir(), - Skills: []string{"not-the-skill"}, - }, - }, - } - - if _, err := findSeedSkillScript(context.Background(), client); err == nil { - t.Fatal("expected error when skill is missing") - } -} - -func TestSafeJoinUnder_Ugly(t *testing.T) { - if _, err := safeJoinUnder("", "skills"); err == nil { - t.Fatal("expected error for empty base path") - } -} diff --git a/internal/bugseti/stats.go b/internal/bugseti/stats.go deleted file mode 100644 index f8bc2672..00000000 --- a/internal/bugseti/stats.go +++ /dev/null @@ -1,359 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "encoding/json" - "log" - "os" - "path/filepath" - "sync" - "time" -) - -// StatsService tracks user contribution statistics. -type StatsService struct { - config *ConfigService - stats *Stats - mu sync.RWMutex -} - -// Stats contains all tracked statistics. -type Stats struct { - // Issue stats - IssuesAttempted int `json:"issuesAttempted"` - IssuesCompleted int `json:"issuesCompleted"` - IssuesSkipped int `json:"issuesSkipped"` - - // PR stats - PRsSubmitted int `json:"prsSubmitted"` - PRsMerged int `json:"prsMerged"` - PRsRejected int `json:"prsRejected"` - - // Repository stats - ReposContributed map[string]*RepoStats `json:"reposContributed"` - - // Streaks - CurrentStreak int `json:"currentStreak"` - LongestStreak int `json:"longestStreak"` - LastActivity time.Time `json:"lastActivity"` - - // Time tracking - TotalTimeSpent time.Duration `json:"totalTimeSpent"` - AverageTimePerPR time.Duration `json:"averageTimePerPR"` - - // Activity history (last 30 days) - DailyActivity map[string]*DayStats `json:"dailyActivity"` -} - -// RepoStats contains statistics for a single repository. -type RepoStats struct { - Name string `json:"name"` - IssuesFixed int `json:"issuesFixed"` - PRsSubmitted int `json:"prsSubmitted"` - PRsMerged int `json:"prsMerged"` - FirstContrib time.Time `json:"firstContrib"` - LastContrib time.Time `json:"lastContrib"` -} - -// DayStats contains statistics for a single day. -type DayStats struct { - Date string `json:"date"` - IssuesWorked int `json:"issuesWorked"` - PRsSubmitted int `json:"prsSubmitted"` - TimeSpent int `json:"timeSpentMinutes"` -} - -// NewStatsService creates a new StatsService. -func NewStatsService(config *ConfigService) *StatsService { - s := &StatsService{ - config: config, - stats: &Stats{ - ReposContributed: make(map[string]*RepoStats), - DailyActivity: make(map[string]*DayStats), - }, - } - s.load() - return s -} - -// ServiceName returns the service name for Wails. -func (s *StatsService) ServiceName() string { - return "StatsService" -} - -// GetStats returns a copy of the current statistics. -func (s *StatsService) GetStats() Stats { - s.mu.RLock() - defer s.mu.RUnlock() - return *s.stats -} - -// RecordIssueAttempted records that an issue was started. -func (s *StatsService) RecordIssueAttempted(repo string) { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.IssuesAttempted++ - s.ensureRepo(repo) - s.updateStreak() - s.updateDailyActivity("issue") - s.save() -} - -// RecordIssueCompleted records that an issue was completed. -func (s *StatsService) RecordIssueCompleted(repo string) { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.IssuesCompleted++ - if rs, ok := s.stats.ReposContributed[repo]; ok { - rs.IssuesFixed++ - rs.LastContrib = time.Now() - } - s.save() -} - -// RecordIssueSkipped records that an issue was skipped. -func (s *StatsService) RecordIssueSkipped() { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.IssuesSkipped++ - s.save() -} - -// RecordPRSubmitted records that a PR was submitted. -func (s *StatsService) RecordPRSubmitted(repo string) { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.PRsSubmitted++ - if rs, ok := s.stats.ReposContributed[repo]; ok { - rs.PRsSubmitted++ - rs.LastContrib = time.Now() - } - s.updateDailyActivity("pr") - s.save() -} - -// RecordPRMerged records that a PR was merged. -func (s *StatsService) RecordPRMerged(repo string) { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.PRsMerged++ - if rs, ok := s.stats.ReposContributed[repo]; ok { - rs.PRsMerged++ - } - s.save() -} - -// RecordPRRejected records that a PR was rejected. -func (s *StatsService) RecordPRRejected() { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.PRsRejected++ - s.save() -} - -// RecordTimeSpent adds time spent on an issue. -func (s *StatsService) RecordTimeSpent(duration time.Duration) { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats.TotalTimeSpent += duration - - // Recalculate average - if s.stats.PRsSubmitted > 0 { - s.stats.AverageTimePerPR = s.stats.TotalTimeSpent / time.Duration(s.stats.PRsSubmitted) - } - - // Update daily activity - today := time.Now().Format("2006-01-02") - if day, ok := s.stats.DailyActivity[today]; ok { - day.TimeSpent += int(duration.Minutes()) - } - - s.save() -} - -// GetRepoStats returns statistics for a specific repository. -func (s *StatsService) GetRepoStats(repo string) *RepoStats { - s.mu.RLock() - defer s.mu.RUnlock() - return s.stats.ReposContributed[repo] -} - -// GetTopRepos returns the top N repositories by contributions. -func (s *StatsService) GetTopRepos(n int) []*RepoStats { - s.mu.RLock() - defer s.mu.RUnlock() - - repos := make([]*RepoStats, 0, len(s.stats.ReposContributed)) - for _, rs := range s.stats.ReposContributed { - repos = append(repos, rs) - } - - // Sort by PRs merged (descending) - for i := 0; i < len(repos)-1; i++ { - for j := i + 1; j < len(repos); j++ { - if repos[j].PRsMerged > repos[i].PRsMerged { - repos[i], repos[j] = repos[j], repos[i] - } - } - } - - if n > len(repos) { - n = len(repos) - } - return repos[:n] -} - -// GetActivityHistory returns the activity for the last N days. -func (s *StatsService) GetActivityHistory(days int) []*DayStats { - s.mu.RLock() - defer s.mu.RUnlock() - - result := make([]*DayStats, 0, days) - now := time.Now() - - for i := 0; i < days; i++ { - date := now.AddDate(0, 0, -i).Format("2006-01-02") - if day, ok := s.stats.DailyActivity[date]; ok { - result = append(result, day) - } else { - result = append(result, &DayStats{Date: date}) - } - } - - return result -} - -// ensureRepo creates a repo stats entry if it doesn't exist. -func (s *StatsService) ensureRepo(repo string) { - if _, ok := s.stats.ReposContributed[repo]; !ok { - s.stats.ReposContributed[repo] = &RepoStats{ - Name: repo, - FirstContrib: time.Now(), - LastContrib: time.Now(), - } - } -} - -// updateStreak updates the contribution streak. -func (s *StatsService) updateStreak() { - now := time.Now() - lastActivity := s.stats.LastActivity - - if lastActivity.IsZero() { - s.stats.CurrentStreak = 1 - } else { - daysSince := int(now.Sub(lastActivity).Hours() / 24) - if daysSince <= 1 { - // Same day or next day - if daysSince == 1 || now.Day() != lastActivity.Day() { - s.stats.CurrentStreak++ - } - } else { - // Streak broken - s.stats.CurrentStreak = 1 - } - } - - if s.stats.CurrentStreak > s.stats.LongestStreak { - s.stats.LongestStreak = s.stats.CurrentStreak - } - - s.stats.LastActivity = now -} - -// updateDailyActivity updates today's activity. -func (s *StatsService) updateDailyActivity(activityType string) { - today := time.Now().Format("2006-01-02") - - if _, ok := s.stats.DailyActivity[today]; !ok { - s.stats.DailyActivity[today] = &DayStats{Date: today} - } - - day := s.stats.DailyActivity[today] - switch activityType { - case "issue": - day.IssuesWorked++ - case "pr": - day.PRsSubmitted++ - } - - // Clean up old entries (keep last 90 days) - cutoff := time.Now().AddDate(0, 0, -90).Format("2006-01-02") - for date := range s.stats.DailyActivity { - if date < cutoff { - delete(s.stats.DailyActivity, date) - } - } -} - -// save persists stats to disk. -func (s *StatsService) save() { - dataDir := s.config.GetDataDir() - if dataDir == "" { - return - } - - path := filepath.Join(dataDir, "stats.json") - data, err := json.MarshalIndent(s.stats, "", " ") - if err != nil { - log.Printf("Failed to marshal stats: %v", err) - return - } - - if err := os.WriteFile(path, data, 0644); err != nil { - log.Printf("Failed to save stats: %v", err) - } -} - -// load restores stats from disk. -func (s *StatsService) load() { - dataDir := s.config.GetDataDir() - if dataDir == "" { - return - } - - path := filepath.Join(dataDir, "stats.json") - data, err := os.ReadFile(path) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("Failed to read stats: %v", err) - } - return - } - - var stats Stats - if err := json.Unmarshal(data, &stats); err != nil { - log.Printf("Failed to unmarshal stats: %v", err) - return - } - - // Ensure maps are initialized - if stats.ReposContributed == nil { - stats.ReposContributed = make(map[string]*RepoStats) - } - if stats.DailyActivity == nil { - stats.DailyActivity = make(map[string]*DayStats) - } - - s.stats = &stats -} - -// Reset clears all statistics. -func (s *StatsService) Reset() error { - s.mu.Lock() - defer s.mu.Unlock() - - s.stats = &Stats{ - ReposContributed: make(map[string]*RepoStats), - DailyActivity: make(map[string]*DayStats), - } - s.save() - return nil -} diff --git a/internal/bugseti/submit.go b/internal/bugseti/submit.go deleted file mode 100644 index 1b4feca1..00000000 --- a/internal/bugseti/submit.go +++ /dev/null @@ -1,366 +0,0 @@ -// Package bugseti provides services for the BugSETI distributed bug fixing application. -package bugseti - -import ( - "bytes" - "context" - "fmt" - "log" - "os/exec" - "strings" - "time" - - forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" - - "forge.lthn.ai/core/go/pkg/forge" -) - -// SubmitService handles the PR submission flow. -type SubmitService struct { - config *ConfigService - notify *NotifyService - stats *StatsService - forge *forge.Client -} - -// NewSubmitService creates a new SubmitService. -func NewSubmitService(config *ConfigService, notify *NotifyService, stats *StatsService, forgeClient *forge.Client) *SubmitService { - return &SubmitService{ - config: config, - notify: notify, - stats: stats, - forge: forgeClient, - } -} - -// ServiceName returns the service name for Wails. -func (s *SubmitService) ServiceName() string { - return "SubmitService" -} - -// PRSubmission contains the data for a pull request submission. -type PRSubmission struct { - Issue *Issue `json:"issue"` - Title string `json:"title"` - Body string `json:"body"` - Branch string `json:"branch"` - CommitMsg string `json:"commitMsg"` - Files []string `json:"files"` - WorkDir string `json:"workDir"` -} - -// PRResult contains the result of a PR submission. -type PRResult struct { - Success bool `json:"success"` - PRURL string `json:"prUrl,omitempty"` - PRNumber int `json:"prNumber,omitempty"` - Error string `json:"error,omitempty"` - ForkOwner string `json:"forkOwner,omitempty"` -} - -// Submit creates a pull request for the given issue. -// Flow: Fork -> Branch -> Commit -> Push -> PR -func (s *SubmitService) Submit(submission *PRSubmission) (*PRResult, error) { - if submission == nil || submission.Issue == nil { - return nil, fmt.Errorf("invalid submission") - } - - issue := submission.Issue - workDir := submission.WorkDir - if workDir == "" { - return nil, fmt.Errorf("work directory not specified") - } - - guard := getEthicsGuardWithRoot(context.Background(), s.config.GetMarketplaceMCPRoot()) - issueTitle := guard.SanitizeTitle(issue.Title) - - owner, repoName, err := splitRepo(issue.Repo) - if err != nil { - return &PRResult{Success: false, Error: err.Error()}, err - } - - // Step 1: Ensure we have a fork - forkOwner, err := s.ensureFork(owner, repoName) - if err != nil { - return &PRResult{Success: false, Error: fmt.Sprintf("fork failed: %v", err)}, err - } - - // Step 2: Create branch - branch := submission.Branch - if branch == "" { - branch = fmt.Sprintf("bugseti/issue-%d", issue.Number) - } - if err := s.createBranch(workDir, branch); err != nil { - return &PRResult{Success: false, Error: fmt.Sprintf("branch creation failed: %v", err)}, err - } - - // Step 3: Stage and commit changes - commitMsg := submission.CommitMsg - if commitMsg == "" { - commitMsg = fmt.Sprintf("fix: resolve issue #%d\n\n%s\n\nFixes #%d", issue.Number, issueTitle, issue.Number) - } else { - commitMsg = guard.SanitizeBody(commitMsg) - } - if err := s.commitChanges(workDir, submission.Files, commitMsg); err != nil { - return &PRResult{Success: false, Error: fmt.Sprintf("commit failed: %v", err)}, err - } - - // Step 4: Push to fork - if err := s.pushToFork(workDir, forkOwner, repoName, branch); err != nil { - return &PRResult{Success: false, Error: fmt.Sprintf("push failed: %v", err)}, err - } - - // Step 5: Create PR - prTitle := submission.Title - if prTitle == "" { - prTitle = fmt.Sprintf("Fix #%d: %s", issue.Number, issueTitle) - } else { - prTitle = guard.SanitizeTitle(prTitle) - } - prBody := submission.Body - if prBody == "" { - prBody = s.generatePRBody(issue) - } - prBody = guard.SanitizeBody(prBody) - - prURL, prNumber, err := s.createPR(owner, repoName, forkOwner, branch, prTitle, prBody) - if err != nil { - return &PRResult{Success: false, Error: fmt.Sprintf("PR creation failed: %v", err)}, err - } - - // Update stats - s.stats.RecordPRSubmitted(issue.Repo) - - // Notify user - s.notify.Notify("BugSETI", fmt.Sprintf("PR #%d submitted for issue #%d", prNumber, issue.Number)) - - return &PRResult{ - Success: true, - PRURL: prURL, - PRNumber: prNumber, - ForkOwner: forkOwner, - }, nil -} - -// ensureFork ensures a fork exists for the repo, returns the fork owner's username. -func (s *SubmitService) ensureFork(owner, repo string) (string, error) { - // Get current user - user, err := s.forge.GetCurrentUser() - if err != nil { - return "", fmt.Errorf("failed to get current user: %w", err) - } - username := user.UserName - - // Check if fork already exists - _, err = s.forge.GetRepo(username, repo) - if err == nil { - return username, nil - } - - // Fork doesn't exist, create it - log.Printf("Creating fork of %s/%s...", owner, repo) - _, err = s.forge.ForkRepo(owner, repo, "") - if err != nil { - return "", fmt.Errorf("failed to create fork: %w", err) - } - - // Wait for Forgejo to process the fork - time.Sleep(2 * time.Second) - - return username, nil -} - -// createBranch creates a new branch in the repository. -func (s *SubmitService) createBranch(workDir, branch string) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Fetch latest from upstream - cmd := exec.CommandContext(ctx, "git", "fetch", "origin") - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - log.Printf("WARNING: git fetch origin failed in %s: %v (proceeding with potentially stale data)", workDir, err) - } - - // Create and checkout new branch - cmd = exec.CommandContext(ctx, "git", "checkout", "-b", branch) - cmd.Dir = workDir - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - // Branch might already exist, try to checkout - cmd = exec.CommandContext(ctx, "git", "checkout", branch) - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create/checkout branch: %s: %w", stderr.String(), err) - } - } - - return nil -} - -// commitChanges stages and commits the specified files. -func (s *SubmitService) commitChanges(workDir string, files []string, message string) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Stage files - if len(files) == 0 { - // Stage all changes - cmd := exec.CommandContext(ctx, "git", "add", "-A") - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to stage changes: %w", err) - } - } else { - // Stage specific files - args := append([]string{"add"}, files...) - cmd := exec.CommandContext(ctx, "git", args...) - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to stage files: %w", err) - } - } - - // Check if there are changes to commit - cmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--quiet") - cmd.Dir = workDir - if err := cmd.Run(); err == nil { - return fmt.Errorf("no changes to commit") - } - - // Commit - cmd = exec.CommandContext(ctx, "git", "commit", "-m", message) - cmd.Dir = workDir - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to commit: %s: %w", stderr.String(), err) - } - - return nil -} - -// pushToFork pushes the branch to the user's fork. -func (s *SubmitService) pushToFork(workDir, forkOwner, repoName, branch string) error { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - - // Add fork as remote if not exists - forkRemote := "fork" - cmd := exec.CommandContext(ctx, "git", "remote", "get-url", forkRemote) - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - // Construct fork URL using the forge instance URL - forkURL := fmt.Sprintf("%s/%s/%s.git", strings.TrimRight(s.forge.URL(), "/"), forkOwner, repoName) - - // Embed token for HTTPS push auth - if s.forge.Token() != "" { - forkURL = strings.Replace(forkURL, "://", fmt.Sprintf("://bugseti:%s@", s.forge.Token()), 1) - } - - cmd = exec.CommandContext(ctx, "git", "remote", "add", forkRemote, forkURL) - cmd.Dir = workDir - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to add fork remote: %w", err) - } - } - - // Push to fork - cmd = exec.CommandContext(ctx, "git", "push", "-u", forkRemote, branch) - cmd.Dir = workDir - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to push: %s: %w", stderr.String(), err) - } - - return nil -} - -// createPR creates a pull request using the Forgejo API. -func (s *SubmitService) createPR(owner, repo, forkOwner, branch, title, body string) (string, int, error) { - pr, err := s.forge.CreatePullRequest(owner, repo, forgejo.CreatePullRequestOption{ - Head: fmt.Sprintf("%s:%s", forkOwner, branch), - Base: "main", - Title: title, - Body: body, - }) - if err != nil { - return "", 0, fmt.Errorf("failed to create PR: %w", err) - } - - return pr.HTMLURL, int(pr.Index), nil -} - -// generatePRBody creates a default PR body for an issue. -func (s *SubmitService) generatePRBody(issue *Issue) string { - var body strings.Builder - - body.WriteString("## Summary\n\n") - body.WriteString(fmt.Sprintf("This PR addresses issue #%d.\n\n", issue.Number)) - - if issue.Context != nil && issue.Context.Summary != "" { - body.WriteString("## Context\n\n") - body.WriteString(issue.Context.Summary) - body.WriteString("\n\n") - } - - body.WriteString("## Changes\n\n") - body.WriteString("\n\n") - - body.WriteString("## Testing\n\n") - body.WriteString("\n\n") - - body.WriteString("---\n\n") - body.WriteString("*Submitted via [BugSETI](https://forge.lthn.ai/core/cli) - Distributed Bug Fixing*\n") - - return body.String() -} - -// GetPRStatus checks the status of a submitted PR. -func (s *SubmitService) GetPRStatus(repo string, prNumber int) (*PRStatus, error) { - owner, repoName, err := splitRepo(repo) - if err != nil { - return nil, err - } - - pr, err := s.forge.GetPullRequest(owner, repoName, int64(prNumber)) - if err != nil { - return nil, fmt.Errorf("failed to get PR status: %w", err) - } - - status := &PRStatus{ - State: string(pr.State), - Mergeable: pr.Mergeable, - } - - // Check CI status via combined commit status - if pr.Head != nil { - combined, err := s.forge.GetCombinedStatus(owner, repoName, pr.Head.Sha) - if err == nil && combined != nil { - status.CIPassing = combined.State == forgejo.StatusSuccess - } - } - - // Check review status - reviews, err := s.forge.ListPRReviews(owner, repoName, int64(prNumber)) - if err == nil { - for _, review := range reviews { - if review.State == forgejo.ReviewStateApproved { - status.Approved = true - break - } - } - } - - return status, nil -} - -// PRStatus represents the current status of a PR. -type PRStatus struct { - State string `json:"state"` - Mergeable bool `json:"mergeable"` - CIPassing bool `json:"ciPassing"` - Approved bool `json:"approved"` -} diff --git a/internal/bugseti/submit_test.go b/internal/bugseti/submit_test.go deleted file mode 100644 index 80a3999f..00000000 --- a/internal/bugseti/submit_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package bugseti - -import ( - "strings" - "testing" -) - -func testSubmitService(t *testing.T) *SubmitService { - t.Helper() - cfg := testConfigService(t, nil, nil) - notify := &NotifyService{enabled: false, config: cfg} - stats := &StatsService{ - config: cfg, - stats: &Stats{ - ReposContributed: make(map[string]*RepoStats), - DailyActivity: make(map[string]*DayStats), - }, - } - return NewSubmitService(cfg, notify, stats, nil) -} - -// --- NewSubmitService / ServiceName --- - -func TestNewSubmitService_Good(t *testing.T) { - s := testSubmitService(t) - if s == nil { - t.Fatal("expected non-nil SubmitService") - } - if s.config == nil || s.notify == nil || s.stats == nil { - t.Fatal("expected all dependencies set") - } -} - -func TestServiceName_Good(t *testing.T) { - s := testSubmitService(t) - if got := s.ServiceName(); got != "SubmitService" { - t.Fatalf("expected %q, got %q", "SubmitService", got) - } -} - -// --- Submit validation --- - -func TestSubmit_Bad_NilSubmission(t *testing.T) { - s := testSubmitService(t) - _, err := s.Submit(nil) - if err == nil { - t.Fatal("expected error for nil submission") - } - if !strings.Contains(err.Error(), "invalid submission") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestSubmit_Bad_NilIssue(t *testing.T) { - s := testSubmitService(t) - _, err := s.Submit(&PRSubmission{Issue: nil}) - if err == nil { - t.Fatal("expected error for nil issue") - } - if !strings.Contains(err.Error(), "invalid submission") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestSubmit_Bad_EmptyWorkDir(t *testing.T) { - s := testSubmitService(t) - _, err := s.Submit(&PRSubmission{ - Issue: &Issue{Number: 1, Repo: "owner/repo", Title: "test"}, - WorkDir: "", - }) - if err == nil { - t.Fatal("expected error for empty work directory") - } - if !strings.Contains(err.Error(), "work directory not specified") { - t.Fatalf("unexpected error: %v", err) - } -} - -// --- generatePRBody --- - -func TestGeneratePRBody_Good_Basic(t *testing.T) { - s := testSubmitService(t) - issue := &Issue{Number: 42, Repo: "owner/repo", Title: "A bug"} - body := s.generatePRBody(issue) - - if !strings.Contains(body, "#42") { - t.Fatal("PR body should reference issue number") - } - if !strings.Contains(body, "## Summary") { - t.Fatal("PR body should have Summary section") - } - if !strings.Contains(body, "## Changes") { - t.Fatal("PR body should have Changes section") - } - if !strings.Contains(body, "## Testing") { - t.Fatal("PR body should have Testing section") - } - if !strings.Contains(body, "BugSETI") { - t.Fatal("PR body should have BugSETI attribution") - } -} - -func TestGeneratePRBody_Good_WithContext(t *testing.T) { - s := testSubmitService(t) - issue := &Issue{ - Number: 7, - Repo: "owner/repo", - Title: "Fix login", - Context: &IssueContext{ - Summary: "The login endpoint returns 500 on empty password.", - }, - } - body := s.generatePRBody(issue) - - if !strings.Contains(body, "## Context") { - t.Fatal("PR body should have Context section when context exists") - } - if !strings.Contains(body, "login endpoint returns 500") { - t.Fatal("PR body should include context summary") - } -} - -func TestGeneratePRBody_Good_WithoutContext(t *testing.T) { - s := testSubmitService(t) - issue := &Issue{Number: 7, Repo: "owner/repo", Title: "Fix login"} - body := s.generatePRBody(issue) - - if strings.Contains(body, "## Context") { - t.Fatal("PR body should omit Context section when no context") - } -} - -func TestGeneratePRBody_Good_EmptyContextSummary(t *testing.T) { - s := testSubmitService(t) - issue := &Issue{ - Number: 7, - Repo: "owner/repo", - Title: "Fix login", - Context: &IssueContext{Summary: ""}, - } - body := s.generatePRBody(issue) - - if strings.Contains(body, "## Context") { - t.Fatal("PR body should omit Context section when summary is empty") - } -} - -// --- PRSubmission / PRResult struct tests --- - -func TestPRSubmission_Good_Defaults(t *testing.T) { - sub := &PRSubmission{ - Issue: &Issue{Number: 10, Repo: "o/r"}, - WorkDir: "/tmp/work", - } - if sub.Branch != "" { - t.Fatal("expected empty branch to be default") - } - if sub.Title != "" { - t.Fatal("expected empty title to be default") - } - if sub.CommitMsg != "" { - t.Fatal("expected empty commit msg to be default") - } -} - -func TestPRResult_Good_Success(t *testing.T) { - r := &PRResult{ - Success: true, - PRURL: "https://forge.lthn.ai/o/r/pulls/1", - PRNumber: 1, - ForkOwner: "me", - } - if !r.Success { - t.Fatal("expected success") - } - if r.Error != "" { - t.Fatal("expected no error on success") - } -} - -func TestPRResult_Good_Failure(t *testing.T) { - r := &PRResult{ - Success: false, - Error: "fork failed: something", - } - if r.Success { - t.Fatal("expected failure") - } - if r.Error == "" { - t.Fatal("expected error message") - } -} - -// --- PRStatus struct --- - -func TestPRStatus_Good(t *testing.T) { - s := &PRStatus{ - State: "open", - Mergeable: true, - CIPassing: true, - Approved: false, - } - if s.State != "open" { - t.Fatalf("expected open, got %s", s.State) - } - if !s.Mergeable { - t.Fatal("expected mergeable") - } - if s.Approved { - t.Fatal("expected not approved") - } -} - -// --- splitRepo --- - -func TestSplitRepo_Good(t *testing.T) { - owner, repo, err := splitRepo("myorg/myrepo") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if owner != "myorg" || repo != "myrepo" { - t.Fatalf("expected myorg/myrepo, got %s/%s", owner, repo) - } -} - -func TestSplitRepo_Bad(t *testing.T) { - _, _, err := splitRepo("invalidrepo") - if err == nil { - t.Fatal("expected error for invalid repo format") - } - if !strings.Contains(err.Error(), "invalid repo format") { - t.Fatalf("unexpected error: %v", err) - } -} diff --git a/internal/bugseti/updater/channels.go b/internal/bugseti/updater/channels.go deleted file mode 100644 index 79ec4a82..00000000 --- a/internal/bugseti/updater/channels.go +++ /dev/null @@ -1,176 +0,0 @@ -// Package updater provides auto-update functionality for BugSETI. -package updater - -import ( - "fmt" - "regexp" - "strings" -) - -// Channel represents an update channel. -type Channel string - -const ( - // ChannelStable is the production release channel. - // Tags: bugseti-vX.Y.Z (e.g., bugseti-v1.0.0) - ChannelStable Channel = "stable" - - // ChannelBeta is the pre-release testing channel. - // Tags: bugseti-vX.Y.Z-beta.N (e.g., bugseti-v1.0.0-beta.1) - ChannelBeta Channel = "beta" - - // ChannelNightly is the latest development builds channel. - // Tags: bugseti-nightly-YYYYMMDD (e.g., bugseti-nightly-20260205) - ChannelNightly Channel = "nightly" -) - -// String returns the string representation of the channel. -func (c Channel) String() string { - return string(c) -} - -// DisplayName returns a human-readable name for the channel. -func (c Channel) DisplayName() string { - switch c { - case ChannelStable: - return "Stable" - case ChannelBeta: - return "Beta" - case ChannelNightly: - return "Nightly" - default: - return "Unknown" - } -} - -// Description returns a description of the channel. -func (c Channel) Description() string { - switch c { - case ChannelStable: - return "Production releases - most stable, recommended for most users" - case ChannelBeta: - return "Pre-release builds - new features being tested before stable release" - case ChannelNightly: - return "Latest development builds - bleeding edge, may be unstable" - default: - return "Unknown channel" - } -} - -// TagPrefix returns the tag prefix used for this channel. -func (c Channel) TagPrefix() string { - switch c { - case ChannelStable: - return "bugseti-v" - case ChannelBeta: - return "bugseti-v" - case ChannelNightly: - return "bugseti-nightly-" - default: - return "" - } -} - -// TagPattern returns a regex pattern to match tags for this channel. -func (c Channel) TagPattern() *regexp.Regexp { - switch c { - case ChannelStable: - // Match bugseti-vX.Y.Z but NOT bugseti-vX.Y.Z-beta.N - return regexp.MustCompile(`^bugseti-v(\d+\.\d+\.\d+)$`) - case ChannelBeta: - // Match bugseti-vX.Y.Z-beta.N - return regexp.MustCompile(`^bugseti-v(\d+\.\d+\.\d+-beta\.\d+)$`) - case ChannelNightly: - // Match bugseti-nightly-YYYYMMDD - return regexp.MustCompile(`^bugseti-nightly-(\d{8})$`) - default: - return nil - } -} - -// MatchesTag returns true if the given tag matches this channel's pattern. -func (c Channel) MatchesTag(tag string) bool { - pattern := c.TagPattern() - if pattern == nil { - return false - } - return pattern.MatchString(tag) -} - -// ExtractVersion extracts the version from a tag for this channel. -func (c Channel) ExtractVersion(tag string) string { - pattern := c.TagPattern() - if pattern == nil { - return "" - } - matches := pattern.FindStringSubmatch(tag) - if len(matches) < 2 { - return "" - } - return matches[1] -} - -// AllChannels returns all available channels. -func AllChannels() []Channel { - return []Channel{ChannelStable, ChannelBeta, ChannelNightly} -} - -// ParseChannel parses a string into a Channel. -func ParseChannel(s string) (Channel, error) { - switch strings.ToLower(s) { - case "stable": - return ChannelStable, nil - case "beta": - return ChannelBeta, nil - case "nightly": - return ChannelNightly, nil - default: - return "", fmt.Errorf("unknown channel: %s", s) - } -} - -// ChannelInfo contains information about an update channel. -type ChannelInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` -} - -// GetChannelInfo returns information about a channel. -func GetChannelInfo(c Channel) ChannelInfo { - return ChannelInfo{ - ID: c.String(), - Name: c.DisplayName(), - Description: c.Description(), - } -} - -// GetAllChannelInfo returns information about all channels. -func GetAllChannelInfo() []ChannelInfo { - channels := AllChannels() - info := make([]ChannelInfo, len(channels)) - for i, c := range channels { - info[i] = GetChannelInfo(c) - } - return info -} - -// IncludesPrerelease returns true if the channel includes pre-release versions. -func (c Channel) IncludesPrerelease() bool { - return c == ChannelBeta || c == ChannelNightly -} - -// IncludesChannel returns true if this channel should include releases from the given channel. -// For example, beta channel includes stable releases, nightly includes both. -func (c Channel) IncludesChannel(other Channel) bool { - switch c { - case ChannelStable: - return other == ChannelStable - case ChannelBeta: - return other == ChannelStable || other == ChannelBeta - case ChannelNightly: - return true // Nightly users can see all releases - default: - return false - } -} diff --git a/internal/bugseti/updater/checker.go b/internal/bugseti/updater/checker.go deleted file mode 100644 index 368cb9e3..00000000 --- a/internal/bugseti/updater/checker.go +++ /dev/null @@ -1,379 +0,0 @@ -// Package updater provides auto-update functionality for BugSETI. -package updater - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "runtime" - "sort" - "strings" - "time" - - "golang.org/x/mod/semver" -) - -const ( - // GitHubReleasesAPI is the GitHub API endpoint for releases. - GitHubReleasesAPI = "https://api.github.com/repos/%s/%s/releases" - - // DefaultOwner is the default GitHub repository owner. - DefaultOwner = "host-uk" - - // DefaultRepo is the default GitHub repository name. - DefaultRepo = "core" - - // DefaultCheckInterval is the default interval between update checks. - DefaultCheckInterval = 6 * time.Hour -) - -// GitHubRelease represents a GitHub release from the API. -type GitHubRelease struct { - TagName string `json:"tag_name"` - Name string `json:"name"` - Body string `json:"body"` - Draft bool `json:"draft"` - Prerelease bool `json:"prerelease"` - PublishedAt time.Time `json:"published_at"` - Assets []GitHubAsset `json:"assets"` - HTMLURL string `json:"html_url"` -} - -// GitHubAsset represents a release asset from the GitHub API. -type GitHubAsset struct { - Name string `json:"name"` - Size int64 `json:"size"` - BrowserDownloadURL string `json:"browser_download_url"` - ContentType string `json:"content_type"` -} - -// ReleaseInfo contains information about an available release. -type ReleaseInfo struct { - Version string `json:"version"` - Channel Channel `json:"channel"` - Tag string `json:"tag"` - Name string `json:"name"` - Body string `json:"body"` - PublishedAt time.Time `json:"publishedAt"` - HTMLURL string `json:"htmlUrl"` - BinaryURL string `json:"binaryUrl"` - ArchiveURL string `json:"archiveUrl"` - ChecksumURL string `json:"checksumUrl"` - Size int64 `json:"size"` -} - -// UpdateCheckResult contains the result of an update check. -type UpdateCheckResult struct { - Available bool `json:"available"` - CurrentVersion string `json:"currentVersion"` - LatestVersion string `json:"latestVersion"` - Release *ReleaseInfo `json:"release,omitempty"` - Error string `json:"error,omitempty"` - CheckedAt time.Time `json:"checkedAt"` -} - -// Checker checks for available updates. -type Checker struct { - owner string - repo string - httpClient *http.Client -} - -// NewChecker creates a new update checker. -func NewChecker() *Checker { - return &Checker{ - owner: DefaultOwner, - repo: DefaultRepo, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// CheckForUpdate checks if a newer version is available. -func (c *Checker) CheckForUpdate(ctx context.Context, currentVersion string, channel Channel) (*UpdateCheckResult, error) { - result := &UpdateCheckResult{ - CurrentVersion: currentVersion, - CheckedAt: time.Now(), - } - - // Fetch releases from GitHub - releases, err := c.fetchReleases(ctx) - if err != nil { - result.Error = err.Error() - return result, err - } - - // Find the latest release for the channel - latest := c.findLatestRelease(releases, channel) - if latest == nil { - result.LatestVersion = currentVersion - return result, nil - } - - result.LatestVersion = latest.Version - result.Release = latest - - // Compare versions - if c.isNewerVersion(currentVersion, latest.Version, channel) { - result.Available = true - } - - return result, nil -} - -// fetchReleases fetches all releases from GitHub. -func (c *Checker) fetchReleases(ctx context.Context) ([]GitHubRelease, error) { - url := fmt.Sprintf(GitHubReleasesAPI, c.owner, c.repo) - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Accept", "application/vnd.github.v3+json") - req.Header.Set("User-Agent", "BugSETI-Updater") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to fetch releases: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode) - } - - var releases []GitHubRelease - if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { - return nil, fmt.Errorf("failed to decode releases: %w", err) - } - - return releases, nil -} - -// findLatestRelease finds the latest release for the given channel. -func (c *Checker) findLatestRelease(releases []GitHubRelease, channel Channel) *ReleaseInfo { - var candidates []ReleaseInfo - - for _, release := range releases { - // Skip drafts - if release.Draft { - continue - } - - // Check if the tag matches our BugSETI release pattern - if !strings.HasPrefix(release.TagName, "bugseti-") { - continue - } - - // Determine the channel for this release - releaseChannel := c.determineChannel(release.TagName) - if releaseChannel == "" { - continue - } - - // Check if this release should be considered for the requested channel - if !channel.IncludesChannel(releaseChannel) { - continue - } - - // Extract version - version := releaseChannel.ExtractVersion(release.TagName) - if version == "" { - continue - } - - // Find the appropriate asset for this platform - binaryName := c.getBinaryName() - archiveName := c.getArchiveName() - checksumName := archiveName + ".sha256" - - var binaryURL, archiveURL, checksumURL string - var size int64 - - for _, asset := range release.Assets { - switch asset.Name { - case binaryName: - binaryURL = asset.BrowserDownloadURL - size = asset.Size - case archiveName: - archiveURL = asset.BrowserDownloadURL - if size == 0 { - size = asset.Size - } - case checksumName: - checksumURL = asset.BrowserDownloadURL - } - } - - // Skip if no binary available for this platform - if binaryURL == "" && archiveURL == "" { - continue - } - - candidates = append(candidates, ReleaseInfo{ - Version: version, - Channel: releaseChannel, - Tag: release.TagName, - Name: release.Name, - Body: release.Body, - PublishedAt: release.PublishedAt, - HTMLURL: release.HTMLURL, - BinaryURL: binaryURL, - ArchiveURL: archiveURL, - ChecksumURL: checksumURL, - Size: size, - }) - } - - if len(candidates) == 0 { - return nil - } - - // Sort by version (newest first) - sort.Slice(candidates, func(i, j int) bool { - return c.compareVersions(candidates[i].Version, candidates[j].Version, channel) > 0 - }) - - return &candidates[0] -} - -// determineChannel determines the channel from a release tag. -func (c *Checker) determineChannel(tag string) Channel { - for _, ch := range AllChannels() { - if ch.MatchesTag(tag) { - return ch - } - } - return "" -} - -// getBinaryName returns the binary name for the current platform. -func (c *Checker) getBinaryName() string { - ext := "" - if runtime.GOOS == "windows" { - ext = ".exe" - } - return fmt.Sprintf("bugseti-%s-%s%s", runtime.GOOS, runtime.GOARCH, ext) -} - -// getArchiveName returns the archive name for the current platform. -func (c *Checker) getArchiveName() string { - ext := "tar.gz" - if runtime.GOOS == "windows" { - ext = "zip" - } - return fmt.Sprintf("bugseti-%s-%s.%s", runtime.GOOS, runtime.GOARCH, ext) -} - -// isNewerVersion returns true if newVersion is newer than currentVersion. -func (c *Checker) isNewerVersion(currentVersion, newVersion string, channel Channel) bool { - // Handle nightly versions (date-based) - if channel == ChannelNightly { - return newVersion > currentVersion - } - - // Handle dev builds - if currentVersion == "dev" { - return true - } - - // Use semver comparison - current := c.normalizeSemver(currentVersion) - new := c.normalizeSemver(newVersion) - - return semver.Compare(new, current) > 0 -} - -// compareVersions compares two versions. -func (c *Checker) compareVersions(v1, v2 string, channel Channel) int { - // Handle nightly versions (date-based) - if channel == ChannelNightly { - if v1 > v2 { - return 1 - } else if v1 < v2 { - return -1 - } - return 0 - } - - // Use semver comparison - return semver.Compare(c.normalizeSemver(v1), c.normalizeSemver(v2)) -} - -// normalizeSemver ensures a version string has the 'v' prefix for semver. -func (c *Checker) normalizeSemver(version string) string { - if !strings.HasPrefix(version, "v") { - return "v" + version - } - return version -} - -// GetAllReleases returns all BugSETI releases from GitHub. -func (c *Checker) GetAllReleases(ctx context.Context) ([]ReleaseInfo, error) { - releases, err := c.fetchReleases(ctx) - if err != nil { - return nil, err - } - - var result []ReleaseInfo - for _, release := range releases { - if release.Draft { - continue - } - - if !strings.HasPrefix(release.TagName, "bugseti-") { - continue - } - - releaseChannel := c.determineChannel(release.TagName) - if releaseChannel == "" { - continue - } - - version := releaseChannel.ExtractVersion(release.TagName) - if version == "" { - continue - } - - binaryName := c.getBinaryName() - archiveName := c.getArchiveName() - checksumName := archiveName + ".sha256" - - var binaryURL, archiveURL, checksumURL string - var size int64 - - for _, asset := range release.Assets { - switch asset.Name { - case binaryName: - binaryURL = asset.BrowserDownloadURL - size = asset.Size - case archiveName: - archiveURL = asset.BrowserDownloadURL - if size == 0 { - size = asset.Size - } - case checksumName: - checksumURL = asset.BrowserDownloadURL - } - } - - result = append(result, ReleaseInfo{ - Version: version, - Channel: releaseChannel, - Tag: release.TagName, - Name: release.Name, - Body: release.Body, - PublishedAt: release.PublishedAt, - HTMLURL: release.HTMLURL, - BinaryURL: binaryURL, - ArchiveURL: archiveURL, - ChecksumURL: checksumURL, - Size: size, - }) - } - - return result, nil -} diff --git a/internal/bugseti/updater/download.go b/internal/bugseti/updater/download.go deleted file mode 100644 index 2ce6120b..00000000 --- a/internal/bugseti/updater/download.go +++ /dev/null @@ -1,427 +0,0 @@ -// Package updater provides auto-update functionality for BugSETI. -package updater - -import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "runtime" - "strings" -) - -// DownloadProgress reports download progress. -type DownloadProgress struct { - BytesDownloaded int64 `json:"bytesDownloaded"` - TotalBytes int64 `json:"totalBytes"` - Percent float64 `json:"percent"` -} - -// DownloadResult contains the result of a download operation. -type DownloadResult struct { - BinaryPath string `json:"binaryPath"` - Version string `json:"version"` - Checksum string `json:"checksum"` - VerifiedOK bool `json:"verifiedOK"` -} - -// Downloader handles downloading and verifying updates. -type Downloader struct { - httpClient *http.Client - stagingDir string - onProgress func(DownloadProgress) -} - -// NewDownloader creates a new update downloader. -func NewDownloader() (*Downloader, error) { - // Create staging directory in user's temp dir - stagingDir := filepath.Join(os.TempDir(), "bugseti-updates") - if err := os.MkdirAll(stagingDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create staging directory: %w", err) - } - - return &Downloader{ - httpClient: &http.Client{}, - stagingDir: stagingDir, - }, nil -} - -// SetProgressCallback sets a callback for download progress updates. -func (d *Downloader) SetProgressCallback(cb func(DownloadProgress)) { - d.onProgress = cb -} - -// Download downloads a release and stages it for installation. -func (d *Downloader) Download(ctx context.Context, release *ReleaseInfo) (*DownloadResult, error) { - result := &DownloadResult{ - Version: release.Version, - } - - // Prefer archive download for extraction - downloadURL := release.ArchiveURL - if downloadURL == "" { - downloadURL = release.BinaryURL - } - if downloadURL == "" { - return nil, fmt.Errorf("no download URL available for release %s", release.Version) - } - - // Download the checksum first if available - var expectedChecksum string - if release.ChecksumURL != "" { - checksum, err := d.downloadChecksum(ctx, release.ChecksumURL) - if err != nil { - // Log but don't fail - checksum verification is optional - fmt.Printf("Warning: could not download checksum: %v\n", err) - } else { - expectedChecksum = checksum - } - } - - // Download the file - downloadedPath, err := d.downloadFile(ctx, downloadURL, release.Size) - if err != nil { - return nil, fmt.Errorf("failed to download update: %w", err) - } - - // Verify checksum if available - actualChecksum, err := d.calculateChecksum(downloadedPath) - if err != nil { - os.Remove(downloadedPath) - return nil, fmt.Errorf("failed to calculate checksum: %w", err) - } - result.Checksum = actualChecksum - - if expectedChecksum != "" { - if actualChecksum != expectedChecksum { - os.Remove(downloadedPath) - return nil, fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum) - } - result.VerifiedOK = true - } - - // Extract if it's an archive - var binaryPath string - if strings.HasSuffix(downloadURL, ".tar.gz") { - binaryPath, err = d.extractTarGz(downloadedPath) - } else if strings.HasSuffix(downloadURL, ".zip") { - binaryPath, err = d.extractZip(downloadedPath) - } else { - // It's a raw binary - binaryPath = downloadedPath - } - - if err != nil { - os.Remove(downloadedPath) - return nil, fmt.Errorf("failed to extract archive: %w", err) - } - - // Make the binary executable (Unix only) - if runtime.GOOS != "windows" { - if err := os.Chmod(binaryPath, 0755); err != nil { - return nil, fmt.Errorf("failed to make binary executable: %w", err) - } - } - - result.BinaryPath = binaryPath - return result, nil -} - -// downloadChecksum downloads and parses a checksum file. -func (d *Downloader) downloadChecksum(ctx context.Context, url string) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return "", err - } - req.Header.Set("User-Agent", "BugSETI-Updater") - - resp, err := d.httpClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("HTTP %d", resp.StatusCode) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // Checksum file format: "hash filename" or just "hash" - parts := strings.Fields(strings.TrimSpace(string(data))) - if len(parts) == 0 { - return "", fmt.Errorf("empty checksum file") - } - - return parts[0], nil -} - -// downloadFile downloads a file with progress reporting. -func (d *Downloader) downloadFile(ctx context.Context, url string, expectedSize int64) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return "", err - } - req.Header.Set("User-Agent", "BugSETI-Updater") - - resp, err := d.httpClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("HTTP %d", resp.StatusCode) - } - - // Get total size from response or use expected size - totalSize := resp.ContentLength - if totalSize <= 0 { - totalSize = expectedSize - } - - // Create output file - filename := filepath.Base(url) - outPath := filepath.Join(d.stagingDir, filename) - out, err := os.Create(outPath) - if err != nil { - return "", err - } - defer out.Close() - - // Download with progress - var downloaded int64 - buf := make([]byte, 32*1024) // 32KB buffer - - for { - select { - case <-ctx.Done(): - os.Remove(outPath) - return "", ctx.Err() - default: - } - - n, readErr := resp.Body.Read(buf) - if n > 0 { - _, writeErr := out.Write(buf[:n]) - if writeErr != nil { - os.Remove(outPath) - return "", writeErr - } - downloaded += int64(n) - - // Report progress - if d.onProgress != nil && totalSize > 0 { - d.onProgress(DownloadProgress{ - BytesDownloaded: downloaded, - TotalBytes: totalSize, - Percent: float64(downloaded) / float64(totalSize) * 100, - }) - } - } - - if readErr == io.EOF { - break - } - if readErr != nil { - os.Remove(outPath) - return "", readErr - } - } - - return outPath, nil -} - -// calculateChecksum calculates the SHA256 checksum of a file. -func (d *Downloader) calculateChecksum(path string) (string, error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - return hex.EncodeToString(h.Sum(nil)), nil -} - -// extractTarGz extracts a .tar.gz archive and returns the path to the binary. -func (d *Downloader) extractTarGz(archivePath string) (string, error) { - f, err := os.Open(archivePath) - if err != nil { - return "", err - } - defer f.Close() - - gzr, err := gzip.NewReader(f) - if err != nil { - return "", err - } - defer gzr.Close() - - tr := tar.NewReader(gzr) - - extractDir := filepath.Join(d.stagingDir, "extracted") - os.RemoveAll(extractDir) - if err := os.MkdirAll(extractDir, 0755); err != nil { - return "", err - } - - var binaryPath string - binaryName := "bugseti" - if runtime.GOOS == "windows" { - binaryName = "bugseti.exe" - } - - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - - target := filepath.Join(extractDir, header.Name) - - // Prevent directory traversal - if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(extractDir)) { - return "", fmt.Errorf("invalid file path in archive: %s", header.Name) - } - - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { - return "", err - } - case tar.TypeReg: - // Create parent directory - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return "", err - } - - outFile, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) - if err != nil { - return "", err - } - - if _, err := io.Copy(outFile, tr); err != nil { - outFile.Close() - return "", err - } - outFile.Close() - - // Check if this is the binary we're looking for - if filepath.Base(header.Name) == binaryName { - binaryPath = target - } - } - } - - // Clean up archive - os.Remove(archivePath) - - if binaryPath == "" { - return "", fmt.Errorf("binary not found in archive") - } - - return binaryPath, nil -} - -// extractZip extracts a .zip archive and returns the path to the binary. -func (d *Downloader) extractZip(archivePath string) (string, error) { - r, err := zip.OpenReader(archivePath) - if err != nil { - return "", err - } - defer r.Close() - - extractDir := filepath.Join(d.stagingDir, "extracted") - os.RemoveAll(extractDir) - if err := os.MkdirAll(extractDir, 0755); err != nil { - return "", err - } - - var binaryPath string - binaryName := "bugseti" - if runtime.GOOS == "windows" { - binaryName = "bugseti.exe" - } - - for _, f := range r.File { - target := filepath.Join(extractDir, f.Name) - - // Prevent directory traversal - if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(extractDir)) { - return "", fmt.Errorf("invalid file path in archive: %s", f.Name) - } - - if f.FileInfo().IsDir() { - if err := os.MkdirAll(target, 0755); err != nil { - return "", err - } - continue - } - - // Create parent directory - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return "", err - } - - rc, err := f.Open() - if err != nil { - return "", err - } - - outFile, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) - if err != nil { - rc.Close() - return "", err - } - - _, err = io.Copy(outFile, rc) - rc.Close() - outFile.Close() - - if err != nil { - return "", err - } - - // Check if this is the binary we're looking for - if filepath.Base(f.Name) == binaryName { - binaryPath = target - } - } - - // Clean up archive - os.Remove(archivePath) - - if binaryPath == "" { - return "", fmt.Errorf("binary not found in archive") - } - - return binaryPath, nil -} - -// Cleanup removes all staged files. -func (d *Downloader) Cleanup() error { - return os.RemoveAll(d.stagingDir) -} - -// GetStagingDir returns the staging directory path. -func (d *Downloader) GetStagingDir() string { - return d.stagingDir -} diff --git a/internal/bugseti/updater/go.mod b/internal/bugseti/updater/go.mod deleted file mode 100644 index 5af27f0d..00000000 --- a/internal/bugseti/updater/go.mod +++ /dev/null @@ -1,30 +0,0 @@ -module forge.lthn.ai/core/cli/internal/bugseti/updater - -go 1.25.5 - -require ( - forge.lthn.ai/core/cli/internal/bugseti v0.0.0 - golang.org/x/mod v0.32.0 -) - -require ( - codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect - github.com/42wim/httpsig v1.2.3 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/go-fed/httpsig v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect - github.com/mark3labs/mcp-go v0.43.2 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace forge.lthn.ai/core/cli/internal/bugseti => ../ diff --git a/internal/bugseti/updater/go.sum b/internal/bugseti/updater/go.sum deleted file mode 100644 index 481c06a1..00000000 --- a/internal/bugseti/updater/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/bugseti/updater/install.go b/internal/bugseti/updater/install.go deleted file mode 100644 index a443fa9b..00000000 --- a/internal/bugseti/updater/install.go +++ /dev/null @@ -1,284 +0,0 @@ -// Package updater provides auto-update functionality for BugSETI. -package updater - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "syscall" -) - -// InstallResult contains the result of an installation. -type InstallResult struct { - Success bool `json:"success"` - OldPath string `json:"oldPath"` - NewPath string `json:"newPath"` - BackupPath string `json:"backupPath"` - RestartNeeded bool `json:"restartNeeded"` - Error string `json:"error,omitempty"` -} - -// Installer handles installing updates and restarting the application. -type Installer struct { - executablePath string -} - -// NewInstaller creates a new installer. -func NewInstaller() (*Installer, error) { - execPath, err := os.Executable() - if err != nil { - return nil, fmt.Errorf("failed to get executable path: %w", err) - } - - // Resolve symlinks to get the real path - execPath, err = filepath.EvalSymlinks(execPath) - if err != nil { - return nil, fmt.Errorf("failed to resolve executable path: %w", err) - } - - return &Installer{ - executablePath: execPath, - }, nil -} - -// Install replaces the current binary with the new one. -func (i *Installer) Install(newBinaryPath string) (*InstallResult, error) { - result := &InstallResult{ - OldPath: i.executablePath, - NewPath: newBinaryPath, - RestartNeeded: true, - } - - // Verify the new binary exists and is executable - if _, err := os.Stat(newBinaryPath); err != nil { - result.Error = fmt.Sprintf("new binary not found: %v", err) - return result, fmt.Errorf("new binary not found: %w", err) - } - - // Create backup of current binary - backupPath := i.executablePath + ".bak" - result.BackupPath = backupPath - - // Platform-specific installation - var err error - switch runtime.GOOS { - case "windows": - err = i.installWindows(newBinaryPath, backupPath) - default: - err = i.installUnix(newBinaryPath, backupPath) - } - - if err != nil { - result.Error = err.Error() - return result, err - } - - result.Success = true - return result, nil -} - -// installUnix performs the installation on Unix-like systems. -func (i *Installer) installUnix(newBinaryPath, backupPath string) error { - // Remove old backup if exists - os.Remove(backupPath) - - // Rename current binary to backup - if err := os.Rename(i.executablePath, backupPath); err != nil { - return fmt.Errorf("failed to backup current binary: %w", err) - } - - // Copy new binary to target location - // We use copy instead of rename in case they're on different filesystems - if err := copyFile(newBinaryPath, i.executablePath); err != nil { - // Try to restore backup - os.Rename(backupPath, i.executablePath) - return fmt.Errorf("failed to install new binary: %w", err) - } - - // Make executable - if err := os.Chmod(i.executablePath, 0755); err != nil { - // Try to restore backup - os.Remove(i.executablePath) - os.Rename(backupPath, i.executablePath) - return fmt.Errorf("failed to make binary executable: %w", err) - } - - return nil -} - -// installWindows performs the installation on Windows. -// On Windows, we can't replace a running executable, so we use a different approach: -// 1. Rename current executable to .old -// 2. Copy new executable to target location -// 3. On next start, clean up the .old file -func (i *Installer) installWindows(newBinaryPath, backupPath string) error { - // Remove old backup if exists - os.Remove(backupPath) - - // On Windows, we can rename the running executable - if err := os.Rename(i.executablePath, backupPath); err != nil { - return fmt.Errorf("failed to backup current binary: %w", err) - } - - // Copy new binary to target location - if err := copyFile(newBinaryPath, i.executablePath); err != nil { - // Try to restore backup - os.Rename(backupPath, i.executablePath) - return fmt.Errorf("failed to install new binary: %w", err) - } - - return nil -} - -// Restart restarts the application with the new binary. -func (i *Installer) Restart() error { - args := os.Args - env := os.Environ() - - switch runtime.GOOS { - case "windows": - return i.restartWindows(args, env) - default: - return i.restartUnix(args, env) - } -} - -// restartUnix restarts the application on Unix-like systems using exec. -func (i *Installer) restartUnix(args []string, env []string) error { - // Use syscall.Exec to replace the current process - // This is the cleanest way to restart on Unix - return syscall.Exec(i.executablePath, args, env) -} - -// restartWindows restarts the application on Windows. -func (i *Installer) restartWindows(args []string, env []string) error { - // On Windows, we can't use exec to replace the process - // Instead, we start a new process and exit the current one - cmd := exec.Command(i.executablePath, args[1:]...) - cmd.Env = env - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start new process: %w", err) - } - - // Exit current process - os.Exit(0) - return nil // Never reached -} - -// RestartLater schedules a restart for when the app next starts. -// This is useful when the user wants to continue working and restart later. -func (i *Installer) RestartLater() error { - // Create a marker file that indicates a restart is pending - markerPath := filepath.Join(filepath.Dir(i.executablePath), ".bugseti-restart-pending") - return os.WriteFile(markerPath, []byte("restart"), 0644) -} - -// CheckPendingRestart checks if a restart was scheduled. -func (i *Installer) CheckPendingRestart() bool { - markerPath := filepath.Join(filepath.Dir(i.executablePath), ".bugseti-restart-pending") - _, err := os.Stat(markerPath) - return err == nil -} - -// ClearPendingRestart clears the pending restart marker. -func (i *Installer) ClearPendingRestart() error { - markerPath := filepath.Join(filepath.Dir(i.executablePath), ".bugseti-restart-pending") - return os.Remove(markerPath) -} - -// CleanupBackup removes the backup binary after a successful update. -func (i *Installer) CleanupBackup() error { - backupPath := i.executablePath + ".bak" - if _, err := os.Stat(backupPath); err == nil { - return os.Remove(backupPath) - } - return nil -} - -// Rollback restores the previous version from backup. -func (i *Installer) Rollback() error { - backupPath := i.executablePath + ".bak" - - // Check if backup exists - if _, err := os.Stat(backupPath); err != nil { - return fmt.Errorf("backup not found: %w", err) - } - - // Remove current binary - if err := os.Remove(i.executablePath); err != nil { - return fmt.Errorf("failed to remove current binary: %w", err) - } - - // Restore backup - if err := os.Rename(backupPath, i.executablePath); err != nil { - return fmt.Errorf("failed to restore backup: %w", err) - } - - return nil -} - -// GetExecutablePath returns the path to the current executable. -func (i *Installer) GetExecutablePath() string { - return i.executablePath -} - -// copyFile copies a file from src to dst. -func copyFile(src, dst string) error { - sourceFile, err := os.Open(src) - if err != nil { - return err - } - defer sourceFile.Close() - - // Get source file info for permissions - sourceInfo, err := sourceFile.Stat() - if err != nil { - return err - } - - destFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, sourceInfo.Mode()) - if err != nil { - return err - } - defer destFile.Close() - - _, err = destFile.ReadFrom(sourceFile) - return err -} - -// CanSelfUpdate checks if the application has permission to update itself. -func CanSelfUpdate() bool { - execPath, err := os.Executable() - if err != nil { - return false - } - - execPath, err = filepath.EvalSymlinks(execPath) - if err != nil { - return false - } - - // Check if we can write to the executable's directory - dir := filepath.Dir(execPath) - testFile := filepath.Join(dir, ".bugseti-update-test") - - f, err := os.Create(testFile) - if err != nil { - return false - } - f.Close() - os.Remove(testFile) - - return true -} - -// NeedsElevation returns true if the update requires elevated privileges. -func NeedsElevation() bool { - return !CanSelfUpdate() -} diff --git a/internal/bugseti/updater/service.go b/internal/bugseti/updater/service.go deleted file mode 100644 index 54c60510..00000000 --- a/internal/bugseti/updater/service.go +++ /dev/null @@ -1,322 +0,0 @@ -// Package updater provides auto-update functionality for BugSETI. -package updater - -import ( - "context" - "log" - "sync" - "time" - - "forge.lthn.ai/core/cli/internal/bugseti" -) - -// Service provides update functionality and Wails bindings. -type Service struct { - config *bugseti.ConfigService - checker *Checker - downloader *Downloader - installer *Installer - - mu sync.RWMutex - lastResult *UpdateCheckResult - pendingUpdate *DownloadResult - - // Background check - stopCh chan struct{} - running bool -} - -// NewService creates a new update service. -func NewService(config *bugseti.ConfigService) (*Service, error) { - downloader, err := NewDownloader() - if err != nil { - return nil, err - } - - installer, err := NewInstaller() - if err != nil { - return nil, err - } - - return &Service{ - config: config, - checker: NewChecker(), - downloader: downloader, - installer: installer, - }, nil -} - -// ServiceName returns the service name for Wails. -func (s *Service) ServiceName() string { - return "UpdateService" -} - -// Start begins the background update checker. -func (s *Service) Start() { - s.mu.Lock() - if s.running { - s.mu.Unlock() - return - } - s.running = true - s.stopCh = make(chan struct{}) - s.mu.Unlock() - - go s.runBackgroundChecker() -} - -// Stop stops the background update checker. -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.running { - return - } - - s.running = false - close(s.stopCh) -} - -// runBackgroundChecker runs periodic update checks. -func (s *Service) runBackgroundChecker() { - // Initial check after a short delay - time.Sleep(30 * time.Second) - - for { - select { - case <-s.stopCh: - return - default: - } - - if s.config.ShouldCheckForUpdates() { - log.Println("Checking for updates...") - _, err := s.CheckForUpdate() - if err != nil { - log.Printf("Update check failed: %v", err) - } - } - - // Check interval from config (minimum 1 hour) - interval := time.Duration(s.config.GetUpdateCheckInterval()) * time.Hour - if interval < time.Hour { - interval = time.Hour - } - - select { - case <-s.stopCh: - return - case <-time.After(interval): - } - } -} - -// GetSettings returns the update settings. -func (s *Service) GetSettings() bugseti.UpdateSettings { - return s.config.GetUpdateSettings() -} - -// SetSettings updates the update settings. -func (s *Service) SetSettings(settings bugseti.UpdateSettings) error { - return s.config.SetUpdateSettings(settings) -} - -// GetVersionInfo returns the current version information. -func (s *Service) GetVersionInfo() bugseti.VersionInfo { - return bugseti.GetVersionInfo() -} - -// GetChannels returns all available update channels. -func (s *Service) GetChannels() []ChannelInfo { - return GetAllChannelInfo() -} - -// CheckForUpdate checks if an update is available. -func (s *Service) CheckForUpdate() (*UpdateCheckResult, error) { - currentVersion := bugseti.GetVersion() - channelStr := s.config.GetUpdateChannel() - - channel, err := ParseChannel(channelStr) - if err != nil { - channel = ChannelStable - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - result, err := s.checker.CheckForUpdate(ctx, currentVersion, channel) - if err != nil { - return result, err - } - - // Update last check time - s.config.SetLastUpdateCheck(time.Now()) - - // Store result - s.mu.Lock() - s.lastResult = result - s.mu.Unlock() - - // If auto-update is enabled and an update is available, download it - if result.Available && s.config.IsAutoUpdateEnabled() { - go s.downloadUpdate(result.Release) - } - - return result, nil -} - -// GetLastCheckResult returns the last update check result. -func (s *Service) GetLastCheckResult() *UpdateCheckResult { - s.mu.RLock() - defer s.mu.RUnlock() - return s.lastResult -} - -// downloadUpdate downloads an update in the background. -func (s *Service) downloadUpdate(release *ReleaseInfo) { - if release == nil { - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - log.Printf("Downloading update %s...", release.Version) - - result, err := s.downloader.Download(ctx, release) - if err != nil { - log.Printf("Failed to download update: %v", err) - return - } - - log.Printf("Update %s downloaded and staged at %s", release.Version, result.BinaryPath) - - s.mu.Lock() - s.pendingUpdate = result - s.mu.Unlock() -} - -// DownloadUpdate downloads the latest available update. -func (s *Service) DownloadUpdate() (*DownloadResult, error) { - s.mu.RLock() - lastResult := s.lastResult - s.mu.RUnlock() - - if lastResult == nil || !lastResult.Available || lastResult.Release == nil { - // Need to check first - result, err := s.CheckForUpdate() - if err != nil { - return nil, err - } - if !result.Available { - return nil, nil - } - lastResult = result - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - downloadResult, err := s.downloader.Download(ctx, lastResult.Release) - if err != nil { - return nil, err - } - - s.mu.Lock() - s.pendingUpdate = downloadResult - s.mu.Unlock() - - return downloadResult, nil -} - -// InstallUpdate installs a previously downloaded update. -func (s *Service) InstallUpdate() (*InstallResult, error) { - s.mu.RLock() - pending := s.pendingUpdate - s.mu.RUnlock() - - if pending == nil { - // Try to download first - downloadResult, err := s.DownloadUpdate() - if err != nil { - return nil, err - } - if downloadResult == nil { - return &InstallResult{ - Success: false, - Error: "No update available", - }, nil - } - pending = downloadResult - } - - result, err := s.installer.Install(pending.BinaryPath) - if err != nil { - return result, err - } - - // Clear pending update - s.mu.Lock() - s.pendingUpdate = nil - s.mu.Unlock() - - return result, nil -} - -// InstallAndRestart installs the update and restarts the application. -func (s *Service) InstallAndRestart() error { - result, err := s.InstallUpdate() - if err != nil { - return err - } - - if !result.Success { - return nil - } - - return s.installer.Restart() -} - -// HasPendingUpdate returns true if there's a downloaded update ready to install. -func (s *Service) HasPendingUpdate() bool { - s.mu.RLock() - defer s.mu.RUnlock() - return s.pendingUpdate != nil -} - -// GetPendingUpdate returns information about the pending update. -func (s *Service) GetPendingUpdate() *DownloadResult { - s.mu.RLock() - defer s.mu.RUnlock() - return s.pendingUpdate -} - -// CancelPendingUpdate cancels and removes the pending update. -func (s *Service) CancelPendingUpdate() error { - s.mu.Lock() - defer s.mu.Unlock() - - s.pendingUpdate = nil - return s.downloader.Cleanup() -} - -// CanSelfUpdate returns true if the application can update itself. -func (s *Service) CanSelfUpdate() bool { - return CanSelfUpdate() -} - -// NeedsElevation returns true if the update requires elevated privileges. -func (s *Service) NeedsElevation() bool { - return NeedsElevation() -} - -// Rollback restores the previous version. -func (s *Service) Rollback() error { - return s.installer.Rollback() -} - -// CleanupAfterUpdate cleans up backup files after a successful update. -func (s *Service) CleanupAfterUpdate() error { - return s.installer.CleanupBackup() -} diff --git a/internal/bugseti/version.go b/internal/bugseti/version.go deleted file mode 100644 index 506425c5..00000000 --- a/internal/bugseti/version.go +++ /dev/null @@ -1,122 +0,0 @@ -// Package bugseti provides version information for the BugSETI application. -package bugseti - -import ( - "fmt" - "runtime" -) - -// Version information - these are set at build time via ldflags -// Example: go build -ldflags "-X forge.lthn.ai/core/cli/internal/bugseti.Version=1.0.0" -var ( - // Version is the semantic version (e.g., "1.0.0", "1.0.0-beta.1", "nightly-20260205") - Version = "dev" - - // Channel is the release channel (stable, beta, nightly) - Channel = "dev" - - // Commit is the git commit SHA - Commit = "unknown" - - // BuildTime is the UTC build timestamp - BuildTime = "unknown" -) - -// VersionInfo contains all version-related information. -type VersionInfo struct { - Version string `json:"version"` - Channel string `json:"channel"` - Commit string `json:"commit"` - BuildTime string `json:"buildTime"` - GoVersion string `json:"goVersion"` - OS string `json:"os"` - Arch string `json:"arch"` -} - -// GetVersion returns the current version string. -func GetVersion() string { - return Version -} - -// GetChannel returns the release channel. -func GetChannel() string { - return Channel -} - -// GetVersionInfo returns complete version information. -func GetVersionInfo() VersionInfo { - return VersionInfo{ - Version: Version, - Channel: Channel, - Commit: Commit, - BuildTime: BuildTime, - GoVersion: runtime.Version(), - OS: runtime.GOOS, - Arch: runtime.GOARCH, - } -} - -// GetVersionString returns a formatted version string for display. -func GetVersionString() string { - if Channel == "dev" { - return fmt.Sprintf("BugSETI %s (development build)", Version) - } - if Channel == "nightly" { - return fmt.Sprintf("BugSETI %s (nightly)", Version) - } - if Channel == "beta" { - return fmt.Sprintf("BugSETI v%s (beta)", Version) - } - return fmt.Sprintf("BugSETI v%s", Version) -} - -// GetShortCommit returns the first 7 characters of the commit hash. -func GetShortCommit() string { - if len(Commit) >= 7 { - return Commit[:7] - } - return Commit -} - -// IsDevelopment returns true if this is a development build. -func IsDevelopment() bool { - return Channel == "dev" || Version == "dev" -} - -// IsPrerelease returns true if this is a prerelease build (beta or nightly). -func IsPrerelease() bool { - return Channel == "beta" || Channel == "nightly" -} - -// VersionService provides version information to the frontend via Wails. -type VersionService struct{} - -// NewVersionService creates a new VersionService. -func NewVersionService() *VersionService { - return &VersionService{} -} - -// ServiceName returns the service name for Wails. -func (v *VersionService) ServiceName() string { - return "VersionService" -} - -// GetVersion returns the version string. -func (v *VersionService) GetVersion() string { - return GetVersion() -} - -// GetChannel returns the release channel. -func (v *VersionService) GetChannel() string { - return GetChannel() -} - -// GetVersionInfo returns complete version information. -func (v *VersionService) GetVersionInfo() VersionInfo { - return GetVersionInfo() -} - -// GetVersionString returns a formatted version string. -func (v *VersionService) GetVersionString() string { - return GetVersionString() -} diff --git a/internal/cmd/ci/cmd_changelog.go b/internal/cmd/ci/cmd_changelog.go deleted file mode 100644 index 8f91f955..00000000 --- a/internal/cmd/ci/cmd_changelog.go +++ /dev/null @@ -1,57 +0,0 @@ -package ci - -import ( - "os" - "os/exec" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/release" -) - -func runChangelog(fromRef, toRef string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - // Auto-detect refs if not provided - if fromRef == "" || toRef == "" { - tag, err := latestTag(cwd) - if err == nil { - if fromRef == "" { - fromRef = tag - } - if toRef == "" { - toRef = "HEAD" - } - } else { - // No tags, use initial commit? Or just HEAD? - cli.Text(i18n.T("cmd.ci.changelog.no_tags")) - return nil - } - } - - cli.Print("%s %s..%s\n\n", releaseDimStyle.Render(i18n.T("cmd.ci.changelog.generating")), fromRef, toRef) - - // Generate changelog - changelog, err := release.Generate(cwd, fromRef, toRef) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.generate", "changelog"), err) - } - - cli.Text(changelog) - - return nil -} - -func latestTag(dir string) (string, error) { - cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") - cmd.Dir = dir - out, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil -} diff --git a/internal/cmd/ci/cmd_ci.go b/internal/cmd/ci/cmd_ci.go deleted file mode 100644 index 0190416c..00000000 --- a/internal/cmd/ci/cmd_ci.go +++ /dev/null @@ -1,84 +0,0 @@ -// Package ci provides release publishing commands. -package ci - -import ( - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" -) - -// Style aliases from shared -var ( - releaseHeaderStyle = cli.RepoStyle - releaseSuccessStyle = cli.SuccessStyle - releaseErrorStyle = cli.ErrorStyle - releaseDimStyle = cli.DimStyle - releaseValueStyle = cli.ValueStyle -) - -// Flag variables for ci command -var ( - ciGoForLaunch bool - ciVersion string - ciDraft bool - ciPrerelease bool -) - -// Flag variables for changelog subcommand -var ( - changelogFromRef string - changelogToRef string -) - -var ciCmd = &cli.Command{ - Use: "ci", - Short: i18n.T("cmd.ci.short"), - Long: i18n.T("cmd.ci.long"), - RunE: func(cmd *cli.Command, args []string) error { - dryRun := !ciGoForLaunch - return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease) - }, -} - -var ciInitCmd = &cli.Command{ - Use: "init", - Short: i18n.T("cmd.ci.init.short"), - Long: i18n.T("cmd.ci.init.long"), - RunE: func(cmd *cli.Command, args []string) error { - return runCIReleaseInit() - }, -} - -var ciChangelogCmd = &cli.Command{ - Use: "changelog", - Short: i18n.T("cmd.ci.changelog.short"), - Long: i18n.T("cmd.ci.changelog.long"), - RunE: func(cmd *cli.Command, args []string) error { - return runChangelog(changelogFromRef, changelogToRef) - }, -} - -var ciVersionCmd = &cli.Command{ - Use: "version", - Short: i18n.T("cmd.ci.version.short"), - Long: i18n.T("cmd.ci.version.long"), - RunE: func(cmd *cli.Command, args []string) error { - return runCIReleaseVersion() - }, -} - -func init() { - // Main ci command flags - ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.ci.flag.go_for_launch")) - ciCmd.Flags().StringVar(&ciVersion, "version", "", i18n.T("cmd.ci.flag.version")) - ciCmd.Flags().BoolVar(&ciDraft, "draft", false, i18n.T("cmd.ci.flag.draft")) - ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, i18n.T("cmd.ci.flag.prerelease")) - - // Changelog subcommand flags - ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", i18n.T("cmd.ci.changelog.flag.from")) - ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", i18n.T("cmd.ci.changelog.flag.to")) - - // Add subcommands - ciCmd.AddCommand(ciInitCmd) - ciCmd.AddCommand(ciChangelogCmd) - ciCmd.AddCommand(ciVersionCmd) -} diff --git a/internal/cmd/ci/cmd_commands.go b/internal/cmd/ci/cmd_commands.go deleted file mode 100644 index d1ff882a..00000000 --- a/internal/cmd/ci/cmd_commands.go +++ /dev/null @@ -1,23 +0,0 @@ -// Package ci provides release publishing commands for CI/CD pipelines. -// -// Publishes pre-built artifacts from dist/ to configured targets: -// - GitHub Releases -// - S3-compatible storage -// - Custom endpoints -// -// Safe by default: runs in dry-run mode unless --we-are-go-for-launch is specified. -// Configuration via .core/release.yaml. -package ci - -import ( - "forge.lthn.ai/core/go/pkg/cli" -) - -func init() { - cli.RegisterCommands(AddCICommands) -} - -// AddCICommands registers the 'ci' command and all subcommands. -func AddCICommands(root *cli.Command) { - root.AddCommand(ciCmd) -} diff --git a/internal/cmd/ci/cmd_init.go b/internal/cmd/ci/cmd_init.go deleted file mode 100644 index 0548ad0d..00000000 --- a/internal/cmd/ci/cmd_init.go +++ /dev/null @@ -1,43 +0,0 @@ -package ci - -import ( - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/release" -) - -func runCIReleaseInit() error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - cli.Print("%s %s\n\n", releaseDimStyle.Render(i18n.Label("init")), i18n.T("cmd.ci.init.initializing")) - - // Check if already initialized - if release.ConfigExists(cwd) { - cli.Text(i18n.T("cmd.ci.init.already_initialized")) - return nil - } - - // Create release config - cfg := release.DefaultConfig() - if err := release.WriteConfig(cfg, cwd); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.create", "config"), err) - } - - cli.Blank() - cli.Print("%s %s\n", releaseSuccessStyle.Render("v"), i18n.T("cmd.ci.init.created_config")) - - // Templates init removed as functionality not exposed - - cli.Blank() - - cli.Text(i18n.T("cmd.ci.init.next_steps")) - cli.Print(" %s\n", i18n.T("cmd.ci.init.edit_config")) - cli.Print(" %s\n", i18n.T("cmd.ci.init.run_ci")) - - return nil -} diff --git a/internal/cmd/ci/cmd_publish.go b/internal/cmd/ci/cmd_publish.go deleted file mode 100644 index aff35fff..00000000 --- a/internal/cmd/ci/cmd_publish.go +++ /dev/null @@ -1,81 +0,0 @@ -package ci - -import ( - "context" - "errors" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/release" -) - -// runCIPublish publishes pre-built artifacts from dist/. -// It does NOT build - use `core build` first. -func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { - ctx := context.Background() - - // Get current directory - projectDir, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - - // Load configuration - cfg, err := release.LoadConfig(projectDir) - if err != nil { - return cli.WrapVerb(err, "load", "config") - } - - // Apply CLI overrides - if version != "" { - cfg.SetVersion(version) - } - - // Apply draft/prerelease overrides to all publishers - if draft || prerelease { - for i := range cfg.Publishers { - if draft { - cfg.Publishers[i].Draft = true - } - if prerelease { - cfg.Publishers[i].Prerelease = true - } - } - } - - // Print header - cli.Print("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing")) - if dryRun { - cli.Print(" %s\n", releaseDimStyle.Render(i18n.T("cmd.ci.dry_run_hint"))) - } else { - cli.Print(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch"))) - } - cli.Blank() - - // Check for publishers - if len(cfg.Publishers) == 0 { - return errors.New(i18n.T("cmd.ci.error.no_publishers")) - } - - // Publish pre-built artifacts - rel, err := release.Publish(ctx, cfg, dryRun) - if err != nil { - cli.Print("%s %v\n", releaseErrorStyle.Render(i18n.Label("error")), err) - return err - } - - // Print summary - cli.Blank() - cli.Print("%s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed")) - cli.Print(" %s %s\n", i18n.Label("version"), releaseValueStyle.Render(rel.Version)) - cli.Print(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts)) - - if !dryRun { - for _, pub := range cfg.Publishers { - cli.Print(" %s %s\n", i18n.T("cmd.ci.label.published"), releaseValueStyle.Render(pub.Type)) - } - } - - return nil -} diff --git a/internal/cmd/ci/cmd_version.go b/internal/cmd/ci/cmd_version.go deleted file mode 100644 index 5afb237d..00000000 --- a/internal/cmd/ci/cmd_version.go +++ /dev/null @@ -1,25 +0,0 @@ -package ci - -import ( - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/release" -) - -// runCIReleaseVersion shows the determined version. -func runCIReleaseVersion() error { - projectDir, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - - version, err := release.DetermineVersion(projectDir) - if err != nil { - return cli.WrapVerb(err, "determine", "version") - } - - cli.Print("%s %s\n", i18n.Label("version"), releaseValueStyle.Render(version)) - return nil -} diff --git a/internal/cmd/php/cmd.go b/internal/cmd/php/cmd.go deleted file mode 100644 index 810414c8..00000000 --- a/internal/cmd/php/cmd.go +++ /dev/null @@ -1,158 +0,0 @@ -package php - -import ( - "os" - "path/filepath" - - "forge.lthn.ai/core/cli/internal/cmd/workspace" - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/io" - "github.com/spf13/cobra" -) - -// DefaultMedium is the default filesystem medium used by the php package. -// It defaults to io.Local (unsandboxed filesystem access). -// Use SetMedium to change this for testing or sandboxed operation. -var DefaultMedium io.Medium = io.Local - -// SetMedium sets the default medium for filesystem operations. -// This is primarily useful for testing with mock mediums. -func SetMedium(m io.Medium) { - DefaultMedium = m -} - -// getMedium returns the default medium for filesystem operations. -func getMedium() io.Medium { - return DefaultMedium -} - -func init() { - cli.RegisterCommands(AddPHPCommands) -} - -// Style aliases from shared -var ( - successStyle = cli.SuccessStyle - errorStyle = cli.ErrorStyle - dimStyle = cli.DimStyle - linkStyle = cli.LinkStyle -) - -// Service colors for log output (domain-specific, keep local) -var ( - phpFrankenPHPStyle = cli.NewStyle().Foreground(cli.ColourIndigo500) - phpViteStyle = cli.NewStyle().Foreground(cli.ColourYellow500) - phpHorizonStyle = cli.NewStyle().Foreground(cli.ColourOrange500) - phpReverbStyle = cli.NewStyle().Foreground(cli.ColourViolet500) - phpRedisStyle = cli.NewStyle().Foreground(cli.ColourRed500) -) - -// Status styles (from shared) -var ( - phpStatusRunning = cli.SuccessStyle - phpStatusStopped = cli.DimStyle - phpStatusError = cli.ErrorStyle -) - -// QA command styles (from shared) -var ( - phpQAPassedStyle = cli.SuccessStyle - phpQAFailedStyle = cli.ErrorStyle - phpQAWarningStyle = cli.WarningStyle - phpQAStageStyle = cli.HeaderStyle -) - -// Security severity styles (from shared) -var ( - phpSecurityCriticalStyle = cli.NewStyle().Bold().Foreground(cli.ColourRed500) - phpSecurityHighStyle = cli.NewStyle().Bold().Foreground(cli.ColourOrange500) - phpSecurityMediumStyle = cli.NewStyle().Foreground(cli.ColourAmber500) - phpSecurityLowStyle = cli.NewStyle().Foreground(cli.ColourGray500) -) - -// AddPHPCommands adds PHP/Laravel development commands. -func AddPHPCommands(root *cobra.Command) { - phpCmd := &cobra.Command{ - Use: "php", - Short: i18n.T("cmd.php.short"), - Long: i18n.T("cmd.php.long"), - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Check if we are in a workspace root - wsRoot, err := workspace.FindWorkspaceRoot() - if err != nil { - return nil // Not in a workspace, regular behavior - } - - // Load workspace config - config, err := workspace.LoadConfig(wsRoot) - if err != nil || config == nil { - return nil // Failed to load or no config, ignore - } - - if config.Active == "" { - return nil // No active package - } - - // Calculate package path - pkgDir := config.PackagesDir - if pkgDir == "" { - pkgDir = "./packages" - } - if !filepath.IsAbs(pkgDir) { - pkgDir = filepath.Join(wsRoot, pkgDir) - } - - targetDir := filepath.Join(pkgDir, config.Active) - - // Check if target directory exists - if !getMedium().IsDir(targetDir) { - cli.Warnf("Active package directory not found: %s", targetDir) - return nil - } - - // Change working directory - if err := os.Chdir(targetDir); err != nil { - return cli.Err("failed to change directory to active package: %w", err) - } - - cli.Print("%s %s\n", dimStyle.Render("Workspace:"), config.Active) - return nil - }, - } - root.AddCommand(phpCmd) - - // Development - addPHPDevCommand(phpCmd) - addPHPLogsCommand(phpCmd) - addPHPStopCommand(phpCmd) - addPHPStatusCommand(phpCmd) - addPHPSSLCommand(phpCmd) - - // Build & Deploy - addPHPBuildCommand(phpCmd) - addPHPServeCommand(phpCmd) - addPHPShellCommand(phpCmd) - - // Quality (existing) - addPHPTestCommand(phpCmd) - addPHPFmtCommand(phpCmd) - addPHPStanCommand(phpCmd) - - // Quality (new) - addPHPPsalmCommand(phpCmd) - addPHPAuditCommand(phpCmd) - addPHPSecurityCommand(phpCmd) - addPHPQACommand(phpCmd) - addPHPRectorCommand(phpCmd) - addPHPInfectionCommand(phpCmd) - - // CI/CD Integration - addPHPCICommand(phpCmd) - - // Package Management - addPHPPackagesCommands(phpCmd) - - // Deployment - addPHPDeployCommands(phpCmd) -} diff --git a/internal/cmd/php/cmd_build.go b/internal/cmd/php/cmd_build.go deleted file mode 100644 index b8b75836..00000000 --- a/internal/cmd/php/cmd_build.go +++ /dev/null @@ -1,291 +0,0 @@ -package php - -import ( - "context" - "errors" - "os" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -var ( - buildType string - buildImageName string - buildTag string - buildPlatform string - buildDockerfile string - buildOutputPath string - buildFormat string - buildTemplate string - buildNoCache bool -) - -func addPHPBuildCommand(parent *cobra.Command) { - buildCmd := &cobra.Command{ - Use: "build", - Short: i18n.T("cmd.php.build.short"), - Long: i18n.T("cmd.php.build.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - ctx := context.Background() - - switch strings.ToLower(buildType) { - case "linuxkit": - return runPHPBuildLinuxKit(ctx, cwd, linuxKitBuildOptions{ - OutputPath: buildOutputPath, - Format: buildFormat, - Template: buildTemplate, - }) - default: - return runPHPBuildDocker(ctx, cwd, dockerBuildOptions{ - ImageName: buildImageName, - Tag: buildTag, - Platform: buildPlatform, - Dockerfile: buildDockerfile, - NoCache: buildNoCache, - }) - } - }, - } - - buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.php.build.flag.type")) - buildCmd.Flags().StringVar(&buildImageName, "name", "", i18n.T("cmd.php.build.flag.name")) - buildCmd.Flags().StringVar(&buildTag, "tag", "", i18n.T("common.flag.tag")) - buildCmd.Flags().StringVar(&buildPlatform, "platform", "", i18n.T("cmd.php.build.flag.platform")) - buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", i18n.T("cmd.php.build.flag.dockerfile")) - buildCmd.Flags().StringVar(&buildOutputPath, "output", "", i18n.T("cmd.php.build.flag.output")) - buildCmd.Flags().StringVar(&buildFormat, "format", "", i18n.T("cmd.php.build.flag.format")) - buildCmd.Flags().StringVar(&buildTemplate, "template", "", i18n.T("cmd.php.build.flag.template")) - buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, i18n.T("cmd.php.build.flag.no_cache")) - - parent.AddCommand(buildCmd) -} - -type dockerBuildOptions struct { - ImageName string - Tag string - Platform string - Dockerfile string - NoCache bool -} - -type linuxKitBuildOptions struct { - OutputPath string - Format string - Template string -} - -func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error { - if !IsPHPProject(projectDir) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker")) - - // Show detected configuration - config, err := DetectDockerfileConfig(projectDir) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.detect", "project configuration"), err) - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.php_version")), config.PHPVersion) - cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.laravel")), config.IsLaravel) - cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.octane")), config.HasOctane) - cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.frontend")), config.HasAssets) - if len(config.PHPExtensions) > 0 { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.extensions")), strings.Join(config.PHPExtensions, ", ")) - } - cli.Blank() - - // Build options - buildOpts := DockerBuildOptions{ - ProjectDir: projectDir, - ImageName: opts.ImageName, - Tag: opts.Tag, - Platform: opts.Platform, - Dockerfile: opts.Dockerfile, - NoBuildCache: opts.NoCache, - Output: os.Stdout, - } - - if buildOpts.ImageName == "" { - buildOpts.ImageName = GetLaravelAppName(projectDir) - if buildOpts.ImageName == "" { - buildOpts.ImageName = "php-app" - } - // Sanitize for Docker - buildOpts.ImageName = strings.ToLower(strings.ReplaceAll(buildOpts.ImageName, " ", "-")) - } - - if buildOpts.Tag == "" { - buildOpts.Tag = "latest" - } - - cli.Print("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), buildOpts.ImageName, buildOpts.Tag) - if opts.Platform != "" { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.platform")), opts.Platform) - } - cli.Blank() - - if err := BuildDocker(ctx, buildOpts); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.build"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Docker image built"})) - cli.Print("%s docker run -p 80:80 -p 443:443 %s:%s\n", - dimStyle.Render(i18n.T("cmd.php.build.docker_run_with")), - buildOpts.ImageName, buildOpts.Tag) - - return nil -} - -func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error { - if !IsPHPProject(projectDir) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit")) - - buildOpts := LinuxKitBuildOptions{ - ProjectDir: projectDir, - OutputPath: opts.OutputPath, - Format: opts.Format, - Template: opts.Template, - Output: os.Stdout, - } - - if buildOpts.Format == "" { - buildOpts.Format = "qcow2" - } - if buildOpts.Template == "" { - buildOpts.Template = "server-php" - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("template")), buildOpts.Template) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format) - cli.Blank() - - if err := BuildLinuxKit(ctx, buildOpts); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.build"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "LinuxKit image built"})) - return nil -} - -var ( - serveImageName string - serveTag string - serveContainerName string - servePort int - serveHTTPSPort int - serveDetach bool - serveEnvFile string -) - -func addPHPServeCommand(parent *cobra.Command) { - serveCmd := &cobra.Command{ - Use: "serve", - Short: i18n.T("cmd.php.serve.short"), - Long: i18n.T("cmd.php.serve.long"), - RunE: func(cmd *cobra.Command, args []string) error { - imageName := serveImageName - if imageName == "" { - // Try to detect from current directory - cwd, err := os.Getwd() - if err == nil { - imageName = GetLaravelAppName(cwd) - if imageName != "" { - imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-")) - } - } - if imageName == "" { - return errors.New(i18n.T("cmd.php.serve.name_required")) - } - } - - ctx := context.Background() - - opts := ServeOptions{ - ImageName: imageName, - Tag: serveTag, - ContainerName: serveContainerName, - Port: servePort, - HTTPSPort: serveHTTPSPort, - Detach: serveDetach, - EnvFile: serveEnvFile, - Output: os.Stdout, - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "production container")) - cli.Print("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), imageName, func() string { - if serveTag == "" { - return "latest" - } - return serveTag - }()) - - effectivePort := servePort - if effectivePort == 0 { - effectivePort = 80 - } - effectiveHTTPSPort := serveHTTPSPort - if effectiveHTTPSPort == 0 { - effectiveHTTPSPort = 443 - } - - cli.Print("%s http://localhost:%d, https://localhost:%d\n", - dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) - cli.Blank() - - if err := ServeProduction(ctx, opts); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.start", "container"), err) - } - - if !serveDetach { - cli.Print("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.stopped")) - } - - return nil - }, - } - - serveCmd.Flags().StringVar(&serveImageName, "name", "", i18n.T("cmd.php.serve.flag.name")) - serveCmd.Flags().StringVar(&serveTag, "tag", "", i18n.T("common.flag.tag")) - serveCmd.Flags().StringVar(&serveContainerName, "container", "", i18n.T("cmd.php.serve.flag.container")) - serveCmd.Flags().IntVar(&servePort, "port", 0, i18n.T("cmd.php.serve.flag.port")) - serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, i18n.T("cmd.php.serve.flag.https_port")) - serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, i18n.T("cmd.php.serve.flag.detach")) - serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", i18n.T("cmd.php.serve.flag.env_file")) - - parent.AddCommand(serveCmd) -} - -func addPHPShellCommand(parent *cobra.Command) { - shellCmd := &cobra.Command{ - Use: "shell [container]", - Short: i18n.T("cmd.php.shell.short"), - Long: i18n.T("cmd.php.shell.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]})) - - if err := Shell(ctx, args[0]); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.open", "shell"), err) - } - - return nil - }, - } - - parent.AddCommand(shellCmd) -} diff --git a/internal/cmd/php/cmd_ci.go b/internal/cmd/php/cmd_ci.go deleted file mode 100644 index 1c4344f3..00000000 --- a/internal/cmd/php/cmd_ci.go +++ /dev/null @@ -1,562 +0,0 @@ -// cmd_ci.go implements the 'php ci' command for CI/CD pipeline integration. -// -// Usage: -// core php ci # Run full CI pipeline -// core php ci --json # Output combined JSON report -// core php ci --summary # Output markdown summary -// core php ci --sarif # Generate SARIF files -// core php ci --upload-sarif # Upload SARIF to GitHub Security -// core php ci --fail-on=high # Only fail on high+ severity - -package php - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -// CI command flags -var ( - ciJSON bool - ciSummary bool - ciSARIF bool - ciUploadSARIF bool - ciFailOn string -) - -// CIResult represents the overall CI pipeline result -type CIResult struct { - Passed bool `json:"passed"` - ExitCode int `json:"exit_code"` - Duration string `json:"duration"` - StartedAt time.Time `json:"started_at"` - Checks []CICheckResult `json:"checks"` - Summary CISummary `json:"summary"` - Artifacts []string `json:"artifacts,omitempty"` -} - -// CICheckResult represents an individual check result -type CICheckResult struct { - Name string `json:"name"` - Status string `json:"status"` // passed, failed, warning, skipped - Duration string `json:"duration"` - Details string `json:"details,omitempty"` - Issues int `json:"issues,omitempty"` - Errors int `json:"errors,omitempty"` - Warnings int `json:"warnings,omitempty"` -} - -// CISummary contains aggregate statistics -type CISummary struct { - Total int `json:"total"` - Passed int `json:"passed"` - Failed int `json:"failed"` - Warnings int `json:"warnings"` - Skipped int `json:"skipped"` -} - -func addPHPCICommand(parent *cobra.Command) { - ciCmd := &cobra.Command{ - Use: "ci", - Short: i18n.T("cmd.php.ci.short"), - Long: i18n.T("cmd.php.ci.long"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPCI() - }, - } - - ciCmd.Flags().BoolVar(&ciJSON, "json", false, i18n.T("cmd.php.ci.flag.json")) - ciCmd.Flags().BoolVar(&ciSummary, "summary", false, i18n.T("cmd.php.ci.flag.summary")) - ciCmd.Flags().BoolVar(&ciSARIF, "sarif", false, i18n.T("cmd.php.ci.flag.sarif")) - ciCmd.Flags().BoolVar(&ciUploadSARIF, "upload-sarif", false, i18n.T("cmd.php.ci.flag.upload_sarif")) - ciCmd.Flags().StringVar(&ciFailOn, "fail-on", "error", i18n.T("cmd.php.ci.flag.fail_on")) - - parent.AddCommand(ciCmd) -} - -func runPHPCI() error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - startTime := time.Now() - ctx := context.Background() - - // Define checks to run in order - checks := []struct { - name string - run func(context.Context, string) (CICheckResult, error) - sarif bool // Whether this check can generate SARIF - }{ - {"test", runCITest, false}, - {"stan", runCIStan, true}, - {"psalm", runCIPsalm, true}, - {"fmt", runCIFmt, false}, - {"audit", runCIAudit, false}, - {"security", runCISecurity, false}, - } - - result := CIResult{ - StartedAt: startTime, - Passed: true, - Checks: make([]CICheckResult, 0, len(checks)), - } - - var artifacts []string - - // Print header unless JSON output - if !ciJSON { - cli.Print("\n%s\n", cli.BoldStyle.Render("core php ci - QA Pipeline")) - cli.Print("%s\n\n", strings.Repeat("─", 40)) - } - - // Run each check - for _, check := range checks { - if !ciJSON { - cli.Print(" %s %s...", dimStyle.Render("→"), check.name) - } - - checkResult, err := check.run(ctx, cwd) - if err != nil { - checkResult = CICheckResult{ - Name: check.name, - Status: "failed", - Details: err.Error(), - } - } - - result.Checks = append(result.Checks, checkResult) - - // Update summary - result.Summary.Total++ - switch checkResult.Status { - case "passed": - result.Summary.Passed++ - case "failed": - result.Summary.Failed++ - if shouldFailOn(checkResult, ciFailOn) { - result.Passed = false - } - case "warning": - result.Summary.Warnings++ - case "skipped": - result.Summary.Skipped++ - } - - // Print result - if !ciJSON { - cli.Print("\r %s %s %s\n", getStatusIcon(checkResult.Status), check.name, dimStyle.Render(checkResult.Details)) - } - - // Generate SARIF if requested - if (ciSARIF || ciUploadSARIF) && check.sarif { - sarifFile := filepath.Join(cwd, check.name+".sarif") - if generateSARIF(ctx, cwd, check.name, sarifFile) == nil { - artifacts = append(artifacts, sarifFile) - } - } - } - - result.Duration = time.Since(startTime).Round(time.Millisecond).String() - result.Artifacts = artifacts - - // Set exit code - if result.Passed { - result.ExitCode = 0 - } else { - result.ExitCode = 1 - } - - // Output based on flags - if ciJSON { - if err := outputCIJSON(result); err != nil { - return err - } - if !result.Passed { - return cli.Exit(result.ExitCode, cli.Err("CI pipeline failed")) - } - return nil - } - - if ciSummary { - if err := outputCISummary(result); err != nil { - return err - } - if !result.Passed { - return cli.Err("CI pipeline failed") - } - return nil - } - - // Default table output - cli.Print("\n%s\n", strings.Repeat("─", 40)) - - if result.Passed { - cli.Print("%s %s\n", successStyle.Render("✓ CI PASSED"), dimStyle.Render(result.Duration)) - } else { - cli.Print("%s %s\n", errorStyle.Render("✗ CI FAILED"), dimStyle.Render(result.Duration)) - } - - if len(artifacts) > 0 { - cli.Print("\n%s\n", dimStyle.Render("Artifacts:")) - for _, a := range artifacts { - cli.Print(" → %s\n", filepath.Base(a)) - } - } - - // Upload SARIF if requested - if ciUploadSARIF && len(artifacts) > 0 { - cli.Blank() - for _, sarifFile := range artifacts { - if err := uploadSARIFToGitHub(ctx, sarifFile); err != nil { - cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), filepath.Base(sarifFile), err) - } else { - cli.Print(" %s %s uploaded\n", successStyle.Render("✓"), filepath.Base(sarifFile)) - } - } - } - - if !result.Passed { - return cli.Err("CI pipeline failed") - } - return nil -} - -// runCITest runs Pest/PHPUnit tests -func runCITest(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "test", Status: "passed"} - - opts := TestOptions{ - Dir: dir, - Output: nil, // Suppress output - } - - if err := RunTests(ctx, opts); err != nil { - result.Status = "failed" - result.Details = err.Error() - } else { - result.Details = "all tests passed" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// runCIStan runs PHPStan -func runCIStan(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "stan", Status: "passed"} - - _, found := DetectAnalyser(dir) - if !found { - result.Status = "skipped" - result.Details = "PHPStan not configured" - return result, nil - } - - opts := AnalyseOptions{ - Dir: dir, - Output: nil, - } - - if err := Analyse(ctx, opts); err != nil { - result.Status = "failed" - result.Details = "errors found" - } else { - result.Details = "0 errors" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// runCIPsalm runs Psalm -func runCIPsalm(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "psalm", Status: "passed"} - - _, found := DetectPsalm(dir) - if !found { - result.Status = "skipped" - result.Details = "Psalm not configured" - return result, nil - } - - opts := PsalmOptions{ - Dir: dir, - Output: nil, - } - - if err := RunPsalm(ctx, opts); err != nil { - result.Status = "failed" - result.Details = "errors found" - } else { - result.Details = "0 errors" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// runCIFmt checks code formatting -func runCIFmt(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "fmt", Status: "passed"} - - _, found := DetectFormatter(dir) - if !found { - result.Status = "skipped" - result.Details = "no formatter configured" - return result, nil - } - - opts := FormatOptions{ - Dir: dir, - Fix: false, // Check only - Output: nil, - } - - if err := Format(ctx, opts); err != nil { - result.Status = "warning" - result.Details = "formatting issues" - } else { - result.Details = "code style OK" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// runCIAudit runs composer audit -func runCIAudit(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "audit", Status: "passed"} - - results, err := RunAudit(ctx, AuditOptions{ - Dir: dir, - Output: nil, - }) - if err != nil { - result.Status = "failed" - result.Details = err.Error() - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil - } - - totalVulns := 0 - for _, r := range results { - totalVulns += r.Vulnerabilities - } - - if totalVulns > 0 { - result.Status = "failed" - result.Details = fmt.Sprintf("%d vulnerabilities", totalVulns) - result.Issues = totalVulns - } else { - result.Details = "no vulnerabilities" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// runCISecurity runs security checks -func runCISecurity(ctx context.Context, dir string) (CICheckResult, error) { - start := time.Now() - result := CICheckResult{Name: "security", Status: "passed"} - - secResult, err := RunSecurityChecks(ctx, SecurityOptions{ - Dir: dir, - Output: nil, - }) - if err != nil { - result.Status = "failed" - result.Details = err.Error() - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil - } - - if secResult.Summary.Critical > 0 || secResult.Summary.High > 0 { - result.Status = "failed" - result.Details = fmt.Sprintf("%d critical, %d high", secResult.Summary.Critical, secResult.Summary.High) - result.Issues = secResult.Summary.Critical + secResult.Summary.High - } else if secResult.Summary.Medium > 0 { - result.Status = "warning" - result.Details = fmt.Sprintf("%d medium issues", secResult.Summary.Medium) - result.Warnings = secResult.Summary.Medium - } else { - result.Details = "no issues" - } - - result.Duration = time.Since(start).Round(time.Millisecond).String() - return result, nil -} - -// shouldFailOn determines if a check should cause CI failure based on --fail-on -func shouldFailOn(check CICheckResult, level string) bool { - switch level { - case "critical": - return check.Status == "failed" && check.Issues > 0 - case "high", "error": - return check.Status == "failed" - case "warning": - return check.Status == "failed" || check.Status == "warning" - default: - return check.Status == "failed" - } -} - -// getStatusIcon returns the icon for a check status -func getStatusIcon(status string) string { - switch status { - case "passed": - return successStyle.Render("✓") - case "failed": - return errorStyle.Render("✗") - case "warning": - return phpQAWarningStyle.Render("⚠") - case "skipped": - return dimStyle.Render("-") - default: - return dimStyle.Render("?") - } -} - -// outputCIJSON outputs the result as JSON -func outputCIJSON(result CIResult) error { - data, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil -} - -// outputCISummary outputs a markdown summary -func outputCISummary(result CIResult) error { - var sb strings.Builder - - sb.WriteString("## CI Pipeline Results\n\n") - - if result.Passed { - sb.WriteString("**Status:** ✅ Passed\n\n") - } else { - sb.WriteString("**Status:** ❌ Failed\n\n") - } - - sb.WriteString("| Check | Status | Details |\n") - sb.WriteString("|-------|--------|----------|\n") - - for _, check := range result.Checks { - icon := "✅" - switch check.Status { - case "failed": - icon = "❌" - case "warning": - icon = "⚠️" - case "skipped": - icon = "⏭️" - } - sb.WriteString(fmt.Sprintf("| %s | %s | %s |\n", check.Name, icon, check.Details)) - } - - sb.WriteString(fmt.Sprintf("\n**Duration:** %s\n", result.Duration)) - - fmt.Print(sb.String()) - return nil -} - -// generateSARIF generates a SARIF file for a specific check -func generateSARIF(ctx context.Context, dir, checkName, outputFile string) error { - var args []string - - switch checkName { - case "stan": - args = []string{"vendor/bin/phpstan", "analyse", "--error-format=sarif", "--no-progress"} - case "psalm": - args = []string{"vendor/bin/psalm", "--output-format=sarif"} - default: - return fmt.Errorf("SARIF not supported for %s", checkName) - } - - cmd := exec.CommandContext(ctx, "php", args...) - cmd.Dir = dir - - // Capture output - command may exit non-zero when issues are found - // but still produce valid SARIF output - output, err := cmd.CombinedOutput() - if len(output) == 0 { - if err != nil { - return fmt.Errorf("failed to generate SARIF: %w", err) - } - return fmt.Errorf("no SARIF output generated") - } - - // Validate output is valid JSON - var js json.RawMessage - if err := json.Unmarshal(output, &js); err != nil { - return fmt.Errorf("invalid SARIF output: %w", err) - } - - return getMedium().Write(outputFile, string(output)) -} - -// uploadSARIFToGitHub uploads a SARIF file to GitHub Security tab -func uploadSARIFToGitHub(ctx context.Context, sarifFile string) error { - // Validate commit SHA before calling API - sha := getGitSHA() - if sha == "" { - return errors.New("cannot upload SARIF: git commit SHA not available (ensure you're in a git repository)") - } - - // Use gh CLI to upload - cmd := exec.CommandContext(ctx, "gh", "api", - "repos/{owner}/{repo}/code-scanning/sarifs", - "-X", "POST", - "-F", "sarif=@"+sarifFile, - "-F", "ref="+getGitRef(), - "-F", "commit_sha="+sha, - ) - - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("%s: %s", err, string(output)) - } - return nil -} - -// getGitRef returns the current git ref -func getGitRef() string { - cmd := exec.Command("git", "symbolic-ref", "HEAD") - output, err := cmd.Output() - if err != nil { - return "refs/heads/main" - } - return strings.TrimSpace(string(output)) -} - -// getGitSHA returns the current git commit SHA -func getGitSHA() string { - cmd := exec.Command("git", "rev-parse", "HEAD") - output, err := cmd.Output() - if err != nil { - return "" - } - return strings.TrimSpace(string(output)) -} diff --git a/internal/cmd/php/cmd_commands.go b/internal/cmd/php/cmd_commands.go deleted file mode 100644 index c0a2444e..00000000 --- a/internal/cmd/php/cmd_commands.go +++ /dev/null @@ -1,41 +0,0 @@ -// Package php provides Laravel/PHP development and deployment commands. -// -// Development Commands: -// - dev: Start Laravel environment (FrankenPHP, Vite, Horizon, Reverb, Redis) -// - logs: Stream unified service logs -// - stop: Stop all running services -// - status: Show service status -// - ssl: Setup SSL certificates with mkcert -// -// Build Commands: -// - build: Build Docker or LinuxKit image -// - serve: Run production container -// - shell: Open shell in running container -// -// Code Quality: -// - test: Run PHPUnit/Pest tests -// - fmt: Format code with Laravel Pint -// - stan: Run PHPStan/Larastan static analysis -// - psalm: Run Psalm static analysis -// - audit: Security audit for dependencies -// - security: Security vulnerability scanning -// - qa: Run full QA pipeline -// - rector: Automated code refactoring -// - infection: Mutation testing for test quality -// -// Package Management: -// - packages link/unlink/update/list: Manage local Composer packages -// -// Deployment (Coolify): -// - deploy: Deploy to Coolify -// - deploy:status: Check deployment status -// - deploy:rollback: Rollback deployment -// - deploy:list: List recent deployments -package php - -import "github.com/spf13/cobra" - -// AddCommands registers the 'php' command and all subcommands. -func AddCommands(root *cobra.Command) { - AddPHPCommands(root) -} diff --git a/internal/cmd/php/cmd_deploy.go b/internal/cmd/php/cmd_deploy.go deleted file mode 100644 index 2298a43b..00000000 --- a/internal/cmd/php/cmd_deploy.go +++ /dev/null @@ -1,361 +0,0 @@ -package php - -import ( - "context" - "os" - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -// Deploy command styles (aliases to shared) -var ( - phpDeployStyle = cli.SuccessStyle - phpDeployPendingStyle = cli.WarningStyle - phpDeployFailedStyle = cli.ErrorStyle -) - -func addPHPDeployCommands(parent *cobra.Command) { - // Main deploy command - addPHPDeployCommand(parent) - - // Deploy status subcommand (using colon notation: deploy:status) - addPHPDeployStatusCommand(parent) - - // Deploy rollback subcommand - addPHPDeployRollbackCommand(parent) - - // Deploy list subcommand - addPHPDeployListCommand(parent) -} - -var ( - deployStaging bool - deployForce bool - deployWait bool -) - -func addPHPDeployCommand(parent *cobra.Command) { - deployCmd := &cobra.Command{ - Use: "deploy", - Short: i18n.T("cmd.php.deploy.short"), - Long: i18n.T("cmd.php.deploy.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - env := EnvProduction - if deployStaging { - env = EnvStaging - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env})) - - ctx := context.Background() - - opts := DeployOptions{ - Dir: cwd, - Environment: env, - Force: deployForce, - Wait: deployWait, - } - - status, err := Deploy(ctx, opts) - if err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err) - } - - printDeploymentStatus(status) - - if deployWait { - if IsDeploymentSuccessful(status.Status) { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"})) - } else { - cli.Print("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status})) - } - } else { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy.triggered")) - } - - return nil - }, - } - - deployCmd.Flags().BoolVar(&deployStaging, "staging", false, i18n.T("cmd.php.deploy.flag.staging")) - deployCmd.Flags().BoolVar(&deployForce, "force", false, i18n.T("cmd.php.deploy.flag.force")) - deployCmd.Flags().BoolVar(&deployWait, "wait", false, i18n.T("cmd.php.deploy.flag.wait")) - - parent.AddCommand(deployCmd) -} - -var ( - deployStatusStaging bool - deployStatusDeploymentID string -) - -func addPHPDeployStatusCommand(parent *cobra.Command) { - statusCmd := &cobra.Command{ - Use: "deploy:status", - Short: i18n.T("cmd.php.deploy_status.short"), - Long: i18n.T("cmd.php.deploy_status.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - env := EnvProduction - if deployStatusStaging { - env = EnvStaging - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.ProgressSubject("check", "deployment status")) - - ctx := context.Background() - - opts := StatusOptions{ - Dir: cwd, - Environment: env, - DeploymentID: deployStatusDeploymentID, - } - - status, err := DeployStatus(ctx, opts) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "status"), err) - } - - printDeploymentStatus(status) - - return nil - }, - } - - statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, i18n.T("cmd.php.deploy_status.flag.staging")) - statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", i18n.T("cmd.php.deploy_status.flag.id")) - - parent.AddCommand(statusCmd) -} - -var ( - rollbackStaging bool - rollbackDeploymentID string - rollbackWait bool -) - -func addPHPDeployRollbackCommand(parent *cobra.Command) { - rollbackCmd := &cobra.Command{ - Use: "deploy:rollback", - Short: i18n.T("cmd.php.deploy_rollback.short"), - Long: i18n.T("cmd.php.deploy_rollback.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - env := EnvProduction - if rollbackStaging { - env = EnvStaging - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env})) - - ctx := context.Background() - - opts := RollbackOptions{ - Dir: cwd, - Environment: env, - DeploymentID: rollbackDeploymentID, - Wait: rollbackWait, - } - - status, err := Rollback(ctx, opts) - if err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err) - } - - printDeploymentStatus(status) - - if rollbackWait { - if IsDeploymentSuccessful(status.Status) { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"})) - } else { - cli.Print("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status})) - } - } else { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy_rollback.triggered")) - } - - return nil - }, - } - - rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, i18n.T("cmd.php.deploy_rollback.flag.staging")) - rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", i18n.T("cmd.php.deploy_rollback.flag.id")) - rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, i18n.T("cmd.php.deploy_rollback.flag.wait")) - - parent.AddCommand(rollbackCmd) -} - -var ( - deployListStaging bool - deployListLimit int -) - -func addPHPDeployListCommand(parent *cobra.Command) { - listCmd := &cobra.Command{ - Use: "deploy:list", - Short: i18n.T("cmd.php.deploy_list.short"), - Long: i18n.T("cmd.php.deploy_list.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - env := EnvProduction - if deployListStaging { - env = EnvStaging - } - - limit := deployListLimit - if limit == 0 { - limit = 10 - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_list.recent", map[string]interface{}{"Environment": env})) - - ctx := context.Background() - - deployments, err := ListDeployments(ctx, cwd, env, limit) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.list", "deployments"), err) - } - - if len(deployments) == 0 { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.deploy_list.none_found")) - return nil - } - - for i, d := range deployments { - printDeploymentSummary(i+1, &d) - } - - return nil - }, - } - - listCmd.Flags().BoolVar(&deployListStaging, "staging", false, i18n.T("cmd.php.deploy_list.flag.staging")) - listCmd.Flags().IntVar(&deployListLimit, "limit", 0, i18n.T("cmd.php.deploy_list.flag.limit")) - - parent.AddCommand(listCmd) -} - -func printDeploymentStatus(status *DeploymentStatus) { - // Status with color - statusStyle := phpDeployStyle - switch status.Status { - case "queued", "building", "deploying", "pending", "rolling_back": - statusStyle = phpDeployPendingStyle - case "failed", "error", "cancelled": - statusStyle = phpDeployFailedStyle - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), statusStyle.Render(status.Status)) - - if status.ID != "" { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.id")), status.ID) - } - - if status.URL != "" { - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("url")), linkStyle.Render(status.URL)) - } - - if status.Branch != "" { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.branch")), status.Branch) - } - - if status.Commit != "" { - commit := status.Commit - if len(commit) > 7 { - commit = commit[:7] - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.commit")), commit) - if status.CommitMessage != "" { - // Truncate long messages - msg := status.CommitMessage - if len(msg) > 60 { - msg = msg[:57] + "..." - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.message")), msg) - } - } - - if !status.StartedAt.IsZero() { - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("started")), status.StartedAt.Format(time.RFC3339)) - } - - if !status.CompletedAt.IsZero() { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.completed")), status.CompletedAt.Format(time.RFC3339)) - if !status.StartedAt.IsZero() { - duration := status.CompletedAt.Sub(status.StartedAt) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.duration")), duration.Round(time.Second)) - } - } -} - -func printDeploymentSummary(index int, status *DeploymentStatus) { - // Status with color - statusStyle := phpDeployStyle - switch status.Status { - case "queued", "building", "deploying", "pending", "rolling_back": - statusStyle = phpDeployPendingStyle - case "failed", "error", "cancelled": - statusStyle = phpDeployFailedStyle - } - - // Format: #1 [finished] abc1234 - commit message (2 hours ago) - id := status.ID - if len(id) > 8 { - id = id[:8] - } - - commit := status.Commit - if len(commit) > 7 { - commit = commit[:7] - } - - msg := status.CommitMessage - if len(msg) > 40 { - msg = msg[:37] + "..." - } - - age := "" - if !status.StartedAt.IsZero() { - age = i18n.TimeAgo(status.StartedAt) - } - - cli.Print(" %s %s %s", - dimStyle.Render(cli.Sprintf("#%d", index)), - statusStyle.Render(cli.Sprintf("[%s]", status.Status)), - id, - ) - - if commit != "" { - cli.Print(" %s", commit) - } - - if msg != "" { - cli.Print(" - %s", msg) - } - - if age != "" { - cli.Print(" %s", dimStyle.Render(cli.Sprintf("(%s)", age))) - } - - cli.Blank() -} diff --git a/internal/cmd/php/cmd_dev.go b/internal/cmd/php/cmd_dev.go deleted file mode 100644 index d2d8de04..00000000 --- a/internal/cmd/php/cmd_dev.go +++ /dev/null @@ -1,497 +0,0 @@ -package php - -import ( - "bufio" - "context" - "errors" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -var ( - devNoVite bool - devNoHorizon bool - devNoReverb bool - devNoRedis bool - devHTTPS bool - devDomain string - devPort int -) - -func addPHPDevCommand(parent *cobra.Command) { - devCmd := &cobra.Command{ - Use: "dev", - Short: i18n.T("cmd.php.dev.short"), - Long: i18n.T("cmd.php.dev.long"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPDev(phpDevOptions{ - NoVite: devNoVite, - NoHorizon: devNoHorizon, - NoReverb: devNoReverb, - NoRedis: devNoRedis, - HTTPS: devHTTPS, - Domain: devDomain, - Port: devPort, - }) - }, - } - - devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, i18n.T("cmd.php.dev.flag.no_vite")) - devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, i18n.T("cmd.php.dev.flag.no_horizon")) - devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, i18n.T("cmd.php.dev.flag.no_reverb")) - devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, i18n.T("cmd.php.dev.flag.no_redis")) - devCmd.Flags().BoolVar(&devHTTPS, "https", false, i18n.T("cmd.php.dev.flag.https")) - devCmd.Flags().StringVar(&devDomain, "domain", "", i18n.T("cmd.php.dev.flag.domain")) - devCmd.Flags().IntVar(&devPort, "port", 0, i18n.T("cmd.php.dev.flag.port")) - - parent.AddCommand(devCmd) -} - -type phpDevOptions struct { - NoVite bool - NoHorizon bool - NoReverb bool - NoRedis bool - HTTPS bool - Domain string - Port int -} - -func runPHPDev(opts phpDevOptions) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("failed to get working directory: %w", err) - } - - // Check if this is a Laravel project - if !IsLaravelProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_laravel")) - } - - // Get app name for display - appName := GetLaravelAppName(cwd) - if appName == "" { - appName = "Laravel" - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName})) - - // Detect services - services := DetectServices(cwd) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services")) - for _, svc := range services { - cli.Print(" %s %s\n", successStyle.Render("*"), svc) - } - cli.Blank() - - // Setup options - port := opts.Port - if port == 0 { - port = 8000 - } - - devOpts := Options{ - Dir: cwd, - NoVite: opts.NoVite, - NoHorizon: opts.NoHorizon, - NoReverb: opts.NoReverb, - NoRedis: opts.NoRedis, - HTTPS: opts.HTTPS, - Domain: opts.Domain, - FrankenPHPPort: port, - } - - // Create and start dev server - server := NewDevServer(devOpts) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Handle shutdown signals - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigCh - cli.Print("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.shutting_down")) - cancel() - }() - - if err := server.Start(ctx, devOpts); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.start", "services"), err) - } - - // Print status - cli.Print("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.running")), i18n.T("cmd.php.dev.services_started")) - printServiceStatuses(server.Status()) - cli.Blank() - - // Print URLs - appURL := GetLaravelAppURL(cwd) - if appURL == "" { - if opts.HTTPS { - appURL = cli.Sprintf("https://localhost:%d", port) - } else { - appURL = cli.Sprintf("http://localhost:%d", port) - } - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL)) - - // Check for Vite - if !opts.NoVite && containsService(services, ServiceVite) { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173")) - } - - cli.Print("\n%s\n\n", dimStyle.Render(i18n.T("cmd.php.dev.press_ctrl_c"))) - - // Stream unified logs - logsReader, err := server.Logs("", true) - if err != nil { - cli.Print("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("i18n.fail.get", "logs")) - } else { - defer func() { _ = logsReader.Close() }() - - scanner := bufio.NewScanner(logsReader) - for scanner.Scan() { - select { - case <-ctx.Done(): - goto shutdown - default: - line := scanner.Text() - printColoredLog(line) - } - } - } - -shutdown: - // Stop services - if err := server.Stop(); err != nil { - cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.dev.stop_error", map[string]interface{}{"Error": err})) - } - - cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) - return nil -} - -var ( - logsFollow bool - logsService string -) - -func addPHPLogsCommand(parent *cobra.Command) { - logsCmd := &cobra.Command{ - Use: "logs", - Short: i18n.T("cmd.php.logs.short"), - Long: i18n.T("cmd.php.logs.long"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPLogs(logsService, logsFollow) - }, - } - - logsCmd.Flags().BoolVar(&logsFollow, "follow", false, i18n.T("common.flag.follow")) - logsCmd.Flags().StringVar(&logsService, "service", "", i18n.T("cmd.php.logs.flag.service")) - - parent.AddCommand(logsCmd) -} - -func runPHPLogs(service string, follow bool) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - if !IsLaravelProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_laravel_short")) - } - - // Create a minimal server just to access logs - server := NewDevServer(Options{Dir: cwd}) - - logsReader, err := server.Logs(service, follow) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "logs"), err) - } - defer func() { _ = logsReader.Close() }() - - // Handle interrupt - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigCh - cancel() - }() - - scanner := bufio.NewScanner(logsReader) - for scanner.Scan() { - select { - case <-ctx.Done(): - return nil - default: - printColoredLog(scanner.Text()) - } - } - - return scanner.Err() -} - -func addPHPStopCommand(parent *cobra.Command) { - stopCmd := &cobra.Command{ - Use: "stop", - Short: i18n.T("cmd.php.stop.short"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPStop() - }, - } - - parent.AddCommand(stopCmd) -} - -func runPHPStop() error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.stop.stopping")) - - // We need to find running processes - // This is a simplified version - in practice you'd want to track PIDs - server := NewDevServer(Options{Dir: cwd}) - if err := server.Stop(); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.stop", "services"), err) - } - - cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) - return nil -} - -func addPHPStatusCommand(parent *cobra.Command) { - statusCmd := &cobra.Command{ - Use: "status", - Short: i18n.T("cmd.php.status.short"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPStatus() - }, - } - - parent.AddCommand(statusCmd) -} - -func runPHPStatus() error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - if !IsLaravelProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_laravel_short")) - } - - appName := GetLaravelAppName(cwd) - if appName == "" { - appName = "Laravel" - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("project")), appName) - - // Detect available services - services := DetectServices(cwd) - cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services"))) - for _, svc := range services { - style := getServiceStyle(string(svc)) - cli.Print(" %s %s\n", style.Render("*"), svc) - } - cli.Blank() - - // Package manager - pm := DetectPackageManager(cwd) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm) - - // FrankenPHP status - if IsFrankenPHPProject(cwd) { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP") - } - - // SSL status - appURL := GetLaravelAppURL(cwd) - if appURL != "" { - domain := ExtractDomainFromURL(appURL) - if CertsExist(domain, SSLOptions{}) { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed"))) - } else { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup"))) - } - } - - return nil -} - -var sslDomain string - -func addPHPSSLCommand(parent *cobra.Command) { - sslCmd := &cobra.Command{ - Use: "ssl", - Short: i18n.T("cmd.php.ssl.short"), - RunE: func(cmd *cobra.Command, args []string) error { - return runPHPSSL(sslDomain) - }, - } - - sslCmd.Flags().StringVar(&sslDomain, "domain", "", i18n.T("cmd.php.ssl.flag.domain")) - - parent.AddCommand(sslCmd) -} - -func runPHPSSL(domain string) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - // Get domain from APP_URL if not specified - if domain == "" { - appURL := GetLaravelAppURL(cwd) - if appURL != "" { - domain = ExtractDomainFromURL(appURL) - } - } - if domain == "" { - domain = "localhost" - } - - // Check if mkcert is installed - if !IsMkcertInstalled() { - cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.ssl.mkcert_not_installed")) - cli.Print("\n%s\n", i18n.T("common.hint.install_with")) - cli.Print(" %s\n", i18n.T("cmd.php.ssl.install_macos")) - cli.Print(" %s\n", i18n.T("cmd.php.ssl.install_linux")) - return errors.New(i18n.T("cmd.php.error.mkcert_not_installed")) - } - - cli.Print("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain})) - - // Check if certs already exist - if CertsExist(domain, SSLOptions{}) { - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("skip")), i18n.T("cmd.php.ssl.certs_exist")) - - certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) - return nil - } - - // Setup SSL - if err := SetupSSL(domain, SSLOptions{}); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.setup", "SSL"), err) - } - - certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) - - cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.ssl.certs_created")) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) - - return nil -} - -// Helper functions for dev commands - -func printServiceStatuses(statuses []ServiceStatus) { - for _, s := range statuses { - style := getServiceStyle(s.Name) - var statusText string - - if s.Error != nil { - statusText = phpStatusError.Render(i18n.T("cmd.php.status.error", map[string]interface{}{"Error": s.Error})) - } else if s.Running { - statusText = phpStatusRunning.Render(i18n.T("cmd.php.status.running")) - if s.Port > 0 { - statusText += dimStyle.Render(cli.Sprintf(" (%s)", i18n.T("cmd.php.status.port", map[string]interface{}{"Port": s.Port}))) - } - if s.PID > 0 { - statusText += dimStyle.Render(cli.Sprintf(" [%s]", i18n.T("cmd.php.status.pid", map[string]interface{}{"PID": s.PID}))) - } - } else { - statusText = phpStatusStopped.Render(i18n.T("cmd.php.status.stopped")) - } - - cli.Print(" %s %s\n", style.Render(s.Name+":"), statusText) - } -} - -func printColoredLog(line string) { - // Parse service prefix from log line - timestamp := time.Now().Format("15:04:05") - - var style *cli.AnsiStyle - serviceName := "" - - if strings.HasPrefix(line, "[FrankenPHP]") { - style = phpFrankenPHPStyle - serviceName = "FrankenPHP" - line = strings.TrimPrefix(line, "[FrankenPHP] ") - } else if strings.HasPrefix(line, "[Vite]") { - style = phpViteStyle - serviceName = "Vite" - line = strings.TrimPrefix(line, "[Vite] ") - } else if strings.HasPrefix(line, "[Horizon]") { - style = phpHorizonStyle - serviceName = "Horizon" - line = strings.TrimPrefix(line, "[Horizon] ") - } else if strings.HasPrefix(line, "[Reverb]") { - style = phpReverbStyle - serviceName = "Reverb" - line = strings.TrimPrefix(line, "[Reverb] ") - } else if strings.HasPrefix(line, "[Redis]") { - style = phpRedisStyle - serviceName = "Redis" - line = strings.TrimPrefix(line, "[Redis] ") - } else { - // Unknown service, print as-is - cli.Print("%s %s\n", dimStyle.Render(timestamp), line) - return - } - - cli.Print("%s %s %s\n", - dimStyle.Render(timestamp), - style.Render(cli.Sprintf("[%s]", serviceName)), - line, - ) -} - -func getServiceStyle(name string) *cli.AnsiStyle { - switch strings.ToLower(name) { - case "frankenphp": - return phpFrankenPHPStyle - case "vite": - return phpViteStyle - case "horizon": - return phpHorizonStyle - case "reverb": - return phpReverbStyle - case "redis": - return phpRedisStyle - default: - return dimStyle - } -} - -func containsService(services []DetectedService, target DetectedService) bool { - for _, s := range services { - if s == target { - return true - } - } - return false -} diff --git a/internal/cmd/php/cmd_packages.go b/internal/cmd/php/cmd_packages.go deleted file mode 100644 index fa1172be..00000000 --- a/internal/cmd/php/cmd_packages.go +++ /dev/null @@ -1,146 +0,0 @@ -package php - -import ( - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -func addPHPPackagesCommands(parent *cobra.Command) { - packagesCmd := &cobra.Command{ - Use: "packages", - Short: i18n.T("cmd.php.packages.short"), - Long: i18n.T("cmd.php.packages.long"), - } - parent.AddCommand(packagesCmd) - - addPHPPackagesLinkCommand(packagesCmd) - addPHPPackagesUnlinkCommand(packagesCmd) - addPHPPackagesUpdateCommand(packagesCmd) - addPHPPackagesListCommand(packagesCmd) -} - -func addPHPPackagesLinkCommand(parent *cobra.Command) { - linkCmd := &cobra.Command{ - Use: "link [paths...]", - Short: i18n.T("cmd.php.packages.link.short"), - Long: i18n.T("cmd.php.packages.link.long"), - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking")) - - if err := LinkPackages(cwd, args); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.link", "packages"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.link.done")) - return nil - }, - } - - parent.AddCommand(linkCmd) -} - -func addPHPPackagesUnlinkCommand(parent *cobra.Command) { - unlinkCmd := &cobra.Command{ - Use: "unlink [packages...]", - Short: i18n.T("cmd.php.packages.unlink.short"), - Long: i18n.T("cmd.php.packages.unlink.long"), - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking")) - - if err := UnlinkPackages(cwd, args); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.unlink", "packages"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.unlink.done")) - return nil - }, - } - - parent.AddCommand(unlinkCmd) -} - -func addPHPPackagesUpdateCommand(parent *cobra.Command) { - updateCmd := &cobra.Command{ - Use: "update [packages...]", - Short: i18n.T("cmd.php.packages.update.short"), - Long: i18n.T("cmd.php.packages.update.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating")) - - if err := UpdatePackages(cwd, args); err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.update_packages"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.update.done")) - return nil - }, - } - - parent.AddCommand(updateCmd) -} - -func addPHPPackagesListCommand(parent *cobra.Command) { - listCmd := &cobra.Command{ - Use: "list", - Short: i18n.T("cmd.php.packages.list.short"), - Long: i18n.T("cmd.php.packages.list.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - packages, err := ListLinkedPackages(cwd) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.list", "packages"), err) - } - - if len(packages) == 0 { - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.none_found")) - return nil - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.linked")) - - for _, pkg := range packages { - name := pkg.Name - if name == "" { - name = i18n.T("cmd.php.packages.list.unknown") - } - version := pkg.Version - if version == "" { - version = "dev" - } - - cli.Print(" %s %s\n", successStyle.Render("*"), name) - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("path")), pkg.Path) - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("version")), version) - cli.Blank() - } - - return nil - }, - } - - parent.AddCommand(listCmd) -} diff --git a/internal/cmd/php/cmd_qa_runner.go b/internal/cmd/php/cmd_qa_runner.go deleted file mode 100644 index 7e9d7ae8..00000000 --- a/internal/cmd/php/cmd_qa_runner.go +++ /dev/null @@ -1,343 +0,0 @@ -package php - -import ( - "context" - "path/filepath" - "strings" - "sync" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/framework" - "forge.lthn.ai/core/go/pkg/i18n" - "forge.lthn.ai/core/go/pkg/process" -) - -// QARunner orchestrates PHP QA checks using pkg/process. -type QARunner struct { - dir string - fix bool - service *process.Service - core *framework.Core - - // Output tracking - outputMu sync.Mutex - checkOutputs map[string][]string -} - -// NewQARunner creates a QA runner for the given directory. -func NewQARunner(dir string, fix bool) (*QARunner, error) { - // Create a Core with process service for the QA session - core, err := framework.New( - framework.WithName("process", process.NewService(process.Options{})), - ) - if err != nil { - return nil, cli.WrapVerb(err, "create", "process service") - } - - svc, err := framework.ServiceFor[*process.Service](core, "process") - if err != nil { - return nil, cli.WrapVerb(err, "get", "process service") - } - - runner := &QARunner{ - dir: dir, - fix: fix, - service: svc, - core: core, - checkOutputs: make(map[string][]string), - } - - return runner, nil -} - -// BuildSpecs creates RunSpecs for the given QA checks. -func (r *QARunner) BuildSpecs(checks []string) []process.RunSpec { - specs := make([]process.RunSpec, 0, len(checks)) - - for _, check := range checks { - spec := r.buildSpec(check) - if spec != nil { - specs = append(specs, *spec) - } - } - - return specs -} - -// buildSpec creates a RunSpec for a single check. -func (r *QARunner) buildSpec(check string) *process.RunSpec { - switch check { - case "audit": - return &process.RunSpec{ - Name: "audit", - Command: "composer", - Args: []string{"audit", "--format=summary"}, - Dir: r.dir, - } - - case "fmt": - m := getMedium() - formatter, found := DetectFormatter(r.dir) - if !found { - return nil - } - if formatter == FormatterPint { - vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint") - cmd := "pint" - if m.IsFile(vendorBin) { - cmd = vendorBin - } - args := []string{} - if !r.fix { - args = append(args, "--test") - } - return &process.RunSpec{ - Name: "fmt", - Command: cmd, - Args: args, - Dir: r.dir, - After: []string{"audit"}, - } - } - return nil - - case "stan": - m := getMedium() - _, found := DetectAnalyser(r.dir) - if !found { - return nil - } - vendorBin := filepath.Join(r.dir, "vendor", "bin", "phpstan") - cmd := "phpstan" - if m.IsFile(vendorBin) { - cmd = vendorBin - } - return &process.RunSpec{ - Name: "stan", - Command: cmd, - Args: []string{"analyse", "--no-progress"}, - Dir: r.dir, - After: []string{"fmt"}, - } - - case "psalm": - m := getMedium() - _, found := DetectPsalm(r.dir) - if !found { - return nil - } - vendorBin := filepath.Join(r.dir, "vendor", "bin", "psalm") - cmd := "psalm" - if m.IsFile(vendorBin) { - cmd = vendorBin - } - args := []string{"--no-progress"} - if r.fix { - args = append(args, "--alter", "--issues=all") - } - return &process.RunSpec{ - Name: "psalm", - Command: cmd, - Args: args, - Dir: r.dir, - After: []string{"stan"}, - } - - case "test": - m := getMedium() - // Check for Pest first, fall back to PHPUnit - pestBin := filepath.Join(r.dir, "vendor", "bin", "pest") - phpunitBin := filepath.Join(r.dir, "vendor", "bin", "phpunit") - - var cmd string - if m.IsFile(pestBin) { - cmd = pestBin - } else if m.IsFile(phpunitBin) { - cmd = phpunitBin - } else { - return nil - } - - // Tests depend on stan (or psalm if available) - after := []string{"stan"} - if _, found := DetectPsalm(r.dir); found { - after = []string{"psalm"} - } - - return &process.RunSpec{ - Name: "test", - Command: cmd, - Args: []string{}, - Dir: r.dir, - After: after, - } - - case "rector": - m := getMedium() - if !DetectRector(r.dir) { - return nil - } - vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector") - cmd := "rector" - if m.IsFile(vendorBin) { - cmd = vendorBin - } - args := []string{"process"} - if !r.fix { - args = append(args, "--dry-run") - } - return &process.RunSpec{ - Name: "rector", - Command: cmd, - Args: args, - Dir: r.dir, - After: []string{"test"}, - AllowFailure: true, // Dry-run returns non-zero if changes would be made - } - - case "infection": - m := getMedium() - if !DetectInfection(r.dir) { - return nil - } - vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection") - cmd := "infection" - if m.IsFile(vendorBin) { - cmd = vendorBin - } - return &process.RunSpec{ - Name: "infection", - Command: cmd, - Args: []string{"--min-msi=50", "--min-covered-msi=70", "--threads=4"}, - Dir: r.dir, - After: []string{"test"}, - AllowFailure: true, - } - } - - return nil -} - -// Run executes all QA checks and returns the results. -func (r *QARunner) Run(ctx context.Context, stages []QAStage) (*QARunResult, error) { - // Collect all checks from all stages - var allChecks []string - for _, stage := range stages { - checks := GetQAChecks(r.dir, stage) - allChecks = append(allChecks, checks...) - } - - if len(allChecks) == 0 { - return &QARunResult{Passed: true}, nil - } - - // Build specs - specs := r.BuildSpecs(allChecks) - if len(specs) == 0 { - return &QARunResult{Passed: true}, nil - } - - // Register output handler - r.core.RegisterAction(func(c *framework.Core, msg framework.Message) error { - switch m := msg.(type) { - case process.ActionProcessOutput: - r.outputMu.Lock() - // Extract check name from process ID mapping - for _, spec := range specs { - if strings.Contains(m.ID, spec.Name) || m.ID != "" { - // Store output for later display if needed - r.checkOutputs[spec.Name] = append(r.checkOutputs[spec.Name], m.Line) - break - } - } - r.outputMu.Unlock() - } - return nil - }) - - // Create runner and execute - runner := process.NewRunner(r.service) - result, err := runner.RunAll(ctx, specs) - if err != nil { - return nil, err - } - - // Convert to QA result - qaResult := &QARunResult{ - Passed: result.Success(), - Duration: result.Duration.String(), - Results: make([]QACheckRunResult, 0, len(result.Results)), - } - - for _, res := range result.Results { - qaResult.Results = append(qaResult.Results, QACheckRunResult{ - Name: res.Name, - Passed: res.Passed(), - Skipped: res.Skipped, - ExitCode: res.ExitCode, - Duration: res.Duration.String(), - Output: res.Output, - }) - if res.Passed() { - qaResult.PassedCount++ - } else if res.Skipped { - qaResult.SkippedCount++ - } else { - qaResult.FailedCount++ - } - } - - return qaResult, nil -} - -// GetCheckOutput returns captured output for a check. -func (r *QARunner) GetCheckOutput(check string) []string { - r.outputMu.Lock() - defer r.outputMu.Unlock() - return r.checkOutputs[check] -} - -// QARunResult holds the results of running QA checks. -type QARunResult struct { - Passed bool `json:"passed"` - Duration string `json:"duration"` - Results []QACheckRunResult `json:"results"` - PassedCount int `json:"passed_count"` - FailedCount int `json:"failed_count"` - SkippedCount int `json:"skipped_count"` -} - -// QACheckRunResult holds the result of a single QA check. -type QACheckRunResult struct { - Name string `json:"name"` - Passed bool `json:"passed"` - Skipped bool `json:"skipped"` - ExitCode int `json:"exit_code"` - Duration string `json:"duration"` - Output string `json:"output,omitempty"` -} - -// GetIssueMessage returns an issue message for a check. -func (r QACheckRunResult) GetIssueMessage() string { - if r.Passed || r.Skipped { - return "" - } - switch r.Name { - case "audit": - return i18n.T("i18n.done.find", "vulnerabilities") - case "fmt": - return i18n.T("i18n.done.find", "style issues") - case "stan": - return i18n.T("i18n.done.find", "analysis errors") - case "psalm": - return i18n.T("i18n.done.find", "type errors") - case "test": - return i18n.T("i18n.done.fail", "tests") - case "rector": - return i18n.T("i18n.done.find", "refactoring suggestions") - case "infection": - return i18n.T("i18n.fail.pass", "mutation testing") - default: - return i18n.T("i18n.done.find", "issues") - } -} diff --git a/internal/cmd/php/cmd_quality.go b/internal/cmd/php/cmd_quality.go deleted file mode 100644 index e76363ee..00000000 --- a/internal/cmd/php/cmd_quality.go +++ /dev/null @@ -1,815 +0,0 @@ -package php - -import ( - "context" - "encoding/json" - "errors" - "os" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" - "github.com/spf13/cobra" -) - -var ( - testParallel bool - testCoverage bool - testFilter string - testGroup string - testJSON bool -) - -func addPHPTestCommand(parent *cobra.Command) { - testCmd := &cobra.Command{ - Use: "test", - Short: i18n.T("cmd.php.test.short"), - Long: i18n.T("cmd.php.test.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - if !testJSON { - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests")) - } - - ctx := context.Background() - - opts := TestOptions{ - Dir: cwd, - Filter: testFilter, - Parallel: testParallel, - Coverage: testCoverage, - JUnit: testJSON, - Output: os.Stdout, - } - - if testGroup != "" { - opts.Groups = []string{testGroup} - } - - if err := RunTests(ctx, opts); err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.run", "tests"), err) - } - - return nil - }, - } - - testCmd.Flags().BoolVar(&testParallel, "parallel", false, i18n.T("cmd.php.test.flag.parallel")) - testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.php.test.flag.coverage")) - testCmd.Flags().StringVar(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter")) - testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group")) - testCmd.Flags().BoolVar(&testJSON, "junit", false, i18n.T("cmd.php.test.flag.junit")) - - parent.AddCommand(testCmd) -} - -var ( - fmtFix bool - fmtDiff bool - fmtJSON bool -) - -func addPHPFmtCommand(parent *cobra.Command) { - fmtCmd := &cobra.Command{ - Use: "fmt [paths...]", - Short: i18n.T("cmd.php.fmt.short"), - Long: i18n.T("cmd.php.fmt.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Detect formatter - formatter, found := DetectFormatter(cwd) - if !found { - return errors.New(i18n.T("cmd.php.fmt.no_formatter")) - } - - if !fmtJSON { - var msg string - if fmtFix { - msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter}) - } else { - msg = i18n.ProgressSubject("check", "code style") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg) - } - - ctx := context.Background() - - opts := FormatOptions{ - Dir: cwd, - Fix: fmtFix, - Diff: fmtDiff, - JSON: fmtJSON, - Output: os.Stdout, - } - - // Get any additional paths from args - if len(args) > 0 { - opts.Paths = args - } - - if err := Format(ctx, opts); err != nil { - if fmtFix { - return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err) - } - return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err) - } - - if !fmtJSON { - if fmtFix { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"})) - } else { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues")) - } - } - - return nil - }, - } - - fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix")) - fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("common.flag.diff")) - fmtCmd.Flags().BoolVar(&fmtJSON, "json", false, i18n.T("common.flag.json")) - - parent.AddCommand(fmtCmd) -} - -var ( - stanLevel int - stanMemory string - stanJSON bool - stanSARIF bool -) - -func addPHPStanCommand(parent *cobra.Command) { - stanCmd := &cobra.Command{ - Use: "stan [paths...]", - Short: i18n.T("cmd.php.analyse.short"), - Long: i18n.T("cmd.php.analyse.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Detect analyser - _, found := DetectAnalyser(cwd) - if !found { - return errors.New(i18n.T("cmd.php.analyse.no_analyser")) - } - - if stanJSON && stanSARIF { - return errors.New(i18n.T("common.error.json_sarif_exclusive")) - } - - if !stanJSON && !stanSARIF { - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis")) - } - - ctx := context.Background() - - opts := AnalyseOptions{ - Dir: cwd, - Level: stanLevel, - Memory: stanMemory, - JSON: stanJSON, - SARIF: stanSARIF, - Output: os.Stdout, - } - - // Get any additional paths from args - if len(args) > 0 { - opts.Paths = args - } - - if err := Analyse(ctx, opts); err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) - } - - if !stanJSON && !stanSARIF { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) - } - return nil - }, - } - - stanCmd.Flags().IntVar(&stanLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level")) - stanCmd.Flags().StringVar(&stanMemory, "memory", "", i18n.T("cmd.php.analyse.flag.memory")) - stanCmd.Flags().BoolVar(&stanJSON, "json", false, i18n.T("common.flag.json")) - stanCmd.Flags().BoolVar(&stanSARIF, "sarif", false, i18n.T("common.flag.sarif")) - - parent.AddCommand(stanCmd) -} - -// ============================================================================= -// New QA Commands -// ============================================================================= - -var ( - psalmLevel int - psalmFix bool - psalmBaseline bool - psalmShowInfo bool - psalmJSON bool - psalmSARIF bool -) - -func addPHPPsalmCommand(parent *cobra.Command) { - psalmCmd := &cobra.Command{ - Use: "psalm", - Short: i18n.T("cmd.php.psalm.short"), - Long: i18n.T("cmd.php.psalm.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Check if Psalm is available - _, found := DetectPsalm(cwd) - if !found { - cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.psalm.not_found")) - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.psalm.install")) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup")) - return errors.New(i18n.T("cmd.php.error.psalm_not_installed")) - } - - if psalmJSON && psalmSARIF { - return errors.New(i18n.T("common.error.json_sarif_exclusive")) - } - - if !psalmJSON && !psalmSARIF { - var msg string - if psalmFix { - msg = i18n.T("cmd.php.psalm.analysing_fixing") - } else { - msg = i18n.T("cmd.php.psalm.analysing") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg) - } - - ctx := context.Background() - - opts := PsalmOptions{ - Dir: cwd, - Level: psalmLevel, - Fix: psalmFix, - Baseline: psalmBaseline, - ShowInfo: psalmShowInfo, - JSON: psalmJSON, - SARIF: psalmSARIF, - Output: os.Stdout, - } - - if err := RunPsalm(ctx, opts); err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) - } - - if !psalmJSON && !psalmSARIF { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) - } - return nil - }, - } - - psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, i18n.T("cmd.php.psalm.flag.level")) - psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, i18n.T("common.flag.fix")) - psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline")) - psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, i18n.T("cmd.php.psalm.flag.show_info")) - psalmCmd.Flags().BoolVar(&psalmJSON, "json", false, i18n.T("common.flag.json")) - psalmCmd.Flags().BoolVar(&psalmSARIF, "sarif", false, i18n.T("common.flag.sarif")) - - parent.AddCommand(psalmCmd) -} - -var ( - auditJSONOutput bool - auditFix bool -) - -func addPHPAuditCommand(parent *cobra.Command) { - auditCmd := &cobra.Command{ - Use: "audit", - Short: i18n.T("cmd.php.audit.short"), - Long: i18n.T("cmd.php.audit.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning")) - - ctx := context.Background() - - results, err := RunAudit(ctx, AuditOptions{ - Dir: cwd, - JSON: auditJSONOutput, - Fix: auditFix, - Output: os.Stdout, - }) - if err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.audit_failed"), err) - } - - // Print results - totalVulns := 0 - hasErrors := false - - for _, result := range results { - icon := successStyle.Render("✓") - status := successStyle.Render(i18n.T("cmd.php.audit.secure")) - - if result.Error != nil { - icon = errorStyle.Render("✗") - status = errorStyle.Render(i18n.T("cmd.php.audit.error")) - hasErrors = true - } else if result.Vulnerabilities > 0 { - icon = errorStyle.Render("✗") - status = errorStyle.Render(i18n.T("cmd.php.audit.vulnerabilities", map[string]interface{}{"Count": result.Vulnerabilities})) - totalVulns += result.Vulnerabilities - } - - cli.Print(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status) - - // Show advisories - for _, adv := range result.Advisories { - severity := adv.Severity - if severity == "" { - severity = "unknown" - } - sevStyle := getSeverityStyle(severity) - cli.Print(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package) - if adv.Title != "" { - cli.Print(" %s\n", dimStyle.Render(adv.Title)) - } - } - } - - cli.Blank() - - if totalVulns > 0 { - cli.Print("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns})) - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("fix")), i18n.T("common.hint.fix_deps")) - return errors.New(i18n.T("cmd.php.error.vulns_found")) - } - - if hasErrors { - return errors.New(i18n.T("cmd.php.audit.completed_errors")) - } - - cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.audit.all_secure")) - return nil - }, - } - - auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, i18n.T("common.flag.json")) - auditCmd.Flags().BoolVar(&auditFix, "fix", false, i18n.T("cmd.php.audit.flag.fix")) - - parent.AddCommand(auditCmd) -} - -var ( - securitySeverity string - securityJSONOutput bool - securitySarif bool - securityURL string -) - -func addPHPSecurityCommand(parent *cobra.Command) { - securityCmd := &cobra.Command{ - Use: "security", - Short: i18n.T("cmd.php.security.short"), - Long: i18n.T("cmd.php.security.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.ProgressSubject("run", "security checks")) - - ctx := context.Background() - - result, err := RunSecurityChecks(ctx, SecurityOptions{ - Dir: cwd, - Severity: securitySeverity, - JSON: securityJSONOutput, - SARIF: securitySarif, - URL: securityURL, - Output: os.Stdout, - }) - if err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.security_failed"), err) - } - - // Print results by category - currentCategory := "" - for _, check := range result.Checks { - category := strings.Split(check.ID, "_")[0] - if category != currentCategory { - if currentCategory != "" { - cli.Blank() - } - currentCategory = category - cli.Print(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix"))) - } - - icon := successStyle.Render("✓") - if !check.Passed { - icon = getSeverityStyle(check.Severity).Render("✗") - } - - cli.Print(" %s %s\n", icon, check.Name) - if !check.Passed && check.Message != "" { - cli.Print(" %s\n", dimStyle.Render(check.Message)) - if check.Fix != "" { - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("fix")), check.Fix) - } - } - } - - cli.Blank() - - // Print summary - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.php.security.summary")) - cli.Print(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total) - - if result.Summary.Critical > 0 { - cli.Print(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical) - } - if result.Summary.High > 0 { - cli.Print(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High) - } - if result.Summary.Medium > 0 { - cli.Print(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium) - } - if result.Summary.Low > 0 { - cli.Print(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low) - } - - if result.Summary.Critical > 0 || result.Summary.High > 0 { - return errors.New(i18n.T("cmd.php.error.critical_high_issues")) - } - - return nil - }, - } - - securityCmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.php.security.flag.severity")) - securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, i18n.T("common.flag.json")) - securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, i18n.T("cmd.php.security.flag.sarif")) - securityCmd.Flags().StringVar(&securityURL, "url", "", i18n.T("cmd.php.security.flag.url")) - - parent.AddCommand(securityCmd) -} - -var ( - qaQuick bool - qaFull bool - qaFix bool - qaJSON bool -) - -func addPHPQACommand(parent *cobra.Command) { - qaCmd := &cobra.Command{ - Use: "qa", - Short: i18n.T("cmd.php.qa.short"), - Long: i18n.T("cmd.php.qa.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Determine stages - opts := QAOptions{ - Dir: cwd, - Quick: qaQuick, - Full: qaFull, - Fix: qaFix, - JSON: qaJSON, - } - stages := GetQAStages(opts) - - // Print header - if !qaJSON { - cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) - } - - ctx := context.Background() - - // Create QA runner using pkg/process - runner, err := NewQARunner(cwd, qaFix) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.create", "QA runner"), err) - } - - // Run all checks with dependency ordering - result, err := runner.Run(ctx, stages) - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err) - } - - // Display results by stage (skip when JSON output is enabled) - if !qaJSON { - currentStage := "" - for _, checkResult := range result.Results { - // Determine stage for this check - stage := getCheckStage(checkResult.Name, stages, cwd) - if stage != currentStage { - if currentStage != "" { - cli.Blank() - } - currentStage = stage - cli.Print("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──")) - } - - icon := phpQAPassedStyle.Render("✓") - status := phpQAPassedStyle.Render(i18n.T("i18n.done.pass")) - if checkResult.Skipped { - icon = dimStyle.Render("-") - status = dimStyle.Render(i18n.T("i18n.done.skip")) - } else if !checkResult.Passed { - icon = phpQAFailedStyle.Render("✗") - status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail")) - } - - cli.Print(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration)) - } - cli.Blank() - - // Print summary - if result.Passed { - cli.Print("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass")) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration) - return nil - } - - cli.Print("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+cli.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass")) - - // Show what needs fixing - cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix"))) - for _, checkResult := range result.Results { - if checkResult.Passed || checkResult.Skipped { - continue - } - fixCmd := getQAFixCommand(checkResult.Name, qaFix) - issue := checkResult.GetIssueMessage() - if issue == "" { - issue = "issues found" - } - cli.Print(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue) - if fixCmd != "" { - cli.Print(" %s %s\n", dimStyle.Render("->"), fixCmd) - } - } - - return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) - } - - // JSON mode: output results as JSON - output, err := json.MarshalIndent(result, "", " ") - if err != nil { - return cli.Wrap(err, "marshal JSON output") - } - cli.Text(string(output)) - - if !result.Passed { - return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) - } - return nil - }, - } - - qaCmd.Flags().BoolVar(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick")) - qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full")) - qaCmd.Flags().BoolVar(&qaFix, "fix", false, i18n.T("common.flag.fix")) - qaCmd.Flags().BoolVar(&qaJSON, "json", false, i18n.T("common.flag.json")) - - parent.AddCommand(qaCmd) -} - -// getCheckStage determines which stage a check belongs to. -func getCheckStage(checkName string, stages []QAStage, dir string) string { - for _, stage := range stages { - checks := GetQAChecks(dir, stage) - for _, c := range checks { - if c == checkName { - return string(stage) - } - } - } - return "unknown" -} - -func getQAFixCommand(checkName string, fixEnabled bool) string { - switch checkName { - case "audit": - return i18n.T("i18n.progress.update", "dependencies") - case "fmt": - if fixEnabled { - return "" - } - return "core php fmt --fix" - case "stan": - return i18n.T("i18n.progress.fix", "PHPStan errors") - case "psalm": - return i18n.T("i18n.progress.fix", "Psalm errors") - case "test": - return i18n.T("i18n.progress.fix", i18n.T("i18n.done.fail")+" tests") - case "rector": - if fixEnabled { - return "" - } - return "core php rector --fix" - case "infection": - return i18n.T("i18n.progress.improve", "test coverage") - } - return "" -} - -var ( - rectorFix bool - rectorDiff bool - rectorClearCache bool -) - -func addPHPRectorCommand(parent *cobra.Command) { - rectorCmd := &cobra.Command{ - Use: "rector", - Short: i18n.T("cmd.php.rector.short"), - Long: i18n.T("cmd.php.rector.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Check if Rector is available - if !DetectRector(cwd) { - cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.rector.not_found")) - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.rector.install")) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup")) - return errors.New(i18n.T("cmd.php.error.rector_not_installed")) - } - - var msg string - if rectorFix { - msg = i18n.T("cmd.php.rector.refactoring") - } else { - msg = i18n.T("cmd.php.rector.analysing") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg) - - ctx := context.Background() - - opts := RectorOptions{ - Dir: cwd, - Fix: rectorFix, - Diff: rectorDiff, - ClearCache: rectorClearCache, - Output: os.Stdout, - } - - if err := RunRector(ctx, opts); err != nil { - if rectorFix { - return cli.Err("%s: %w", i18n.T("cmd.php.error.rector_failed"), err) - } - // Dry-run returns non-zero if changes would be made - cli.Print("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested")) - return nil - } - - if rectorFix { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code refactored"})) - } else { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.rector.no_changes")) - } - return nil - }, - } - - rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, i18n.T("cmd.php.rector.flag.fix")) - rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, i18n.T("cmd.php.rector.flag.diff")) - rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, i18n.T("cmd.php.rector.flag.clear_cache")) - - parent.AddCommand(rectorCmd) -} - -var ( - infectionMinMSI int - infectionMinCoveredMSI int - infectionThreads int - infectionFilter string - infectionOnlyCovered bool -) - -func addPHPInfectionCommand(parent *cobra.Command) { - infectionCmd := &cobra.Command{ - Use: "infection", - Short: i18n.T("cmd.php.infection.short"), - Long: i18n.T("cmd.php.infection.long"), - RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) - } - - if !IsPHPProject(cwd) { - return errors.New(i18n.T("cmd.php.error.not_php")) - } - - // Check if Infection is available - if !DetectInfection(cwd) { - cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.infection.not_found")) - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.infection.install")) - return errors.New(i18n.T("cmd.php.error.infection_not_installed")) - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.ProgressSubject("run", "mutation testing")) - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note")) - - ctx := context.Background() - - opts := InfectionOptions{ - Dir: cwd, - MinMSI: infectionMinMSI, - MinCoveredMSI: infectionMinCoveredMSI, - Threads: infectionThreads, - Filter: infectionFilter, - OnlyCovered: infectionOnlyCovered, - Output: os.Stdout, - } - - if err := RunInfection(ctx, opts); err != nil { - return cli.Err("%s: %w", i18n.T("cmd.php.error.infection_failed"), err) - } - - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.infection.complete")) - return nil - }, - } - - infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, i18n.T("cmd.php.infection.flag.min_msi")) - infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, i18n.T("cmd.php.infection.flag.min_covered_msi")) - infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, i18n.T("cmd.php.infection.flag.threads")) - infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", i18n.T("cmd.php.infection.flag.filter")) - infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, i18n.T("cmd.php.infection.flag.only_covered")) - - parent.AddCommand(infectionCmd) -} - -func getSeverityStyle(severity string) *cli.AnsiStyle { - switch strings.ToLower(severity) { - case "critical": - return phpSecurityCriticalStyle - case "high": - return phpSecurityHighStyle - case "medium": - return phpSecurityMediumStyle - case "low": - return phpSecurityLowStyle - default: - return dimStyle - } -} diff --git a/internal/cmd/php/container.go b/internal/cmd/php/container.go deleted file mode 100644 index 1df5deae..00000000 --- a/internal/cmd/php/container.go +++ /dev/null @@ -1,451 +0,0 @@ -package php - -import ( - "context" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// DockerBuildOptions configures Docker image building for PHP projects. -type DockerBuildOptions struct { - // ProjectDir is the path to the PHP/Laravel project. - ProjectDir string - - // ImageName is the name for the Docker image. - ImageName string - - // Tag is the image tag (default: "latest"). - Tag string - - // Platform specifies the target platform (e.g., "linux/amd64", "linux/arm64"). - Platform string - - // Dockerfile is the path to a custom Dockerfile. - // If empty, one will be auto-generated for FrankenPHP. - Dockerfile string - - // NoBuildCache disables Docker build cache. - NoBuildCache bool - - // BuildArgs are additional build arguments. - BuildArgs map[string]string - - // Output is the writer for build output (default: os.Stdout). - Output io.Writer -} - -// LinuxKitBuildOptions configures LinuxKit image building for PHP projects. -type LinuxKitBuildOptions struct { - // ProjectDir is the path to the PHP/Laravel project. - ProjectDir string - - // OutputPath is the path for the output image. - OutputPath string - - // Format is the output format: "iso", "qcow2", "raw", "vmdk". - Format string - - // Template is the LinuxKit template name (default: "server-php"). - Template string - - // Variables are template variables to apply. - Variables map[string]string - - // Output is the writer for build output (default: os.Stdout). - Output io.Writer -} - -// ServeOptions configures running a production PHP container. -type ServeOptions struct { - // ImageName is the Docker image to run. - ImageName string - - // Tag is the image tag (default: "latest"). - Tag string - - // ContainerName is the name for the container. - ContainerName string - - // Port is the host port to bind (default: 80). - Port int - - // HTTPSPort is the host HTTPS port to bind (default: 443). - HTTPSPort int - - // Detach runs the container in detached mode. - Detach bool - - // EnvFile is the path to an environment file. - EnvFile string - - // Volumes maps host paths to container paths. - Volumes map[string]string - - // Output is the writer for output (default: os.Stdout). - Output io.Writer -} - -// BuildDocker builds a Docker image for the PHP project. -func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { - if opts.ProjectDir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.ProjectDir = cwd - } - - // Validate project directory - if !IsPHPProject(opts.ProjectDir) { - return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) - } - - // Set defaults - if opts.ImageName == "" { - opts.ImageName = filepath.Base(opts.ProjectDir) - } - if opts.Tag == "" { - opts.Tag = "latest" - } - if opts.Output == nil { - opts.Output = os.Stdout - } - - // Determine Dockerfile path - dockerfilePath := opts.Dockerfile - var tempDockerfile string - - if dockerfilePath == "" { - // Generate Dockerfile - content, err := GenerateDockerfile(opts.ProjectDir) - if err != nil { - return cli.WrapVerb(err, "generate", "Dockerfile") - } - - // Write to temporary file - m := getMedium() - tempDockerfile = filepath.Join(opts.ProjectDir, "Dockerfile.core-generated") - if err := m.Write(tempDockerfile, content); err != nil { - return cli.WrapVerb(err, "write", "Dockerfile") - } - defer func() { _ = m.Delete(tempDockerfile) }() - - dockerfilePath = tempDockerfile - } - - // Build Docker image - imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) - - args := []string{"build", "-t", imageRef, "-f", dockerfilePath} - - if opts.Platform != "" { - args = append(args, "--platform", opts.Platform) - } - - if opts.NoBuildCache { - args = append(args, "--no-cache") - } - - for key, value := range opts.BuildArgs { - args = append(args, "--build-arg", cli.Sprintf("%s=%s", key, value)) - } - - args = append(args, opts.ProjectDir) - - cmd := exec.CommandContext(ctx, "docker", args...) - cmd.Dir = opts.ProjectDir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - if err := cmd.Run(); err != nil { - return cli.Wrap(err, "docker build failed") - } - - return nil -} - -// BuildLinuxKit builds a LinuxKit image for the PHP project. -func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { - if opts.ProjectDir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.ProjectDir = cwd - } - - // Validate project directory - if !IsPHPProject(opts.ProjectDir) { - return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) - } - - // Set defaults - if opts.Template == "" { - opts.Template = "server-php" - } - if opts.Format == "" { - opts.Format = "qcow2" - } - if opts.OutputPath == "" { - opts.OutputPath = filepath.Join(opts.ProjectDir, "dist", filepath.Base(opts.ProjectDir)) - } - if opts.Output == nil { - opts.Output = os.Stdout - } - - // Ensure output directory exists - m := getMedium() - outputDir := filepath.Dir(opts.OutputPath) - if err := m.EnsureDir(outputDir); err != nil { - return cli.WrapVerb(err, "create", "output directory") - } - - // Find linuxkit binary - linuxkitPath, err := lookupLinuxKit() - if err != nil { - return err - } - - // Get template content - templateContent, err := getLinuxKitTemplate(opts.Template) - if err != nil { - return cli.WrapVerb(err, "get", "template") - } - - // Apply variables - if opts.Variables == nil { - opts.Variables = make(map[string]string) - } - // Add project-specific variables - opts.Variables["PROJECT_DIR"] = opts.ProjectDir - opts.Variables["PROJECT_NAME"] = filepath.Base(opts.ProjectDir) - - content, err := applyTemplateVariables(templateContent, opts.Variables) - if err != nil { - return cli.WrapVerb(err, "apply", "template variables") - } - - // Write template to temp file - tempYAML := filepath.Join(opts.ProjectDir, ".core-linuxkit.yml") - if err := m.Write(tempYAML, content); err != nil { - return cli.WrapVerb(err, "write", "template") - } - defer func() { _ = m.Delete(tempYAML) }() - - // Build LinuxKit image - args := []string{ - "build", - "--format", opts.Format, - "--name", opts.OutputPath, - tempYAML, - } - - cmd := exec.CommandContext(ctx, linuxkitPath, args...) - cmd.Dir = opts.ProjectDir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - if err := cmd.Run(); err != nil { - return cli.Wrap(err, "linuxkit build failed") - } - - return nil -} - -// ServeProduction runs a production PHP container. -func ServeProduction(ctx context.Context, opts ServeOptions) error { - if opts.ImageName == "" { - return cli.Err("image name is required") - } - - // Set defaults - if opts.Tag == "" { - opts.Tag = "latest" - } - if opts.Port == 0 { - opts.Port = 80 - } - if opts.HTTPSPort == 0 { - opts.HTTPSPort = 443 - } - if opts.Output == nil { - opts.Output = os.Stdout - } - - imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) - - args := []string{"run"} - - if opts.Detach { - args = append(args, "-d") - } else { - args = append(args, "--rm") - } - - if opts.ContainerName != "" { - args = append(args, "--name", opts.ContainerName) - } - - // Port mappings - args = append(args, "-p", cli.Sprintf("%d:80", opts.Port)) - args = append(args, "-p", cli.Sprintf("%d:443", opts.HTTPSPort)) - - // Environment file - if opts.EnvFile != "" { - args = append(args, "--env-file", opts.EnvFile) - } - - // Volume mounts - for hostPath, containerPath := range opts.Volumes { - args = append(args, "-v", cli.Sprintf("%s:%s", hostPath, containerPath)) - } - - args = append(args, imageRef) - - cmd := exec.CommandContext(ctx, "docker", args...) - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - if opts.Detach { - output, err := cmd.Output() - if err != nil { - return cli.WrapVerb(err, "start", "container") - } - containerID := strings.TrimSpace(string(output)) - cli.Print("Container started: %s\n", containerID[:12]) - return nil - } - - return cmd.Run() -} - -// Shell opens a shell in a running container. -func Shell(ctx context.Context, containerID string) error { - if containerID == "" { - return cli.Err("container ID is required") - } - - // Resolve partial container ID - fullID, err := resolveDockerContainerID(ctx, containerID) - if err != nil { - return err - } - - cmd := exec.CommandContext(ctx, "docker", "exec", "-it", fullID, "/bin/sh") - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return cmd.Run() -} - -// IsPHPProject checks if the given directory is a PHP project. -func IsPHPProject(dir string) bool { - composerPath := filepath.Join(dir, "composer.json") - return getMedium().IsFile(composerPath) -} - -// commonLinuxKitPaths defines default search locations for linuxkit. -var commonLinuxKitPaths = []string{ - "/usr/local/bin/linuxkit", - "/opt/homebrew/bin/linuxkit", -} - -// lookupLinuxKit finds the linuxkit binary. -func lookupLinuxKit() (string, error) { - // Check PATH first - if path, err := exec.LookPath("linuxkit"); err == nil { - return path, nil - } - - m := getMedium() - for _, p := range commonLinuxKitPaths { - if m.IsFile(p) { - return p, nil - } - } - - return "", cli.Err("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit") -} - -// getLinuxKitTemplate retrieves a LinuxKit template by name. -func getLinuxKitTemplate(name string) (string, error) { - // Default server-php template for PHP projects - if name == "server-php" { - return defaultServerPHPTemplate, nil - } - - // Try to load from container package templates - // This would integrate with forge.lthn.ai/core/go/pkg/container - return "", cli.Err("template not found: %s", name) -} - -// applyTemplateVariables applies variable substitution to template content. -func applyTemplateVariables(content string, vars map[string]string) (string, error) { - result := content - for key, value := range vars { - placeholder := "${" + key + "}" - result = strings.ReplaceAll(result, placeholder, value) - } - return result, nil -} - -// resolveDockerContainerID resolves a partial container ID to a full ID. -func resolveDockerContainerID(ctx context.Context, partialID string) (string, error) { - cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--no-trunc", "--format", "{{.ID}}") - output, err := cmd.Output() - if err != nil { - return "", cli.WrapVerb(err, "list", "containers") - } - - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - var matches []string - - for _, line := range lines { - if strings.HasPrefix(line, partialID) { - matches = append(matches, line) - } - } - - switch len(matches) { - case 0: - return "", cli.Err("no container found matching: %s", partialID) - case 1: - return matches[0], nil - default: - return "", cli.Err("multiple containers match '%s', be more specific", partialID) - } -} - -// defaultServerPHPTemplate is the default LinuxKit template for PHP servers. -const defaultServerPHPTemplate = `# LinuxKit configuration for PHP/FrankenPHP server -kernel: - image: linuxkit/kernel:6.6.13 - cmdline: "console=tty0 console=ttyS0" -init: - - linuxkit/init:v1.0.1 - - linuxkit/runc:v1.0.1 - - linuxkit/containerd:v1.0.1 -onboot: - - name: sysctl - image: linuxkit/sysctl:v1.0.1 - - name: dhcpcd - image: linuxkit/dhcpcd:v1.0.1 - command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf"] -services: - - name: getty - image: linuxkit/getty:v1.0.1 - env: - - INSECURE=true - - name: sshd - image: linuxkit/sshd:v1.0.1 -files: - - path: etc/ssh/authorized_keys - contents: | - ${SSH_KEY:-} -` diff --git a/internal/cmd/php/container_test.go b/internal/cmd/php/container_test.go deleted file mode 100644 index c0d0e196..00000000 --- a/internal/cmd/php/container_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package php - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDockerBuildOptions_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := DockerBuildOptions{ - ProjectDir: "/project", - ImageName: "myapp", - Tag: "v1.0.0", - Platform: "linux/amd64", - Dockerfile: "/path/to/Dockerfile", - NoBuildCache: true, - BuildArgs: map[string]string{"ARG1": "value1"}, - Output: os.Stdout, - } - - assert.Equal(t, "/project", opts.ProjectDir) - assert.Equal(t, "myapp", opts.ImageName) - assert.Equal(t, "v1.0.0", opts.Tag) - assert.Equal(t, "linux/amd64", opts.Platform) - assert.Equal(t, "/path/to/Dockerfile", opts.Dockerfile) - assert.True(t, opts.NoBuildCache) - assert.Equal(t, "value1", opts.BuildArgs["ARG1"]) - assert.NotNil(t, opts.Output) - }) -} - -func TestLinuxKitBuildOptions_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := LinuxKitBuildOptions{ - ProjectDir: "/project", - OutputPath: "/output/image.qcow2", - Format: "qcow2", - Template: "server-php", - Variables: map[string]string{"VAR1": "value1"}, - Output: os.Stdout, - } - - assert.Equal(t, "/project", opts.ProjectDir) - assert.Equal(t, "/output/image.qcow2", opts.OutputPath) - assert.Equal(t, "qcow2", opts.Format) - assert.Equal(t, "server-php", opts.Template) - assert.Equal(t, "value1", opts.Variables["VAR1"]) - assert.NotNil(t, opts.Output) - }) -} - -func TestServeOptions_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := ServeOptions{ - ImageName: "myapp", - Tag: "latest", - ContainerName: "myapp-container", - Port: 8080, - HTTPSPort: 8443, - Detach: true, - EnvFile: "/path/to/.env", - Volumes: map[string]string{"/host": "/container"}, - Output: os.Stdout, - } - - assert.Equal(t, "myapp", opts.ImageName) - assert.Equal(t, "latest", opts.Tag) - assert.Equal(t, "myapp-container", opts.ContainerName) - assert.Equal(t, 8080, opts.Port) - assert.Equal(t, 8443, opts.HTTPSPort) - assert.True(t, opts.Detach) - assert.Equal(t, "/path/to/.env", opts.EnvFile) - assert.Equal(t, "/container", opts.Volumes["/host"]) - assert.NotNil(t, opts.Output) - }) -} - -func TestIsPHPProject_Container_Good(t *testing.T) { - t.Run("returns true with composer.json", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(`{}`), 0644) - require.NoError(t, err) - - assert.True(t, IsPHPProject(dir)) - }) -} - -func TestIsPHPProject_Container_Bad(t *testing.T) { - t.Run("returns false without composer.json", func(t *testing.T) { - dir := t.TempDir() - assert.False(t, IsPHPProject(dir)) - }) - - t.Run("returns false for non-existent directory", func(t *testing.T) { - assert.False(t, IsPHPProject("/non/existent/path")) - }) -} - -func TestLookupLinuxKit_Bad(t *testing.T) { - t.Run("returns error when linuxkit not found", func(t *testing.T) { - // Save original PATH and paths - origPath := os.Getenv("PATH") - origCommonPaths := commonLinuxKitPaths - defer func() { - _ = os.Setenv("PATH", origPath) - commonLinuxKitPaths = origCommonPaths - }() - - // Set PATH to empty and clear common paths - _ = os.Setenv("PATH", "") - commonLinuxKitPaths = []string{} - - _, err := lookupLinuxKit() - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "linuxkit not found") - } - }) -} - -func TestGetLinuxKitTemplate_Good(t *testing.T) { - t.Run("returns server-php template", func(t *testing.T) { - content, err := getLinuxKitTemplate("server-php") - assert.NoError(t, err) - assert.Contains(t, content, "kernel:") - assert.Contains(t, content, "linuxkit/kernel") - }) -} - -func TestGetLinuxKitTemplate_Bad(t *testing.T) { - t.Run("returns error for unknown template", func(t *testing.T) { - _, err := getLinuxKitTemplate("unknown-template") - assert.Error(t, err) - assert.Contains(t, err.Error(), "template not found") - }) -} - -func TestApplyTemplateVariables_Good(t *testing.T) { - t.Run("replaces variables", func(t *testing.T) { - content := "Hello ${NAME}, welcome to ${PLACE}!" - vars := map[string]string{ - "NAME": "World", - "PLACE": "Earth", - } - - result, err := applyTemplateVariables(content, vars) - assert.NoError(t, err) - assert.Equal(t, "Hello World, welcome to Earth!", result) - }) - - t.Run("handles empty variables", func(t *testing.T) { - content := "No variables here" - vars := map[string]string{} - - result, err := applyTemplateVariables(content, vars) - assert.NoError(t, err) - assert.Equal(t, "No variables here", result) - }) - - t.Run("leaves unmatched placeholders", func(t *testing.T) { - content := "Hello ${NAME}, ${UNKNOWN} is unknown" - vars := map[string]string{ - "NAME": "World", - } - - result, err := applyTemplateVariables(content, vars) - assert.NoError(t, err) - assert.Contains(t, result, "Hello World") - assert.Contains(t, result, "${UNKNOWN}") - }) - - t.Run("handles multiple occurrences", func(t *testing.T) { - content := "${VAR} and ${VAR} again" - vars := map[string]string{ - "VAR": "value", - } - - result, err := applyTemplateVariables(content, vars) - assert.NoError(t, err) - assert.Equal(t, "value and value again", result) - }) -} - -func TestDefaultServerPHPTemplate_Good(t *testing.T) { - t.Run("template has required sections", func(t *testing.T) { - assert.Contains(t, defaultServerPHPTemplate, "kernel:") - assert.Contains(t, defaultServerPHPTemplate, "init:") - assert.Contains(t, defaultServerPHPTemplate, "services:") - assert.Contains(t, defaultServerPHPTemplate, "onboot:") - }) - - t.Run("template contains placeholders", func(t *testing.T) { - assert.Contains(t, defaultServerPHPTemplate, "${SSH_KEY:-}") - }) -} - -func TestBuildDocker_Bad(t *testing.T) { - t.Skip("requires Docker installed") - - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - err := BuildDocker(context.TODO(), DockerBuildOptions{ProjectDir: dir}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) -} - -func TestBuildLinuxKit_Bad(t *testing.T) { - t.Skip("requires linuxkit installed") - - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - err := BuildLinuxKit(context.TODO(), LinuxKitBuildOptions{ProjectDir: dir}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) -} - -func TestServeProduction_Bad(t *testing.T) { - t.Run("fails without image name", func(t *testing.T) { - err := ServeProduction(context.TODO(), ServeOptions{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "image name is required") - }) -} - -func TestShell_Bad(t *testing.T) { - t.Run("fails without container ID", func(t *testing.T) { - err := Shell(context.TODO(), "") - assert.Error(t, err) - assert.Contains(t, err.Error(), "container ID is required") - }) -} - -func TestResolveDockerContainerID_Bad(t *testing.T) { - t.Skip("requires Docker installed") -} - -func TestBuildDocker_DefaultOptions(t *testing.T) { - t.Run("sets defaults correctly", func(t *testing.T) { - // This tests the default logic without actually running Docker - opts := DockerBuildOptions{} - - // Verify default values would be set in BuildDocker - if opts.Tag == "" { - opts.Tag = "latest" - } - assert.Equal(t, "latest", opts.Tag) - - if opts.ImageName == "" { - opts.ImageName = filepath.Base("/project/myapp") - } - assert.Equal(t, "myapp", opts.ImageName) - }) -} - -func TestBuildLinuxKit_DefaultOptions(t *testing.T) { - t.Run("sets defaults correctly", func(t *testing.T) { - opts := LinuxKitBuildOptions{} - - // Verify default values would be set - if opts.Template == "" { - opts.Template = "server-php" - } - assert.Equal(t, "server-php", opts.Template) - - if opts.Format == "" { - opts.Format = "qcow2" - } - assert.Equal(t, "qcow2", opts.Format) - }) -} - -func TestServeProduction_DefaultOptions(t *testing.T) { - t.Run("sets defaults correctly", func(t *testing.T) { - opts := ServeOptions{ImageName: "myapp"} - - // Verify default values would be set - if opts.Tag == "" { - opts.Tag = "latest" - } - assert.Equal(t, "latest", opts.Tag) - - if opts.Port == 0 { - opts.Port = 80 - } - assert.Equal(t, 80, opts.Port) - - if opts.HTTPSPort == 0 { - opts.HTTPSPort = 443 - } - assert.Equal(t, 443, opts.HTTPSPort) - }) -} - -func TestLookupLinuxKit_Good(t *testing.T) { - t.Skip("requires linuxkit installed") - - t.Run("finds linuxkit in PATH", func(t *testing.T) { - path, err := lookupLinuxKit() - assert.NoError(t, err) - assert.NotEmpty(t, path) - }) -} - -func TestBuildDocker_WithCustomDockerfile(t *testing.T) { - t.Skip("requires Docker installed") - - t.Run("uses custom Dockerfile when provided", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(`{"name":"test"}`), 0644) - require.NoError(t, err) - - dockerfilePath := filepath.Join(dir, "Dockerfile.custom") - err = os.WriteFile(dockerfilePath, []byte("FROM alpine"), 0644) - require.NoError(t, err) - - opts := DockerBuildOptions{ - ProjectDir: dir, - Dockerfile: dockerfilePath, - } - - // The function would use the custom Dockerfile - assert.Equal(t, dockerfilePath, opts.Dockerfile) - }) -} - -func TestBuildDocker_GeneratesDockerfile(t *testing.T) { - t.Skip("requires Docker installed") - - t.Run("generates Dockerfile when not provided", func(t *testing.T) { - dir := t.TempDir() - - // Create valid PHP project - composerJSON := `{"name":"test","require":{"php":"^8.2","laravel/framework":"^11.0"}}` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - opts := DockerBuildOptions{ - ProjectDir: dir, - // Dockerfile not specified - should be generated - } - - assert.Empty(t, opts.Dockerfile) - }) -} - -func TestServeProduction_BuildsCorrectArgs(t *testing.T) { - t.Run("builds correct docker run arguments", func(t *testing.T) { - opts := ServeOptions{ - ImageName: "myapp", - Tag: "v1.0.0", - ContainerName: "myapp-prod", - Port: 8080, - HTTPSPort: 8443, - Detach: true, - EnvFile: "/path/.env", - Volumes: map[string]string{ - "/host/storage": "/app/storage", - }, - } - - // Verify the expected image reference format - imageRef := opts.ImageName + ":" + opts.Tag - assert.Equal(t, "myapp:v1.0.0", imageRef) - - // Verify port format - portMapping := opts.Port - assert.Equal(t, 8080, portMapping) - }) -} - -func TestShell_Integration(t *testing.T) { - t.Skip("requires Docker with running container") -} - -func TestResolveDockerContainerID_Integration(t *testing.T) { - t.Skip("requires Docker with running containers") -} diff --git a/internal/cmd/php/coolify.go b/internal/cmd/php/coolify.go deleted file mode 100644 index fd08a06c..00000000 --- a/internal/cmd/php/coolify.go +++ /dev/null @@ -1,351 +0,0 @@ -package php - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// CoolifyClient is an HTTP client for the Coolify API. -type CoolifyClient struct { - BaseURL string - Token string - HTTPClient *http.Client -} - -// CoolifyConfig holds configuration loaded from environment. -type CoolifyConfig struct { - URL string - Token string - AppID string - StagingAppID string -} - -// CoolifyDeployment represents a deployment from the Coolify API. -type CoolifyDeployment struct { - ID string `json:"id"` - Status string `json:"status"` - CommitSHA string `json:"commit_sha,omitempty"` - CommitMsg string `json:"commit_message,omitempty"` - Branch string `json:"branch,omitempty"` - CreatedAt time.Time `json:"created_at"` - FinishedAt time.Time `json:"finished_at,omitempty"` - Log string `json:"log,omitempty"` - DeployedURL string `json:"deployed_url,omitempty"` -} - -// CoolifyApp represents an application from the Coolify API. -type CoolifyApp struct { - ID string `json:"id"` - Name string `json:"name"` - FQDN string `json:"fqdn,omitempty"` - Status string `json:"status,omitempty"` - Repository string `json:"repository,omitempty"` - Branch string `json:"branch,omitempty"` - Environment string `json:"environment,omitempty"` -} - -// NewCoolifyClient creates a new Coolify API client. -func NewCoolifyClient(baseURL, token string) *CoolifyClient { - // Ensure baseURL doesn't have trailing slash - baseURL = strings.TrimSuffix(baseURL, "/") - - return &CoolifyClient{ - BaseURL: baseURL, - Token: token, - HTTPClient: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// LoadCoolifyConfig loads Coolify configuration from .env file in the given directory. -func LoadCoolifyConfig(dir string) (*CoolifyConfig, error) { - envPath := filepath.Join(dir, ".env") - return LoadCoolifyConfigFromFile(envPath) -} - -// LoadCoolifyConfigFromFile loads Coolify configuration from a specific .env file. -func LoadCoolifyConfigFromFile(path string) (*CoolifyConfig, error) { - m := getMedium() - config := &CoolifyConfig{} - - // First try environment variables - config.URL = os.Getenv("COOLIFY_URL") - config.Token = os.Getenv("COOLIFY_TOKEN") - config.AppID = os.Getenv("COOLIFY_APP_ID") - config.StagingAppID = os.Getenv("COOLIFY_STAGING_APP_ID") - - // Then try .env file - if !m.Exists(path) { - // No .env file, just use env vars - return validateCoolifyConfig(config) - } - - content, err := m.Read(path) - if err != nil { - return nil, cli.WrapVerb(err, "read", ".env file") - } - - // Parse .env file - lines := strings.Split(content, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - // Remove quotes if present - value = strings.Trim(value, `"'`) - - // Only override if not already set from env - switch key { - case "COOLIFY_URL": - if config.URL == "" { - config.URL = value - } - case "COOLIFY_TOKEN": - if config.Token == "" { - config.Token = value - } - case "COOLIFY_APP_ID": - if config.AppID == "" { - config.AppID = value - } - case "COOLIFY_STAGING_APP_ID": - if config.StagingAppID == "" { - config.StagingAppID = value - } - } - } - - return validateCoolifyConfig(config) -} - -// validateCoolifyConfig checks that required fields are set. -func validateCoolifyConfig(config *CoolifyConfig) (*CoolifyConfig, error) { - if config.URL == "" { - return nil, cli.Err("COOLIFY_URL is not set") - } - if config.Token == "" { - return nil, cli.Err("COOLIFY_TOKEN is not set") - } - return config, nil -} - -// TriggerDeploy triggers a deployment for the specified application. -func (c *CoolifyClient) TriggerDeploy(ctx context.Context, appID string, force bool) (*CoolifyDeployment, error) { - endpoint := cli.Sprintf("%s/api/v1/applications/%s/deploy", c.BaseURL, appID) - - payload := map[string]interface{}{} - if force { - payload["force"] = true - } - - body, err := json.Marshal(payload) - if err != nil { - return nil, cli.WrapVerb(err, "marshal", "request") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) - if err != nil { - return nil, cli.WrapVerb(err, "create", "request") - } - - c.setHeaders(req) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, cli.Wrap(err, "request failed") - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted { - return nil, c.parseError(resp) - } - - var deployment CoolifyDeployment - if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil { - // Some Coolify versions return minimal response - return &CoolifyDeployment{ - Status: "queued", - CreatedAt: time.Now(), - }, nil - } - - return &deployment, nil -} - -// GetDeployment retrieves a specific deployment by ID. -func (c *CoolifyClient) GetDeployment(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) { - endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments/%s", c.BaseURL, appID, deploymentID) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, cli.WrapVerb(err, "create", "request") - } - - c.setHeaders(req) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, cli.Wrap(err, "request failed") - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return nil, c.parseError(resp) - } - - var deployment CoolifyDeployment - if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil { - return nil, cli.WrapVerb(err, "decode", "response") - } - - return &deployment, nil -} - -// ListDeployments retrieves deployments for an application. -func (c *CoolifyClient) ListDeployments(ctx context.Context, appID string, limit int) ([]CoolifyDeployment, error) { - endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments", c.BaseURL, appID) - if limit > 0 { - endpoint = cli.Sprintf("%s?limit=%d", endpoint, limit) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, cli.WrapVerb(err, "create", "request") - } - - c.setHeaders(req) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, cli.Wrap(err, "request failed") - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return nil, c.parseError(resp) - } - - var deployments []CoolifyDeployment - if err := json.NewDecoder(resp.Body).Decode(&deployments); err != nil { - return nil, cli.WrapVerb(err, "decode", "response") - } - - return deployments, nil -} - -// Rollback triggers a rollback to a previous deployment. -func (c *CoolifyClient) Rollback(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) { - endpoint := cli.Sprintf("%s/api/v1/applications/%s/rollback", c.BaseURL, appID) - - payload := map[string]interface{}{ - "deployment_id": deploymentID, - } - - body, err := json.Marshal(payload) - if err != nil { - return nil, cli.WrapVerb(err, "marshal", "request") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) - if err != nil { - return nil, cli.WrapVerb(err, "create", "request") - } - - c.setHeaders(req) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, cli.Wrap(err, "request failed") - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted { - return nil, c.parseError(resp) - } - - var deployment CoolifyDeployment - if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil { - return &CoolifyDeployment{ - Status: "rolling_back", - CreatedAt: time.Now(), - }, nil - } - - return &deployment, nil -} - -// GetApp retrieves application details. -func (c *CoolifyClient) GetApp(ctx context.Context, appID string) (*CoolifyApp, error) { - endpoint := cli.Sprintf("%s/api/v1/applications/%s", c.BaseURL, appID) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, cli.WrapVerb(err, "create", "request") - } - - c.setHeaders(req) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, cli.Wrap(err, "request failed") - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return nil, c.parseError(resp) - } - - var app CoolifyApp - if err := json.NewDecoder(resp.Body).Decode(&app); err != nil { - return nil, cli.WrapVerb(err, "decode", "response") - } - - return &app, nil -} - -// setHeaders sets common headers for API requests. -func (c *CoolifyClient) setHeaders(req *http.Request) { - req.Header.Set("Authorization", "Bearer "+c.Token) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") -} - -// parseError extracts error information from an API response. -func (c *CoolifyClient) parseError(resp *http.Response) error { - body, _ := io.ReadAll(resp.Body) - - var errResp struct { - Message string `json:"message"` - Error string `json:"error"` - } - - if err := json.Unmarshal(body, &errResp); err == nil { - if errResp.Message != "" { - return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Message) - } - if errResp.Error != "" { - return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Error) - } - } - - return cli.Err("API error (%d): %s", resp.StatusCode, string(body)) -} diff --git a/internal/cmd/php/coolify_test.go b/internal/cmd/php/coolify_test.go deleted file mode 100644 index 8176c88e..00000000 --- a/internal/cmd/php/coolify_test.go +++ /dev/null @@ -1,502 +0,0 @@ -package php - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCoolifyClient_Good(t *testing.T) { - t.Run("creates client with correct base URL", func(t *testing.T) { - client := NewCoolifyClient("https://coolify.example.com", "token") - - assert.Equal(t, "https://coolify.example.com", client.BaseURL) - assert.Equal(t, "token", client.Token) - assert.NotNil(t, client.HTTPClient) - }) - - t.Run("strips trailing slash from base URL", func(t *testing.T) { - client := NewCoolifyClient("https://coolify.example.com/", "token") - assert.Equal(t, "https://coolify.example.com", client.BaseURL) - }) - - t.Run("http client has timeout", func(t *testing.T) { - client := NewCoolifyClient("https://coolify.example.com", "token") - assert.Equal(t, 30*time.Second, client.HTTPClient.Timeout) - }) -} - -func TestCoolifyConfig_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - config := CoolifyConfig{ - URL: "https://coolify.example.com", - Token: "secret-token", - AppID: "app-123", - StagingAppID: "staging-456", - } - - assert.Equal(t, "https://coolify.example.com", config.URL) - assert.Equal(t, "secret-token", config.Token) - assert.Equal(t, "app-123", config.AppID) - assert.Equal(t, "staging-456", config.StagingAppID) - }) -} - -func TestCoolifyDeployment_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - now := time.Now() - deployment := CoolifyDeployment{ - ID: "dep-123", - Status: "finished", - CommitSHA: "abc123", - CommitMsg: "Test commit", - Branch: "main", - CreatedAt: now, - FinishedAt: now.Add(5 * time.Minute), - Log: "Build successful", - DeployedURL: "https://app.example.com", - } - - assert.Equal(t, "dep-123", deployment.ID) - assert.Equal(t, "finished", deployment.Status) - assert.Equal(t, "abc123", deployment.CommitSHA) - assert.Equal(t, "Test commit", deployment.CommitMsg) - assert.Equal(t, "main", deployment.Branch) - }) -} - -func TestCoolifyApp_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - app := CoolifyApp{ - ID: "app-123", - Name: "MyApp", - FQDN: "https://myapp.example.com", - Status: "running", - Repository: "https://github.com/user/repo", - Branch: "main", - Environment: "production", - } - - assert.Equal(t, "app-123", app.ID) - assert.Equal(t, "MyApp", app.Name) - assert.Equal(t, "https://myapp.example.com", app.FQDN) - assert.Equal(t, "running", app.Status) - }) -} - -func TestLoadCoolifyConfigFromFile_Good(t *testing.T) { - t.Run("loads config from .env file", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL=https://coolify.example.com -COOLIFY_TOKEN=secret-token -COOLIFY_APP_ID=app-123 -COOLIFY_STAGING_APP_ID=staging-456` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.NoError(t, err) - assert.Equal(t, "https://coolify.example.com", config.URL) - assert.Equal(t, "secret-token", config.Token) - assert.Equal(t, "app-123", config.AppID) - assert.Equal(t, "staging-456", config.StagingAppID) - }) - - t.Run("handles quoted values", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL="https://coolify.example.com" -COOLIFY_TOKEN='secret-token'` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.NoError(t, err) - assert.Equal(t, "https://coolify.example.com", config.URL) - assert.Equal(t, "secret-token", config.Token) - }) - - t.Run("ignores comments", func(t *testing.T) { - dir := t.TempDir() - envContent := `# This is a comment -COOLIFY_URL=https://coolify.example.com -# COOLIFY_TOKEN=wrong-token -COOLIFY_TOKEN=correct-token` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.NoError(t, err) - assert.Equal(t, "correct-token", config.Token) - }) - - t.Run("ignores blank lines", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL=https://coolify.example.com - -COOLIFY_TOKEN=secret-token` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.NoError(t, err) - assert.Equal(t, "https://coolify.example.com", config.URL) - }) -} - -func TestLoadCoolifyConfigFromFile_Bad(t *testing.T) { - t.Run("fails when COOLIFY_URL missing", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_TOKEN=secret-token` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - _, err = LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.Error(t, err) - assert.Contains(t, err.Error(), "COOLIFY_URL is not set") - }) - - t.Run("fails when COOLIFY_TOKEN missing", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL=https://coolify.example.com` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - _, err = LoadCoolifyConfigFromFile(filepath.Join(dir, ".env")) - assert.Error(t, err) - assert.Contains(t, err.Error(), "COOLIFY_TOKEN is not set") - }) -} - -func TestLoadCoolifyConfig_FromDirectory_Good(t *testing.T) { - t.Run("loads from directory", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL=https://coolify.example.com -COOLIFY_TOKEN=secret-token` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - config, err := LoadCoolifyConfig(dir) - assert.NoError(t, err) - assert.Equal(t, "https://coolify.example.com", config.URL) - }) -} - -func TestValidateCoolifyConfig_Bad(t *testing.T) { - t.Run("returns error for empty URL", func(t *testing.T) { - config := &CoolifyConfig{Token: "token"} - _, err := validateCoolifyConfig(config) - assert.Error(t, err) - assert.Contains(t, err.Error(), "COOLIFY_URL is not set") - }) - - t.Run("returns error for empty token", func(t *testing.T) { - config := &CoolifyConfig{URL: "https://coolify.example.com"} - _, err := validateCoolifyConfig(config) - assert.Error(t, err) - assert.Contains(t, err.Error(), "COOLIFY_TOKEN is not set") - }) -} - -func TestCoolifyClient_TriggerDeploy_Good(t *testing.T) { - t.Run("triggers deployment successfully", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v1/applications/app-123/deploy", r.URL.Path) - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "Bearer secret-token", r.Header.Get("Authorization")) - assert.Equal(t, "application/json", r.Header.Get("Content-Type")) - - resp := CoolifyDeployment{ - ID: "dep-456", - Status: "queued", - CreatedAt: time.Now(), - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - deployment, err := client.TriggerDeploy(context.Background(), "app-123", false) - - assert.NoError(t, err) - assert.Equal(t, "dep-456", deployment.ID) - assert.Equal(t, "queued", deployment.Status) - }) - - t.Run("triggers deployment with force", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var body map[string]interface{} - _ = json.NewDecoder(r.Body).Decode(&body) - assert.Equal(t, true, body["force"]) - - resp := CoolifyDeployment{ID: "dep-456", Status: "queued"} - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - _, err := client.TriggerDeploy(context.Background(), "app-123", true) - assert.NoError(t, err) - }) - - t.Run("handles minimal response", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Return an invalid JSON response to trigger the fallback - _, _ = w.Write([]byte("not json")) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - deployment, err := client.TriggerDeploy(context.Background(), "app-123", false) - - assert.NoError(t, err) - // The fallback response should be returned - assert.Equal(t, "queued", deployment.Status) - }) -} - -func TestCoolifyClient_TriggerDeploy_Bad(t *testing.T) { - t.Run("fails on HTTP error", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"message": "Internal error"}) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - _, err := client.TriggerDeploy(context.Background(), "app-123", false) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "API error") - }) -} - -func TestCoolifyClient_GetDeployment_Good(t *testing.T) { - t.Run("gets deployment details", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v1/applications/app-123/deployments/dep-456", r.URL.Path) - assert.Equal(t, "GET", r.Method) - - resp := CoolifyDeployment{ - ID: "dep-456", - Status: "finished", - CommitSHA: "abc123", - Branch: "main", - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - deployment, err := client.GetDeployment(context.Background(), "app-123", "dep-456") - - assert.NoError(t, err) - assert.Equal(t, "dep-456", deployment.ID) - assert.Equal(t, "finished", deployment.Status) - assert.Equal(t, "abc123", deployment.CommitSHA) - }) -} - -func TestCoolifyClient_GetDeployment_Bad(t *testing.T) { - t.Run("fails on 404", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "Not found"}) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - _, err := client.GetDeployment(context.Background(), "app-123", "dep-456") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "Not found") - }) -} - -func TestCoolifyClient_ListDeployments_Good(t *testing.T) { - t.Run("lists deployments", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v1/applications/app-123/deployments", r.URL.Path) - assert.Equal(t, "10", r.URL.Query().Get("limit")) - - resp := []CoolifyDeployment{ - {ID: "dep-1", Status: "finished"}, - {ID: "dep-2", Status: "failed"}, - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - deployments, err := client.ListDeployments(context.Background(), "app-123", 10) - - assert.NoError(t, err) - assert.Len(t, deployments, 2) - assert.Equal(t, "dep-1", deployments[0].ID) - assert.Equal(t, "dep-2", deployments[1].ID) - }) - - t.Run("lists without limit", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "", r.URL.Query().Get("limit")) - _ = json.NewEncoder(w).Encode([]CoolifyDeployment{}) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - _, err := client.ListDeployments(context.Background(), "app-123", 0) - assert.NoError(t, err) - }) -} - -func TestCoolifyClient_Rollback_Good(t *testing.T) { - t.Run("triggers rollback", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v1/applications/app-123/rollback", r.URL.Path) - assert.Equal(t, "POST", r.Method) - - var body map[string]string - _ = json.NewDecoder(r.Body).Decode(&body) - assert.Equal(t, "dep-old", body["deployment_id"]) - - resp := CoolifyDeployment{ - ID: "dep-new", - Status: "rolling_back", - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - deployment, err := client.Rollback(context.Background(), "app-123", "dep-old") - - assert.NoError(t, err) - assert.Equal(t, "dep-new", deployment.ID) - assert.Equal(t, "rolling_back", deployment.Status) - }) -} - -func TestCoolifyClient_GetApp_Good(t *testing.T) { - t.Run("gets app details", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v1/applications/app-123", r.URL.Path) - assert.Equal(t, "GET", r.Method) - - resp := CoolifyApp{ - ID: "app-123", - Name: "MyApp", - FQDN: "https://myapp.example.com", - Status: "running", - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "secret-token") - app, err := client.GetApp(context.Background(), "app-123") - - assert.NoError(t, err) - assert.Equal(t, "app-123", app.ID) - assert.Equal(t, "MyApp", app.Name) - assert.Equal(t, "https://myapp.example.com", app.FQDN) - }) -} - -func TestCoolifyClient_SetHeaders(t *testing.T) { - t.Run("sets all required headers", func(t *testing.T) { - client := NewCoolifyClient("https://coolify.example.com", "my-token") - req, _ := http.NewRequest("GET", "https://coolify.example.com", nil) - - client.setHeaders(req) - - assert.Equal(t, "Bearer my-token", req.Header.Get("Authorization")) - assert.Equal(t, "application/json", req.Header.Get("Content-Type")) - assert.Equal(t, "application/json", req.Header.Get("Accept")) - }) -} - -func TestCoolifyClient_ParseError(t *testing.T) { - t.Run("parses message field", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"message": "Bad request message"}) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "token") - _, err := client.GetApp(context.Background(), "app-123") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "Bad request message") - }) - - t.Run("parses error field", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "Error message"}) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "token") - _, err := client.GetApp(context.Background(), "app-123") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error message") - }) - - t.Run("returns raw body when no JSON fields", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("Raw error message")) - })) - defer server.Close() - - client := NewCoolifyClient(server.URL, "token") - _, err := client.GetApp(context.Background(), "app-123") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "Raw error message") - }) -} - -func TestEnvironmentVariablePriority(t *testing.T) { - t.Run("env vars take precedence over .env file", func(t *testing.T) { - dir := t.TempDir() - envContent := `COOLIFY_URL=https://from-file.com -COOLIFY_TOKEN=file-token` - - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - // Set environment variables - origURL := os.Getenv("COOLIFY_URL") - origToken := os.Getenv("COOLIFY_TOKEN") - defer func() { - _ = os.Setenv("COOLIFY_URL", origURL) - _ = os.Setenv("COOLIFY_TOKEN", origToken) - }() - - _ = os.Setenv("COOLIFY_URL", "https://from-env.com") - _ = os.Setenv("COOLIFY_TOKEN", "env-token") - - config, err := LoadCoolifyConfig(dir) - assert.NoError(t, err) - // Environment variables should take precedence - assert.Equal(t, "https://from-env.com", config.URL) - assert.Equal(t, "env-token", config.Token) - }) -} diff --git a/internal/cmd/php/deploy.go b/internal/cmd/php/deploy.go deleted file mode 100644 index 9717ae70..00000000 --- a/internal/cmd/php/deploy.go +++ /dev/null @@ -1,407 +0,0 @@ -package php - -import ( - "context" - "time" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// Environment represents a deployment environment. -type Environment string - -const ( - // EnvProduction is the production environment. - EnvProduction Environment = "production" - // EnvStaging is the staging environment. - EnvStaging Environment = "staging" -) - -// DeployOptions configures a deployment. -type DeployOptions struct { - // Dir is the project directory containing .env config. - Dir string - - // Environment is the target environment (production or staging). - Environment Environment - - // Force triggers a deployment even if no changes are detected. - Force bool - - // Wait blocks until deployment completes. - Wait bool - - // WaitTimeout is the maximum time to wait for deployment. - // Defaults to 10 minutes. - WaitTimeout time.Duration - - // PollInterval is how often to check deployment status when waiting. - // Defaults to 5 seconds. - PollInterval time.Duration -} - -// StatusOptions configures a status check. -type StatusOptions struct { - // Dir is the project directory containing .env config. - Dir string - - // Environment is the target environment (production or staging). - Environment Environment - - // DeploymentID is a specific deployment to check. - // If empty, returns the latest deployment. - DeploymentID string -} - -// RollbackOptions configures a rollback. -type RollbackOptions struct { - // Dir is the project directory containing .env config. - Dir string - - // Environment is the target environment (production or staging). - Environment Environment - - // DeploymentID is the deployment to rollback to. - // If empty, rolls back to the previous successful deployment. - DeploymentID string - - // Wait blocks until rollback completes. - Wait bool - - // WaitTimeout is the maximum time to wait for rollback. - WaitTimeout time.Duration -} - -// DeploymentStatus represents the status of a deployment. -type DeploymentStatus struct { - // ID is the deployment identifier. - ID string - - // Status is the current deployment status. - // Values: queued, building, deploying, finished, failed, cancelled - Status string - - // URL is the deployed application URL. - URL string - - // Commit is the git commit SHA. - Commit string - - // CommitMessage is the git commit message. - CommitMessage string - - // Branch is the git branch. - Branch string - - // StartedAt is when the deployment started. - StartedAt time.Time - - // CompletedAt is when the deployment completed. - CompletedAt time.Time - - // Log contains deployment logs. - Log string -} - -// Deploy triggers a deployment to Coolify. -func Deploy(ctx context.Context, opts DeployOptions) (*DeploymentStatus, error) { - if opts.Dir == "" { - opts.Dir = "." - } - if opts.Environment == "" { - opts.Environment = EnvProduction - } - if opts.WaitTimeout == 0 { - opts.WaitTimeout = 10 * time.Minute - } - if opts.PollInterval == 0 { - opts.PollInterval = 5 * time.Second - } - - // Load config - config, err := LoadCoolifyConfig(opts.Dir) - if err != nil { - return nil, cli.WrapVerb(err, "load", "Coolify config") - } - - // Get app ID for environment - appID := getAppIDForEnvironment(config, opts.Environment) - if appID == "" { - return nil, cli.Err("no app ID configured for %s environment", opts.Environment) - } - - // Create client - client := NewCoolifyClient(config.URL, config.Token) - - // Trigger deployment - deployment, err := client.TriggerDeploy(ctx, appID, opts.Force) - if err != nil { - return nil, cli.WrapVerb(err, "trigger", "deployment") - } - - status := convertDeployment(deployment) - - // Wait for completion if requested - if opts.Wait && deployment.ID != "" { - status, err = waitForDeployment(ctx, client, appID, deployment.ID, opts.WaitTimeout, opts.PollInterval) - if err != nil { - return status, err - } - } - - // Get app info for URL - app, err := client.GetApp(ctx, appID) - if err == nil && app.FQDN != "" { - status.URL = app.FQDN - } - - return status, nil -} - -// DeployStatus retrieves the status of a deployment. -func DeployStatus(ctx context.Context, opts StatusOptions) (*DeploymentStatus, error) { - if opts.Dir == "" { - opts.Dir = "." - } - if opts.Environment == "" { - opts.Environment = EnvProduction - } - - // Load config - config, err := LoadCoolifyConfig(opts.Dir) - if err != nil { - return nil, cli.WrapVerb(err, "load", "Coolify config") - } - - // Get app ID for environment - appID := getAppIDForEnvironment(config, opts.Environment) - if appID == "" { - return nil, cli.Err("no app ID configured for %s environment", opts.Environment) - } - - // Create client - client := NewCoolifyClient(config.URL, config.Token) - - var deployment *CoolifyDeployment - - if opts.DeploymentID != "" { - // Get specific deployment - deployment, err = client.GetDeployment(ctx, appID, opts.DeploymentID) - if err != nil { - return nil, cli.WrapVerb(err, "get", "deployment") - } - } else { - // Get latest deployment - deployments, err := client.ListDeployments(ctx, appID, 1) - if err != nil { - return nil, cli.WrapVerb(err, "list", "deployments") - } - if len(deployments) == 0 { - return nil, cli.Err("no deployments found") - } - deployment = &deployments[0] - } - - status := convertDeployment(deployment) - - // Get app info for URL - app, err := client.GetApp(ctx, appID) - if err == nil && app.FQDN != "" { - status.URL = app.FQDN - } - - return status, nil -} - -// Rollback triggers a rollback to a previous deployment. -func Rollback(ctx context.Context, opts RollbackOptions) (*DeploymentStatus, error) { - if opts.Dir == "" { - opts.Dir = "." - } - if opts.Environment == "" { - opts.Environment = EnvProduction - } - if opts.WaitTimeout == 0 { - opts.WaitTimeout = 10 * time.Minute - } - - // Load config - config, err := LoadCoolifyConfig(opts.Dir) - if err != nil { - return nil, cli.WrapVerb(err, "load", "Coolify config") - } - - // Get app ID for environment - appID := getAppIDForEnvironment(config, opts.Environment) - if appID == "" { - return nil, cli.Err("no app ID configured for %s environment", opts.Environment) - } - - // Create client - client := NewCoolifyClient(config.URL, config.Token) - - // Find deployment to rollback to - deploymentID := opts.DeploymentID - if deploymentID == "" { - // Find previous successful deployment - deployments, err := client.ListDeployments(ctx, appID, 10) - if err != nil { - return nil, cli.WrapVerb(err, "list", "deployments") - } - - // Skip the first (current) deployment, find the last successful one - for i, d := range deployments { - if i == 0 { - continue // Skip current deployment - } - if d.Status == "finished" || d.Status == "success" { - deploymentID = d.ID - break - } - } - - if deploymentID == "" { - return nil, cli.Err("no previous successful deployment found to rollback to") - } - } - - // Trigger rollback - deployment, err := client.Rollback(ctx, appID, deploymentID) - if err != nil { - return nil, cli.WrapVerb(err, "trigger", "rollback") - } - - status := convertDeployment(deployment) - - // Wait for completion if requested - if opts.Wait && deployment.ID != "" { - status, err = waitForDeployment(ctx, client, appID, deployment.ID, opts.WaitTimeout, 5*time.Second) - if err != nil { - return status, err - } - } - - return status, nil -} - -// ListDeployments retrieves recent deployments. -func ListDeployments(ctx context.Context, dir string, env Environment, limit int) ([]DeploymentStatus, error) { - if dir == "" { - dir = "." - } - if env == "" { - env = EnvProduction - } - if limit == 0 { - limit = 10 - } - - // Load config - config, err := LoadCoolifyConfig(dir) - if err != nil { - return nil, cli.WrapVerb(err, "load", "Coolify config") - } - - // Get app ID for environment - appID := getAppIDForEnvironment(config, env) - if appID == "" { - return nil, cli.Err("no app ID configured for %s environment", env) - } - - // Create client - client := NewCoolifyClient(config.URL, config.Token) - - deployments, err := client.ListDeployments(ctx, appID, limit) - if err != nil { - return nil, cli.WrapVerb(err, "list", "deployments") - } - - result := make([]DeploymentStatus, len(deployments)) - for i, d := range deployments { - result[i] = *convertDeployment(&d) - } - - return result, nil -} - -// getAppIDForEnvironment returns the app ID for the given environment. -func getAppIDForEnvironment(config *CoolifyConfig, env Environment) string { - switch env { - case EnvStaging: - if config.StagingAppID != "" { - return config.StagingAppID - } - return config.AppID // Fallback to production - default: - return config.AppID - } -} - -// convertDeployment converts a CoolifyDeployment to DeploymentStatus. -func convertDeployment(d *CoolifyDeployment) *DeploymentStatus { - return &DeploymentStatus{ - ID: d.ID, - Status: d.Status, - URL: d.DeployedURL, - Commit: d.CommitSHA, - CommitMessage: d.CommitMsg, - Branch: d.Branch, - StartedAt: d.CreatedAt, - CompletedAt: d.FinishedAt, - Log: d.Log, - } -} - -// waitForDeployment polls for deployment completion. -func waitForDeployment(ctx context.Context, client *CoolifyClient, appID, deploymentID string, timeout, interval time.Duration) (*DeploymentStatus, error) { - deadline := time.Now().Add(timeout) - - for time.Now().Before(deadline) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - deployment, err := client.GetDeployment(ctx, appID, deploymentID) - if err != nil { - return nil, cli.WrapVerb(err, "get", "deployment status") - } - - status := convertDeployment(deployment) - - // Check if deployment is complete - switch deployment.Status { - case "finished", "success": - return status, nil - case "failed", "error": - return status, cli.Err("deployment failed: %s", deployment.Status) - case "cancelled": - return status, cli.Err("deployment was cancelled") - } - - // Still in progress, wait and retry - select { - case <-ctx.Done(): - return status, ctx.Err() - case <-time.After(interval): - } - } - - return nil, cli.Err("deployment timed out after %v", timeout) -} - -// IsDeploymentComplete returns true if the status indicates completion. -func IsDeploymentComplete(status string) bool { - switch status { - case "finished", "success", "failed", "error", "cancelled": - return true - default: - return false - } -} - -// IsDeploymentSuccessful returns true if the status indicates success. -func IsDeploymentSuccessful(status string) bool { - return status == "finished" || status == "success" -} diff --git a/internal/cmd/php/deploy_internal_test.go b/internal/cmd/php/deploy_internal_test.go deleted file mode 100644 index 9362aaf5..00000000 --- a/internal/cmd/php/deploy_internal_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package php - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestConvertDeployment_Good(t *testing.T) { - t.Run("converts all fields", func(t *testing.T) { - now := time.Now() - coolify := &CoolifyDeployment{ - ID: "dep-123", - Status: "finished", - CommitSHA: "abc123", - CommitMsg: "Test commit", - Branch: "main", - CreatedAt: now, - FinishedAt: now.Add(5 * time.Minute), - Log: "Build successful", - DeployedURL: "https://app.example.com", - } - - status := convertDeployment(coolify) - - assert.Equal(t, "dep-123", status.ID) - assert.Equal(t, "finished", status.Status) - assert.Equal(t, "https://app.example.com", status.URL) - assert.Equal(t, "abc123", status.Commit) - assert.Equal(t, "Test commit", status.CommitMessage) - assert.Equal(t, "main", status.Branch) - assert.Equal(t, now, status.StartedAt) - assert.Equal(t, now.Add(5*time.Minute), status.CompletedAt) - assert.Equal(t, "Build successful", status.Log) - }) - - t.Run("handles empty deployment", func(t *testing.T) { - coolify := &CoolifyDeployment{} - status := convertDeployment(coolify) - - assert.Empty(t, status.ID) - assert.Empty(t, status.Status) - }) -} - -func TestDeploymentStatus_Struct_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - now := time.Now() - status := DeploymentStatus{ - ID: "dep-123", - Status: "finished", - URL: "https://app.example.com", - Commit: "abc123", - CommitMessage: "Test commit", - Branch: "main", - StartedAt: now, - CompletedAt: now.Add(5 * time.Minute), - Log: "Build log", - } - - assert.Equal(t, "dep-123", status.ID) - assert.Equal(t, "finished", status.Status) - assert.Equal(t, "https://app.example.com", status.URL) - assert.Equal(t, "abc123", status.Commit) - assert.Equal(t, "Test commit", status.CommitMessage) - assert.Equal(t, "main", status.Branch) - assert.Equal(t, "Build log", status.Log) - }) -} - -func TestDeployOptions_Struct_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := DeployOptions{ - Dir: "/project", - Environment: EnvProduction, - Force: true, - Wait: true, - WaitTimeout: 10 * time.Minute, - PollInterval: 5 * time.Second, - } - - assert.Equal(t, "/project", opts.Dir) - assert.Equal(t, EnvProduction, opts.Environment) - assert.True(t, opts.Force) - assert.True(t, opts.Wait) - assert.Equal(t, 10*time.Minute, opts.WaitTimeout) - assert.Equal(t, 5*time.Second, opts.PollInterval) - }) -} - -func TestStatusOptions_Struct_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := StatusOptions{ - Dir: "/project", - Environment: EnvStaging, - DeploymentID: "dep-123", - } - - assert.Equal(t, "/project", opts.Dir) - assert.Equal(t, EnvStaging, opts.Environment) - assert.Equal(t, "dep-123", opts.DeploymentID) - }) -} - -func TestRollbackOptions_Struct_Good(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := RollbackOptions{ - Dir: "/project", - Environment: EnvProduction, - DeploymentID: "dep-old", - Wait: true, - WaitTimeout: 5 * time.Minute, - } - - assert.Equal(t, "/project", opts.Dir) - assert.Equal(t, EnvProduction, opts.Environment) - assert.Equal(t, "dep-old", opts.DeploymentID) - assert.True(t, opts.Wait) - assert.Equal(t, 5*time.Minute, opts.WaitTimeout) - }) -} - -func TestEnvironment_Constants(t *testing.T) { - t.Run("constants are defined", func(t *testing.T) { - assert.Equal(t, Environment("production"), EnvProduction) - assert.Equal(t, Environment("staging"), EnvStaging) - }) -} - -func TestGetAppIDForEnvironment_Edge(t *testing.T) { - t.Run("staging without staging ID falls back to production", func(t *testing.T) { - config := &CoolifyConfig{ - AppID: "prod-123", - // No StagingAppID set - } - - id := getAppIDForEnvironment(config, EnvStaging) - assert.Equal(t, "prod-123", id) - }) - - t.Run("staging with staging ID uses staging", func(t *testing.T) { - config := &CoolifyConfig{ - AppID: "prod-123", - StagingAppID: "staging-456", - } - - id := getAppIDForEnvironment(config, EnvStaging) - assert.Equal(t, "staging-456", id) - }) - - t.Run("production uses production ID", func(t *testing.T) { - config := &CoolifyConfig{ - AppID: "prod-123", - StagingAppID: "staging-456", - } - - id := getAppIDForEnvironment(config, EnvProduction) - assert.Equal(t, "prod-123", id) - }) - - t.Run("unknown environment uses production", func(t *testing.T) { - config := &CoolifyConfig{ - AppID: "prod-123", - } - - id := getAppIDForEnvironment(config, "unknown") - assert.Equal(t, "prod-123", id) - }) -} - -func TestIsDeploymentComplete_Edge(t *testing.T) { - tests := []struct { - status string - expected bool - }{ - {"finished", true}, - {"success", true}, - {"failed", true}, - {"error", true}, - {"cancelled", true}, - {"queued", false}, - {"building", false}, - {"deploying", false}, - {"pending", false}, - {"rolling_back", false}, - {"", false}, - {"unknown", false}, - } - - for _, tt := range tests { - t.Run(tt.status, func(t *testing.T) { - result := IsDeploymentComplete(tt.status) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestIsDeploymentSuccessful_Edge(t *testing.T) { - tests := []struct { - status string - expected bool - }{ - {"finished", true}, - {"success", true}, - {"failed", false}, - {"error", false}, - {"cancelled", false}, - {"queued", false}, - {"building", false}, - {"deploying", false}, - {"", false}, - } - - for _, tt := range tests { - t.Run(tt.status, func(t *testing.T) { - result := IsDeploymentSuccessful(tt.status) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/internal/cmd/php/deploy_test.go b/internal/cmd/php/deploy_test.go deleted file mode 100644 index 228de7d2..00000000 --- a/internal/cmd/php/deploy_test.go +++ /dev/null @@ -1,257 +0,0 @@ -package php - -import ( - "os" - "path/filepath" - "testing" -) - -func TestLoadCoolifyConfig_Good(t *testing.T) { - tests := []struct { - name string - envContent string - wantURL string - wantToken string - wantAppID string - wantStaging string - }{ - { - name: "all values set", - envContent: `COOLIFY_URL=https://coolify.example.com -COOLIFY_TOKEN=secret-token -COOLIFY_APP_ID=app-123 -COOLIFY_STAGING_APP_ID=staging-456`, - wantURL: "https://coolify.example.com", - wantToken: "secret-token", - wantAppID: "app-123", - wantStaging: "staging-456", - }, - { - name: "quoted values", - envContent: `COOLIFY_URL="https://coolify.example.com" -COOLIFY_TOKEN='secret-token' -COOLIFY_APP_ID="app-123"`, - wantURL: "https://coolify.example.com", - wantToken: "secret-token", - wantAppID: "app-123", - }, - { - name: "with comments and blank lines", - envContent: `# Coolify configuration -COOLIFY_URL=https://coolify.example.com - -# API token -COOLIFY_TOKEN=secret-token -COOLIFY_APP_ID=app-123 -# COOLIFY_STAGING_APP_ID=not-this`, - wantURL: "https://coolify.example.com", - wantToken: "secret-token", - wantAppID: "app-123", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create temp directory - dir := t.TempDir() - envPath := filepath.Join(dir, ".env") - - // Write .env file - if err := os.WriteFile(envPath, []byte(tt.envContent), 0644); err != nil { - t.Fatalf("failed to write .env: %v", err) - } - - // Load config - config, err := LoadCoolifyConfig(dir) - if err != nil { - t.Fatalf("LoadCoolifyConfig() error = %v", err) - } - - if config.URL != tt.wantURL { - t.Errorf("URL = %q, want %q", config.URL, tt.wantURL) - } - if config.Token != tt.wantToken { - t.Errorf("Token = %q, want %q", config.Token, tt.wantToken) - } - if config.AppID != tt.wantAppID { - t.Errorf("AppID = %q, want %q", config.AppID, tt.wantAppID) - } - if tt.wantStaging != "" && config.StagingAppID != tt.wantStaging { - t.Errorf("StagingAppID = %q, want %q", config.StagingAppID, tt.wantStaging) - } - }) - } -} - -func TestLoadCoolifyConfig_Bad(t *testing.T) { - tests := []struct { - name string - envContent string - wantErr string - }{ - { - name: "missing URL", - envContent: "COOLIFY_TOKEN=secret", - wantErr: "COOLIFY_URL is not set", - }, - { - name: "missing token", - envContent: "COOLIFY_URL=https://coolify.example.com", - wantErr: "COOLIFY_TOKEN is not set", - }, - { - name: "empty values", - envContent: "COOLIFY_URL=\nCOOLIFY_TOKEN=", - wantErr: "COOLIFY_URL is not set", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create temp directory - dir := t.TempDir() - envPath := filepath.Join(dir, ".env") - - // Write .env file - if err := os.WriteFile(envPath, []byte(tt.envContent), 0644); err != nil { - t.Fatalf("failed to write .env: %v", err) - } - - // Load config - _, err := LoadCoolifyConfig(dir) - if err == nil { - t.Fatal("LoadCoolifyConfig() expected error, got nil") - } - - if err.Error() != tt.wantErr { - t.Errorf("error = %q, want %q", err.Error(), tt.wantErr) - } - }) - } -} - -func TestGetAppIDForEnvironment_Good(t *testing.T) { - config := &CoolifyConfig{ - URL: "https://coolify.example.com", - Token: "token", - AppID: "prod-123", - StagingAppID: "staging-456", - } - - tests := []struct { - name string - env Environment - wantID string - }{ - { - name: "production environment", - env: EnvProduction, - wantID: "prod-123", - }, - { - name: "staging environment", - env: EnvStaging, - wantID: "staging-456", - }, - { - name: "empty defaults to production", - env: "", - wantID: "prod-123", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id := getAppIDForEnvironment(config, tt.env) - if id != tt.wantID { - t.Errorf("getAppIDForEnvironment() = %q, want %q", id, tt.wantID) - } - }) - } -} - -func TestGetAppIDForEnvironment_FallbackToProduction(t *testing.T) { - config := &CoolifyConfig{ - URL: "https://coolify.example.com", - Token: "token", - AppID: "prod-123", - // No staging app ID - } - - // Staging should fall back to production - id := getAppIDForEnvironment(config, EnvStaging) - if id != "prod-123" { - t.Errorf("getAppIDForEnvironment(EnvStaging) = %q, want %q (should fallback)", id, "prod-123") - } -} - -func TestIsDeploymentComplete_Good(t *testing.T) { - completeStatuses := []string{"finished", "success", "failed", "error", "cancelled"} - for _, status := range completeStatuses { - if !IsDeploymentComplete(status) { - t.Errorf("IsDeploymentComplete(%q) = false, want true", status) - } - } - - incompleteStatuses := []string{"queued", "building", "deploying", "pending", "rolling_back"} - for _, status := range incompleteStatuses { - if IsDeploymentComplete(status) { - t.Errorf("IsDeploymentComplete(%q) = true, want false", status) - } - } -} - -func TestIsDeploymentSuccessful_Good(t *testing.T) { - successStatuses := []string{"finished", "success"} - for _, status := range successStatuses { - if !IsDeploymentSuccessful(status) { - t.Errorf("IsDeploymentSuccessful(%q) = false, want true", status) - } - } - - failedStatuses := []string{"failed", "error", "cancelled", "queued", "building"} - for _, status := range failedStatuses { - if IsDeploymentSuccessful(status) { - t.Errorf("IsDeploymentSuccessful(%q) = true, want false", status) - } - } -} - -func TestNewCoolifyClient_Good(t *testing.T) { - tests := []struct { - name string - baseURL string - wantBaseURL string - }{ - { - name: "URL without trailing slash", - baseURL: "https://coolify.example.com", - wantBaseURL: "https://coolify.example.com", - }, - { - name: "URL with trailing slash", - baseURL: "https://coolify.example.com/", - wantBaseURL: "https://coolify.example.com", - }, - { - name: "URL with api path", - baseURL: "https://coolify.example.com/api/", - wantBaseURL: "https://coolify.example.com/api", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := NewCoolifyClient(tt.baseURL, "token") - if client.BaseURL != tt.wantBaseURL { - t.Errorf("BaseURL = %q, want %q", client.BaseURL, tt.wantBaseURL) - } - if client.Token != "token" { - t.Errorf("Token = %q, want %q", client.Token, "token") - } - if client.HTTPClient == nil { - t.Error("HTTPClient is nil") - } - }) - } -} diff --git a/internal/cmd/php/detect.go b/internal/cmd/php/detect.go deleted file mode 100644 index c13da9d7..00000000 --- a/internal/cmd/php/detect.go +++ /dev/null @@ -1,296 +0,0 @@ -package php - -import ( - "encoding/json" - "path/filepath" - "strings" -) - -// DetectedService represents a service that was detected in a Laravel project. -type DetectedService string - -// Detected service constants for Laravel projects. -const ( - // ServiceFrankenPHP indicates FrankenPHP server is detected. - ServiceFrankenPHP DetectedService = "frankenphp" - // ServiceVite indicates Vite frontend bundler is detected. - ServiceVite DetectedService = "vite" - // ServiceHorizon indicates Laravel Horizon queue dashboard is detected. - ServiceHorizon DetectedService = "horizon" - // ServiceReverb indicates Laravel Reverb WebSocket server is detected. - ServiceReverb DetectedService = "reverb" - // ServiceRedis indicates Redis cache/queue backend is detected. - ServiceRedis DetectedService = "redis" -) - -// IsLaravelProject checks if the given directory is a Laravel project. -// It looks for the presence of artisan file and laravel in composer.json. -func IsLaravelProject(dir string) bool { - m := getMedium() - - // Check for artisan file - artisanPath := filepath.Join(dir, "artisan") - if !m.Exists(artisanPath) { - return false - } - - // Check composer.json for laravel/framework - composerPath := filepath.Join(dir, "composer.json") - data, err := m.Read(composerPath) - if err != nil { - return false - } - - var composer struct { - Require map[string]string `json:"require"` - RequireDev map[string]string `json:"require-dev"` - } - - if err := json.Unmarshal([]byte(data), &composer); err != nil { - return false - } - - // Check for laravel/framework in require - if _, ok := composer.Require["laravel/framework"]; ok { - return true - } - - // Also check require-dev (less common but possible) - if _, ok := composer.RequireDev["laravel/framework"]; ok { - return true - } - - return false -} - -// IsFrankenPHPProject checks if the project is configured for FrankenPHP. -// It looks for laravel/octane with frankenphp driver. -func IsFrankenPHPProject(dir string) bool { - m := getMedium() - - // Check composer.json for laravel/octane - composerPath := filepath.Join(dir, "composer.json") - data, err := m.Read(composerPath) - if err != nil { - return false - } - - var composer struct { - Require map[string]string `json:"require"` - } - - if err := json.Unmarshal([]byte(data), &composer); err != nil { - return false - } - - if _, ok := composer.Require["laravel/octane"]; !ok { - return false - } - - // Check octane config for frankenphp - configPath := filepath.Join(dir, "config", "octane.php") - if !m.Exists(configPath) { - // If no config exists but octane is installed, assume frankenphp - return true - } - - configData, err := m.Read(configPath) - if err != nil { - return true // Assume frankenphp if we can't read config - } - - // Look for frankenphp in the config - return strings.Contains(configData, "frankenphp") -} - -// DetectServices detects which services are needed based on project files. -func DetectServices(dir string) []DetectedService { - services := []DetectedService{} - - // FrankenPHP/Octane is always needed for a Laravel dev environment - if IsFrankenPHPProject(dir) || IsLaravelProject(dir) { - services = append(services, ServiceFrankenPHP) - } - - // Check for Vite - if hasVite(dir) { - services = append(services, ServiceVite) - } - - // Check for Horizon - if hasHorizon(dir) { - services = append(services, ServiceHorizon) - } - - // Check for Reverb - if hasReverb(dir) { - services = append(services, ServiceReverb) - } - - // Check for Redis - if needsRedis(dir) { - services = append(services, ServiceRedis) - } - - return services -} - -// hasVite checks if the project uses Vite. -func hasVite(dir string) bool { - m := getMedium() - viteConfigs := []string{ - "vite.config.js", - "vite.config.ts", - "vite.config.mjs", - "vite.config.mts", - } - - for _, config := range viteConfigs { - if m.Exists(filepath.Join(dir, config)) { - return true - } - } - - return false -} - -// hasHorizon checks if Laravel Horizon is configured. -func hasHorizon(dir string) bool { - horizonConfig := filepath.Join(dir, "config", "horizon.php") - return getMedium().Exists(horizonConfig) -} - -// hasReverb checks if Laravel Reverb is configured. -func hasReverb(dir string) bool { - reverbConfig := filepath.Join(dir, "config", "reverb.php") - return getMedium().Exists(reverbConfig) -} - -// needsRedis checks if the project uses Redis based on .env configuration. -func needsRedis(dir string) bool { - m := getMedium() - envPath := filepath.Join(dir, ".env") - content, err := m.Read(envPath) - if err != nil { - return false - } - - lines := strings.Split(content, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "#") { - continue - } - - // Check for Redis-related environment variables - redisIndicators := []string{ - "REDIS_HOST=", - "CACHE_DRIVER=redis", - "QUEUE_CONNECTION=redis", - "SESSION_DRIVER=redis", - "BROADCAST_DRIVER=redis", - } - - for _, indicator := range redisIndicators { - if strings.HasPrefix(line, indicator) { - // Check if it's set to localhost or 127.0.0.1 - if strings.Contains(line, "127.0.0.1") || strings.Contains(line, "localhost") || - indicator != "REDIS_HOST=" { - return true - } - } - } - } - - return false -} - -// DetectPackageManager detects which package manager is used in the project. -// Returns "npm", "pnpm", "yarn", or "bun". -func DetectPackageManager(dir string) string { - m := getMedium() - // Check for lock files in order of preference - lockFiles := []struct { - file string - manager string - }{ - {"bun.lockb", "bun"}, - {"pnpm-lock.yaml", "pnpm"}, - {"yarn.lock", "yarn"}, - {"package-lock.json", "npm"}, - } - - for _, lf := range lockFiles { - if m.Exists(filepath.Join(dir, lf.file)) { - return lf.manager - } - } - - // Default to npm if no lock file found - return "npm" -} - -// GetLaravelAppName extracts the application name from Laravel's .env file. -func GetLaravelAppName(dir string) string { - m := getMedium() - envPath := filepath.Join(dir, ".env") - content, err := m.Read(envPath) - if err != nil { - return "" - } - - lines := strings.Split(content, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "APP_NAME=") { - value := strings.TrimPrefix(line, "APP_NAME=") - // Remove quotes if present - value = strings.Trim(value, `"'`) - return value - } - } - - return "" -} - -// GetLaravelAppURL extracts the application URL from Laravel's .env file. -func GetLaravelAppURL(dir string) string { - m := getMedium() - envPath := filepath.Join(dir, ".env") - content, err := m.Read(envPath) - if err != nil { - return "" - } - - lines := strings.Split(content, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "APP_URL=") { - value := strings.TrimPrefix(line, "APP_URL=") - // Remove quotes if present - value = strings.Trim(value, `"'`) - return value - } - } - - return "" -} - -// ExtractDomainFromURL extracts the domain from a URL string. -func ExtractDomainFromURL(url string) string { - // Remove protocol - domain := strings.TrimPrefix(url, "https://") - domain = strings.TrimPrefix(domain, "http://") - - // Remove port if present - if idx := strings.Index(domain, ":"); idx != -1 { - domain = domain[:idx] - } - - // Remove path if present - if idx := strings.Index(domain, "/"); idx != -1 { - domain = domain[:idx] - } - - return domain -} diff --git a/internal/cmd/php/detect_test.go b/internal/cmd/php/detect_test.go deleted file mode 100644 index 9b72f843..00000000 --- a/internal/cmd/php/detect_test.go +++ /dev/null @@ -1,663 +0,0 @@ -package php - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestIsLaravelProject_Good(t *testing.T) { - t.Run("valid Laravel project with artisan and composer.json", func(t *testing.T) { - dir := t.TempDir() - - // Create artisan file - artisanPath := filepath.Join(dir, "artisan") - err := os.WriteFile(artisanPath, []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - // Create composer.json with laravel/framework - composerJSON := `{ - "name": "test/laravel-project", - "require": { - "php": "^8.2", - "laravel/framework": "^11.0" - } - }` - composerPath := filepath.Join(dir, "composer.json") - err = os.WriteFile(composerPath, []byte(composerJSON), 0644) - require.NoError(t, err) - - assert.True(t, IsLaravelProject(dir)) - }) - - t.Run("Laravel in require-dev", func(t *testing.T) { - dir := t.TempDir() - - // Create artisan file - artisanPath := filepath.Join(dir, "artisan") - err := os.WriteFile(artisanPath, []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - // Create composer.json with laravel/framework in require-dev - composerJSON := `{ - "name": "test/laravel-project", - "require-dev": { - "laravel/framework": "^11.0" - } - }` - composerPath := filepath.Join(dir, "composer.json") - err = os.WriteFile(composerPath, []byte(composerJSON), 0644) - require.NoError(t, err) - - assert.True(t, IsLaravelProject(dir)) - }) -} - -func TestIsLaravelProject_Bad(t *testing.T) { - t.Run("missing artisan file", func(t *testing.T) { - dir := t.TempDir() - - // Create composer.json but no artisan - composerJSON := `{ - "name": "test/laravel-project", - "require": { - "laravel/framework": "^11.0" - } - }` - composerPath := filepath.Join(dir, "composer.json") - err := os.WriteFile(composerPath, []byte(composerJSON), 0644) - require.NoError(t, err) - - assert.False(t, IsLaravelProject(dir)) - }) - - t.Run("missing composer.json", func(t *testing.T) { - dir := t.TempDir() - - // Create artisan but no composer.json - artisanPath := filepath.Join(dir, "artisan") - err := os.WriteFile(artisanPath, []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - assert.False(t, IsLaravelProject(dir)) - }) - - t.Run("composer.json without Laravel", func(t *testing.T) { - dir := t.TempDir() - - // Create artisan file - artisanPath := filepath.Join(dir, "artisan") - err := os.WriteFile(artisanPath, []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - // Create composer.json without laravel/framework - composerJSON := `{ - "name": "test/symfony-project", - "require": { - "symfony/framework-bundle": "^7.0" - } - }` - composerPath := filepath.Join(dir, "composer.json") - err = os.WriteFile(composerPath, []byte(composerJSON), 0644) - require.NoError(t, err) - - assert.False(t, IsLaravelProject(dir)) - }) - - t.Run("invalid composer.json", func(t *testing.T) { - dir := t.TempDir() - - // Create artisan file - artisanPath := filepath.Join(dir, "artisan") - err := os.WriteFile(artisanPath, []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - // Create invalid composer.json - composerPath := filepath.Join(dir, "composer.json") - err = os.WriteFile(composerPath, []byte("not valid json{"), 0644) - require.NoError(t, err) - - assert.False(t, IsLaravelProject(dir)) - }) - - t.Run("empty directory", func(t *testing.T) { - dir := t.TempDir() - assert.False(t, IsLaravelProject(dir)) - }) - - t.Run("non-existent directory", func(t *testing.T) { - assert.False(t, IsLaravelProject("/non/existent/path")) - }) -} - -func TestIsFrankenPHPProject_Good(t *testing.T) { - t.Run("project with octane and frankenphp config", func(t *testing.T) { - dir := t.TempDir() - - // Create composer.json with laravel/octane - composerJSON := `{ - "require": { - "laravel/octane": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // Create config directory and octane.php - configDir := filepath.Join(dir, "config") - err = os.MkdirAll(configDir, 0755) - require.NoError(t, err) - - octaneConfig := ` 'frankenphp', -];` - err = os.WriteFile(filepath.Join(configDir, "octane.php"), []byte(octaneConfig), 0644) - require.NoError(t, err) - - assert.True(t, IsFrankenPHPProject(dir)) - }) - - t.Run("project with octane but no config file", func(t *testing.T) { - dir := t.TempDir() - - // Create composer.json with laravel/octane - composerJSON := `{ - "require": { - "laravel/octane": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // No config file - should still return true (assume frankenphp) - assert.True(t, IsFrankenPHPProject(dir)) - }) - - t.Run("project with octane but unreadable config file", func(t *testing.T) { - if os.Geteuid() == 0 { - t.Skip("root can read any file") - } - dir := t.TempDir() - - // Create composer.json with laravel/octane - composerJSON := `{ - "require": { - "laravel/octane": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // Create config directory and octane.php with no read permissions - configDir := filepath.Join(dir, "config") - err = os.MkdirAll(configDir, 0755) - require.NoError(t, err) - - octanePath := filepath.Join(configDir, "octane.php") - err = os.WriteFile(octanePath, []byte(" 'swoole', -];` - err = os.WriteFile(filepath.Join(configDir, "octane.php"), []byte(octaneConfig), 0644) - require.NoError(t, err) - - assert.False(t, IsFrankenPHPProject(dir)) - }) -} diff --git a/internal/cmd/php/dockerfile.go b/internal/cmd/php/dockerfile.go deleted file mode 100644 index be7afd1a..00000000 --- a/internal/cmd/php/dockerfile.go +++ /dev/null @@ -1,398 +0,0 @@ -package php - -import ( - "encoding/json" - "path/filepath" - "sort" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// DockerfileConfig holds configuration for generating a Dockerfile. -type DockerfileConfig struct { - // PHPVersion is the PHP version to use (default: "8.3"). - PHPVersion string - - // BaseImage is the base Docker image (default: "dunglas/frankenphp"). - BaseImage string - - // PHPExtensions is the list of PHP extensions to install. - PHPExtensions []string - - // HasAssets indicates if the project has frontend assets to build. - HasAssets bool - - // PackageManager is the Node.js package manager (npm, pnpm, yarn, bun). - PackageManager string - - // IsLaravel indicates if this is a Laravel project. - IsLaravel bool - - // HasOctane indicates if Laravel Octane is installed. - HasOctane bool - - // UseAlpine uses the Alpine-based image (smaller). - UseAlpine bool -} - -// GenerateDockerfile generates a Dockerfile for a PHP/Laravel project. -// It auto-detects dependencies from composer.json and project structure. -func GenerateDockerfile(dir string) (string, error) { - config, err := DetectDockerfileConfig(dir) - if err != nil { - return "", err - } - - return GenerateDockerfileFromConfig(config), nil -} - -// DetectDockerfileConfig detects configuration from project files. -func DetectDockerfileConfig(dir string) (*DockerfileConfig, error) { - m := getMedium() - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - } - - // Read composer.json - composerPath := filepath.Join(dir, "composer.json") - composerContent, err := m.Read(composerPath) - if err != nil { - return nil, cli.WrapVerb(err, "read", "composer.json") - } - - var composer ComposerJSON - if err := json.Unmarshal([]byte(composerContent), &composer); err != nil { - return nil, cli.WrapVerb(err, "parse", "composer.json") - } - - // Detect PHP version from composer.json - if phpVersion, ok := composer.Require["php"]; ok { - config.PHPVersion = extractPHPVersion(phpVersion) - } - - // Detect if Laravel - if _, ok := composer.Require["laravel/framework"]; ok { - config.IsLaravel = true - } - - // Detect if Octane - if _, ok := composer.Require["laravel/octane"]; ok { - config.HasOctane = true - } - - // Detect required PHP extensions - config.PHPExtensions = detectPHPExtensions(composer) - - // Detect frontend assets - config.HasAssets = hasNodeAssets(dir) - if config.HasAssets { - config.PackageManager = DetectPackageManager(dir) - } - - return config, nil -} - -// GenerateDockerfileFromConfig generates a Dockerfile from the given configuration. -func GenerateDockerfileFromConfig(config *DockerfileConfig) string { - var sb strings.Builder - - // Base image - baseTag := cli.Sprintf("latest-php%s", config.PHPVersion) - if config.UseAlpine { - baseTag += "-alpine" - } - - sb.WriteString("# Auto-generated Dockerfile for FrankenPHP\n") - sb.WriteString("# Generated by Core Framework\n\n") - - // Multi-stage build for smaller images - if config.HasAssets { - // Frontend build stage - sb.WriteString("# Stage 1: Build frontend assets\n") - sb.WriteString("FROM node:20-alpine AS frontend\n\n") - sb.WriteString("WORKDIR /app\n\n") - - // Copy package files based on package manager - switch config.PackageManager { - case "pnpm": - sb.WriteString("RUN corepack enable && corepack prepare pnpm@latest --activate\n\n") - sb.WriteString("COPY package.json pnpm-lock.yaml ./\n") - sb.WriteString("RUN pnpm install --frozen-lockfile\n\n") - case "yarn": - sb.WriteString("COPY package.json yarn.lock ./\n") - sb.WriteString("RUN yarn install --frozen-lockfile\n\n") - case "bun": - sb.WriteString("RUN npm install -g bun\n\n") - sb.WriteString("COPY package.json bun.lockb ./\n") - sb.WriteString("RUN bun install --frozen-lockfile\n\n") - default: // npm - sb.WriteString("COPY package.json package-lock.json ./\n") - sb.WriteString("RUN npm ci\n\n") - } - - sb.WriteString("COPY . .\n\n") - - // Build command - switch config.PackageManager { - case "pnpm": - sb.WriteString("RUN pnpm run build\n\n") - case "yarn": - sb.WriteString("RUN yarn build\n\n") - case "bun": - sb.WriteString("RUN bun run build\n\n") - default: - sb.WriteString("RUN npm run build\n\n") - } - } - - // PHP build stage - stageNum := 2 - if config.HasAssets { - sb.WriteString(cli.Sprintf("# Stage %d: PHP application\n", stageNum)) - } - sb.WriteString(cli.Sprintf("FROM %s:%s AS app\n\n", config.BaseImage, baseTag)) - - sb.WriteString("WORKDIR /app\n\n") - - // Install PHP extensions if needed - if len(config.PHPExtensions) > 0 { - sb.WriteString("# Install PHP extensions\n") - sb.WriteString(cli.Sprintf("RUN install-php-extensions %s\n\n", strings.Join(config.PHPExtensions, " "))) - } - - // Copy composer files first for better caching - sb.WriteString("# Copy composer files\n") - sb.WriteString("COPY composer.json composer.lock ./\n\n") - - // Install composer dependencies - sb.WriteString("# Install PHP dependencies\n") - sb.WriteString("RUN composer install --no-dev --no-scripts --optimize-autoloader --no-interaction\n\n") - - // Copy application code - sb.WriteString("# Copy application code\n") - sb.WriteString("COPY . .\n\n") - - // Run post-install scripts - sb.WriteString("# Run composer scripts\n") - sb.WriteString("RUN composer dump-autoload --optimize\n\n") - - // Copy frontend assets if built - if config.HasAssets { - sb.WriteString("# Copy built frontend assets\n") - sb.WriteString("COPY --from=frontend /app/public/build public/build\n\n") - } - - // Laravel-specific setup - if config.IsLaravel { - sb.WriteString("# Laravel setup\n") - sb.WriteString("RUN php artisan config:cache \\\n") - sb.WriteString(" && php artisan route:cache \\\n") - sb.WriteString(" && php artisan view:cache\n\n") - - // Set permissions - sb.WriteString("# Set permissions for Laravel\n") - sb.WriteString("RUN chown -R www-data:www-data storage bootstrap/cache \\\n") - sb.WriteString(" && chmod -R 775 storage bootstrap/cache\n\n") - } - - // Expose ports - sb.WriteString("# Expose ports\n") - sb.WriteString("EXPOSE 80 443\n\n") - - // Health check - sb.WriteString("# Health check\n") - sb.WriteString("HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n") - sb.WriteString(" CMD curl -f http://localhost/up || exit 1\n\n") - - // Start command - sb.WriteString("# Start FrankenPHP\n") - if config.HasOctane { - sb.WriteString("CMD [\"php\", \"artisan\", \"octane:start\", \"--server=frankenphp\", \"--host=0.0.0.0\", \"--port=80\"]\n") - } else { - sb.WriteString("CMD [\"frankenphp\", \"run\", \"--config\", \"/etc/caddy/Caddyfile\"]\n") - } - - return sb.String() -} - -// ComposerJSON represents the structure of composer.json. -type ComposerJSON struct { - Name string `json:"name"` - Require map[string]string `json:"require"` - RequireDev map[string]string `json:"require-dev"` -} - -// detectPHPExtensions detects required PHP extensions from composer.json. -func detectPHPExtensions(composer ComposerJSON) []string { - extensionMap := make(map[string]bool) - - // Check for common packages and their required extensions - packageExtensions := map[string][]string{ - // Database - "doctrine/dbal": {"pdo_mysql", "pdo_pgsql"}, - "illuminate/database": {"pdo_mysql"}, - "laravel/framework": {"pdo_mysql", "bcmath", "ctype", "fileinfo", "mbstring", "openssl", "tokenizer", "xml"}, - "mongodb/mongodb": {"mongodb"}, - "predis/predis": {"redis"}, - "phpredis/phpredis": {"redis"}, - "laravel/horizon": {"redis", "pcntl"}, - "aws/aws-sdk-php": {"curl"}, - "intervention/image": {"gd"}, - "intervention/image-laravel": {"gd"}, - "spatie/image": {"gd"}, - "league/flysystem-aws-s3-v3": {"curl"}, - "guzzlehttp/guzzle": {"curl"}, - "nelmio/cors-bundle": {}, - // Queues - "laravel/reverb": {"pcntl"}, - "php-amqplib/php-amqplib": {"sockets"}, - // Misc - "moneyphp/money": {"bcmath", "intl"}, - "symfony/intl": {"intl"}, - "nesbot/carbon": {"intl"}, - "spatie/laravel-medialibrary": {"exif", "gd"}, - } - - // Check all require and require-dev dependencies - allDeps := make(map[string]string) - for pkg, ver := range composer.Require { - allDeps[pkg] = ver - } - for pkg, ver := range composer.RequireDev { - allDeps[pkg] = ver - } - - // Find required extensions - for pkg := range allDeps { - if exts, ok := packageExtensions[pkg]; ok { - for _, ext := range exts { - extensionMap[ext] = true - } - } - - // Check for direct ext- requirements - if strings.HasPrefix(pkg, "ext-") { - ext := strings.TrimPrefix(pkg, "ext-") - // Skip extensions that are built into PHP - builtIn := map[string]bool{ - "json": true, "ctype": true, "iconv": true, - "session": true, "simplexml": true, "pdo": true, - "xml": true, "tokenizer": true, - } - if !builtIn[ext] { - extensionMap[ext] = true - } - } - } - - // Convert to sorted slice - extensions := make([]string, 0, len(extensionMap)) - for ext := range extensionMap { - extensions = append(extensions, ext) - } - sort.Strings(extensions) - - return extensions -} - -// extractPHPVersion extracts a clean PHP version from a composer constraint. -func extractPHPVersion(constraint string) string { - // Handle common formats: ^8.2, >=8.2, 8.2.*, ~8.2 - constraint = strings.TrimLeft(constraint, "^>=~") - constraint = strings.TrimRight(constraint, ".*") - - // Extract major.minor - parts := strings.Split(constraint, ".") - if len(parts) >= 2 { - return parts[0] + "." + parts[1] - } - if len(parts) == 1 { - return parts[0] + ".0" - } - - return "8.3" // default -} - -// hasNodeAssets checks if the project has frontend assets. -func hasNodeAssets(dir string) bool { - m := getMedium() - packageJSON := filepath.Join(dir, "package.json") - if !m.IsFile(packageJSON) { - return false - } - - // Check for build script in package.json - content, err := m.Read(packageJSON) - if err != nil { - return false - } - - var pkg struct { - Scripts map[string]string `json:"scripts"` - } - - if err := json.Unmarshal([]byte(content), &pkg); err != nil { - return false - } - - // Check if there's a build script - _, hasBuild := pkg.Scripts["build"] - return hasBuild -} - -// GenerateDockerignore generates a .dockerignore file content for PHP projects. -func GenerateDockerignore(dir string) string { - var sb strings.Builder - - sb.WriteString("# Git\n") - sb.WriteString(".git\n") - sb.WriteString(".gitignore\n") - sb.WriteString(".gitattributes\n\n") - - sb.WriteString("# Node\n") - sb.WriteString("node_modules\n\n") - - sb.WriteString("# Development\n") - sb.WriteString(".env\n") - sb.WriteString(".env.local\n") - sb.WriteString(".env.*.local\n") - sb.WriteString("*.log\n") - sb.WriteString(".phpunit.result.cache\n") - sb.WriteString("phpunit.xml\n") - sb.WriteString(".php-cs-fixer.cache\n") - sb.WriteString("phpstan.neon\n\n") - - sb.WriteString("# IDE\n") - sb.WriteString(".idea\n") - sb.WriteString(".vscode\n") - sb.WriteString("*.swp\n") - sb.WriteString("*.swo\n\n") - - sb.WriteString("# Laravel specific\n") - sb.WriteString("storage/app/*\n") - sb.WriteString("storage/logs/*\n") - sb.WriteString("storage/framework/cache/*\n") - sb.WriteString("storage/framework/sessions/*\n") - sb.WriteString("storage/framework/views/*\n") - sb.WriteString("bootstrap/cache/*\n\n") - - sb.WriteString("# Build artifacts\n") - sb.WriteString("public/hot\n") - sb.WriteString("public/storage\n") - sb.WriteString("vendor\n\n") - - sb.WriteString("# Docker\n") - sb.WriteString("Dockerfile*\n") - sb.WriteString("docker-compose*.yml\n") - sb.WriteString(".dockerignore\n\n") - - sb.WriteString("# Documentation\n") - sb.WriteString("README.md\n") - sb.WriteString("CHANGELOG.md\n") - sb.WriteString("docs\n") - - return sb.String() -} diff --git a/internal/cmd/php/dockerfile_test.go b/internal/cmd/php/dockerfile_test.go deleted file mode 100644 index 5c3b1ce1..00000000 --- a/internal/cmd/php/dockerfile_test.go +++ /dev/null @@ -1,634 +0,0 @@ -package php - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateDockerfile_Good(t *testing.T) { - t.Run("basic Laravel project", func(t *testing.T) { - dir := t.TempDir() - - // Create composer.json - composerJSON := `{ - "name": "test/laravel-project", - "require": { - "php": "^8.2", - "laravel/framework": "^11.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // Create composer.lock - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - // Check content - assert.Contains(t, content, "FROM dunglas/frankenphp") - assert.Contains(t, content, "php8.2") - assert.Contains(t, content, "COPY composer.json composer.lock") - assert.Contains(t, content, "composer install") - assert.Contains(t, content, "EXPOSE 80 443") - }) - - t.Run("Laravel project with Octane", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/laravel-octane", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0", - "laravel/octane": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - assert.Contains(t, content, "php8.3") - assert.Contains(t, content, "octane:start") - }) - - t.Run("project with frontend assets", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/laravel-vite", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - packageJSON := `{ - "name": "test-app", - "scripts": { - "dev": "vite", - "build": "vite build" - } - }` - err = os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - // Should have multi-stage build - assert.Contains(t, content, "FROM node:20-alpine AS frontend") - assert.Contains(t, content, "npm ci") - assert.Contains(t, content, "npm run build") - assert.Contains(t, content, "COPY --from=frontend") - }) - - t.Run("project with pnpm", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/laravel-pnpm", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - packageJSON := `{ - "name": "test-app", - "scripts": { - "build": "vite build" - } - }` - err = os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - - // Create pnpm-lock.yaml - err = os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte("lockfileVersion: 6.0"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - assert.Contains(t, content, "pnpm install") - assert.Contains(t, content, "pnpm run build") - }) - - t.Run("project with Redis dependency", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/laravel-redis", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0", - "predis/predis": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - assert.Contains(t, content, "install-php-extensions") - assert.Contains(t, content, "redis") - }) - - t.Run("project with explicit ext- requirements", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/with-extensions", - "require": { - "php": "^8.3", - "ext-gd": "*", - "ext-imagick": "*", - "ext-intl": "*" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - assert.Contains(t, content, "install-php-extensions") - assert.Contains(t, content, "gd") - assert.Contains(t, content, "imagick") - assert.Contains(t, content, "intl") - }) -} - -func TestGenerateDockerfile_Bad(t *testing.T) { - t.Run("missing composer.json", func(t *testing.T) { - dir := t.TempDir() - - _, err := GenerateDockerfile(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "composer.json") - }) - - t.Run("invalid composer.json", func(t *testing.T) { - dir := t.TempDir() - - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("not json{"), 0644) - require.NoError(t, err) - - _, err = GenerateDockerfile(dir) - assert.Error(t, err) - }) -} - -func TestDetectDockerfileConfig_Good(t *testing.T) { - t.Run("full Laravel project", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/full-laravel", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0", - "laravel/octane": "^2.0", - "predis/predis": "^2.0", - "intervention/image": "^3.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - packageJSON := `{"scripts": {"build": "vite build"}}` - err = os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644) - require.NoError(t, err) - - config, err := DetectDockerfileConfig(dir) - require.NoError(t, err) - - assert.Equal(t, "8.3", config.PHPVersion) - assert.True(t, config.IsLaravel) - assert.True(t, config.HasOctane) - assert.True(t, config.HasAssets) - assert.Equal(t, "yarn", config.PackageManager) - assert.Contains(t, config.PHPExtensions, "redis") - assert.Contains(t, config.PHPExtensions, "gd") - }) -} - -func TestDetectDockerfileConfig_Bad(t *testing.T) { - t.Run("non-existent directory", func(t *testing.T) { - _, err := DetectDockerfileConfig("/non/existent/path") - assert.Error(t, err) - }) -} - -func TestExtractPHPVersion_Good(t *testing.T) { - tests := []struct { - constraint string - expected string - }{ - {"^8.2", "8.2"}, - {"^8.3", "8.3"}, - {">=8.2", "8.2"}, - {"~8.2", "8.2"}, - {"8.2.*", "8.2"}, - {"8.2.0", "8.2"}, - {"8", "8.0"}, - } - - for _, tt := range tests { - t.Run(tt.constraint, func(t *testing.T) { - result := extractPHPVersion(tt.constraint) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestDetectPHPExtensions_Good(t *testing.T) { - t.Run("detects Redis from predis", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "predis/predis": "^2.0", - }, - } - - extensions := detectPHPExtensions(composer) - assert.Contains(t, extensions, "redis") - }) - - t.Run("detects GD from intervention/image", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "intervention/image": "^3.0", - }, - } - - extensions := detectPHPExtensions(composer) - assert.Contains(t, extensions, "gd") - }) - - t.Run("detects multiple extensions from Laravel", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "laravel/framework": "^11.0", - }, - } - - extensions := detectPHPExtensions(composer) - assert.Contains(t, extensions, "pdo_mysql") - assert.Contains(t, extensions, "bcmath") - }) - - t.Run("detects explicit ext- requirements", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "ext-gd": "*", - "ext-imagick": "*", - }, - } - - extensions := detectPHPExtensions(composer) - assert.Contains(t, extensions, "gd") - assert.Contains(t, extensions, "imagick") - }) - - t.Run("skips built-in extensions", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "ext-json": "*", - "ext-session": "*", - "ext-pdo": "*", - }, - } - - extensions := detectPHPExtensions(composer) - assert.NotContains(t, extensions, "json") - assert.NotContains(t, extensions, "session") - assert.NotContains(t, extensions, "pdo") - }) - - t.Run("sorts extensions alphabetically", func(t *testing.T) { - composer := ComposerJSON{ - Require: map[string]string{ - "ext-zip": "*", - "ext-gd": "*", - "ext-intl": "*", - }, - } - - extensions := detectPHPExtensions(composer) - - // Check they are sorted - for i := 1; i < len(extensions); i++ { - assert.True(t, extensions[i-1] < extensions[i], - "extensions should be sorted: %v", extensions) - } - }) -} - -func TestHasNodeAssets_Good(t *testing.T) { - t.Run("with build script", func(t *testing.T) { - dir := t.TempDir() - - packageJSON := `{ - "name": "test", - "scripts": { - "dev": "vite", - "build": "vite build" - } - }` - err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - - assert.True(t, hasNodeAssets(dir)) - }) -} - -func TestHasNodeAssets_Bad(t *testing.T) { - t.Run("no package.json", func(t *testing.T) { - dir := t.TempDir() - assert.False(t, hasNodeAssets(dir)) - }) - - t.Run("no build script", func(t *testing.T) { - dir := t.TempDir() - - packageJSON := `{ - "name": "test", - "scripts": { - "dev": "vite" - } - }` - err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - - assert.False(t, hasNodeAssets(dir)) - }) - - t.Run("invalid package.json", func(t *testing.T) { - dir := t.TempDir() - - err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("invalid{"), 0644) - require.NoError(t, err) - - assert.False(t, hasNodeAssets(dir)) - }) -} - -func TestGenerateDockerignore_Good(t *testing.T) { - t.Run("generates complete dockerignore", func(t *testing.T) { - dir := t.TempDir() - content := GenerateDockerignore(dir) - - // Check key entries - assert.Contains(t, content, ".git") - assert.Contains(t, content, "node_modules") - assert.Contains(t, content, ".env") - assert.Contains(t, content, "vendor") - assert.Contains(t, content, "storage/logs/*") - assert.Contains(t, content, ".idea") - assert.Contains(t, content, ".vscode") - }) -} - -func TestGenerateDockerfileFromConfig_Good(t *testing.T) { - t.Run("minimal config", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "FROM dunglas/frankenphp:latest-php8.3-alpine") - assert.Contains(t, content, "WORKDIR /app") - assert.Contains(t, content, "COPY composer.json composer.lock") - assert.Contains(t, content, "EXPOSE 80 443") - }) - - t.Run("with extensions", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - PHPExtensions: []string{"redis", "gd", "intl"}, - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "install-php-extensions redis gd intl") - }) - - t.Run("Laravel with Octane", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - IsLaravel: true, - HasOctane: true, - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "php artisan config:cache") - assert.Contains(t, content, "php artisan route:cache") - assert.Contains(t, content, "php artisan view:cache") - assert.Contains(t, content, "chown -R www-data:www-data storage") - assert.Contains(t, content, "octane:start") - }) - - t.Run("with frontend assets", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - HasAssets: true, - PackageManager: "npm", - } - - content := GenerateDockerfileFromConfig(config) - - // Multi-stage build - assert.Contains(t, content, "FROM node:20-alpine AS frontend") - assert.Contains(t, content, "COPY package.json package-lock.json") - assert.Contains(t, content, "RUN npm ci") - assert.Contains(t, content, "RUN npm run build") - assert.Contains(t, content, "COPY --from=frontend /app/public/build public/build") - }) - - t.Run("with yarn", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - HasAssets: true, - PackageManager: "yarn", - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "COPY package.json yarn.lock") - assert.Contains(t, content, "yarn install --frozen-lockfile") - assert.Contains(t, content, "yarn build") - }) - - t.Run("with bun", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: true, - HasAssets: true, - PackageManager: "bun", - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "npm install -g bun") - assert.Contains(t, content, "COPY package.json bun.lockb") - assert.Contains(t, content, "bun install --frozen-lockfile") - assert.Contains(t, content, "bun run build") - }) - - t.Run("non-alpine image", func(t *testing.T) { - config := &DockerfileConfig{ - PHPVersion: "8.3", - BaseImage: "dunglas/frankenphp", - UseAlpine: false, - } - - content := GenerateDockerfileFromConfig(config) - - assert.Contains(t, content, "FROM dunglas/frankenphp:latest-php8.3 AS app") - assert.NotContains(t, content, "alpine") - }) -} - -func TestIsPHPProject_Good(t *testing.T) { - t.Run("project with composer.json", func(t *testing.T) { - dir := t.TempDir() - - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{}"), 0644) - require.NoError(t, err) - - assert.True(t, IsPHPProject(dir)) - }) -} - -func TestIsPHPProject_Bad(t *testing.T) { - t.Run("project without composer.json", func(t *testing.T) { - dir := t.TempDir() - assert.False(t, IsPHPProject(dir)) - }) - - t.Run("non-existent directory", func(t *testing.T) { - assert.False(t, IsPHPProject("/non/existent/path")) - }) -} - -func TestExtractPHPVersion_Edge(t *testing.T) { - t.Run("handles single major version", func(t *testing.T) { - result := extractPHPVersion("8") - assert.Equal(t, "8.0", result) - }) -} - -func TestDetectPHPExtensions_RequireDev(t *testing.T) { - t.Run("detects extensions from require-dev", func(t *testing.T) { - composer := ComposerJSON{ - RequireDev: map[string]string{ - "predis/predis": "^2.0", - }, - } - - extensions := detectPHPExtensions(composer) - assert.Contains(t, extensions, "redis") - }) -} - -func TestDockerfileStructure_Good(t *testing.T) { - t.Run("Dockerfile has proper structure", func(t *testing.T) { - dir := t.TempDir() - - composerJSON := `{ - "name": "test/app", - "require": { - "php": "^8.3", - "laravel/framework": "^11.0", - "laravel/octane": "^2.0", - "predis/predis": "^2.0" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "composer.lock"), []byte("{}"), 0644) - require.NoError(t, err) - - packageJSON := `{"scripts": {"build": "vite build"}}` - err = os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte("{}"), 0644) - require.NoError(t, err) - - content, err := GenerateDockerfile(dir) - require.NoError(t, err) - - lines := strings.Split(content, "\n") - var fromCount, workdirCount, copyCount, runCount, exposeCount, cmdCount int - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - switch { - case strings.HasPrefix(trimmed, "FROM "): - fromCount++ - case strings.HasPrefix(trimmed, "WORKDIR "): - workdirCount++ - case strings.HasPrefix(trimmed, "COPY "): - copyCount++ - case strings.HasPrefix(trimmed, "RUN "): - runCount++ - case strings.HasPrefix(trimmed, "EXPOSE "): - exposeCount++ - case strings.HasPrefix(trimmed, "CMD ["): - // Only count actual CMD instructions, not HEALTHCHECK CMD - cmdCount++ - } - } - - // Multi-stage build should have 2 FROM statements - assert.Equal(t, 2, fromCount, "should have 2 FROM statements for multi-stage build") - - // Should have proper structure - assert.GreaterOrEqual(t, workdirCount, 1, "should have WORKDIR") - assert.GreaterOrEqual(t, copyCount, 3, "should have multiple COPY statements") - assert.GreaterOrEqual(t, runCount, 2, "should have multiple RUN statements") - assert.Equal(t, 1, exposeCount, "should have exactly one EXPOSE") - assert.Equal(t, 1, cmdCount, "should have exactly one CMD") - }) -} diff --git a/internal/cmd/php/i18n.go b/internal/cmd/php/i18n.go deleted file mode 100644 index 96a60a94..00000000 --- a/internal/cmd/php/i18n.go +++ /dev/null @@ -1,16 +0,0 @@ -// Package php provides PHP/Laravel development tools. -package php - -import ( - "embed" - - "forge.lthn.ai/core/go/pkg/i18n" -) - -//go:embed locales/*.json -var localeFS embed.FS - -func init() { - // Register PHP translations with the i18n system - i18n.RegisterLocales(localeFS, "locales") -} diff --git a/internal/cmd/php/locales/en_GB.json b/internal/cmd/php/locales/en_GB.json deleted file mode 100644 index 4f74cd89..00000000 --- a/internal/cmd/php/locales/en_GB.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "cmd": { - "php": { - "short": "Laravel/PHP development tools", - "long": "Laravel and PHP development tools including testing, formatting, static analysis, and deployment", - "label": { - "php": "PHP:", - "audit": "Audit:", - "psalm": "Psalm:", - "rector": "Rector:", - "security": "Security:", - "infection": "Infection:", - "info": "Info:", - "setup": "Setup:" - }, - "error": { - "not_php": "Not a PHP project (no composer.json found)", - "fmt_failed": "Formatting failed", - "fmt_issues": "Style issues found", - "analysis_issues": "Analysis errors found", - "audit_failed": "Audit failed", - "vulns_found": "Vulnerabilities found", - "psalm_not_installed": "Psalm not installed", - "psalm_issues": "Psalm found type errors", - "rector_not_installed": "Rector not installed", - "rector_failed": "Rector failed", - "infection_not_installed": "Infection not installed", - "infection_failed": "Mutation testing failed", - "security_failed": "Security check failed", - "critical_high_issues": "Critical or high severity issues found" - }, - "test": { - "short": "Run PHPUnit/Pest tests", - "long": "Run PHPUnit or Pest tests with optional filtering, parallel execution, and coverage", - "flag": { - "parallel": "Run tests in parallel", - "coverage": "Generate code coverage report", - "filter": "Filter tests by name", - "group": "Run only tests in this group" - } - }, - "fmt": { - "short": "Format PHP code with Laravel Pint", - "long": "Format PHP code using Laravel Pint code style fixer", - "no_formatter": "No code formatter found (install laravel/pint)", - "no_issues": "No style issues found", - "formatting": "Formatting with {{.Formatter}}...", - "flag": { - "fix": "Fix style issues (default: check only)" - } - }, - "analyse": { - "short": "Run PHPStan static analysis", - "long": "Run PHPStan/Larastan for static code analysis", - "no_analyser": "No static analyser found (install phpstan/phpstan or nunomaduro/larastan)", - "flag": { - "level": "Analysis level (0-9, default: from config)", - "memory": "Memory limit (e.g., 2G)" - } - }, - "audit": { - "short": "Security audit for dependencies", - "long": "Audit Composer and NPM dependencies for known vulnerabilities", - "scanning": "Scanning dependencies for vulnerabilities...", - "secure": "No vulnerabilities", - "error": "Audit error", - "vulnerabilities": "{{.Count}} vulnerabilities found", - "found_vulns": "Found {{.Count}} vulnerabilities", - "all_secure": "All dependencies secure", - "completed_errors": "Audit completed with errors", - "flag": { - "fix": "Attempt to fix vulnerabilities" - } - }, - "psalm": { - "short": "Run Psalm static analysis", - "long": "Run Psalm for deep static analysis and type checking", - "not_found": "Psalm not found", - "install": "composer require --dev vimeo/psalm", - "setup": "vendor/bin/psalm --init", - "analysing": "Analysing with Psalm...", - "analysing_fixing": "Analysing and fixing with Psalm...", - "flag": { - "level": "Analysis level (1-8)", - "baseline": "Generate or update baseline", - "show_info": "Show informational issues" - } - }, - "rector": { - "short": "Automated code refactoring", - "long": "Run Rector for automated code upgrades and refactoring", - "not_found": "Rector not found", - "install": "composer require --dev rector/rector", - "setup": "vendor/bin/rector init", - "analysing": "Analysing code for refactoring opportunities...", - "refactoring": "Refactoring code...", - "no_changes": "No refactoring changes needed", - "changes_suggested": "Rector suggests changes (run with --fix to apply)", - "flag": { - "fix": "Apply refactoring changes", - "diff": "Show diff of changes", - "clear_cache": "Clear Rector cache before running" - } - }, - "infection": { - "short": "Mutation testing for test quality", - "long": "Run Infection mutation testing to measure test suite quality", - "not_found": "Infection not found", - "install": "composer require --dev infection/infection", - "note": "This may take a while depending on test suite size", - "complete": "Mutation testing complete", - "flag": { - "min_msi": "Minimum Mutation Score Indicator (0-100)", - "min_covered_msi": "Minimum covered code MSI (0-100)", - "threads": "Number of parallel threads", - "filter": "Filter mutants by file path", - "only_covered": "Only mutate covered code" - } - }, - "security": { - "short": "Security vulnerability scanning", - "long": "Run comprehensive security checks on PHP codebase", - "checks_suffix": " CHECKS", - "summary": "Security scan complete", - "passed": "Passed:", - "critical": "Critical:", - "high": "High:", - "medium": "Medium:", - "low": "Low:", - "flag": { - "severity": "Minimum severity to report (low, medium, high, critical)", - "sarif": "Output in SARIF format", - "url": "Application URL for runtime checks" - } - }, - "qa": { - "short": "Run full QA pipeline", - "long": "Run comprehensive quality assurance: audit, format, analyse, test, and more", - "flag": { - "quick": "Run quick checks only (audit, fmt, stan)", - "full": "Run all stages including slow checks", - "fix": "Auto-fix issues where possible" - } - } - } - } -} diff --git a/internal/cmd/php/packages.go b/internal/cmd/php/packages.go deleted file mode 100644 index 03645d66..00000000 --- a/internal/cmd/php/packages.go +++ /dev/null @@ -1,308 +0,0 @@ -package php - -import ( - "encoding/json" - "os" - "os/exec" - "path/filepath" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// LinkedPackage represents a linked local package. -type LinkedPackage struct { - Name string `json:"name"` - Path string `json:"path"` - Version string `json:"version"` -} - -// composerRepository represents a composer repository entry. -type composerRepository struct { - Type string `json:"type"` - URL string `json:"url,omitempty"` - Options map[string]any `json:"options,omitempty"` -} - -// readComposerJSON reads and parses composer.json from the given directory. -func readComposerJSON(dir string) (map[string]json.RawMessage, error) { - m := getMedium() - composerPath := filepath.Join(dir, "composer.json") - content, err := m.Read(composerPath) - if err != nil { - return nil, cli.WrapVerb(err, "read", "composer.json") - } - - var raw map[string]json.RawMessage - if err := json.Unmarshal([]byte(content), &raw); err != nil { - return nil, cli.WrapVerb(err, "parse", "composer.json") - } - - return raw, nil -} - -// writeComposerJSON writes the composer.json to the given directory. -func writeComposerJSON(dir string, raw map[string]json.RawMessage) error { - m := getMedium() - composerPath := filepath.Join(dir, "composer.json") - - data, err := json.MarshalIndent(raw, "", " ") - if err != nil { - return cli.WrapVerb(err, "marshal", "composer.json") - } - - // Add trailing newline - content := string(data) + "\n" - - if err := m.Write(composerPath, content); err != nil { - return cli.WrapVerb(err, "write", "composer.json") - } - - return nil -} - -// getRepositories extracts repositories from raw composer.json. -func getRepositories(raw map[string]json.RawMessage) ([]composerRepository, error) { - reposRaw, ok := raw["repositories"] - if !ok { - return []composerRepository{}, nil - } - - var repos []composerRepository - if err := json.Unmarshal(reposRaw, &repos); err != nil { - return nil, cli.WrapVerb(err, "parse", "repositories") - } - - return repos, nil -} - -// setRepositories sets repositories in raw composer.json. -func setRepositories(raw map[string]json.RawMessage, repos []composerRepository) error { - if len(repos) == 0 { - delete(raw, "repositories") - return nil - } - - reposData, err := json.Marshal(repos) - if err != nil { - return cli.WrapVerb(err, "marshal", "repositories") - } - - raw["repositories"] = reposData - return nil -} - -// getPackageInfo reads package name and version from a composer.json in the given path. -func getPackageInfo(packagePath string) (name, version string, err error) { - m := getMedium() - composerPath := filepath.Join(packagePath, "composer.json") - content, err := m.Read(composerPath) - if err != nil { - return "", "", cli.WrapVerb(err, "read", "package composer.json") - } - - var pkg struct { - Name string `json:"name"` - Version string `json:"version"` - } - - if err := json.Unmarshal([]byte(content), &pkg); err != nil { - return "", "", cli.WrapVerb(err, "parse", "package composer.json") - } - - if pkg.Name == "" { - return "", "", cli.Err("package name not found in composer.json") - } - - return pkg.Name, pkg.Version, nil -} - -// LinkPackages adds path repositories to composer.json for local package development. -func LinkPackages(dir string, packages []string) error { - if !IsPHPProject(dir) { - return cli.Err("not a PHP project (missing composer.json)") - } - - raw, err := readComposerJSON(dir) - if err != nil { - return err - } - - repos, err := getRepositories(raw) - if err != nil { - return err - } - - for _, packagePath := range packages { - // Resolve absolute path - absPath, err := filepath.Abs(packagePath) - if err != nil { - return cli.Err("failed to resolve path %s: %w", packagePath, err) - } - - // Verify the path exists and has a composer.json - if !IsPHPProject(absPath) { - return cli.Err("not a PHP package (missing composer.json): %s", absPath) - } - - // Get package name for validation - pkgName, _, err := getPackageInfo(absPath) - if err != nil { - return cli.Err("failed to get package info from %s: %w", absPath, err) - } - - // Check if already linked - alreadyLinked := false - for _, repo := range repos { - if repo.Type == "path" && repo.URL == absPath { - alreadyLinked = true - break - } - } - - if alreadyLinked { - continue - } - - // Add path repository - repos = append(repos, composerRepository{ - Type: "path", - URL: absPath, - Options: map[string]any{ - "symlink": true, - }, - }) - - cli.Print("Linked: %s -> %s\n", pkgName, absPath) - } - - if err := setRepositories(raw, repos); err != nil { - return err - } - - return writeComposerJSON(dir, raw) -} - -// UnlinkPackages removes path repositories from composer.json. -func UnlinkPackages(dir string, packages []string) error { - if !IsPHPProject(dir) { - return cli.Err("not a PHP project (missing composer.json)") - } - - raw, err := readComposerJSON(dir) - if err != nil { - return err - } - - repos, err := getRepositories(raw) - if err != nil { - return err - } - - // Build set of packages to unlink - toUnlink := make(map[string]bool) - for _, pkg := range packages { - toUnlink[pkg] = true - } - - // Filter out unlinked packages - filtered := make([]composerRepository, 0, len(repos)) - for _, repo := range repos { - if repo.Type != "path" { - filtered = append(filtered, repo) - continue - } - - // Check if this repo should be unlinked - shouldUnlink := false - - // Try to get package name from the path - if IsPHPProject(repo.URL) { - pkgName, _, err := getPackageInfo(repo.URL) - if err == nil && toUnlink[pkgName] { - shouldUnlink = true - cli.Print("Unlinked: %s\n", pkgName) - } - } - - // Also check if path matches any of the provided names - for pkg := range toUnlink { - if repo.URL == pkg || filepath.Base(repo.URL) == pkg { - shouldUnlink = true - cli.Print("Unlinked: %s\n", repo.URL) - break - } - } - - if !shouldUnlink { - filtered = append(filtered, repo) - } - } - - if err := setRepositories(raw, filtered); err != nil { - return err - } - - return writeComposerJSON(dir, raw) -} - -// UpdatePackages runs composer update for specific packages. -func UpdatePackages(dir string, packages []string) error { - if !IsPHPProject(dir) { - return cli.Err("not a PHP project (missing composer.json)") - } - - args := []string{"update"} - args = append(args, packages...) - - cmd := exec.Command("composer", args...) - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return cmd.Run() -} - -// ListLinkedPackages returns all path repositories from composer.json. -func ListLinkedPackages(dir string) ([]LinkedPackage, error) { - if !IsPHPProject(dir) { - return nil, cli.Err("not a PHP project (missing composer.json)") - } - - raw, err := readComposerJSON(dir) - if err != nil { - return nil, err - } - - repos, err := getRepositories(raw) - if err != nil { - return nil, err - } - - linked := make([]LinkedPackage, 0) - for _, repo := range repos { - if repo.Type != "path" { - continue - } - - pkg := LinkedPackage{ - Path: repo.URL, - } - - // Try to get package info - if IsPHPProject(repo.URL) { - name, version, err := getPackageInfo(repo.URL) - if err == nil { - pkg.Name = name - pkg.Version = version - } - } - - if pkg.Name == "" { - pkg.Name = filepath.Base(repo.URL) - } - - linked = append(linked, pkg) - } - - return linked, nil -} diff --git a/internal/cmd/php/packages_test.go b/internal/cmd/php/packages_test.go deleted file mode 100644 index a340a9b0..00000000 --- a/internal/cmd/php/packages_test.go +++ /dev/null @@ -1,543 +0,0 @@ -package php - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadComposerJSON_Good(t *testing.T) { - t.Run("reads valid composer.json", func(t *testing.T) { - dir := t.TempDir() - composerJSON := `{ - "name": "test/project", - "require": { - "php": "^8.2" - } - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - raw, err := readComposerJSON(dir) - assert.NoError(t, err) - assert.NotNil(t, raw) - assert.Contains(t, string(raw["name"]), "test/project") - }) - - t.Run("preserves all fields", func(t *testing.T) { - dir := t.TempDir() - composerJSON := `{ - "name": "test/project", - "description": "Test project", - "require": {"php": "^8.2"}, - "autoload": {"psr-4": {"App\\": "src/"}} - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - raw, err := readComposerJSON(dir) - assert.NoError(t, err) - assert.Contains(t, string(raw["autoload"]), "psr-4") - }) -} - -func TestReadComposerJSON_Bad(t *testing.T) { - t.Run("missing composer.json", func(t *testing.T) { - dir := t.TempDir() - _, err := readComposerJSON(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to read composer.json") - }) - - t.Run("invalid JSON", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("not json{"), 0644) - require.NoError(t, err) - - _, err = readComposerJSON(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to parse composer.json") - }) -} - -func TestWriteComposerJSON_Good(t *testing.T) { - t.Run("writes valid composer.json", func(t *testing.T) { - dir := t.TempDir() - raw := make(map[string]json.RawMessage) - raw["name"] = json.RawMessage(`"test/project"`) - - err := writeComposerJSON(dir, raw) - assert.NoError(t, err) - - // Verify file was written - content, err := os.ReadFile(filepath.Join(dir, "composer.json")) - assert.NoError(t, err) - assert.Contains(t, string(content), "test/project") - // Verify trailing newline - assert.True(t, content[len(content)-1] == '\n') - }) - - t.Run("pretty prints with indentation", func(t *testing.T) { - dir := t.TempDir() - raw := make(map[string]json.RawMessage) - raw["name"] = json.RawMessage(`"test/project"`) - raw["require"] = json.RawMessage(`{"php":"^8.2"}`) - - err := writeComposerJSON(dir, raw) - assert.NoError(t, err) - - content, err := os.ReadFile(filepath.Join(dir, "composer.json")) - assert.NoError(t, err) - // Should be indented - assert.Contains(t, string(content), " ") - }) -} - -func TestWriteComposerJSON_Bad(t *testing.T) { - t.Run("fails for non-existent directory", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["name"] = json.RawMessage(`"test/project"`) - - err := writeComposerJSON("/non/existent/path", raw) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to write composer.json") - }) -} -func TestGetRepositories_Good(t *testing.T) { - t.Run("returns empty slice when no repositories", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["name"] = json.RawMessage(`"test/project"`) - - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Empty(t, repos) - }) - - t.Run("parses existing repositories", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["name"] = json.RawMessage(`"test/project"`) - raw["repositories"] = json.RawMessage(`[{"type":"path","url":"/path/to/package"}]`) - - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 1) - assert.Equal(t, "path", repos[0].Type) - assert.Equal(t, "/path/to/package", repos[0].URL) - }) - - t.Run("parses repositories with options", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["repositories"] = json.RawMessage(`[{"type":"path","url":"/path","options":{"symlink":true}}]`) - - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 1) - assert.NotNil(t, repos[0].Options) - assert.Equal(t, true, repos[0].Options["symlink"]) - }) -} - -func TestGetRepositories_Bad(t *testing.T) { - t.Run("fails for invalid repositories JSON", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["repositories"] = json.RawMessage(`not valid json`) - - _, err := getRepositories(raw) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to parse repositories") - }) -} - -func TestSetRepositories_Good(t *testing.T) { - t.Run("sets repositories", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - repos := []composerRepository{ - {Type: "path", URL: "/path/to/package"}, - } - - err := setRepositories(raw, repos) - assert.NoError(t, err) - assert.Contains(t, string(raw["repositories"]), "/path/to/package") - }) - - t.Run("removes repositories key when empty", func(t *testing.T) { - raw := make(map[string]json.RawMessage) - raw["repositories"] = json.RawMessage(`[{"type":"path"}]`) - - err := setRepositories(raw, []composerRepository{}) - assert.NoError(t, err) - _, exists := raw["repositories"] - assert.False(t, exists) - }) -} - -func TestGetPackageInfo_Good(t *testing.T) { - t.Run("extracts package name and version", func(t *testing.T) { - dir := t.TempDir() - composerJSON := `{ - "name": "vendor/package", - "version": "1.0.0" - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - name, version, err := getPackageInfo(dir) - assert.NoError(t, err) - assert.Equal(t, "vendor/package", name) - assert.Equal(t, "1.0.0", version) - }) - - t.Run("works without version", func(t *testing.T) { - dir := t.TempDir() - composerJSON := `{ - "name": "vendor/package" - }` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - name, version, err := getPackageInfo(dir) - assert.NoError(t, err) - assert.Equal(t, "vendor/package", name) - assert.Equal(t, "", version) - }) -} - -func TestGetPackageInfo_Bad(t *testing.T) { - t.Run("missing composer.json", func(t *testing.T) { - dir := t.TempDir() - _, _, err := getPackageInfo(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to read package composer.json") - }) - - t.Run("invalid JSON", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("not json{"), 0644) - require.NoError(t, err) - - _, _, err = getPackageInfo(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to parse package composer.json") - }) - - t.Run("missing name", func(t *testing.T) { - dir := t.TempDir() - composerJSON := `{"version": "1.0.0"}` - err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - _, _, err = getPackageInfo(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "package name not found") - }) -} - -func TestLinkPackages_Good(t *testing.T) { - t.Run("links a package", func(t *testing.T) { - // Create project directory - projectDir := t.TempDir() - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(`{"name":"test/project"}`), 0644) - require.NoError(t, err) - - // Create package directory - packageDir := t.TempDir() - err = os.WriteFile(filepath.Join(packageDir, "composer.json"), []byte(`{"name":"vendor/package"}`), 0644) - require.NoError(t, err) - - err = LinkPackages(projectDir, []string{packageDir}) - assert.NoError(t, err) - - // Verify repository was added - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 1) - assert.Equal(t, "path", repos[0].Type) - }) - - t.Run("skips already linked package", func(t *testing.T) { - // Create project with existing repository - projectDir := t.TempDir() - packageDir := t.TempDir() - - err := os.WriteFile(filepath.Join(packageDir, "composer.json"), []byte(`{"name":"vendor/package"}`), 0644) - require.NoError(t, err) - - absPackagePath, _ := filepath.Abs(packageDir) - composerJSON := `{ - "name": "test/project", - "repositories": [{"type":"path","url":"` + absPackagePath + `"}] - }` - err = os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // Link again - should not add duplicate - err = LinkPackages(projectDir, []string{packageDir}) - assert.NoError(t, err) - - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 1) // Still only one - }) - - t.Run("links multiple packages", func(t *testing.T) { - projectDir := t.TempDir() - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(`{"name":"test/project"}`), 0644) - require.NoError(t, err) - - pkg1Dir := t.TempDir() - err = os.WriteFile(filepath.Join(pkg1Dir, "composer.json"), []byte(`{"name":"vendor/pkg1"}`), 0644) - require.NoError(t, err) - - pkg2Dir := t.TempDir() - err = os.WriteFile(filepath.Join(pkg2Dir, "composer.json"), []byte(`{"name":"vendor/pkg2"}`), 0644) - require.NoError(t, err) - - err = LinkPackages(projectDir, []string{pkg1Dir, pkg2Dir}) - assert.NoError(t, err) - - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 2) - }) -} - -func TestLinkPackages_Bad(t *testing.T) { - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - err := LinkPackages(dir, []string{"/path/to/package"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) - - t.Run("fails for non-PHP package", func(t *testing.T) { - projectDir := t.TempDir() - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(`{"name":"test/project"}`), 0644) - require.NoError(t, err) - - packageDir := t.TempDir() - // No composer.json in package - - err = LinkPackages(projectDir, []string{packageDir}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP package") - }) -} - -func TestUnlinkPackages_Good(t *testing.T) { - t.Run("unlinks package by name", func(t *testing.T) { - projectDir := t.TempDir() - packageDir := t.TempDir() - - err := os.WriteFile(filepath.Join(packageDir, "composer.json"), []byte(`{"name":"vendor/package"}`), 0644) - require.NoError(t, err) - - absPackagePath, _ := filepath.Abs(packageDir) - composerJSON := `{ - "name": "test/project", - "repositories": [{"type":"path","url":"` + absPackagePath + `"}] - }` - err = os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - err = UnlinkPackages(projectDir, []string{"vendor/package"}) - assert.NoError(t, err) - - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 0) - }) - - t.Run("unlinks package by path", func(t *testing.T) { - projectDir := t.TempDir() - packageDir := t.TempDir() - - absPackagePath, _ := filepath.Abs(packageDir) - composerJSON := `{ - "name": "test/project", - "repositories": [{"type":"path","url":"` + absPackagePath + `"}] - }` - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - err = UnlinkPackages(projectDir, []string{absPackagePath}) - assert.NoError(t, err) - - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 0) - }) - - t.Run("keeps non-path repositories", func(t *testing.T) { - projectDir := t.TempDir() - composerJSON := `{ - "name": "test/project", - "repositories": [ - {"type":"vcs","url":"https://github.com/vendor/package"}, - {"type":"path","url":"/local/path"} - ] - }` - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - err = UnlinkPackages(projectDir, []string{"/local/path"}) - assert.NoError(t, err) - - raw, err := readComposerJSON(projectDir) - assert.NoError(t, err) - repos, err := getRepositories(raw) - assert.NoError(t, err) - assert.Len(t, repos, 1) - assert.Equal(t, "vcs", repos[0].Type) - }) -} - -func TestUnlinkPackages_Bad(t *testing.T) { - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - err := UnlinkPackages(dir, []string{"vendor/package"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) -} - -func TestListLinkedPackages_Good(t *testing.T) { - t.Run("lists linked packages", func(t *testing.T) { - projectDir := t.TempDir() - packageDir := t.TempDir() - - err := os.WriteFile(filepath.Join(packageDir, "composer.json"), []byte(`{"name":"vendor/package","version":"1.0.0"}`), 0644) - require.NoError(t, err) - - absPackagePath, _ := filepath.Abs(packageDir) - composerJSON := `{ - "name": "test/project", - "repositories": [{"type":"path","url":"` + absPackagePath + `"}] - }` - err = os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - linked, err := ListLinkedPackages(projectDir) - assert.NoError(t, err) - assert.Len(t, linked, 1) - assert.Equal(t, "vendor/package", linked[0].Name) - assert.Equal(t, "1.0.0", linked[0].Version) - assert.Equal(t, absPackagePath, linked[0].Path) - }) - - t.Run("returns empty list when no linked packages", func(t *testing.T) { - projectDir := t.TempDir() - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(`{"name":"test/project"}`), 0644) - require.NoError(t, err) - - linked, err := ListLinkedPackages(projectDir) - assert.NoError(t, err) - assert.Empty(t, linked) - }) - - t.Run("uses basename when package info unavailable", func(t *testing.T) { - projectDir := t.TempDir() - composerJSON := `{ - "name": "test/project", - "repositories": [{"type":"path","url":"/nonexistent/package-name"}] - }` - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - linked, err := ListLinkedPackages(projectDir) - assert.NoError(t, err) - assert.Len(t, linked, 1) - assert.Equal(t, "package-name", linked[0].Name) - }) - - t.Run("ignores non-path repositories", func(t *testing.T) { - projectDir := t.TempDir() - composerJSON := `{ - "name": "test/project", - "repositories": [ - {"type":"vcs","url":"https://github.com/vendor/package"} - ] - }` - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - linked, err := ListLinkedPackages(projectDir) - assert.NoError(t, err) - assert.Empty(t, linked) - }) -} - -func TestListLinkedPackages_Bad(t *testing.T) { - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - _, err := ListLinkedPackages(dir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) -} - -func TestUpdatePackages_Bad(t *testing.T) { - t.Run("fails for non-PHP project", func(t *testing.T) { - dir := t.TempDir() - err := UpdatePackages(dir, []string{"vendor/package"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a PHP project") - }) -} - -func TestUpdatePackages_Good(t *testing.T) { - t.Skip("requires Composer installed") - - t.Run("runs composer update", func(t *testing.T) { - projectDir := t.TempDir() - err := os.WriteFile(filepath.Join(projectDir, "composer.json"), []byte(`{"name":"test/project"}`), 0644) - require.NoError(t, err) - - _ = UpdatePackages(projectDir, []string{"vendor/package"}) - // This will fail because composer update needs real dependencies - // but it validates the command runs - }) -} - -func TestLinkedPackage_Struct(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - pkg := LinkedPackage{ - Name: "vendor/package", - Path: "/path/to/package", - Version: "1.0.0", - } - - assert.Equal(t, "vendor/package", pkg.Name) - assert.Equal(t, "/path/to/package", pkg.Path) - assert.Equal(t, "1.0.0", pkg.Version) - }) -} - -func TestComposerRepository_Struct(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - repo := composerRepository{ - Type: "path", - URL: "/path/to/package", - Options: map[string]any{ - "symlink": true, - }, - } - - assert.Equal(t, "path", repo.Type) - assert.Equal(t, "/path/to/package", repo.URL) - assert.Equal(t, true, repo.Options["symlink"]) - }) -} diff --git a/internal/cmd/php/php.go b/internal/cmd/php/php.go deleted file mode 100644 index 96393eb5..00000000 --- a/internal/cmd/php/php.go +++ /dev/null @@ -1,397 +0,0 @@ -package php - -import ( - "context" - "io" - "os" - "sync" - "time" - - "forge.lthn.ai/core/go/pkg/cli" -) - -// Options configures the development server. -type Options struct { - // Dir is the Laravel project directory. - Dir string - - // Services specifies which services to start. - // If empty, services are auto-detected. - Services []DetectedService - - // NoVite disables the Vite dev server. - NoVite bool - - // NoHorizon disables Laravel Horizon. - NoHorizon bool - - // NoReverb disables Laravel Reverb. - NoReverb bool - - // NoRedis disables the Redis server. - NoRedis bool - - // HTTPS enables HTTPS with mkcert certificates. - HTTPS bool - - // Domain is the domain for SSL certificates. - // Defaults to APP_URL from .env or "localhost". - Domain string - - // Ports for each service - FrankenPHPPort int - HTTPSPort int - VitePort int - ReverbPort int - RedisPort int -} - -// DevServer manages all development services. -type DevServer struct { - opts Options - services []Service - ctx context.Context - cancel context.CancelFunc - mu sync.RWMutex - running bool -} - -// NewDevServer creates a new development server manager. -func NewDevServer(opts Options) *DevServer { - return &DevServer{ - opts: opts, - services: make([]Service, 0), - } -} - -// Start starts all detected/configured services. -func (d *DevServer) Start(ctx context.Context, opts Options) error { - d.mu.Lock() - defer d.mu.Unlock() - - if d.running { - return cli.Err("dev server is already running") - } - - // Merge options - if opts.Dir != "" { - d.opts.Dir = opts.Dir - } - if d.opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - d.opts.Dir = cwd - } - - // Verify this is a Laravel project - if !IsLaravelProject(d.opts.Dir) { - return cli.Err("not a Laravel project: %s", d.opts.Dir) - } - - // Create cancellable context - d.ctx, d.cancel = context.WithCancel(ctx) - - // Detect or use provided services - services := opts.Services - if len(services) == 0 { - services = DetectServices(d.opts.Dir) - } - - // Filter out disabled services - services = d.filterServices(services, opts) - - // Setup SSL if HTTPS is enabled - var certFile, keyFile string - if opts.HTTPS { - domain := opts.Domain - if domain == "" { - // Try to get domain from APP_URL - appURL := GetLaravelAppURL(d.opts.Dir) - if appURL != "" { - domain = ExtractDomainFromURL(appURL) - } - } - if domain == "" { - domain = "localhost" - } - - var err error - certFile, keyFile, err = SetupSSLIfNeeded(domain, SSLOptions{}) - if err != nil { - return cli.WrapVerb(err, "setup", "SSL") - } - } - - // Create services - d.services = make([]Service, 0) - - for _, svc := range services { - var service Service - - switch svc { - case ServiceFrankenPHP: - port := opts.FrankenPHPPort - if port == 0 { - port = 8000 - } - httpsPort := opts.HTTPSPort - if httpsPort == 0 { - httpsPort = 443 - } - service = NewFrankenPHPService(d.opts.Dir, FrankenPHPOptions{ - Port: port, - HTTPSPort: httpsPort, - HTTPS: opts.HTTPS, - CertFile: certFile, - KeyFile: keyFile, - }) - - case ServiceVite: - port := opts.VitePort - if port == 0 { - port = 5173 - } - service = NewViteService(d.opts.Dir, ViteOptions{ - Port: port, - }) - - case ServiceHorizon: - service = NewHorizonService(d.opts.Dir) - - case ServiceReverb: - port := opts.ReverbPort - if port == 0 { - port = 8080 - } - service = NewReverbService(d.opts.Dir, ReverbOptions{ - Port: port, - }) - - case ServiceRedis: - port := opts.RedisPort - if port == 0 { - port = 6379 - } - service = NewRedisService(d.opts.Dir, RedisOptions{ - Port: port, - }) - } - - if service != nil { - d.services = append(d.services, service) - } - } - - // Start all services - var startErrors []error - for _, svc := range d.services { - if err := svc.Start(d.ctx); err != nil { - startErrors = append(startErrors, cli.Err("%s: %v", svc.Name(), err)) - } - } - - if len(startErrors) > 0 { - // Stop any services that did start - for _, svc := range d.services { - _ = svc.Stop() - } - return cli.Err("failed to start services: %v", startErrors) - } - - d.running = true - return nil -} - -// filterServices removes disabled services from the list. -func (d *DevServer) filterServices(services []DetectedService, opts Options) []DetectedService { - filtered := make([]DetectedService, 0) - - for _, svc := range services { - switch svc { - case ServiceVite: - if !opts.NoVite { - filtered = append(filtered, svc) - } - case ServiceHorizon: - if !opts.NoHorizon { - filtered = append(filtered, svc) - } - case ServiceReverb: - if !opts.NoReverb { - filtered = append(filtered, svc) - } - case ServiceRedis: - if !opts.NoRedis { - filtered = append(filtered, svc) - } - default: - filtered = append(filtered, svc) - } - } - - return filtered -} - -// Stop stops all services gracefully. -func (d *DevServer) Stop() error { - d.mu.Lock() - defer d.mu.Unlock() - - if !d.running { - return nil - } - - // Cancel context first - if d.cancel != nil { - d.cancel() - } - - // Stop all services in reverse order - var stopErrors []error - for i := len(d.services) - 1; i >= 0; i-- { - svc := d.services[i] - if err := svc.Stop(); err != nil { - stopErrors = append(stopErrors, cli.Err("%s: %v", svc.Name(), err)) - } - } - - d.running = false - - if len(stopErrors) > 0 { - return cli.Err("errors stopping services: %v", stopErrors) - } - - return nil -} - -// Logs returns a reader for the specified service's logs. -// If service is empty, returns unified logs from all services. -func (d *DevServer) Logs(service string, follow bool) (io.ReadCloser, error) { - d.mu.RLock() - defer d.mu.RUnlock() - - if service == "" { - // Return unified logs - return d.unifiedLogs(follow) - } - - // Find specific service - for _, svc := range d.services { - if svc.Name() == service { - return svc.Logs(follow) - } - } - - return nil, cli.Err("service not found: %s", service) -} - -// unifiedLogs creates a reader that combines logs from all services. -func (d *DevServer) unifiedLogs(follow bool) (io.ReadCloser, error) { - readers := make([]io.ReadCloser, 0) - - for _, svc := range d.services { - reader, err := svc.Logs(follow) - if err != nil { - // Close any readers we already opened - for _, r := range readers { - _ = r.Close() - } - return nil, cli.Err("failed to get logs for %s: %v", svc.Name(), err) - } - readers = append(readers, reader) - } - - return newMultiServiceReader(d.services, readers, follow), nil -} - -// Status returns the status of all services. -func (d *DevServer) Status() []ServiceStatus { - d.mu.RLock() - defer d.mu.RUnlock() - - statuses := make([]ServiceStatus, 0, len(d.services)) - for _, svc := range d.services { - statuses = append(statuses, svc.Status()) - } - - return statuses -} - -// IsRunning returns true if the dev server is running. -func (d *DevServer) IsRunning() bool { - d.mu.RLock() - defer d.mu.RUnlock() - return d.running -} - -// Services returns the list of managed services. -func (d *DevServer) Services() []Service { - d.mu.RLock() - defer d.mu.RUnlock() - return d.services -} - -// multiServiceReader combines multiple service log readers. -type multiServiceReader struct { - services []Service - readers []io.ReadCloser - follow bool - closed bool - mu sync.RWMutex -} - -func newMultiServiceReader(services []Service, readers []io.ReadCloser, follow bool) *multiServiceReader { - return &multiServiceReader{ - services: services, - readers: readers, - follow: follow, - } -} - -func (m *multiServiceReader) Read(p []byte) (n int, err error) { - m.mu.RLock() - if m.closed { - m.mu.RUnlock() - return 0, io.EOF - } - m.mu.RUnlock() - - // Round-robin read from all readers - for i, reader := range m.readers { - buf := make([]byte, len(p)) - n, err := reader.Read(buf) - if n > 0 { - // Prefix with service name - prefix := cli.Sprintf("[%s] ", m.services[i].Name()) - copy(p, prefix) - copy(p[len(prefix):], buf[:n]) - return n + len(prefix), nil - } - if err != nil && err != io.EOF { - return 0, err - } - } - - if m.follow { - time.Sleep(100 * time.Millisecond) - return 0, nil - } - - return 0, io.EOF -} - -func (m *multiServiceReader) Close() error { - m.mu.Lock() - m.closed = true - m.mu.Unlock() - - var closeErr error - for _, reader := range m.readers { - if err := reader.Close(); err != nil && closeErr == nil { - closeErr = err - } - } - return closeErr -} diff --git a/internal/cmd/php/php_test.go b/internal/cmd/php/php_test.go deleted file mode 100644 index e295d73e..00000000 --- a/internal/cmd/php/php_test.go +++ /dev/null @@ -1,644 +0,0 @@ -package php - -import ( - "context" - "io" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewDevServer_Good(t *testing.T) { - t.Run("creates dev server with default options", func(t *testing.T) { - opts := Options{} - server := NewDevServer(opts) - - assert.NotNil(t, server) - assert.Empty(t, server.services) - assert.False(t, server.running) - }) - - t.Run("creates dev server with custom options", func(t *testing.T) { - opts := Options{ - Dir: "/tmp/test", - NoVite: true, - NoHorizon: true, - FrankenPHPPort: 9000, - } - server := NewDevServer(opts) - - assert.NotNil(t, server) - assert.Equal(t, "/tmp/test", server.opts.Dir) - assert.True(t, server.opts.NoVite) - }) -} - -func TestDevServer_IsRunning_Good(t *testing.T) { - t.Run("returns false when not running", func(t *testing.T) { - server := NewDevServer(Options{}) - assert.False(t, server.IsRunning()) - }) -} - -func TestDevServer_Status_Good(t *testing.T) { - t.Run("returns empty status when no services", func(t *testing.T) { - server := NewDevServer(Options{}) - statuses := server.Status() - assert.Empty(t, statuses) - }) -} - -func TestDevServer_Services_Good(t *testing.T) { - t.Run("returns empty services list initially", func(t *testing.T) { - server := NewDevServer(Options{}) - services := server.Services() - assert.Empty(t, services) - }) -} - -func TestDevServer_Stop_Good(t *testing.T) { - t.Run("returns nil when not running", func(t *testing.T) { - server := NewDevServer(Options{}) - err := server.Stop() - assert.NoError(t, err) - }) -} - -func TestDevServer_Start_Bad(t *testing.T) { - t.Run("fails when already running", func(t *testing.T) { - server := NewDevServer(Options{}) - server.running = true - - err := server.Start(context.Background(), Options{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "already running") - }) - - t.Run("fails for non-Laravel project", func(t *testing.T) { - dir := t.TempDir() - server := NewDevServer(Options{Dir: dir}) - - err := server.Start(context.Background(), Options{Dir: dir}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not a Laravel project") - }) -} - -func TestDevServer_Logs_Bad(t *testing.T) { - t.Run("fails for non-existent service", func(t *testing.T) { - server := NewDevServer(Options{}) - - _, err := server.Logs("nonexistent", false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "service not found") - }) -} - -func TestDevServer_filterServices_Good(t *testing.T) { - tests := []struct { - name string - services []DetectedService - opts Options - expected []DetectedService - }{ - { - name: "no filtering with default options", - services: []DetectedService{ServiceFrankenPHP, ServiceVite, ServiceHorizon}, - opts: Options{}, - expected: []DetectedService{ServiceFrankenPHP, ServiceVite, ServiceHorizon}, - }, - { - name: "filters Vite when NoVite is true", - services: []DetectedService{ServiceFrankenPHP, ServiceVite, ServiceHorizon}, - opts: Options{NoVite: true}, - expected: []DetectedService{ServiceFrankenPHP, ServiceHorizon}, - }, - { - name: "filters Horizon when NoHorizon is true", - services: []DetectedService{ServiceFrankenPHP, ServiceVite, ServiceHorizon}, - opts: Options{NoHorizon: true}, - expected: []DetectedService{ServiceFrankenPHP, ServiceVite}, - }, - { - name: "filters Reverb when NoReverb is true", - services: []DetectedService{ServiceFrankenPHP, ServiceReverb}, - opts: Options{NoReverb: true}, - expected: []DetectedService{ServiceFrankenPHP}, - }, - { - name: "filters Redis when NoRedis is true", - services: []DetectedService{ServiceFrankenPHP, ServiceRedis}, - opts: Options{NoRedis: true}, - expected: []DetectedService{ServiceFrankenPHP}, - }, - { - name: "filters multiple services", - services: []DetectedService{ServiceFrankenPHP, ServiceVite, ServiceHorizon, ServiceReverb, ServiceRedis}, - opts: Options{NoVite: true, NoHorizon: true, NoReverb: true, NoRedis: true}, - expected: []DetectedService{ServiceFrankenPHP}, - }, - { - name: "keeps unknown services", - services: []DetectedService{ServiceFrankenPHP}, - opts: Options{NoVite: true}, - expected: []DetectedService{ServiceFrankenPHP}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := NewDevServer(Options{}) - result := server.filterServices(tt.services, tt.opts) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestMultiServiceReader_Good(t *testing.T) { - t.Run("closes all readers on Close", func(t *testing.T) { - // Create mock readers using files - dir := t.TempDir() - file1, err := os.CreateTemp(dir, "log1-*.log") - require.NoError(t, err) - _, _ = file1.WriteString("test1") - _, _ = file1.Seek(0, 0) - - file2, err := os.CreateTemp(dir, "log2-*.log") - require.NoError(t, err) - _, _ = file2.WriteString("test2") - _, _ = file2.Seek(0, 0) - - // Create mock services - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1"}}, - &ViteService{baseService: baseService{name: "svc2"}}, - } - readers := []io.ReadCloser{file1, file2} - - reader := newMultiServiceReader(services, readers, false) - assert.NotNil(t, reader) - - err = reader.Close() - assert.NoError(t, err) - assert.True(t, reader.closed) - }) - - t.Run("returns EOF when closed", func(t *testing.T) { - reader := &multiServiceReader{closed: true} - buf := make([]byte, 10) - n, err := reader.Read(buf) - assert.Equal(t, 0, n) - assert.Equal(t, io.EOF, err) - }) -} - -func TestMultiServiceReader_Read_Good(t *testing.T) { - t.Run("reads from readers with service prefix", func(t *testing.T) { - dir := t.TempDir() - file1, err := os.CreateTemp(dir, "log-*.log") - require.NoError(t, err) - _, _ = file1.WriteString("log content") - _, _ = file1.Seek(0, 0) - - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "TestService"}}, - } - readers := []io.ReadCloser{file1} - - reader := newMultiServiceReader(services, readers, false) - buf := make([]byte, 100) - n, err := reader.Read(buf) - - assert.NoError(t, err) - assert.Greater(t, n, 0) - result := string(buf[:n]) - assert.Contains(t, result, "[TestService]") - }) - - t.Run("returns EOF when all readers are exhausted in non-follow mode", func(t *testing.T) { - dir := t.TempDir() - file1, err := os.CreateTemp(dir, "log-*.log") - require.NoError(t, err) - _ = file1.Close() // Empty file - - file1, err = os.Open(file1.Name()) - require.NoError(t, err) - - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "TestService"}}, - } - readers := []io.ReadCloser{file1} - - reader := newMultiServiceReader(services, readers, false) - buf := make([]byte, 100) - n, err := reader.Read(buf) - - assert.Equal(t, 0, n) - assert.Equal(t, io.EOF, err) - }) -} - -func TestOptions_Good(t *testing.T) { - t.Run("all fields are accessible", func(t *testing.T) { - opts := Options{ - Dir: "/test", - Services: []DetectedService{ServiceFrankenPHP}, - NoVite: true, - NoHorizon: true, - NoReverb: true, - NoRedis: true, - HTTPS: true, - Domain: "test.local", - FrankenPHPPort: 8000, - HTTPSPort: 443, - VitePort: 5173, - ReverbPort: 8080, - RedisPort: 6379, - } - - assert.Equal(t, "/test", opts.Dir) - assert.Equal(t, []DetectedService{ServiceFrankenPHP}, opts.Services) - assert.True(t, opts.NoVite) - assert.True(t, opts.NoHorizon) - assert.True(t, opts.NoReverb) - assert.True(t, opts.NoRedis) - assert.True(t, opts.HTTPS) - assert.Equal(t, "test.local", opts.Domain) - assert.Equal(t, 8000, opts.FrankenPHPPort) - assert.Equal(t, 443, opts.HTTPSPort) - assert.Equal(t, 5173, opts.VitePort) - assert.Equal(t, 8080, opts.ReverbPort) - assert.Equal(t, 6379, opts.RedisPort) - }) -} - -func TestDevServer_StartStop_Integration(t *testing.T) { - t.Skip("requires PHP/FrankenPHP installed") - - dir := t.TempDir() - setupLaravelProject(t, dir) - - server := NewDevServer(Options{Dir: dir}) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - err := server.Start(ctx, Options{Dir: dir}) - require.NoError(t, err) - assert.True(t, server.IsRunning()) - - err = server.Stop() - require.NoError(t, err) - assert.False(t, server.IsRunning()) -} - -// setupLaravelProject creates a minimal Laravel project structure for testing. -func setupLaravelProject(t *testing.T, dir string) { - t.Helper() - - // Create artisan file - err := os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - // Create composer.json with Laravel - composerJSON := `{ - "name": "test/laravel-project", - "require": { - "php": "^8.2", - "laravel/framework": "^11.0", - "laravel/octane": "^2.0" - } - }` - err = os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) -} - -func TestDevServer_UnifiedLogs_Bad(t *testing.T) { - t.Run("returns error when service logs fail", func(t *testing.T) { - server := NewDevServer(Options{}) - - // Create a mock service that will fail to provide logs - mockService := &FrankenPHPService{ - baseService: baseService{ - name: "FailingService", - logPath: "", // No log path set will cause error - }, - } - server.services = []Service{mockService} - - _, err := server.Logs("", false) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to get logs") - }) -} - -func TestDevServer_Logs_Good(t *testing.T) { - t.Run("finds specific service logs", func(t *testing.T) { - dir := t.TempDir() - logFile := filepath.Join(dir, "test.log") - err := os.WriteFile(logFile, []byte("test log content"), 0644) - require.NoError(t, err) - - server := NewDevServer(Options{}) - mockService := &FrankenPHPService{ - baseService: baseService{ - name: "TestService", - logPath: logFile, - }, - } - server.services = []Service{mockService} - - reader, err := server.Logs("TestService", false) - assert.NoError(t, err) - assert.NotNil(t, reader) - _ = reader.Close() - }) -} - -func TestDevServer_MergeOptions_Good(t *testing.T) { - t.Run("start merges options correctly", func(t *testing.T) { - dir := t.TempDir() - server := NewDevServer(Options{Dir: "/original"}) - - // Setup a minimal non-Laravel project to trigger an error - // but still test the options merge happens first - err := server.Start(context.Background(), Options{Dir: dir}) - assert.Error(t, err) // Will fail because not Laravel project - // But the directory should have been merged - assert.Equal(t, dir, server.opts.Dir) - }) -} - -func TestDetectedService_Constants(t *testing.T) { - t.Run("all service constants are defined", func(t *testing.T) { - assert.Equal(t, DetectedService("frankenphp"), ServiceFrankenPHP) - assert.Equal(t, DetectedService("vite"), ServiceVite) - assert.Equal(t, DetectedService("horizon"), ServiceHorizon) - assert.Equal(t, DetectedService("reverb"), ServiceReverb) - assert.Equal(t, DetectedService("redis"), ServiceRedis) - }) -} - -func TestDevServer_HTTPSSetup(t *testing.T) { - t.Run("extracts domain from APP_URL when HTTPS enabled", func(t *testing.T) { - dir := t.TempDir() - - // Create Laravel project - err := os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php\n"), 0755) - require.NoError(t, err) - - composerJSON := `{ - "require": { - "laravel/framework": "^11.0", - "laravel/octane": "^2.0" - } - }` - err = os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composerJSON), 0644) - require.NoError(t, err) - - // Create .env with APP_URL - envContent := "APP_URL=https://myapp.test" - err = os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - // Verify we can extract the domain - url := GetLaravelAppURL(dir) - domain := ExtractDomainFromURL(url) - assert.Equal(t, "myapp.test", domain) - }) -} - -func TestDevServer_PortDefaults(t *testing.T) { - t.Run("uses default ports when not specified", func(t *testing.T) { - // This tests the logic in Start() for default port assignment - // We verify the constants/defaults by checking what would be created - - // FrankenPHP default port is 8000 - svc := NewFrankenPHPService("/tmp", FrankenPHPOptions{}) - assert.Equal(t, 8000, svc.port) - - // Vite default port is 5173 - vite := NewViteService("/tmp", ViteOptions{}) - assert.Equal(t, 5173, vite.port) - - // Reverb default port is 8080 - reverb := NewReverbService("/tmp", ReverbOptions{}) - assert.Equal(t, 8080, reverb.port) - - // Redis default port is 6379 - redis := NewRedisService("/tmp", RedisOptions{}) - assert.Equal(t, 6379, redis.port) - }) -} - -func TestDevServer_ServiceCreation(t *testing.T) { - t.Run("creates correct services based on detected services", func(t *testing.T) { - // Test that the switch statement in Start() creates the right service types - services := []DetectedService{ - ServiceFrankenPHP, - ServiceVite, - ServiceHorizon, - ServiceReverb, - ServiceRedis, - } - - // Verify each service type string - expected := []string{"frankenphp", "vite", "horizon", "reverb", "redis"} - for i, svc := range services { - assert.Equal(t, expected[i], string(svc)) - } - }) -} - -func TestMultiServiceReader_CloseError(t *testing.T) { - t.Run("returns first close error", func(t *testing.T) { - dir := t.TempDir() - - // Create a real file that we can close - file1, err := os.CreateTemp(dir, "log-*.log") - require.NoError(t, err) - file1Name := file1.Name() - _ = file1.Close() - - // Reopen for reading - file1, err = os.Open(file1Name) - require.NoError(t, err) - - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1"}}, - } - readers := []io.ReadCloser{file1} - - reader := newMultiServiceReader(services, readers, false) - err = reader.Close() - assert.NoError(t, err) - - // Second close should still work (files already closed) - // The closed flag prevents double-processing - assert.True(t, reader.closed) - }) -} - -func TestMultiServiceReader_FollowMode(t *testing.T) { - t.Run("returns 0 bytes without error in follow mode when no data", func(t *testing.T) { - dir := t.TempDir() - file1, err := os.CreateTemp(dir, "log-*.log") - require.NoError(t, err) - file1Name := file1.Name() - _ = file1.Close() - - // Reopen for reading (empty file) - file1, err = os.Open(file1Name) - require.NoError(t, err) - - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1"}}, - } - readers := []io.ReadCloser{file1} - - reader := newMultiServiceReader(services, readers, true) // follow=true - - // Use a channel to timeout the read since follow mode waits - done := make(chan bool) - go func() { - buf := make([]byte, 100) - n, err := reader.Read(buf) - // In follow mode, should return 0 bytes and nil error (waiting for more data) - assert.Equal(t, 0, n) - assert.NoError(t, err) - done <- true - }() - - select { - case <-done: - // Good, read completed - case <-time.After(500 * time.Millisecond): - // Also acceptable - follow mode is waiting - } - - _ = reader.Close() - }) -} - -func TestGetLaravelAppURL_Bad(t *testing.T) { - t.Run("no .env file", func(t *testing.T) { - dir := t.TempDir() - assert.Equal(t, "", GetLaravelAppURL(dir)) - }) - - t.Run("no APP_URL in .env", func(t *testing.T) { - dir := t.TempDir() - envContent := "APP_NAME=Test\nAPP_ENV=local" - err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644) - require.NoError(t, err) - - assert.Equal(t, "", GetLaravelAppURL(dir)) - }) -} - -func TestExtractDomainFromURL_Edge(t *testing.T) { - tests := []struct { - name string - url string - expected string - }{ - {"empty string", "", ""}, - {"just domain", "example.com", "example.com"}, - {"http only", "http://", ""}, - {"https only", "https://", ""}, - {"domain with trailing slash", "https://example.com/", "example.com"}, - {"complex path", "https://example.com:8080/path/to/page?query=1", "example.com"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Strip protocol - result := ExtractDomainFromURL(tt.url) - if tt.url != "" && !strings.HasPrefix(tt.url, "http://") && !strings.HasPrefix(tt.url, "https://") && !strings.Contains(tt.url, ":") && !strings.Contains(tt.url, "/") { - assert.Equal(t, tt.expected, result) - } - }) - } -} - -func TestDevServer_StatusWithServices(t *testing.T) { - t.Run("returns statuses for all services", func(t *testing.T) { - server := NewDevServer(Options{}) - - // Add mock services - server.services = []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1", running: true, port: 8000}}, - &ViteService{baseService: baseService{name: "svc2", running: false, port: 5173}}, - } - - statuses := server.Status() - assert.Len(t, statuses, 2) - assert.Equal(t, "svc1", statuses[0].Name) - assert.True(t, statuses[0].Running) - assert.Equal(t, "svc2", statuses[1].Name) - assert.False(t, statuses[1].Running) - }) -} - -func TestDevServer_ServicesReturnsAll(t *testing.T) { - t.Run("returns all services", func(t *testing.T) { - server := NewDevServer(Options{}) - - // Add mock services - server.services = []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1"}}, - &ViteService{baseService: baseService{name: "svc2"}}, - &HorizonService{baseService: baseService{name: "svc3"}}, - } - - services := server.Services() - assert.Len(t, services, 3) - }) -} - -func TestDevServer_StopWithCancel(t *testing.T) { - t.Run("calls cancel when running", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - server := NewDevServer(Options{}) - server.running = true - server.cancel = cancel - server.ctx = ctx - - // Add a mock service that won't error - server.services = []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1", running: false}}, - } - - err := server.Stop() - assert.NoError(t, err) - assert.False(t, server.running) - }) -} - -func TestMultiServiceReader_CloseWithErrors(t *testing.T) { - t.Run("handles multiple close errors", func(t *testing.T) { - dir := t.TempDir() - - // Create files - file1, err := os.CreateTemp(dir, "log1-*.log") - require.NoError(t, err) - file2, err := os.CreateTemp(dir, "log2-*.log") - require.NoError(t, err) - - services := []Service{ - &FrankenPHPService{baseService: baseService{name: "svc1"}}, - &ViteService{baseService: baseService{name: "svc2"}}, - } - readers := []io.ReadCloser{file1, file2} - - reader := newMultiServiceReader(services, readers, false) - - // Close successfully - err = reader.Close() - assert.NoError(t, err) - }) -} diff --git a/internal/cmd/php/quality.go b/internal/cmd/php/quality.go deleted file mode 100644 index a7f96388..00000000 --- a/internal/cmd/php/quality.go +++ /dev/null @@ -1,994 +0,0 @@ -package php - -import ( - "context" - "encoding/json" - goio "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go/pkg/i18n" -) - -// FormatOptions configures PHP code formatting. -type FormatOptions struct { - // Dir is the project directory (defaults to current working directory). - Dir string - - // Fix automatically fixes formatting issues. - Fix bool - - // Diff shows a diff of changes instead of modifying files. - Diff bool - - // JSON outputs results in JSON format. - JSON bool - - // Paths limits formatting to specific paths. - Paths []string - - // Output is the writer for output (defaults to os.Stdout). - Output goio.Writer -} - -// AnalyseOptions configures PHP static analysis. -type AnalyseOptions struct { - // Dir is the project directory (defaults to current working directory). - Dir string - - // Level is the PHPStan analysis level (0-9). - Level int - - // Paths limits analysis to specific paths. - Paths []string - - // Memory is the memory limit for analysis (e.g., "2G"). - Memory string - - // JSON outputs results in JSON format. - JSON bool - - // SARIF outputs results in SARIF format for GitHub Security tab. - SARIF bool - - // Output is the writer for output (defaults to os.Stdout). - Output goio.Writer -} - -// FormatterType represents the detected formatter. -type FormatterType string - -// Formatter type constants. -const ( - // FormatterPint indicates Laravel Pint code formatter. - FormatterPint FormatterType = "pint" -) - -// AnalyserType represents the detected static analyser. -type AnalyserType string - -// Static analyser type constants. -const ( - // AnalyserPHPStan indicates standard PHPStan analyser. - AnalyserPHPStan AnalyserType = "phpstan" - // AnalyserLarastan indicates Laravel-specific Larastan analyser. - AnalyserLarastan AnalyserType = "larastan" -) - -// DetectFormatter detects which formatter is available in the project. -func DetectFormatter(dir string) (FormatterType, bool) { - m := getMedium() - - // Check for Pint config - pintConfig := filepath.Join(dir, "pint.json") - if m.Exists(pintConfig) { - return FormatterPint, true - } - - // Check for vendor binary - pintBin := filepath.Join(dir, "vendor", "bin", "pint") - if m.Exists(pintBin) { - return FormatterPint, true - } - - return "", false -} - -// DetectAnalyser detects which static analyser is available in the project. -func DetectAnalyser(dir string) (AnalyserType, bool) { - m := getMedium() - - // Check for PHPStan config - phpstanConfig := filepath.Join(dir, "phpstan.neon") - phpstanDistConfig := filepath.Join(dir, "phpstan.neon.dist") - - hasConfig := m.Exists(phpstanConfig) || m.Exists(phpstanDistConfig) - - // Check for vendor binary - phpstanBin := filepath.Join(dir, "vendor", "bin", "phpstan") - hasBin := m.Exists(phpstanBin) - - if hasConfig || hasBin { - // Check if it's Larastan (Laravel-specific PHPStan) - larastanPath := filepath.Join(dir, "vendor", "larastan", "larastan") - if m.Exists(larastanPath) { - return AnalyserLarastan, true - } - // Also check nunomaduro/larastan - larastanPath2 := filepath.Join(dir, "vendor", "nunomaduro", "larastan") - if m.Exists(larastanPath2) { - return AnalyserLarastan, true - } - return AnalyserPHPStan, true - } - - return "", false -} - -// Format runs Laravel Pint to format PHP code. -func Format(ctx context.Context, opts FormatOptions) error { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - // Check if formatter is available - formatter, found := DetectFormatter(opts.Dir) - if !found { - return cli.Err("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") - } - - var cmdName string - var args []string - - switch formatter { - case FormatterPint: - cmdName, args = buildPintCommand(opts) - } - - cmd := exec.CommandContext(ctx, cmdName, args...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - return cmd.Run() -} - -// Analyse runs PHPStan or Larastan for static analysis. -func Analyse(ctx context.Context, opts AnalyseOptions) error { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - // Check if analyser is available - analyser, found := DetectAnalyser(opts.Dir) - if !found { - return cli.Err("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") - } - - var cmdName string - var args []string - - switch analyser { - case AnalyserPHPStan, AnalyserLarastan: - cmdName, args = buildPHPStanCommand(opts) - } - - cmd := exec.CommandContext(ctx, cmdName, args...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - return cmd.Run() -} - -// buildPintCommand builds the command for running Laravel Pint. -func buildPintCommand(opts FormatOptions) (string, []string) { - m := getMedium() - - // Check for vendor binary first - vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "pint") - cmdName := "pint" - if m.Exists(vendorBin) { - cmdName = vendorBin - } - - var args []string - - if !opts.Fix { - args = append(args, "--test") - } - - if opts.Diff { - args = append(args, "--diff") - } - - if opts.JSON { - args = append(args, "--format=json") - } - - // Add specific paths if provided - args = append(args, opts.Paths...) - - return cmdName, args -} - -// buildPHPStanCommand builds the command for running PHPStan. -func buildPHPStanCommand(opts AnalyseOptions) (string, []string) { - m := getMedium() - - // Check for vendor binary first - vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "phpstan") - cmdName := "phpstan" - if m.Exists(vendorBin) { - cmdName = vendorBin - } - - args := []string{"analyse"} - - if opts.Level > 0 { - args = append(args, "--level", cli.Sprintf("%d", opts.Level)) - } - - if opts.Memory != "" { - args = append(args, "--memory-limit", opts.Memory) - } - - // Output format - SARIF takes precedence over JSON - if opts.SARIF { - args = append(args, "--error-format=sarif") - } else if opts.JSON { - args = append(args, "--error-format=json") - } - - // Add specific paths if provided - args = append(args, opts.Paths...) - - return cmdName, args -} - -// ============================================================================= -// Psalm Static Analysis -// ============================================================================= - -// PsalmOptions configures Psalm static analysis. -type PsalmOptions struct { - Dir string - Level int // Error level (1=strictest, 8=most lenient) - Fix bool // Auto-fix issues where possible - Baseline bool // Generate/update baseline file - ShowInfo bool // Show info-level issues - JSON bool // Output in JSON format - SARIF bool // Output in SARIF format for GitHub Security tab - Output goio.Writer -} - -// PsalmType represents the detected Psalm configuration. -type PsalmType string - -// Psalm configuration type constants. -const ( - // PsalmStandard indicates standard Psalm configuration. - PsalmStandard PsalmType = "psalm" -) - -// DetectPsalm checks if Psalm is available in the project. -func DetectPsalm(dir string) (PsalmType, bool) { - m := getMedium() - - // Check for psalm.xml config - psalmConfig := filepath.Join(dir, "psalm.xml") - psalmDistConfig := filepath.Join(dir, "psalm.xml.dist") - - hasConfig := m.Exists(psalmConfig) || m.Exists(psalmDistConfig) - - // Check for vendor binary - psalmBin := filepath.Join(dir, "vendor", "bin", "psalm") - if m.Exists(psalmBin) { - return PsalmStandard, true - } - - if hasConfig { - return PsalmStandard, true - } - - return "", false -} - -// RunPsalm runs Psalm static analysis. -func RunPsalm(ctx context.Context, opts PsalmOptions) error { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - m := getMedium() - - // Build command - vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "psalm") - cmdName := "psalm" - if m.Exists(vendorBin) { - cmdName = vendorBin - } - - args := []string{"--no-progress"} - - if opts.Level > 0 && opts.Level <= 8 { - args = append(args, cli.Sprintf("--error-level=%d", opts.Level)) - } - - if opts.Fix { - args = append(args, "--alter", "--issues=all") - } - - if opts.Baseline { - args = append(args, "--set-baseline=psalm-baseline.xml") - } - - if opts.ShowInfo { - args = append(args, "--show-info=true") - } - - // Output format - SARIF takes precedence over JSON - if opts.SARIF { - args = append(args, "--output-format=sarif") - } else if opts.JSON { - args = append(args, "--output-format=json") - } - - cmd := exec.CommandContext(ctx, cmdName, args...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - return cmd.Run() -} - -// ============================================================================= -// Security Audit -// ============================================================================= - -// AuditOptions configures dependency security auditing. -type AuditOptions struct { - Dir string - JSON bool // Output in JSON format - Fix bool // Auto-fix vulnerabilities (npm only) - Output goio.Writer -} - -// AuditResult holds the results of a security audit. -type AuditResult struct { - Tool string - Vulnerabilities int - Advisories []AuditAdvisory - Error error -} - -// AuditAdvisory represents a single security advisory. -type AuditAdvisory struct { - Package string - Severity string - Title string - URL string - Identifiers []string -} - -// RunAudit runs security audits on dependencies. -func RunAudit(ctx context.Context, opts AuditOptions) ([]AuditResult, error) { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return nil, cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - var results []AuditResult - - // Run composer audit - composerResult := runComposerAudit(ctx, opts) - results = append(results, composerResult) - - // Run npm audit if package.json exists - if getMedium().Exists(filepath.Join(opts.Dir, "package.json")) { - npmResult := runNpmAudit(ctx, opts) - results = append(results, npmResult) - } - - return results, nil -} - -func runComposerAudit(ctx context.Context, opts AuditOptions) AuditResult { - result := AuditResult{Tool: "composer"} - - args := []string{"audit", "--format=json"} - - cmd := exec.CommandContext(ctx, "composer", args...) - cmd.Dir = opts.Dir - - output, err := cmd.Output() - if err != nil { - // composer audit returns non-zero if vulnerabilities found - if exitErr, ok := err.(*exec.ExitError); ok { - output = append(output, exitErr.Stderr...) - } - } - - // Parse JSON output - var auditData struct { - Advisories map[string][]struct { - Title string `json:"title"` - Link string `json:"link"` - CVE string `json:"cve"` - AffectedRanges string `json:"affectedVersions"` - } `json:"advisories"` - } - - if jsonErr := json.Unmarshal(output, &auditData); jsonErr == nil { - for pkg, advisories := range auditData.Advisories { - for _, adv := range advisories { - result.Advisories = append(result.Advisories, AuditAdvisory{ - Package: pkg, - Title: adv.Title, - URL: adv.Link, - Identifiers: []string{adv.CVE}, - }) - } - } - result.Vulnerabilities = len(result.Advisories) - } else if err != nil { - result.Error = err - } - - return result -} - -func runNpmAudit(ctx context.Context, opts AuditOptions) AuditResult { - result := AuditResult{Tool: "npm"} - - args := []string{"audit", "--json"} - if opts.Fix { - args = []string{"audit", "fix"} - } - - cmd := exec.CommandContext(ctx, "npm", args...) - cmd.Dir = opts.Dir - - output, err := cmd.Output() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - output = append(output, exitErr.Stderr...) - } - } - - if !opts.Fix { - // Parse JSON output - var auditData struct { - Metadata struct { - Vulnerabilities struct { - Total int `json:"total"` - } `json:"vulnerabilities"` - } `json:"metadata"` - Vulnerabilities map[string]struct { - Severity string `json:"severity"` - Via []any `json:"via"` - } `json:"vulnerabilities"` - } - - if jsonErr := json.Unmarshal(output, &auditData); jsonErr == nil { - result.Vulnerabilities = auditData.Metadata.Vulnerabilities.Total - for pkg, vuln := range auditData.Vulnerabilities { - result.Advisories = append(result.Advisories, AuditAdvisory{ - Package: pkg, - Severity: vuln.Severity, - }) - } - } else if err != nil { - result.Error = err - } - } - - return result -} - -// ============================================================================= -// Rector Automated Refactoring -// ============================================================================= - -// RectorOptions configures Rector code refactoring. -type RectorOptions struct { - Dir string - Fix bool // Apply changes (default is dry-run) - Diff bool // Show detailed diff - ClearCache bool // Clear cache before running - Output goio.Writer -} - -// DetectRector checks if Rector is available in the project. -func DetectRector(dir string) bool { - m := getMedium() - - // Check for rector.php config - rectorConfig := filepath.Join(dir, "rector.php") - if m.Exists(rectorConfig) { - return true - } - - // Check for vendor binary - rectorBin := filepath.Join(dir, "vendor", "bin", "rector") - if m.Exists(rectorBin) { - return true - } - - return false -} - -// RunRector runs Rector for automated code refactoring. -func RunRector(ctx context.Context, opts RectorOptions) error { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - m := getMedium() - - // Build command - vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "rector") - cmdName := "rector" - if m.Exists(vendorBin) { - cmdName = vendorBin - } - - args := []string{"process"} - - if !opts.Fix { - args = append(args, "--dry-run") - } - - if opts.Diff { - args = append(args, "--output-format", "diff") - } - - if opts.ClearCache { - args = append(args, "--clear-cache") - } - - cmd := exec.CommandContext(ctx, cmdName, args...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - return cmd.Run() -} - -// ============================================================================= -// Infection Mutation Testing -// ============================================================================= - -// InfectionOptions configures Infection mutation testing. -type InfectionOptions struct { - Dir string - MinMSI int // Minimum mutation score indicator (0-100) - MinCoveredMSI int // Minimum covered mutation score (0-100) - Threads int // Number of parallel threads - Filter string // Filter files by pattern - OnlyCovered bool // Only mutate covered code - Output goio.Writer -} - -// DetectInfection checks if Infection is available in the project. -func DetectInfection(dir string) bool { - m := getMedium() - - // Check for infection config files - configs := []string{"infection.json", "infection.json5", "infection.json.dist"} - for _, config := range configs { - if m.Exists(filepath.Join(dir, config)) { - return true - } - } - - // Check for vendor binary - infectionBin := filepath.Join(dir, "vendor", "bin", "infection") - if m.Exists(infectionBin) { - return true - } - - return false -} - -// RunInfection runs Infection mutation testing. -func RunInfection(ctx context.Context, opts InfectionOptions) error { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - if opts.Output == nil { - opts.Output = os.Stdout - } - - m := getMedium() - - // Build command - vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "infection") - cmdName := "infection" - if m.Exists(vendorBin) { - cmdName = vendorBin - } - - var args []string - - // Set defaults - minMSI := opts.MinMSI - if minMSI == 0 { - minMSI = 50 - } - minCoveredMSI := opts.MinCoveredMSI - if minCoveredMSI == 0 { - minCoveredMSI = 70 - } - threads := opts.Threads - if threads == 0 { - threads = 4 - } - - args = append(args, cli.Sprintf("--min-msi=%d", minMSI)) - args = append(args, cli.Sprintf("--min-covered-msi=%d", minCoveredMSI)) - args = append(args, cli.Sprintf("--threads=%d", threads)) - - if opts.Filter != "" { - args = append(args, "--filter="+opts.Filter) - } - - if opts.OnlyCovered { - args = append(args, "--only-covered") - } - - cmd := exec.CommandContext(ctx, cmdName, args...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Output - cmd.Stderr = opts.Output - - return cmd.Run() -} - -// ============================================================================= -// QA Pipeline -// ============================================================================= - -// QAOptions configures the full QA pipeline. -type QAOptions struct { - Dir string - Quick bool // Only run quick checks - Full bool // Run all stages including slow checks - Fix bool // Auto-fix issues where possible - JSON bool // Output results as JSON -} - -// QAStage represents a stage in the QA pipeline. -type QAStage string - -// QA pipeline stage constants. -const ( - // QAStageQuick runs fast checks only (audit, fmt, stan). - QAStageQuick QAStage = "quick" - // QAStageStandard runs standard checks including tests. - QAStageStandard QAStage = "standard" - // QAStageFull runs all checks including slow security scans. - QAStageFull QAStage = "full" -) - -// QACheckResult holds the result of a single QA check. -type QACheckResult struct { - Name string - Stage QAStage - Passed bool - Duration string - Error error - Output string -} - -// QAResult holds the results of the full QA pipeline. -type QAResult struct { - Stages []QAStage - Checks []QACheckResult - Passed bool - Summary string -} - -// GetQAStages returns the stages to run based on options. -func GetQAStages(opts QAOptions) []QAStage { - if opts.Quick { - return []QAStage{QAStageQuick} - } - if opts.Full { - return []QAStage{QAStageQuick, QAStageStandard, QAStageFull} - } - // Default: quick + standard - return []QAStage{QAStageQuick, QAStageStandard} -} - -// GetQAChecks returns the checks for a given stage. -func GetQAChecks(dir string, stage QAStage) []string { - switch stage { - case QAStageQuick: - checks := []string{"audit", "fmt", "stan"} - return checks - case QAStageStandard: - checks := []string{} - if _, found := DetectPsalm(dir); found { - checks = append(checks, "psalm") - } - checks = append(checks, "test") - return checks - case QAStageFull: - checks := []string{} - if DetectRector(dir) { - checks = append(checks, "rector") - } - if DetectInfection(dir) { - checks = append(checks, "infection") - } - return checks - } - return nil -} - -// ============================================================================= -// Security Checks -// ============================================================================= - -// SecurityOptions configures security scanning. -type SecurityOptions struct { - Dir string - Severity string // Minimum severity (critical, high, medium, low) - JSON bool // Output in JSON format - SARIF bool // Output in SARIF format - URL string // URL to check HTTP headers (optional) - Output goio.Writer -} - -// SecurityResult holds the results of security scanning. -type SecurityResult struct { - Checks []SecurityCheck - Summary SecuritySummary -} - -// SecurityCheck represents a single security check result. -type SecurityCheck struct { - ID string - Name string - Description string - Severity string - Passed bool - Message string - Fix string - CWE string -} - -// SecuritySummary summarizes security check results. -type SecuritySummary struct { - Total int - Passed int - Critical int - High int - Medium int - Low int -} - -// RunSecurityChecks runs security checks on the project. -func RunSecurityChecks(ctx context.Context, opts SecurityOptions) (*SecurityResult, error) { - if opts.Dir == "" { - cwd, err := os.Getwd() - if err != nil { - return nil, cli.WrapVerb(err, "get", "working directory") - } - opts.Dir = cwd - } - - result := &SecurityResult{} - - // Run composer audit - auditResults, _ := RunAudit(ctx, AuditOptions{Dir: opts.Dir}) - for _, audit := range auditResults { - check := SecurityCheck{ - ID: audit.Tool + "_audit", - Name: i18n.Title(audit.Tool) + " Security Audit", - Description: "Check " + audit.Tool + " dependencies for vulnerabilities", - Severity: "critical", - Passed: audit.Vulnerabilities == 0 && audit.Error == nil, - CWE: "CWE-1395", - } - if !check.Passed { - check.Message = cli.Sprintf("Found %d vulnerabilities", audit.Vulnerabilities) - } - result.Checks = append(result.Checks, check) - } - - // Check .env file for security issues - envChecks := runEnvSecurityChecks(opts.Dir) - result.Checks = append(result.Checks, envChecks...) - - // Check filesystem security - fsChecks := runFilesystemSecurityChecks(opts.Dir) - result.Checks = append(result.Checks, fsChecks...) - - // Calculate summary - for _, check := range result.Checks { - result.Summary.Total++ - if check.Passed { - result.Summary.Passed++ - } else { - switch check.Severity { - case "critical": - result.Summary.Critical++ - case "high": - result.Summary.High++ - case "medium": - result.Summary.Medium++ - case "low": - result.Summary.Low++ - } - } - } - - return result, nil -} - -func runEnvSecurityChecks(dir string) []SecurityCheck { - var checks []SecurityCheck - - m := getMedium() - envPath := filepath.Join(dir, ".env") - envContent, err := m.Read(envPath) - if err != nil { - return checks - } - - envLines := strings.Split(envContent, "\n") - envMap := make(map[string]string) - for _, line := range envLines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - envMap[parts[0]] = parts[1] - } - } - - // Check APP_DEBUG - if debug, ok := envMap["APP_DEBUG"]; ok { - check := SecurityCheck{ - ID: "debug_mode", - Name: "Debug Mode Disabled", - Description: "APP_DEBUG should be false in production", - Severity: "critical", - Passed: strings.ToLower(debug) != "true", - CWE: "CWE-215", - } - if !check.Passed { - check.Message = "Debug mode exposes sensitive information" - check.Fix = "Set APP_DEBUG=false in .env" - } - checks = append(checks, check) - } - - // Check APP_KEY - if key, ok := envMap["APP_KEY"]; ok { - check := SecurityCheck{ - ID: "app_key_set", - Name: "Application Key Set", - Description: "APP_KEY must be set and valid", - Severity: "critical", - Passed: len(key) >= 32, - CWE: "CWE-321", - } - if !check.Passed { - check.Message = "Missing or weak encryption key" - check.Fix = "Run: php artisan key:generate" - } - checks = append(checks, check) - } - - // Check APP_URL for HTTPS - if url, ok := envMap["APP_URL"]; ok { - check := SecurityCheck{ - ID: "https_enforced", - Name: "HTTPS Enforced", - Description: "APP_URL should use HTTPS in production", - Severity: "high", - Passed: strings.HasPrefix(url, "https://"), - CWE: "CWE-319", - } - if !check.Passed { - check.Message = "Application not using HTTPS" - check.Fix = "Update APP_URL to use https://" - } - checks = append(checks, check) - } - - return checks -} - -func runFilesystemSecurityChecks(dir string) []SecurityCheck { - var checks []SecurityCheck - m := getMedium() - - // Check .env not in public - publicEnvPaths := []string{"public/.env", "public_html/.env"} - for _, path := range publicEnvPaths { - fullPath := filepath.Join(dir, path) - if m.Exists(fullPath) { - checks = append(checks, SecurityCheck{ - ID: "env_not_public", - Name: ".env Not Publicly Accessible", - Description: ".env file should not be in public directory", - Severity: "critical", - Passed: false, - Message: "Environment file exposed to web at " + path, - CWE: "CWE-538", - }) - } - } - - // Check .git not in public - publicGitPaths := []string{"public/.git", "public_html/.git"} - for _, path := range publicGitPaths { - fullPath := filepath.Join(dir, path) - if m.Exists(fullPath) { - checks = append(checks, SecurityCheck{ - ID: "git_not_public", - Name: ".git Not Publicly Accessible", - Description: ".git directory should not be in public", - Severity: "critical", - Passed: false, - Message: "Git repository exposed to web (source code leak)", - CWE: "CWE-538", - }) - } - } - - return checks -} diff --git a/internal/cmd/php/quality_extended_test.go b/internal/cmd/php/quality_extended_test.go deleted file mode 100644 index 8c1c00e3..00000000 --- a/internal/cmd/php/quality_extended_test.go +++ /dev/null @@ -1,304 +0,0 @@ -package php - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestFormatOptions_Struct(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := FormatOptions{ - Dir: "/project", - Fix: true, - Diff: true, - Paths: []string{"app", "tests"}, - Output: os.Stdout, - } - - assert.Equal(t, "/project", opts.Dir) - assert.True(t, opts.Fix) - assert.True(t, opts.Diff) - assert.Equal(t, []string{"app", "tests"}, opts.Paths) - assert.NotNil(t, opts.Output) - }) -} - -func TestAnalyseOptions_Struct(t *testing.T) { - t.Run("all fields accessible", func(t *testing.T) { - opts := AnalyseOptions{ - Dir: "/project", - Level: 5, - Paths: []string{"src"}, - Memory: "2G", - Output: os.Stdout, - } - - assert.Equal(t, "/project", opts.Dir) - assert.Equal(t, 5, opts.Level) - assert.Equal(t, []string{"src"}, opts.Paths) - assert.Equal(t, "2G", opts.Memory) - assert.NotNil(t, opts.Output) - }) -} - -func TestFormatterType_Constants(t *testing.T) { - t.Run("constants are defined", func(t *testing.T) { - assert.Equal(t, FormatterType("pint"), FormatterPint) - }) -} - -func TestAnalyserType_Constants(t *testing.T) { - t.Run("constants are defined", func(t *testing.T) { - assert.Equal(t, AnalyserType("phpstan"), AnalyserPHPStan) - assert.Equal(t, AnalyserType("larastan"), AnalyserLarastan) - }) -} - -func TestDetectFormatter_Extended(t *testing.T) { - t.Run("returns not found for empty directory", func(t *testing.T) { - dir := t.TempDir() - _, found := DetectFormatter(dir) - assert.False(t, found) - }) - - t.Run("prefers pint.json over vendor binary", func(t *testing.T) { - dir := t.TempDir() - - // Create pint.json - err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644) - require.NoError(t, err) - - formatter, found := DetectFormatter(dir) - assert.True(t, found) - assert.Equal(t, FormatterPint, formatter) - }) -} - -func TestDetectAnalyser_Extended(t *testing.T) { - t.Run("returns not found for empty directory", func(t *testing.T) { - dir := t.TempDir() - _, found := DetectAnalyser(dir) - assert.False(t, found) - }) - - t.Run("detects phpstan from vendor binary alone", func(t *testing.T) { - dir := t.TempDir() - - // Create vendor binary - binDir := filepath.Join(dir, "vendor", "bin") - err := os.MkdirAll(binDir, 0755) - require.NoError(t, err) - - err = os.WriteFile(filepath.Join(binDir, "phpstan"), []byte(""), 0755) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserPHPStan, analyser) - }) - - t.Run("detects larastan from larastan/larastan vendor path", func(t *testing.T) { - dir := t.TempDir() - - // Create phpstan.neon - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - // Create larastan/larastan path - larastanPath := filepath.Join(dir, "vendor", "larastan", "larastan") - err = os.MkdirAll(larastanPath, 0755) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserLarastan, analyser) - }) - - t.Run("detects larastan from nunomaduro/larastan vendor path", func(t *testing.T) { - dir := t.TempDir() - - // Create phpstan.neon - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - // Create nunomaduro/larastan path - larastanPath := filepath.Join(dir, "vendor", "nunomaduro", "larastan") - err = os.MkdirAll(larastanPath, 0755) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserLarastan, analyser) - }) -} - -func TestBuildPintCommand_Extended(t *testing.T) { - t.Run("uses global pint when no vendor binary", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir} - - cmd, _ := buildPintCommand(opts) - assert.Equal(t, "pint", cmd) - }) - - t.Run("adds test flag when Fix is false", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Fix: false} - - _, args := buildPintCommand(opts) - assert.Contains(t, args, "--test") - }) - - t.Run("does not add test flag when Fix is true", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Fix: true} - - _, args := buildPintCommand(opts) - assert.NotContains(t, args, "--test") - }) - - t.Run("adds diff flag", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Diff: true} - - _, args := buildPintCommand(opts) - assert.Contains(t, args, "--diff") - }) - - t.Run("adds paths", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Paths: []string{"app", "tests"}} - - _, args := buildPintCommand(opts) - assert.Contains(t, args, "app") - assert.Contains(t, args, "tests") - }) -} - -func TestBuildPHPStanCommand_Extended(t *testing.T) { - t.Run("uses global phpstan when no vendor binary", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir} - - cmd, _ := buildPHPStanCommand(opts) - assert.Equal(t, "phpstan", cmd) - }) - - t.Run("adds level flag", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Level: 8} - - _, args := buildPHPStanCommand(opts) - assert.Contains(t, args, "--level") - assert.Contains(t, args, "8") - }) - - t.Run("does not add level flag when zero", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Level: 0} - - _, args := buildPHPStanCommand(opts) - assert.NotContains(t, args, "--level") - }) - - t.Run("adds memory limit", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Memory: "4G"} - - _, args := buildPHPStanCommand(opts) - assert.Contains(t, args, "--memory-limit") - assert.Contains(t, args, "4G") - }) - - t.Run("does not add memory flag when empty", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Memory: ""} - - _, args := buildPHPStanCommand(opts) - assert.NotContains(t, args, "--memory-limit") - }) - - t.Run("adds paths", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Paths: []string{"src", "app"}} - - _, args := buildPHPStanCommand(opts) - assert.Contains(t, args, "src") - assert.Contains(t, args, "app") - }) -} - -func TestFormat_Bad(t *testing.T) { - t.Run("fails when no formatter found", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir} - - err := Format(context.TODO(), opts) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no formatter found") - }) - - t.Run("uses cwd when dir not specified", func(t *testing.T) { - // When no formatter found in cwd, should still fail with "no formatter found" - opts := FormatOptions{Dir: ""} - - err := Format(context.TODO(), opts) - // May or may not find a formatter depending on cwd, but function should not panic - if err != nil { - // Expected - no formatter in cwd - assert.Contains(t, err.Error(), "no formatter") - } - }) - - t.Run("uses stdout when output not specified", func(t *testing.T) { - dir := t.TempDir() - // Create pint.json to enable formatter detection - err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644) - require.NoError(t, err) - - opts := FormatOptions{Dir: dir, Output: nil} - - // Will fail because pint isn't actually installed, but tests the code path - err = Format(context.Background(), opts) - assert.Error(t, err) // Pint not installed - }) -} - -func TestAnalyse_Bad(t *testing.T) { - t.Run("fails when no analyser found", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir} - - err := Analyse(context.TODO(), opts) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no static analyser found") - }) - - t.Run("uses cwd when dir not specified", func(t *testing.T) { - opts := AnalyseOptions{Dir: ""} - - err := Analyse(context.TODO(), opts) - // May or may not find an analyser depending on cwd - if err != nil { - assert.Contains(t, err.Error(), "no static analyser") - } - }) - - t.Run("uses stdout when output not specified", func(t *testing.T) { - dir := t.TempDir() - // Create phpstan.neon to enable analyser detection - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - opts := AnalyseOptions{Dir: dir, Output: nil} - - // Will fail because phpstan isn't actually installed, but tests the code path - err = Analyse(context.Background(), opts) - assert.Error(t, err) // PHPStan not installed - }) -} diff --git a/internal/cmd/php/quality_test.go b/internal/cmd/php/quality_test.go deleted file mode 100644 index 710e3fad..00000000 --- a/internal/cmd/php/quality_test.go +++ /dev/null @@ -1,517 +0,0 @@ -package php - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDetectFormatter_Good(t *testing.T) { - t.Run("detects pint.json", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644) - require.NoError(t, err) - - formatter, found := DetectFormatter(dir) - assert.True(t, found) - assert.Equal(t, FormatterPint, formatter) - }) - - t.Run("detects vendor binary", func(t *testing.T) { - dir := t.TempDir() - binDir := filepath.Join(dir, "vendor", "bin") - err := os.MkdirAll(binDir, 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(binDir, "pint"), []byte(""), 0755) - require.NoError(t, err) - - formatter, found := DetectFormatter(dir) - assert.True(t, found) - assert.Equal(t, FormatterPint, formatter) - }) -} - -func TestDetectFormatter_Bad(t *testing.T) { - t.Run("no formatter", func(t *testing.T) { - dir := t.TempDir() - _, found := DetectFormatter(dir) - assert.False(t, found) - }) -} - -func TestDetectAnalyser_Good(t *testing.T) { - t.Run("detects phpstan.neon", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserPHPStan, analyser) - }) - - t.Run("detects phpstan.neon.dist", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "phpstan.neon.dist"), []byte(""), 0644) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserPHPStan, analyser) - }) - - t.Run("detects larastan", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - larastanDir := filepath.Join(dir, "vendor", "larastan", "larastan") - err = os.MkdirAll(larastanDir, 0755) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserLarastan, analyser) - }) - - t.Run("detects nunomaduro/larastan", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644) - require.NoError(t, err) - - larastanDir := filepath.Join(dir, "vendor", "nunomaduro", "larastan") - err = os.MkdirAll(larastanDir, 0755) - require.NoError(t, err) - - analyser, found := DetectAnalyser(dir) - assert.True(t, found) - assert.Equal(t, AnalyserLarastan, analyser) - }) -} - -func TestBuildPintCommand_Good(t *testing.T) { - t.Run("basic command", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir} - cmd, args := buildPintCommand(opts) - assert.Equal(t, "pint", cmd) - assert.Contains(t, args, "--test") - }) - - t.Run("fix enabled", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Fix: true} - _, args := buildPintCommand(opts) - assert.NotContains(t, args, "--test") - }) - - t.Run("diff enabled", func(t *testing.T) { - dir := t.TempDir() - opts := FormatOptions{Dir: dir, Diff: true} - _, args := buildPintCommand(opts) - assert.Contains(t, args, "--diff") - }) - - t.Run("with specific paths", func(t *testing.T) { - dir := t.TempDir() - paths := []string{"app", "tests"} - opts := FormatOptions{Dir: dir, Paths: paths} - _, args := buildPintCommand(opts) - assert.Equal(t, paths, args[len(args)-2:]) - }) - - t.Run("uses vendor binary if exists", func(t *testing.T) { - dir := t.TempDir() - binDir := filepath.Join(dir, "vendor", "bin") - err := os.MkdirAll(binDir, 0755) - require.NoError(t, err) - pintPath := filepath.Join(binDir, "pint") - err = os.WriteFile(pintPath, []byte(""), 0755) - require.NoError(t, err) - - opts := FormatOptions{Dir: dir} - cmd, _ := buildPintCommand(opts) - assert.Equal(t, pintPath, cmd) - }) -} - -func TestBuildPHPStanCommand_Good(t *testing.T) { - t.Run("basic command", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir} - cmd, args := buildPHPStanCommand(opts) - assert.Equal(t, "phpstan", cmd) - assert.Equal(t, []string{"analyse"}, args) - }) - - t.Run("with level", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Level: 5} - _, args := buildPHPStanCommand(opts) - assert.Contains(t, args, "--level") - assert.Contains(t, args, "5") - }) - - t.Run("with memory limit", func(t *testing.T) { - dir := t.TempDir() - opts := AnalyseOptions{Dir: dir, Memory: "2G"} - _, args := buildPHPStanCommand(opts) - assert.Contains(t, args, "--memory-limit") - assert.Contains(t, args, "2G") - }) - - t.Run("uses vendor binary if exists", func(t *testing.T) { - dir := t.TempDir() - binDir := filepath.Join(dir, "vendor", "bin") - err := os.MkdirAll(binDir, 0755) - require.NoError(t, err) - phpstanPath := filepath.Join(binDir, "phpstan") - err = os.WriteFile(phpstanPath, []byte(""), 0755) - require.NoError(t, err) - - opts := AnalyseOptions{Dir: dir} - cmd, _ := buildPHPStanCommand(opts) - assert.Equal(t, phpstanPath, cmd) - }) -} - -// ============================================================================= -// Psalm Detection Tests -// ============================================================================= - -func TestDetectPsalm_Good(t *testing.T) { - t.Run("detects psalm.xml", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "psalm.xml"), []byte(""), 0644) - require.NoError(t, err) - - // Also need vendor binary for it to return true - binDir := filepath.Join(dir, "vendor", "bin") - err = os.MkdirAll(binDir, 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755) - require.NoError(t, err) - - psalmType, found := DetectPsalm(dir) - assert.True(t, found) - assert.Equal(t, PsalmStandard, psalmType) - }) - - t.Run("detects psalm.xml.dist", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "psalm.xml.dist"), []byte(""), 0644) - require.NoError(t, err) - - binDir := filepath.Join(dir, "vendor", "bin") - err = os.MkdirAll(binDir, 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755) - require.NoError(t, err) - - _, found := DetectPsalm(dir) - assert.True(t, found) - }) - - t.Run("detects vendor binary only", func(t *testing.T) { - dir := t.TempDir() - binDir := filepath.Join(dir, "vendor", "bin") - err := os.MkdirAll(binDir, 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755) - require.NoError(t, err) - - _, found := DetectPsalm(dir) - assert.True(t, found) - }) -} - -func TestDetectPsalm_Bad(t *testing.T) { - t.Run("no psalm", func(t *testing.T) { - dir := t.TempDir() - _, found := DetectPsalm(dir) - assert.False(t, found) - }) -} - -// ============================================================================= -// Rector Detection Tests -// ============================================================================= - -func TestDetectRector_Good(t *testing.T) { - t.Run("detects rector.php", func(t *testing.T) { - dir := t.TempDir() - err := os.WriteFile(filepath.Join(dir, "rector.php"), []byte("") - fmt.Fprintln(os.Stderr, "Example: i18n-validate ./...") - os.Exit(2) - } - - // Find the project root (where locales are) - root, err := findProjectRoot() - if err != nil { - fmt.Fprintf(os.Stderr, "Error finding project root: %v\n", err) - os.Exit(2) - } - - // Load valid keys from locale files - validKeys, err := loadValidKeys(filepath.Join(root, "pkg/i18n/locales")) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading locale files: %v\n", err) - os.Exit(2) - } - - // Load valid intents - validIntents := loadValidIntents() - - // Scan source files - usages, err := scanPackages(os.Args[1:]) - if err != nil { - fmt.Fprintf(os.Stderr, "Error scanning packages: %v\n", err) - os.Exit(2) - } - - // Validate - result := validate(usages, validKeys, validIntents) - - // Report - printReport(result) - - if len(result.MissingKeys) > 0 { - os.Exit(1) - } -} - -// findProjectRoot finds the project root by looking for go.mod. -func findProjectRoot() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", err - } - - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - return dir, nil - } - parent := filepath.Dir(dir) - if parent == dir { - return "", fmt.Errorf("could not find go.mod in any parent directory") - } - dir = parent - } -} - -// loadValidKeys loads all valid keys from locale JSON files. -func loadValidKeys(localesDir string) (map[string]bool, error) { - keys := make(map[string]bool) - - entries, err := os.ReadDir(localesDir) - if err != nil { - return nil, fmt.Errorf("reading locales dir: %w", err) - } - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { - continue - } - - data, err := os.ReadFile(filepath.Join(localesDir, entry.Name())) - if err != nil { - return nil, fmt.Errorf("reading %s: %w", entry.Name(), err) - } - - var raw map[string]any - if err := json.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("parsing %s: %w", entry.Name(), err) - } - - extractKeys("", raw, keys) - } - - return keys, nil -} - -// extractKeys recursively extracts flattened keys from nested JSON. -func extractKeys(prefix string, data map[string]any, out map[string]bool) { - for key, value := range data { - fullKey := key - if prefix != "" { - fullKey = prefix + "." + key - } - - switch v := value.(type) { - case string: - out[fullKey] = true - case map[string]any: - // Check if it's a plural/verb/noun object (has specific keys) - if isPluralOrGrammarObject(v) { - out[fullKey] = true - } else { - extractKeys(fullKey, v, out) - } - } - } -} - -// isPluralOrGrammarObject checks if a map is a leaf object (plural forms, verb forms, etc). -func isPluralOrGrammarObject(m map[string]any) bool { - // CLDR plural keys - _, hasOne := m["one"] - _, hasOther := m["other"] - _, hasZero := m["zero"] - _, hasTwo := m["two"] - _, hasFew := m["few"] - _, hasMany := m["many"] - - // Grammar keys - _, hasPast := m["past"] - _, hasGerund := m["gerund"] - _, hasGender := m["gender"] - _, hasBase := m["base"] - - // Article keys - _, hasDefault := m["default"] - _, hasVowel := m["vowel"] - - if hasOne || hasOther || hasZero || hasTwo || hasFew || hasMany { - return true - } - if hasPast || hasGerund || hasGender || hasBase { - return true - } - if hasDefault || hasVowel { - return true - } - - return false -} - -// loadValidIntents returns the set of valid intent keys. -func loadValidIntents() map[string]bool { - // Core intents - these match what's defined in intents.go - return map[string]bool{ - // Destructive - "core.delete": true, - "core.remove": true, - "core.discard": true, - "core.reset": true, - "core.overwrite": true, - // Creation - "core.create": true, - "core.add": true, - "core.clone": true, - "core.copy": true, - // Modification - "core.save": true, - "core.update": true, - "core.rename": true, - "core.move": true, - // Git - "core.commit": true, - "core.push": true, - "core.pull": true, - "core.merge": true, - "core.rebase": true, - // Network - "core.install": true, - "core.download": true, - "core.upload": true, - "core.publish": true, - "core.deploy": true, - // Process - "core.start": true, - "core.stop": true, - "core.restart": true, - "core.run": true, - "core.build": true, - "core.test": true, - // Information - "core.continue": true, - "core.proceed": true, - "core.confirm": true, - // Additional - "core.sync": true, - "core.boot": true, - "core.format": true, - "core.analyse": true, - "core.link": true, - "core.unlink": true, - "core.fetch": true, - "core.generate": true, - "core.validate": true, - "core.check": true, - "core.scan": true, - } -} - -// scanPackages scans Go packages for i18n key usage. -func scanPackages(patterns []string) ([]KeyUsage, error) { - var usages []KeyUsage - - for _, pattern := range patterns { - // Expand pattern - matches, err := expandPattern(pattern) - if err != nil { - return nil, fmt.Errorf("expanding pattern %q: %w", pattern, err) - } - - for _, dir := range matches { - dirUsages, err := scanDirectory(dir) - if err != nil { - return nil, fmt.Errorf("scanning %s: %w", dir, err) - } - usages = append(usages, dirUsages...) - } - } - - return usages, nil -} - -// expandPattern expands a Go package pattern to directories. -func expandPattern(pattern string) ([]string, error) { - // Handle ./... or ... pattern - if strings.HasSuffix(pattern, "...") { - base := strings.TrimSuffix(pattern, "...") - base = strings.TrimSuffix(base, "/") - if base == "" || base == "." { - base = "." - } - return findAllGoDirs(base) - } - - // Single directory - return []string{pattern}, nil -} - -// findAllGoDirs finds all directories containing .go files. -func findAllGoDirs(root string) ([]string, error) { - var dirs []string - seen := make(map[string]bool) - - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil // Continue walking even on error - } - - if info == nil { - return nil - } - - // Skip vendor, testdata, and hidden directories (but not . itself) - if info.IsDir() { - name := info.Name() - if name == "vendor" || name == "testdata" || (strings.HasPrefix(name, ".") && name != ".") { - return filepath.SkipDir - } - return nil - } - - // Check for .go files - if strings.HasSuffix(path, ".go") { - dir := filepath.Dir(path) - if !seen[dir] { - seen[dir] = true - dirs = append(dirs, dir) - } - } - - return nil - }) - - return dirs, err -} - -// scanDirectory scans a directory for i18n key usage. -func scanDirectory(dir string) ([]KeyUsage, error) { - var usages []KeyUsage - - fset := token.NewFileSet() - // Parse all .go files except those ending exactly in _test.go - pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool { - name := fi.Name() - // Only exclude files that are actual test files (ending in _test.go) - // Files like "go_test_cmd.go" should be included - return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") - }, 0) - if err != nil { - return nil, err - } - - for _, pkg := range pkgs { - for filename, file := range pkg.Files { - fileUsages := scanFile(fset, filename, file) - usages = append(usages, fileUsages...) - } - } - - return usages, nil -} - -// scanFile scans a single file for i18n key usage. -func scanFile(fset *token.FileSet, filename string, file *ast.File) []KeyUsage { - var usages []KeyUsage - - ast.Inspect(file, func(n ast.Node) bool { - call, ok := n.(*ast.CallExpr) - if !ok { - return true - } - - funcName := getFuncName(call) - if funcName == "" { - return true - } - - // Check for T(), C(), i18n.T(), i18n.C() - switch funcName { - case "T", "i18n.T", "_", "i18n._": - if key := extractStringArg(call, 0); key != "" { - pos := fset.Position(call.Pos()) - usages = append(usages, KeyUsage{ - Key: key, - File: filename, - Line: pos.Line, - Function: "T", - }) - } - case "C", "i18n.C": - if key := extractStringArg(call, 0); key != "" { - pos := fset.Position(call.Pos()) - usages = append(usages, KeyUsage{ - Key: key, - File: filename, - Line: pos.Line, - Function: "C", - }) - } - case "I", "i18n.I": - if key := extractStringArg(call, 0); key != "" { - pos := fset.Position(call.Pos()) - usages = append(usages, KeyUsage{ - Key: key, - File: filename, - Line: pos.Line, - Function: "C", // I() is an intent builder - }) - } - } - - return true - }) - - return usages -} - -// getFuncName extracts the function name from a call expression. -func getFuncName(call *ast.CallExpr) string { - switch fn := call.Fun.(type) { - case *ast.Ident: - return fn.Name - case *ast.SelectorExpr: - if ident, ok := fn.X.(*ast.Ident); ok { - return ident.Name + "." + fn.Sel.Name - } - } - return "" -} - -// extractStringArg extracts a string literal from a call argument. -func extractStringArg(call *ast.CallExpr, index int) string { - if index >= len(call.Args) { - return "" - } - - arg := call.Args[index] - - // Direct string literal - if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING { - // Remove quotes - s := lit.Value - if len(s) >= 2 { - return s[1 : len(s)-1] - } - } - - // Identifier (constant reference) - we skip these as they're type-safe - if _, ok := arg.(*ast.Ident); ok { - return "" // Skip constants like IntentCoreDelete - } - - // Selector (like i18n.IntentCoreDelete) - skip these too - if _, ok := arg.(*ast.SelectorExpr); ok { - return "" - } - - return "" -} - -// validate validates key usages against valid keys and intents. -func validate(usages []KeyUsage, validKeys, validIntents map[string]bool) ValidationResult { - result := ValidationResult{ - TotalKeys: len(usages), - } - - for _, usage := range usages { - if usage.Function == "C" { - result.IntentKeys++ - // Check intent keys - if validIntents[usage.Key] { - result.ValidKeys++ - } else { - // Also allow custom intents (non-core.* prefix) - if !strings.HasPrefix(usage.Key, "core.") { - result.ValidKeys++ // Assume custom intents are valid - } else { - result.MissingKeys = append(result.MissingKeys, usage) - } - } - } else { - result.MessageKeys++ - // Check message keys - if validKeys[usage.Key] { - result.ValidKeys++ - } else if strings.HasPrefix(usage.Key, "core.") { - // core.* keys used with T() are intent keys - if validIntents[usage.Key] { - result.ValidKeys++ - } else { - result.MissingKeys = append(result.MissingKeys, usage) - } - } else { - result.MissingKeys = append(result.MissingKeys, usage) - } - } - } - - return result -} - -// printReport prints the validation report. -func printReport(result ValidationResult) { - fmt.Printf("i18n Validation Report\n") - fmt.Printf("======================\n\n") - fmt.Printf("Total keys scanned: %d\n", result.TotalKeys) - fmt.Printf(" Message keys (T): %d\n", result.MessageKeys) - fmt.Printf(" Intent keys (C): %d\n", result.IntentKeys) - fmt.Printf("Valid keys: %d\n", result.ValidKeys) - fmt.Printf("Missing keys: %d\n", len(result.MissingKeys)) - - if len(result.MissingKeys) > 0 { - fmt.Printf("\nMissing Keys:\n") - fmt.Printf("-------------\n") - - // Sort by file then line - sort.Slice(result.MissingKeys, func(i, j int) bool { - if result.MissingKeys[i].File != result.MissingKeys[j].File { - return result.MissingKeys[i].File < result.MissingKeys[j].File - } - return result.MissingKeys[i].Line < result.MissingKeys[j].Line - }) - - for _, usage := range result.MissingKeys { - fmt.Printf(" %s:%d: %s(%q)\n", usage.File, usage.Line, usage.Function, usage.Key) - } - - fmt.Printf("\nAdd these keys to pkg/i18n/locales/en_GB.json or use constants from pkg/i18n/keys.go\n") - } else { - fmt.Printf("\nAll keys are valid!\n") - } -} diff --git a/internal/variants/ci.go b/internal/variants/ci.go deleted file mode 100644 index 347b832b..00000000 --- a/internal/variants/ci.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build ci - -// ci.go imports packages for the minimal CI/release binary. -// -// Build with: go build -tags ci -// -// This variant includes only commands needed for CI pipelines: -// - build: Cross-platform compilation -// - ci: Release publishing -// - sdk: API compatibility checks -// - doctor: Environment verification -// -// Use this build to reduce binary size and attack surface in production. - -package variants - -import ( - // Commands via self-registration - _ "forge.lthn.ai/core/cli/internal/cmd/ci" - _ "forge.lthn.ai/core/cli/internal/cmd/doctor" - _ "forge.lthn.ai/core/cli/internal/cmd/sdk" - _ "forge.lthn.ai/core/go/pkg/build/buildcmd" -) diff --git a/internal/variants/core_ide.go b/internal/variants/core_ide.go deleted file mode 100644 index 5e6a94da..00000000 --- a/internal/variants/core_ide.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build ide - -// core_ide.go imports packages for the Core IDE desktop application. -// -// Build with: go build -tags ide -// -// This is the Wails v3 GUI variant featuring: -// - System tray with quick actions -// - Tray panel for status/notifications -// - Angular frontend -// - All CLI commands available via IPC - -package variants - -import ( - // CLI commands available via IPC (IDE GUI is now in core/ide repo) - _ "forge.lthn.ai/core/cli/internal/cmd/ai" - _ "forge.lthn.ai/core/cli/internal/cmd/deploy" - _ "forge.lthn.ai/core/cli/internal/cmd/dev" - _ "forge.lthn.ai/core/cli/internal/cmd/php" - _ "forge.lthn.ai/core/cli/internal/cmd/rag" -) diff --git a/internal/variants/full.go b/internal/variants/full.go deleted file mode 100644 index b6af5a89..00000000 --- a/internal/variants/full.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build !ci && !php && !minimal - -// full.go imports all packages for the full development binary. -// -// Build with: go build (default) -// -// This is the default build variant with all development tools: -// - dev: Multi-repo git workflows (commit, push, pull, sync) -// - ai: AI agent task management + RAG + metrics -// - go: Go module and build tools -// - php: Laravel/Composer development tools -// - build: Cross-platform compilation -// - ci: Release publishing -// - sdk: API compatibility checks -// - pkg: Package management -// - vm: LinuxKit VM management -// - docs: Documentation generation -// - setup: Repository cloning and setup -// - doctor: Environment health checks -// - test: Test runner with coverage -// - qa: Quality assurance workflows -// - monitor: Security monitoring aggregation -// - forge: Forgejo instance management -// - prod: Production infrastructure -// - mcp: MCP server management -// - daemon: Background service daemon -// - session: Session management - -package variants - -import ( - // Commands via self-registration - _ "forge.lthn.ai/core/cli/internal/cmd/ai" - _ "forge.lthn.ai/core/cli/internal/cmd/ci" - _ "forge.lthn.ai/core/cli/internal/cmd/collect" - _ "forge.lthn.ai/core/cli/internal/cmd/config" - _ "forge.lthn.ai/core/cli/internal/cmd/crypt" - _ "forge.lthn.ai/core/cli/internal/cmd/daemon" - _ "forge.lthn.ai/core/cli/internal/cmd/deploy" - _ "forge.lthn.ai/core/cli/internal/cmd/dev" - _ "forge.lthn.ai/core/cli/internal/cmd/docs" - _ "forge.lthn.ai/core/cli/internal/cmd/doctor" - _ "forge.lthn.ai/core/cli/internal/cmd/forge" - _ "forge.lthn.ai/core/cli/internal/cmd/gitcmd" - _ "forge.lthn.ai/core/cli/internal/cmd/go" - _ "forge.lthn.ai/core/cli/internal/cmd/help" - _ "forge.lthn.ai/core/cli/internal/cmd/lab" - _ "forge.lthn.ai/core/cli/internal/cmd/mcpcmd" - _ "forge.lthn.ai/core/cli/internal/cmd/ml" - _ "forge.lthn.ai/core/cli/internal/cmd/monitor" - _ "forge.lthn.ai/core/cli/internal/cmd/php" - _ "forge.lthn.ai/core/cli/internal/cmd/pkgcmd" - _ "forge.lthn.ai/core/cli/internal/cmd/plugin" - _ "forge.lthn.ai/core/cli/internal/cmd/prod" - _ "forge.lthn.ai/core/cli/internal/cmd/qa" - _ "forge.lthn.ai/core/cli/internal/cmd/sdk" - _ "forge.lthn.ai/core/cli/internal/cmd/security" - _ "forge.lthn.ai/core/cli/internal/cmd/session" - _ "forge.lthn.ai/core/cli/internal/cmd/setup" - _ "forge.lthn.ai/core/cli/internal/cmd/test" - _ "forge.lthn.ai/core/cli/internal/cmd/updater" - _ "forge.lthn.ai/core/cli/internal/cmd/vm" - _ "forge.lthn.ai/core/cli/internal/cmd/workspace" - _ "forge.lthn.ai/core/go/pkg/build/buildcmd" -) diff --git a/internal/variants/minimal.go b/internal/variants/minimal.go deleted file mode 100644 index d8c355e8..00000000 --- a/internal/variants/minimal.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build minimal - -// minimal.go imports only core packages for a minimal binary. -// -// Build with: go build -tags minimal -// -// This variant includes only the absolute essentials: -// - doctor: Environment verification -// -// Use this for the smallest possible binary with just health checks. - -package variants - -import ( - // Commands via self-registration - _ "forge.lthn.ai/core/cli/internal/cmd/doctor" -) diff --git a/internal/variants/php.go b/internal/variants/php.go deleted file mode 100644 index 2c92d392..00000000 --- a/internal/variants/php.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build php - -// php.go imports packages for the PHP-only binary. -// -// Build with: go build -tags php -// -// This variant includes only PHP/Laravel development tools: -// - php: Laravel/Composer development tools -// - doctor: Environment verification -// -// Use this for PHP-focused workflows without other tooling. - -package variants - -import ( - // Commands via self-registration - _ "forge.lthn.ai/core/cli/internal/cmd/doctor" - _ "forge.lthn.ai/core/cli/internal/cmd/php" -) diff --git a/main.go b/main.go index c430fe16..8405a202 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,42 @@ package main import ( "forge.lthn.ai/core/go/pkg/cli" - // Build variants import commands via self-registration. - // See internal/variants/ for available variants: full, ci, php, minimal. - _ "forge.lthn.ai/core/cli/internal/variants" + // Commands via self-registration + _ "forge.lthn.ai/core/cli/cmd/ai" + _ "forge.lthn.ai/core/cli/cmd/collect" + _ "forge.lthn.ai/core/cli/cmd/config" + _ "forge.lthn.ai/core/cli/cmd/crypt" + _ "forge.lthn.ai/core/cli/cmd/daemon" + _ "forge.lthn.ai/core/cli/cmd/deploy" + _ "forge.lthn.ai/core/cli/cmd/dev" + _ "forge.lthn.ai/core/cli/cmd/docs" + _ "forge.lthn.ai/core/cli/cmd/doctor" + _ "forge.lthn.ai/core/cli/cmd/forge" + _ "forge.lthn.ai/core/cli/cmd/gitcmd" + _ "forge.lthn.ai/core/cli/cmd/go" + _ "forge.lthn.ai/core/cli/cmd/help" + _ "forge.lthn.ai/core/cli/cmd/lab" + _ "forge.lthn.ai/core/cli/cmd/mcpcmd" + _ "forge.lthn.ai/core/cli/cmd/ml" + _ "forge.lthn.ai/core/cli/cmd/monitor" + _ "forge.lthn.ai/core/cli/cmd/pkgcmd" + _ "forge.lthn.ai/core/cli/cmd/plugin" + _ "forge.lthn.ai/core/cli/cmd/prod" + _ "forge.lthn.ai/core/cli/cmd/qa" + _ "forge.lthn.ai/core/cli/cmd/rag" + _ "forge.lthn.ai/core/cli/cmd/security" + _ "forge.lthn.ai/core/cli/cmd/session" + _ "forge.lthn.ai/core/cli/cmd/setup" + _ "forge.lthn.ai/core/cli/cmd/test" + _ "forge.lthn.ai/core/cli/cmd/unifi" + _ "forge.lthn.ai/core/cli/cmd/updater" + _ "forge.lthn.ai/core/cli/cmd/vm" + _ "forge.lthn.ai/core/cli/cmd/workspace" + _ "forge.lthn.ai/core/go/pkg/build/buildcmd" + + // Variant repos (optional — comment out to exclude) + // _ "forge.lthn.ai/core/php" + // _ "forge.lthn.ai/core/ci" ) func main() {