diff --git a/.core/build.yaml b/.core/build.yaml new file mode 100644 index 0000000..8885f79 --- /dev/null +++ b/.core/build.yaml @@ -0,0 +1,32 @@ +# Core CLI build configuration +# Used by: core build + +version: 1 + +project: + name: core + description: Host UK Core CLI + main: "." + binary: core + +build: + cgo: false + flags: + - -trimpath + ldflags: + - -s + - -w + - -X main.Version={{.Version}} + env: [] + +targets: + - os: linux + arch: amd64 + - os: linux + arch: arm64 + - os: darwin + arch: amd64 + - os: darwin + arch: arm64 + - os: windows + arch: amd64 diff --git a/.core/release.yaml b/.core/release.yaml new file mode 100644 index 0000000..8cf8680 --- /dev/null +++ b/.core/release.yaml @@ -0,0 +1,39 @@ +# Core CLI release configuration +# Used by: core release + +version: 1 + +project: + name: core + repository: host-uk/core + +build: + targets: + - os: linux + arch: amd64 + - os: linux + arch: arm64 + - os: darwin + arch: amd64 + - os: darwin + arch: arm64 + - os: windows + arch: amd64 + +publishers: + - type: github + prerelease: false + draft: false + +changelog: + include: + - feat + - fix + - perf + - refactor + exclude: + - chore + - docs + - style + - test + - ci diff --git a/docs/plans/2026-01-30-cli-commands-design.md b/docs/plans/2026-01-30-cli-commands-design.md deleted file mode 100644 index 73212af..0000000 --- a/docs/plans/2026-01-30-cli-commands-design.md +++ /dev/null @@ -1,185 +0,0 @@ -# CLI Commands Registration Design - -## Overview - -Move CLI commands from `cmd/` into self-registering packages in `pkg/`. This enables build variants with reduced attack surface - only compiled code exists in the binary. - -## Pattern - -Same pattern as `i18n.RegisterLocales()`: -- Packages register themselves during `init()` -- Registration is stored until `cli.Init()` runs -- Build tags control which packages are imported - -## Registration API - -```go -// pkg/cli/commands.go - -type CommandRegistration func(root *cobra.Command) - -var ( - registeredCommands []CommandRegistration - registeredCommandsMu sync.Mutex -) - -// RegisterCommands registers a function that adds commands to the CLI. -func RegisterCommands(fn CommandRegistration) { - registeredCommandsMu.Lock() - defer registeredCommandsMu.Unlock() - registeredCommands = append(registeredCommands, fn) -} - -func attachRegisteredCommands(root *cobra.Command) { - registeredCommandsMu.Lock() - defer registeredCommandsMu.Unlock() - for _, fn := range registeredCommands { - fn(root) - } -} -``` - -## Integration with Core.App - -The CLI stores `rootCmd` in `core.App`, unifying GUI and CLI under the same pattern: - -```go -// pkg/cli/runtime.go - -func Init(opts Options) error { - once.Do(func() { - rootCmd := &cobra.Command{ - Use: opts.AppName, - Version: opts.Version, - } - - attachRegisteredCommands(rootCmd) - - c, err := framework.New( - framework.WithApp(rootCmd), - // ... services ... - ) - // ... - }) - return initErr -} - -func RootCmd() *cobra.Command { - return framework.App().(*cobra.Command) -} - -func Execute() error { - return RootCmd().Execute() -} -``` - -## Package Structure - -Commands move from `cmd/` to `pkg/` with a `cmd.go` file: - -``` -pkg/ -├── php/ -│ ├── i18n.go # registers locales -│ ├── cmd.go # registers commands -│ ├── locales/ -│ └── ... -├── dev/ -│ ├── cmd.go # registers commands -│ └── ... -├── cli/ -│ ├── commands.go # RegisterCommands API -│ ├── runtime.go # Init, Execute -│ └── ... -``` - -Each `cmd.go`: - -```go -// pkg/php/cmd.go -package php - -import "github.com/host-uk/core/pkg/cli" - -func init() { - cli.RegisterCommands(AddCommands) -} - -func AddCommands(root *cobra.Command) { - // ... existing command setup ... -} -``` - -## Build Variants - -Import files with build tags in `cmd/variants/`: - -``` -cmd/ -├── main.go -└── variants/ - ├── full.go # default: all packages - ├── ci.go # CI/release only - ├── php.go # PHP tooling only - └── minimal.go # core only -``` - -```go -// cmd/variants/full.go -//go:build !ci && !php && !minimal - -package variants - -import ( - _ "github.com/host-uk/core/pkg/ai" - _ "github.com/host-uk/core/pkg/build" - _ "github.com/host-uk/core/pkg/ci" - _ "github.com/host-uk/core/pkg/dev" - _ "github.com/host-uk/core/pkg/docs" - _ "github.com/host-uk/core/pkg/doctor" - _ "github.com/host-uk/core/pkg/go" - _ "github.com/host-uk/core/pkg/php" - _ "github.com/host-uk/core/pkg/pkg" - _ "github.com/host-uk/core/pkg/sdk" - _ "github.com/host-uk/core/pkg/setup" - _ "github.com/host-uk/core/pkg/test" - _ "github.com/host-uk/core/pkg/vm" -) -``` - -```go -// cmd/variants/ci.go -//go:build ci - -package variants - -import ( - _ "github.com/host-uk/core/pkg/build" - _ "github.com/host-uk/core/pkg/ci" - _ "github.com/host-uk/core/pkg/doctor" - _ "github.com/host-uk/core/pkg/sdk" -) -``` - -## Build Commands - -- `go build` → full variant (default) -- `go build -tags ci` → CI variant -- `go build -tags php` → PHP-only variant - -## Benefits - -1. **Smaller attack surface** - only compiled code exists in binary -2. **Self-registering packages** - same pattern as `i18n.RegisterLocales()` -3. **Uses existing `core.App`** - no new framework concepts -4. **Simple build variants** - just add `-tags` flag -5. **Defence in depth** - no code = no vulnerabilities - -## Migration Steps - -1. Add `RegisterCommands()` to `pkg/cli/commands.go` -2. Update `pkg/cli/runtime.go` to use `core.App` for rootCmd -3. Move each `cmd/*` package to `pkg/*/cmd.go` -4. Create `cmd/variants/` with build tag files -5. Simplify `cmd/main.go` to minimal entry point -6. Remove old `cmd/core_dev.go` and `cmd/core_ci.go` diff --git a/docs/plans/2026-01-30-core-ipc-design.md b/docs/plans/2026-01-30-core-ipc-design.md deleted file mode 100644 index ec3c9c3..0000000 --- a/docs/plans/2026-01-30-core-ipc-design.md +++ /dev/null @@ -1,373 +0,0 @@ -# Core Framework IPC Design - -> Design document for refactoring CLI commands to use the Core framework's IPC system. - -## Overview - -The Core framework provides a dependency injection and inter-process communication (IPC) system originally designed for orchestrating services. This design extends the framework with request/response patterns and applies it to CLI commands. - -Commands build "worker bundles" - sandboxed Core instances with specific services. The bundle configuration acts as a permissions layer: if a service isn't registered, that capability isn't available. - -## Dispatch Patterns - -Four patterns for service communication: - -| Method | Behaviour | Returns | Use Case | -|--------|-----------|---------|----------| -| `ACTION` | Broadcast to all handlers | `error` | Events, notifications | -| `QUERY` | First responder wins | `(any, bool, error)` | Get data | -| `QUERYALL` | Broadcast, collect all | `([]any, error)` | Aggregate from multiple services | -| `PERFORM` | First responder executes | `(any, bool, error)` | Execute a task with side effects | - -### ACTION (existing) - -Fire-and-forget broadcast. All registered handlers receive the message. Errors are aggregated. - -```go -c.ACTION(ActionServiceStartup{}) -``` - -### QUERY (new) - -Request data from services. Stops at first handler that returns `handled=true`. - -```go -result, handled, err := c.QUERY(git.QueryStatus{Paths: paths}) -if !handled { - // No service registered to handle this query -} -statuses := result.([]git.RepoStatus) -``` - -### QUERYALL (new) - -Broadcast query to all handlers, collect all responses. Useful for aggregating results from multiple services (e.g., multiple QA/lint tools). - -```go -results, err := c.QUERYALL(qa.QueryLint{Paths: paths}) -for _, r := range results { - lint := r.(qa.LintResult) - fmt.Printf("%s found %d issues\n", lint.Tool, len(lint.Issues)) -} -``` - -### PERFORM (new) - -Execute a task with side effects. Stops at first handler that returns `handled=true`. - -```go -result, handled, err := c.PERFORM(agentic.TaskCommit{ - Path: repo.Path, - Name: repo.Name, -}) -if !handled { - // Agentic service not in bundle - commits not available -} -``` - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ cmd/dev/dev_work.go │ -│ - Builds worker bundle │ -│ - Triggers PERFORM(TaskWork{}) │ -└─────────────────────┬───────────────────────────────────────┘ - │ -┌─────────────────────▼───────────────────────────────────────┐ -│ cmd/dev/bundles.go │ -│ - NewWorkBundle() - git + agentic + dev │ -│ - NewStatusBundle() - git + dev only │ -│ - Bundle config = permissions │ -└─────────────────────┬───────────────────────────────────────┘ - │ -┌─────────────────────▼───────────────────────────────────────┐ -│ pkg/dev/service.go │ -│ - Orchestrates workflow │ -│ - QUERY(git.QueryStatus{}) │ -│ - PERFORM(agentic.TaskCommit{}) │ -│ - PERFORM(git.TaskPush{}) │ -└─────────────────────┬───────────────────────────────────────┘ - │ - ┌─────────────┴─────────────┐ - ▼ ▼ -┌───────────────────┐ ┌───────────────────┐ -│ pkg/git/service │ │ pkg/agentic/svc │ -│ │ │ │ -│ Queries: │ │ Tasks: │ -│ - QueryStatus │ │ - TaskCommit │ -│ - QueryDirtyRepos │ │ - TaskPrompt │ -│ - QueryAheadRepos │ │ │ -│ │ │ │ -│ Tasks: │ │ │ -│ - TaskPush │ │ │ -│ - TaskPull │ │ │ -└───────────────────┘ └───────────────────┘ -``` - -## Permissions Model - -Permissions are implicit through bundle configuration: - -```go -// Full capabilities - can commit and push -func NewWorkBundle(opts WorkBundleOptions) (*framework.Runtime, error) { - return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{ - "dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) }, - "git": func() (any, error) { return git.NewService(opts.Git)(nil) }, - "agentic": func() (any, error) { return agentic.NewService(opts.Agentic)(nil) }, - }) -} - -// Read-only - status queries only, no commits -func NewStatusBundle(opts StatusBundleOptions) (*framework.Runtime, error) { - return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{ - "dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) }, - "git": func() (any, error) { return git.NewService(opts.Git)(nil) }, - // No agentic service - TaskCommit will be unhandled - }) -} -``` - -Service options provide fine-grained control: - -```go -agentic.NewService(agentic.ServiceOptions{ - AllowEdit: false, // Claude can only use read-only tools -}) - -agentic.NewService(agentic.ServiceOptions{ - AllowEdit: true, // Claude can use Write/Edit tools -}) -``` - -**Key principle**: Code never checks permissions explicitly. It dispatches actions and either they're handled or they're not. The bundle configuration is the single source of truth for what's allowed. - -## Framework Changes - -### New Types (interfaces.go) - -```go -type Query interface{} -type Task interface{} - -type QueryHandler func(*Core, Query) (any, bool, error) -type TaskHandler func(*Core, Task) (any, bool, error) -``` - -### Core Struct Additions (interfaces.go) - -```go -type Core struct { - // ... existing fields - - queryMu sync.RWMutex - queryHandlers []QueryHandler - - taskMu sync.RWMutex - taskHandlers []TaskHandler -} -``` - -### New Methods (core.go) - -```go -// QUERY - first responder wins -func (c *Core) QUERY(q Query) (any, bool, error) - -// QUERYALL - broadcast, collect all responses -func (c *Core) QUERYALL(q Query) ([]any, error) - -// PERFORM - first responder executes -func (c *Core) PERFORM(t Task) (any, bool, error) - -// Registration -func (c *Core) RegisterQuery(h QueryHandler) -func (c *Core) RegisterTask(h TaskHandler) -``` - -### Re-exports (framework.go) - -```go -type Query = core.Query -type Task = core.Task -type QueryHandler = core.QueryHandler -type TaskHandler = core.TaskHandler -``` - -## Service Implementation Pattern - -Services register handlers during startup: - -```go -func (s *Service) OnStartup(ctx context.Context) error { - s.Core().RegisterAction(s.handleAction) - s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil -} - -func (s *Service) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) { - switch m := q.(type) { - case QueryStatus: - result := s.getStatus(m.Paths, m.Names) - return result, true, nil - case QueryDirtyRepos: - return s.DirtyRepos(), true, nil - } - return nil, false, nil // Not handled -} - -func (s *Service) handleTask(c *framework.Core, t framework.Task) (any, bool, error) { - switch m := t.(type) { - case TaskPush: - err := s.push(m.Path) - return nil, true, err - case TaskPull: - err := s.pull(m.Path) - return nil, true, err - } - return nil, false, nil // Not handled -} -``` - -## Git Service Queries & Tasks - -```go -// pkg/git/queries.go -type QueryStatus struct { - Paths []string - Names map[string]string -} - -type QueryDirtyRepos struct{} -type QueryAheadRepos struct{} - -// pkg/git/tasks.go -type TaskPush struct { - Path string - Name string -} - -type TaskPull struct { - Path string - Name string -} - -type TaskPushMultiple struct { - Paths []string - Names map[string]string -} -``` - -## Agentic Service Tasks - -```go -// pkg/agentic/tasks.go -type TaskCommit struct { - Path string - Name string - CanEdit bool -} - -type TaskPrompt struct { - Prompt string - WorkDir string - AllowedTools []string -} -``` - -## Dev Workflow Service - -```go -// pkg/dev/tasks.go -type TaskWork struct { - RegistryPath string - StatusOnly bool - AutoCommit bool -} - -type TaskCommitAll struct { - RegistryPath string -} - -type TaskPushAll struct { - RegistryPath string - Force bool -} -``` - -## Command Simplification - -Before (dev_work.go - 327 lines of orchestration): - -```go -func runWork(registryPath string, statusOnly, autoCommit bool) error { - // Load registry - // Get git status - // Display table - // Loop dirty repos, shell out to claude - // Re-check status - // Confirm push - // Push repos - // Handle diverged branches - // ... -} -``` - -After (dev_work.go - minimal): - -```go -func runWork(registryPath string, statusOnly, autoCommit bool) error { - bundle, err := NewWorkBundle(WorkBundleOptions{ - RegistryPath: registryPath, - }) - if err != nil { - return err - } - - ctx := context.Background() - bundle.Core.ServiceStartup(ctx, nil) - defer bundle.Core.ServiceShutdown(ctx) - - _, _, err = bundle.Core.PERFORM(dev.TaskWork{ - StatusOnly: statusOnly, - AutoCommit: autoCommit, - }) - return err -} -``` - -All orchestration logic moves to `pkg/dev/service.go` where it can be tested independently and reused. - -## Implementation Tasks - -1. **Framework Core** - Add Query, Task types and QUERY/QUERYALL/PERFORM methods -2. **Framework Re-exports** - Update framework.go with new types -3. **Git Service** - Add query and task handlers -4. **Agentic Service** - Add task handlers -5. **Dev Service** - Create workflow orchestration service -6. **Bundles** - Create bundle factories in cmd/dev/ -7. **Commands** - Simplify cmd/dev/*.go to use bundles - -## Future: CLI-Wide Runtime - -Phase 2 will add a CLI-wide Core instance that: - -- Handles signals (SIGINT, SIGTERM) -- Manages UI state -- Spawns worker bundles as "interactable elements" -- Provides cross-bundle communication - -Worker bundles become sandboxed children of the CLI runtime, with the runtime controlling what capabilities each bundle receives. - -## Testing - -Each layer is independently testable: - -- **Framework**: Unit tests for QUERY/QUERYALL/PERFORM dispatch -- **Services**: Unit tests with mock Core instances -- **Bundles**: Integration tests with real services -- **Commands**: E2E tests via CLI invocation - -The permission model is testable by creating bundles with/without specific services and verifying behaviour. diff --git a/docs/plans/2026-01-30-i18n-v2-design.md b/docs/plans/2026-01-30-i18n-v2-design.md deleted file mode 100644 index c5a4cb8..0000000 --- a/docs/plans/2026-01-30-i18n-v2-design.md +++ /dev/null @@ -1,134 +0,0 @@ -# i18n Package Refactor Design - -## Goal -Refactor pkg/i18n to be extensible without breaking changes in future. Based on Gemini review recommendations. - -## File Structure - -### Renamed/Merged -| Current | New | Reason | -|---------|-----|--------| -| `interfaces.go` | `types.go` | Contains types, not interfaces | -| `mutate.go` | `loader.go` | Loads/flattens JSON | -| `actions.go` | `hooks.go` | Missing key callbacks | -| `checks.go` | (merge into loader.go) | Loading helpers | -| `mode.go` | (merge into types.go) | Just one type | - -### New Files -| File | Purpose | -|------|---------| -| `handler.go` | KeyHandler interface + built-in handlers | -| `context.go` | TranslationContext + C() helper | - -### Unchanged -`grammar.go`, `language.go`, `localise.go`, `debug.go`, `numbers.go`, `time.go`, `i18n.go`, `intents.go`, `compose.go`, `transform.go` - -## Interfaces - -### KeyHandler -```go -// KeyHandler processes translation keys before standard lookup. -type KeyHandler interface { - Match(key string) bool - Handle(key string, args []any, next func() string) string -} -``` - -Built-in handlers: -- `LabelHandler` - `i18n.label.*` → "Status:" -- `ProgressHandler` - `i18n.progress.*` → "Building..." -- `CountHandler` - `i18n.count.*` → "5 files" -- `NumericHandler` - `i18n.numeric.*` → formatted numbers -- `DoneHandler` - `i18n.done.*` → "File deleted" -- `FailHandler` - `i18n.fail.*` → "Failed to delete file" - -### Loader -```go -// Loader provides translation data to the Service. -type Loader interface { - Load(lang string) (map[string]Message, *GrammarData, error) - Languages() []string -} -``` - -Built-in: `FSLoader` for embedded/filesystem JSON. - -### TranslationContext -```go -type TranslationContext struct { - Context string - Gender string - Formality Formality - Extra map[string]any -} - -func C(context string) *TranslationContext -``` - -## Service Changes - -```go -type Service struct { - loader Loader - messages map[string]map[string]Message - grammar map[string]*GrammarData - currentLang string - fallbackLang string - formality Formality - mode Mode - debug bool - handlers []KeyHandler - mu sync.RWMutex -} -``` - -### Constructors -```go -func New() (*Service, error) -func NewWithLoader(loader Loader, opts ...Option) (*Service, error) - -type Option func(*Service) -func WithDefaultHandlers() Option -func WithFallback(lang string) Option -func WithFormality(f Formality) Option -``` - -### T() Flow -1. Parse args → extract Context, Subject, data -2. Run handler chain (each can handle or call next) -3. Standard lookup with context suffix fallback - -## Public API - -### Keep -- `T(key, args...)`, `Raw(key, args...)` -- `S(noun, value)` - Subject builder -- `SetLanguage()`, `CurrentLanguage()`, `SetMode()`, `CurrentMode()` -- `SetFormality()`, `SetDebug()`, `Direction()`, `IsRTL()` -- Grammar: `PastTense()`, `Gerund()`, `Pluralize()`, `Article()`, `Title()`, `Label()`, `Progress()` - -### Add -- `C(context)` - Context builder -- `NewWithLoader()` - Custom loader support -- `AddHandler()`, `PrependHandler()` - Custom handlers - -### Remove (No Aliases) -- `NewSubject()` - use `S()` -- `N()` - use `T("i18n.numeric.*")` - -## Breaking Changes -- Constructor signature changes -- Internal file reorganisation -- No backwards compatibility layer - -## Implementation Order -1. Create new files (types.go, handler.go, loader.go, context.go, hooks.go) -2. Move types from interfaces.go → types.go -3. Implement Loader interface + FSLoader -4. Implement KeyHandler interface + built-in handlers -5. Implement TranslationContext -6. Update Service struct + constructors -7. Update T() to use handler chain -8. Update package-level functions in i18n.go -9. Delete old files -10. Update tests diff --git a/docs/plans/2026-01-30-semantic-i18n-design.md b/docs/plans/2026-01-30-semantic-i18n-design.md deleted file mode 100644 index ca28e9d..0000000 --- a/docs/plans/2026-01-30-semantic-i18n-design.md +++ /dev/null @@ -1,486 +0,0 @@ -# Semantic i18n System Design - -## Overview - -Extend the i18n system beyond simple key-value translation to support **semantic intents** that encode meaning, enabling: - -- Composite translations from reusable fragments -- Grammatical awareness (gender, plurality, formality) -- CLI prompt integration with localized options -- Reduced calling code complexity - -## Goals - -1. **Simple cases stay simple** - `_("key")` works as expected -2. **Complex cases become declarative** - Intent drives output, not caller logic -3. **Translators have power** - Grammar rules live in translations, not code -4. **CLI integration** - Questions, confirmations, choices are first-class - -## API Design - -### Function Reference (Stable API) - -These function names are **permanent** - choose carefully, they cannot change. - -| Function | Alias | Purpose | -|----------|-------|---------| -| `_()` | - | Simple gettext-style lookup | -| `T()` | `C()` | Compose - semantic intent resolution | -| `S()` | `Subject()` | Create typed subject with metadata | - -### Simple Translation: `_()` - -Standard gettext-style lookup. No magic, just key → value. - -```go -i18n._("cli.success") // "Success" -i18n._("common.label.error") // "Error:" -i18n._("common.error.failed", map[string]any{"Action": "load"}) // "Failed to load" -``` - -### Compose: `T()` / `C()` - -Semantic intent resolution. Takes an intent key from `core.*` namespace and returns a `Composed` result with multiple output forms. - -```go -// Full form -result := i18n.T("core.delete", i18n.S("file", path)) -result := i18n.C("core.delete", i18n.S("file", path)) // Alias - -// Result contains all forms -result.Question // "Delete /path/to/file.txt?" -result.Confirm // "Really delete /path/to/file.txt?" -result.Success // "File deleted" -result.Failure // "Failed to delete file" -result.Meta // IntentMeta{Dangerous: true, Default: "no", ...} -``` - -### Subject: `S()` / `Subject()` - -Creates a typed subject with optional metadata for grammar rules. - -```go -// Simple -i18n.S("file", "/path/to/file.txt") - -// With count (plurality) -i18n.S("commit", commits).Count(len(commits)) - -// With gender (for gendered languages) -i18n.S("user", name).Gender("female") - -// Chained -i18n.S("file", path).Count(3).In("/project") -``` - -### Type Signatures - -```go -// Simple lookup -func _(key string, args ...any) string - -// Compose (T and C are aliases) -func T(intent string, subject *Subject) *Composed -func C(intent string, subject *Subject) *Composed - -// Subject builder -func S(noun string, value any) *Subject -func Subject(noun string, value any) *Subject - -// Composed result -type Composed struct { - Question string - Confirm string - Success string - Failure string - Meta IntentMeta -} - -// Subject with metadata -type Subject struct { - Noun string - Value any - count int - gender string - // ... other metadata -} - -func (s *Subject) Count(n int) *Subject -func (s *Subject) Gender(g string) *Subject -func (s *Subject) In(location string) *Subject - -// Intent metadata -type IntentMeta struct { - Type string // "action", "question", "info" - Verb string // Reference to common.verb.* - Dangerous bool // Requires confirmation - Default string // "yes" or "no" - Supports []string // Extra options like "all", "skip" -} -``` - -## CLI Integration - -The CLI package uses `T()` internally for prompts: - -```go -// Confirm uses T() internally -confirmed := cli.Confirm("core.delete", i18n.S("file", path)) -// Internally: result := i18n.T("core.delete", subject) -// Displays: result.Question + localized [y/N] -// Returns: bool - -// Question with options -choice := cli.Question("core.save", i18n.S("changes", 3).Count(3), cli.Options{ - Default: "yes", - Extra: []string{"all"}, -}) -// Displays: "Save 3 changes? [a/y/N]" -// Returns: "yes" | "no" | "all" - -// Choice from list -selected := cli.Choose("core.select.branch", branches) -// Displays localized prompt with arrow selection -``` - -### cli.Confirm() - -```go -func Confirm(intent string, subject *i18n.Subject, opts ...ConfirmOption) bool - -// Options -cli.DefaultYes() // Default to yes instead of no -cli.DefaultNo() // Explicit default no -cli.Required() // No default, must choose -cli.Timeout(30*time.Second) // Auto-select default after timeout -``` - -### cli.Question() - -```go -func Question(intent string, subject *i18n.Subject, opts ...QuestionOption) string - -// Options -cli.Extra("all", "skip") // Extra options beyond y/n -cli.Default("yes") // Which option is default -cli.Validate(func(s string) bool) // Custom validation -``` - -### cli.Choose() - -```go -func Choose[T any](intent string, items []T, opts ...ChooseOption) T - -// Options -cli.Display(func(T) string) // How to display each item -cli.Filter() // Enable fuzzy filtering -cli.Multi() // Allow multiple selection -``` - -## Reserved Namespaces - -### `common.*` - Reusable Fragments - -Atomic translation units that can be composed: - -```json -{ - "common": { - "verb": { - "edit": "edit", - "delete": "delete", - "create": "create", - "save": "save", - "update": "update", - "commit": "commit" - }, - "noun": { - "file": { "one": "file", "other": "files" }, - "commit": { "one": "commit", "other": "commits" }, - "change": { "one": "change", "other": "changes" } - }, - "article": { - "the": "the", - "a": { "one": "a", "vowel": "an" } - }, - "prompt": { - "yes": "y", - "no": "n", - "all": "a", - "skip": "s", - "quit": "q" - } - } -} -``` - -### `core.*` - Semantic Intents - -Intents encode meaning and behavior: - -```json -{ - "core": { - "edit": { - "_meta": { - "type": "action", - "verb": "common.verb.edit", - "dangerous": false - }, - "question": "Should I {{.Verb}} {{.Subject}}?", - "confirm": "{{.Verb | title}} {{.Subject}}?", - "success": "{{.Subject | title}} {{.Verb | past}}", - "failure": "Failed to {{.Verb}} {{.Subject}}" - }, - "delete": { - "_meta": { - "type": "action", - "verb": "common.verb.delete", - "dangerous": true, - "default": "no" - }, - "question": "Delete {{.Subject}}? This cannot be undone.", - "confirm": "Really delete {{.Subject}}?", - "success": "{{.Subject | title}} deleted", - "failure": "Failed to delete {{.Subject}}" - }, - "save": { - "_meta": { - "type": "action", - "verb": "common.verb.save", - "supports": ["all", "skip"] - }, - "question": "Save {{.Subject}}?", - "success": "{{.Subject | title}} saved" - }, - "commit": { - "_meta": { - "type": "action", - "verb": "common.verb.commit", - "dangerous": false - }, - "question": "Commit {{.Subject}}?", - "success": "{{.Subject | title}} committed", - "failure": "Failed to commit {{.Subject}}" - } - } -} -``` - -## Template Functions - -Available in translation templates: - -| Function | Description | Example | -|----------|-------------|---------| -| `title` | Title case | `{{.Name \| title}}` → "Hello World" | -| `lower` | Lower case | `{{.Name \| lower}}` → "hello world" | -| `upper` | Upper case | `{{.Name \| upper}}` → "HELLO WORLD" | -| `past` | Past tense verb | `{{.Verb \| past}}` → "edited" | -| `plural` | Pluralize noun | `{{.Noun \| plural .Count}}` → "files" | -| `article` | Add article | `{{.Noun \| article}}` → "a file" | -| `quote` | Wrap in quotes | `{{.Path \| quote}}` → `"/path/to/file"` | - -## Implementation Plan - -### Phase 1: Foundation -1. Define `Composed` and `Subject` types -2. Add `S()` / `Subject()` builder -3. Add `T()` / `C()` with intent resolution -4. Parse `_meta` from JSON -5. Add template functions (title, lower, past, etc.) - -### Phase 2: CLI Integration -1. Implement `cli.Confirm()` using intents -2. Implement `cli.Question()` with options -3. Implement `cli.Choose()` for lists -4. Localize prompt characters [y/N] → [j/N] etc. - -### Phase 3: Grammar Engine -1. Verb conjugation (past tense, etc.) -2. Noun plurality with irregular forms -3. Article selection (a/an, gender) -4. Language-specific rules - -### Phase 4: Extended Languages -1. Gender agreement (French, German, etc.) -2. Formality levels (Japanese, Korean, etc.) -3. Right-to-left support -4. Plural forms beyond one/other (Russian, Arabic, etc.) - -## Example: Full Flow - -```go -// In cmd/dev/dev_commit.go -path := "/Users/dev/project" -files := []string{"main.go", "config.yaml"} - -// Old way (hardcoded English, manual prompt handling) -fmt.Printf("Commit %d files in %s? [y/N] ", len(files), path) -var response string -fmt.Scanln(&response) -if response != "y" && response != "Y" { - return -} - -// New way (semantic, localized, integrated) -if !cli.Confirm("core.commit", i18n.S("file", path).Count(len(files))) { - return -} - -// For German user, displays: -// "2 Dateien in /Users/dev/project committen? [j/N]" -// (note: "j" for "ja" instead of "y" for "yes") -``` - -## JSON Schema - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "common": { - "description": "Reusable translation fragments", - "type": "object" - }, - "core": { - "description": "Semantic intents with metadata", - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "_meta": { - "type": "object", - "properties": { - "type": { "enum": ["action", "question", "info"] }, - "verb": { "type": "string" }, - "dangerous": { "type": "boolean" }, - "default": { "enum": ["yes", "no"] }, - "supports": { "type": "array", "items": { "type": "string" } } - } - }, - "question": { "type": "string" }, - "confirm": { "type": "string" }, - "success": { "type": "string" }, - "failure": { "type": "string" } - } - } - } - } -} -``` - -## Grammar Fundamentals - -Parts of speech we need to handle: - -| Part | Role | Example | Transforms | -|------|------|---------|------------| -| **Verb** | Action | delete, save, commit | tense (past/present), mood (imperative) | -| **Noun** | Subject/Object | file, commit, user | plurality, gender, case | -| **Article** | Determiner | a/an, the | vowel-awareness, gender agreement | -| **Adjective** | Describes noun | modified, new, deleted | gender/number agreement | -| **Preposition** | Relation | in, from, to | - | - -### Verb Conjugation - -```json -{ - "common": { - "verb": { - "delete": { - "base": "delete", - "past": "deleted", - "gerund": "deleting", - "imperative": "delete" - } - } - } -} -``` - -For most English verbs, derive automatically: -- `past`: base + "ed" (or irregular lookup) -- `gerund`: base + "ing" - -### Noun Handling - -```json -{ - "common": { - "noun": { - "file": { - "one": "file", - "other": "files", - "gender": "neuter" - } - } - } -} -``` - -### Article Selection - -English: a/an based on next word's sound (not letter) -- "a file", "an item", "a user", "an hour" - -Other languages: gender agreement (der/die/das, le/la, etc.) - -## DX Improvements - -### 1. Compile-Time Validation -- `go generate` checks all `T("core.X")` calls have matching JSON keys -- Warns on missing `_meta` fields -- Type-checks template variables - -### 2. IDE Support -- JSON schema for autocomplete in translation files -- Go constants generated from JSON keys: `i18n.CoreDelete` instead of `"core.delete"` - -### 3. Fallback Chain -``` -T("core.delete", subject) - → try core.delete.question - → try core.delete (plain string) - → try common.action.delete - → return "Delete {{.Subject}}?" (hardcoded fallback) -``` - -### 4. Debug Mode -```go -i18n.Debug(true) // Shows: [core.delete] Delete file.txt? -``` - -### 5. Short Subject Syntax -```go -// Instead of: -i18n.T("core.delete", i18n.S("file", path)) - -// Allow: -i18n.T("core.delete", path) // Infers subject type from intent's expected noun -``` - -### 6. Fluent Chaining -```go -i18n.T("core.delete"). - Subject("file", path). - Count(3). - Question() // Returns just the question string -``` - -## Notes for Future Implementation - -- Use `github.com/gertd/go-pluralize` for English plurality -- Consider `github.com/nicksnyder/go-i18n` patterns for CLDR plural rules -- Store compiled templates in sync.Map for caching -- `_meta` parsing happens once at load time, not per-call -- CLI prompt chars from `common.prompt.*` - allows `[j/N]` for German - -## Open Questions - -1. **Verb conjugation library** - Use existing Go library or build custom? -2. **Gender detection** - How to infer gender for subjects in gendered languages? -3. **Fallback behavior** - What happens when intent metadata is missing? -4. **Caching** - Should compiled templates be cached? -5. **Validation** - How to validate intent definitions at build time? diff --git a/docs/plans/2026-01-31-semantic-cli-output.md b/docs/plans/2026-01-31-semantic-cli-output.md deleted file mode 100644 index 23f886c..0000000 --- a/docs/plans/2026-01-31-semantic-cli-output.md +++ /dev/null @@ -1,1685 +0,0 @@ -# Semantic CLI Output Abstraction - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Zero external dependencies for CLI output. Consuming code only imports `cli` - no `fmt`, `i18n`, or `lipgloss`. - -**Restore Point:** `96eaed5` - all deleted code recoverable from git history. - -**Architecture:** -- Internal ANSI styling (~100 lines replaces lipgloss) -- Glyph system with themes (unicode/emoji/ascii) -- Semantic output functions (`cli.Success`, `cli.Error`, `cli.Progress`) -- HLCRF layout system for structured output (ported from RFC-001) -- Simple stdin prompts (replaces huh wizard) - -**Tech Stack:** Go standard library only. Zero external dependencies for CLI output. - -**Reference:** RFC-001-HLCRF-COMPOSITOR.md (lab/host.uk.com/doc/rfc/) - ---- - -## Design Decisions - -### 1. Explicit Styled Functions (NOT Prefix Detection) - -The codebase uses keys like `cmd.dev.ci.short`, not `i18n.success.*`. Instead of prefix detection, use explicit functions: - -```go -cli.Success("Build complete") // ✓ Build complete (green) -cli.Error("Connection failed") // ✗ Connection failed (red) -cli.Warn("Rate limited") // ⚠ Rate limited (amber) -cli.Info("Connecting...") // ℹ Connecting... (blue) - -// With i18n -cli.Success(i18n.T("build.complete")) // Caller handles translation -cli.Echo(key, args...) // Just translate + print, no styling -``` - -### 2. Delete-and-Replace Approach - -No backward compatibility. Delete all lipgloss-based code, rewrite with internal ANSI: -- Delete `var Style = struct {...}` namespace (output.go) -- Delete all 50+ helper functions (styles.go) -- Delete `Symbol*` constants - replaced by glyph system -- Delete `Table` struct - rewrite with internal styling - -### 3. Glyph System Replaces Symbol Constants - -```go -// Before (styles.go) -const SymbolCheck = "✓" -fmt.Print(SuccessStyle.Render(SymbolCheck)) - -// After -cli.Success("Done") // Internally uses Glyph(":check:") -cli.Print(":check: Done") // Or explicit glyph -``` - -### 4. Simple Wizard Prompts - -Replace huh forms with basic stdin: - -```go -cli.Prompt("Project name", "my-project") // text input -cli.Confirm("Continue?") // y/n -cli.Select("Choose", []string{"a", "b"}) // numbered list -``` - ---- - -## Phase -1: Zero-Dependency ANSI Styling - -### Why - -Current dependencies for ANSI escape codes: -- `lipgloss` → 15 transitive deps -- `huh` → 30 transitive deps -- Supply chain attack surface: ~45 packages - -What we actually use: `style.Bold(true).Foreground(color).Render(text)` - -This is ~100 lines of ANSI codes. We own it completely. - -### Task -1.1: ANSI Style Package - -**Files:** -- Create: `pkg/cli/ansi.go` - -**Step 1: Create ansi.go with complete implementation** - -```go -package cli - -import ( - "fmt" - "strconv" - "strings" -) - -// ANSI escape codes -const ( - ansiReset = "\033[0m" - ansiBold = "\033[1m" - ansiDim = "\033[2m" - ansiItalic = "\033[3m" - ansiUnderline = "\033[4m" -) - -// AnsiStyle represents terminal text styling. -// Use NewStyle() to create, chain methods, call Render(). -type AnsiStyle struct { - bold bool - dim bool - italic bool - underline bool - fg string - bg string -} - -// NewStyle creates a new empty style. -func NewStyle() *AnsiStyle { - return &AnsiStyle{} -} - -// Bold enables bold text. -func (s *AnsiStyle) Bold() *AnsiStyle { - s.bold = true - return s -} - -// Dim enables dim text. -func (s *AnsiStyle) Dim() *AnsiStyle { - s.dim = true - return s -} - -// Italic enables italic text. -func (s *AnsiStyle) Italic() *AnsiStyle { - s.italic = true - return s -} - -// Underline enables underlined text. -func (s *AnsiStyle) Underline() *AnsiStyle { - s.underline = true - return s -} - -// Foreground sets foreground color from hex string. -func (s *AnsiStyle) Foreground(hex string) *AnsiStyle { - s.fg = fgColorHex(hex) - return s -} - -// Background sets background color from hex string. -func (s *AnsiStyle) Background(hex string) *AnsiStyle { - s.bg = bgColorHex(hex) - return s -} - -// Render applies the style to text. -func (s *AnsiStyle) Render(text string) string { - if s == nil { - return text - } - - var codes []string - if s.bold { - codes = append(codes, ansiBold) - } - if s.dim { - codes = append(codes, ansiDim) - } - if s.italic { - codes = append(codes, ansiItalic) - } - if s.underline { - codes = append(codes, ansiUnderline) - } - if s.fg != "" { - codes = append(codes, s.fg) - } - if s.bg != "" { - codes = append(codes, s.bg) - } - - if len(codes) == 0 { - return text - } - - return strings.Join(codes, "") + text + ansiReset -} - -// Hex color support -func fgColorHex(hex string) string { - r, g, b := hexToRGB(hex) - return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b) -} - -func bgColorHex(hex string) string { - r, g, b := hexToRGB(hex) - return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b) -} - -func hexToRGB(hex string) (int, int, int) { - hex = strings.TrimPrefix(hex, "#") - if len(hex) != 6 { - return 255, 255, 255 - } - r, _ := strconv.ParseInt(hex[0:2], 16, 64) - g, _ := strconv.ParseInt(hex[2:4], 16, 64) - b, _ := strconv.ParseInt(hex[4:6], 16, 64) - return int(r), int(g), int(b) -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/ansi.go -git commit -m "feat(cli): add zero-dependency ANSI styling - -Replaces lipgloss with ~100 lines of owned code. -Supports bold, dim, italic, underline, RGB/hex colors. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.2: Rewrite styles.go - -**Files:** -- Rewrite: `pkg/cli/styles.go` (delete 672 lines, write ~150) - -**Step 1: Delete entire file content and rewrite** - -```go -// Package cli provides semantic CLI output with zero external dependencies. -package cli - -import ( - "fmt" - "strings" - "time" -) - -// Tailwind colour palette (hex strings) -const ( - ColourBlue50 = "#eff6ff" - ColourBlue100 = "#dbeafe" - ColourBlue200 = "#bfdbfe" - ColourBlue300 = "#93c5fd" - ColourBlue400 = "#60a5fa" - ColourBlue500 = "#3b82f6" - ColourBlue600 = "#2563eb" - ColourBlue700 = "#1d4ed8" - ColourGreen400 = "#4ade80" - ColourGreen500 = "#22c55e" - ColourGreen600 = "#16a34a" - ColourRed400 = "#f87171" - ColourRed500 = "#ef4444" - ColourRed600 = "#dc2626" - ColourAmber400 = "#fbbf24" - ColourAmber500 = "#f59e0b" - ColourAmber600 = "#d97706" - ColourOrange500 = "#f97316" - ColourYellow500 = "#eab308" - ColourEmerald500= "#10b981" - ColourPurple500 = "#a855f7" - ColourViolet400 = "#a78bfa" - ColourViolet500 = "#8b5cf6" - ColourIndigo500 = "#6366f1" - ColourCyan500 = "#06b6d4" - ColourGray50 = "#f9fafb" - ColourGray100 = "#f3f4f6" - ColourGray200 = "#e5e7eb" - ColourGray300 = "#d1d5db" - ColourGray400 = "#9ca3af" - ColourGray500 = "#6b7280" - ColourGray600 = "#4b5563" - ColourGray700 = "#374151" - ColourGray800 = "#1f2937" - ColourGray900 = "#111827" -) - -// Core styles -var ( - SuccessStyle = NewStyle().Bold().Foreground(ColourGreen500) - ErrorStyle = NewStyle().Bold().Foreground(ColourRed500) - WarningStyle = NewStyle().Bold().Foreground(ColourAmber500) - InfoStyle = NewStyle().Foreground(ColourBlue400) - DimStyle = NewStyle().Dim().Foreground(ColourGray500) - MutedStyle = NewStyle().Foreground(ColourGray600) - BoldStyle = NewStyle().Bold() - KeyStyle = NewStyle().Foreground(ColourGray400) - ValueStyle = NewStyle().Foreground(ColourGray200) - AccentStyle = NewStyle().Foreground(ColourCyan500) - LinkStyle = NewStyle().Foreground(ColourBlue500).Underline() - HeaderStyle = NewStyle().Bold().Foreground(ColourGray200) - TitleStyle = NewStyle().Bold().Foreground(ColourBlue500) - CodeStyle = NewStyle().Foreground(ColourGray300) - NumberStyle = NewStyle().Foreground(ColourBlue300) - RepoStyle = NewStyle().Bold().Foreground(ColourBlue500) -) - -// Truncate shortens a string to max length with ellipsis. -func Truncate(s string, max int) string { - if len(s) <= max { - return s - } - if max <= 3 { - return s[:max] - } - return s[:max-3] + "..." -} - -// Pad right-pads a string to width. -func Pad(s string, width int) string { - if len(s) >= width { - return s - } - return s + strings.Repeat(" ", width-len(s)) -} - -// FormatAge formats a time as human-readable age (e.g., "2h ago", "3d ago"). -func FormatAge(t time.Time) string { - d := time.Since(t) - switch { - case d < time.Minute: - return "just now" - case d < time.Hour: - return fmt.Sprintf("%dm ago", int(d.Minutes())) - case d < 24*time.Hour: - return fmt.Sprintf("%dh ago", int(d.Hours())) - case d < 7*24*time.Hour: - return fmt.Sprintf("%dd ago", int(d.Hours()/24)) - case d < 30*24*time.Hour: - return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7))) - default: - return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30))) - } -} - -// Table renders tabular data with aligned columns. -// HLCRF is for layout; Table is for tabular data - they serve different purposes. -type Table struct { - Headers []string - Rows [][]string - Style TableStyle -} - -type TableStyle struct { - HeaderStyle *AnsiStyle - CellStyle *AnsiStyle - Separator string -} - -// DefaultTableStyle returns sensible defaults. -func DefaultTableStyle() TableStyle { - return TableStyle{ - HeaderStyle: HeaderStyle, - CellStyle: nil, - Separator: " ", - } -} - -// NewTable creates a table with headers. -func NewTable(headers ...string) *Table { - return &Table{ - Headers: headers, - Style: DefaultTableStyle(), - } -} - -// AddRow adds a row to the table. -func (t *Table) AddRow(cells ...string) *Table { - t.Rows = append(t.Rows, cells) - return t -} - -// String renders the table. -func (t *Table) String() string { - if len(t.Headers) == 0 && len(t.Rows) == 0 { - return "" - } - - // Calculate column widths - cols := len(t.Headers) - if cols == 0 && len(t.Rows) > 0 { - cols = len(t.Rows[0]) - } - widths := make([]int, cols) - - for i, h := range t.Headers { - if len(h) > widths[i] { - widths[i] = len(h) - } - } - for _, row := range t.Rows { - for i, cell := range row { - if i < cols && len(cell) > widths[i] { - widths[i] = len(cell) - } - } - } - - var sb strings.Builder - sep := t.Style.Separator - - // Headers - if len(t.Headers) > 0 { - for i, h := range t.Headers { - if i > 0 { - sb.WriteString(sep) - } - styled := Pad(h, widths[i]) - if t.Style.HeaderStyle != nil { - styled = t.Style.HeaderStyle.Render(styled) - } - sb.WriteString(styled) - } - sb.WriteString("\n") - } - - // Rows - for _, row := range t.Rows { - for i, cell := range row { - if i > 0 { - sb.WriteString(sep) - } - styled := Pad(cell, widths[i]) - if t.Style.CellStyle != nil { - styled = t.Style.CellStyle.Render(styled) - } - sb.WriteString(styled) - } - sb.WriteString("\n") - } - - return sb.String() -} - -// Render prints the table to stdout. -func (t *Table) Render() { - fmt.Print(t.String()) -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/styles.go -git commit -m "refactor(cli): rewrite styles with zero-dep ANSI - -Deletes 672 lines of lipgloss code, replaces with ~150 lines. -Previous code available at 96eaed5 if needed. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.3: Rewrite output.go - -**Files:** -- Rewrite: `pkg/cli/output.go` (delete Style namespace, add semantic functions) - -**Step 1: Delete entire file content and rewrite** - -```go -package cli - -import ( - "fmt" - - "github.com/host-uk/core/pkg/i18n" -) - -// Blank prints an empty line. -func Blank() { - fmt.Println() -} - -// Echo translates a key via i18n.T and prints with newline. -// No automatic styling - use Success/Error/Warn/Info for styled output. -func Echo(key string, args ...any) { - fmt.Println(i18n.T(key, args...)) -} - -// Print outputs formatted text (no newline). -// Glyph shortcodes like :check: are converted. -func Print(format string, args ...any) { - fmt.Print(compileGlyphs(fmt.Sprintf(format, args...))) -} - -// Println outputs formatted text with newline. -// Glyph shortcodes like :check: are converted. -func Println(format string, args ...any) { - fmt.Println(compileGlyphs(fmt.Sprintf(format, args...))) -} - -// Success prints a success message with checkmark (green). -func Success(msg string) { - fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg)) -} - -// Successf prints a formatted success message. -func Successf(format string, args ...any) { - Success(fmt.Sprintf(format, args...)) -} - -// Error prints an error message with cross (red). -func Error(msg string) { - fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg)) -} - -// Errorf prints a formatted error message. -func Errorf(format string, args ...any) { - Error(fmt.Sprintf(format, args...)) -} - -// Warn prints a warning message with warning symbol (amber). -func Warn(msg string) { - fmt.Println(WarningStyle.Render(Glyph(":warn:") + " " + msg)) -} - -// Warnf prints a formatted warning message. -func Warnf(format string, args ...any) { - Warn(fmt.Sprintf(format, args...)) -} - -// Info prints an info message with info symbol (blue). -func Info(msg string) { - fmt.Println(InfoStyle.Render(Glyph(":info:") + " " + msg)) -} - -// Infof prints a formatted info message. -func Infof(format string, args ...any) { - Info(fmt.Sprintf(format, args...)) -} - -// Dim prints dimmed text. -func Dim(msg string) { - fmt.Println(DimStyle.Render(msg)) -} - -// Progress prints a progress indicator that overwrites the current line. -// Uses i18n.Progress for gerund form ("Checking..."). -func Progress(verb string, current, total int, item ...string) { - msg := i18n.Progress(verb) - if len(item) > 0 && item[0] != "" { - fmt.Printf("\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0]) - } else { - fmt.Printf("\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total) - } -} - -// ProgressDone clears the progress line. -func ProgressDone() { - fmt.Print("\033[2K\r") -} - -// Label prints a "Label: value" line. -func Label(word, value string) { - fmt.Printf("%s %s\n", KeyStyle.Render(i18n.Label(word)), value) -} - -// Scanln reads from stdin. -func Scanln(a ...any) (int, error) { - return fmt.Scanln(a...) -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/output.go -git commit -m "refactor(cli): rewrite output with semantic functions - -Replaces Style namespace with explicit Success/Error/Warn/Info. -Previous code available at 96eaed5 if needed. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.4: Rewrite strings.go - -**Files:** -- Rewrite: `pkg/cli/strings.go` (remove lipgloss import) - -**Step 1: Delete and rewrite** - -```go -package cli - -import "fmt" - -// Sprintf formats a string (fmt.Sprintf wrapper). -func Sprintf(format string, args ...any) string { - return fmt.Sprintf(format, args...) -} - -// Sprint formats using default formats (fmt.Sprint wrapper). -func Sprint(args ...any) string { - return fmt.Sprint(args...) -} - -// Styled returns text with a style applied. -func Styled(style *AnsiStyle, text string) string { - return style.Render(text) -} - -// Styledf returns formatted text with a style applied. -func Styledf(style *AnsiStyle, format string, args ...any) string { - return style.Render(fmt.Sprintf(format, args...)) -} - -// SuccessStr returns success-styled string. -func SuccessStr(msg string) string { - return SuccessStyle.Render(Glyph(":check:") + " " + msg) -} - -// ErrorStr returns error-styled string. -func ErrorStr(msg string) string { - return ErrorStyle.Render(Glyph(":cross:") + " " + msg) -} - -// WarnStr returns warning-styled string. -func WarnStr(msg string) string { - return WarningStyle.Render(Glyph(":warn:") + " " + msg) -} - -// InfoStr returns info-styled string. -func InfoStr(msg string) string { - return InfoStyle.Render(Glyph(":info:") + " " + msg) -} - -// DimStr returns dim-styled string. -func DimStr(msg string) string { - return DimStyle.Render(msg) -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/strings.go -git commit -m "refactor(cli): rewrite strings with zero-dep styling - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.5: Update errors.go - -**Files:** -- Modify: `pkg/cli/errors.go` - -**Step 1: Replace SymbolCross with Glyph** - -```go -// Before -fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg)) - -// After -fmt.Println(ErrorStyle.Render(Glyph(":cross:") + " " + msg)) -``` - -Apply to: `Fatalf`, `FatalWrap`, `FatalWrapVerb` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/errors.go -git commit -m "refactor(cli): update errors to use glyph system - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.6: Migrate pkg/php and pkg/vm - -**Files:** -- Modify: `pkg/php/cmd_quality.go` -- Modify: `pkg/php/cmd_dev.go` -- Modify: `pkg/php/cmd.go` -- Modify: `pkg/vm/cmd_vm.go` - -**Step 1: Replace lipgloss imports with cli** - -In each file: -- Remove `"github.com/charmbracelet/lipgloss"` import -- Replace `lipgloss.NewStyle()...` with `cli.NewStyle()...` -- Replace colour references: `lipgloss.Color(...)` → hex string - -**Step 2: Verify build** - -Run: `go build ./pkg/php/... ./pkg/vm/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/php/*.go pkg/vm/*.go -git commit -m "refactor(php,vm): migrate to cli ANSI styling - -Removes direct lipgloss imports. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.7: Simple Wizard Prompts - -**Files:** -- Create: `pkg/cli/prompt.go` -- Rewrite: `pkg/setup/cmd_wizard.go` - -**Step 1: Create prompt.go** - -```go -package cli - -import ( - "bufio" - "fmt" - "os" - "strconv" - "strings" -) - -var stdin = bufio.NewReader(os.Stdin) - -// Prompt asks for text input with a default value. -func Prompt(label, defaultVal string) (string, error) { - if defaultVal != "" { - fmt.Printf("%s [%s]: ", label, defaultVal) - } else { - fmt.Printf("%s: ", label) - } - - input, err := stdin.ReadString('\n') - if err != nil { - return "", err - } - - input = strings.TrimSpace(input) - if input == "" { - return defaultVal, nil - } - return input, nil -} - -// Confirm asks a yes/no question. -func Confirm(label string) (bool, error) { - fmt.Printf("%s [y/N]: ", label) - - input, err := stdin.ReadString('\n') - if err != nil { - return false, err - } - - input = strings.ToLower(strings.TrimSpace(input)) - return input == "y" || input == "yes", nil -} - -// Select presents numbered options and returns the selected value. -func Select(label string, options []string) (string, error) { - fmt.Println(label) - for i, opt := range options { - fmt.Printf(" %d. %s\n", i+1, opt) - } - fmt.Printf("Choose [1-%d]: ", len(options)) - - input, err := stdin.ReadString('\n') - if err != nil { - return "", err - } - - n, err := strconv.Atoi(strings.TrimSpace(input)) - if err != nil || n < 1 || n > len(options) { - return "", fmt.Errorf("invalid selection") - } - return options[n-1], nil -} - -// MultiSelect presents checkboxes (space-separated numbers). -func MultiSelect(label string, options []string) ([]string, error) { - fmt.Println(label) - for i, opt := range options { - fmt.Printf(" %d. %s\n", i+1, opt) - } - fmt.Printf("Choose (space-separated) [1-%d]: ", len(options)) - - input, err := stdin.ReadString('\n') - if err != nil { - return nil, err - } - - var selected []string - for _, s := range strings.Fields(input) { - n, err := strconv.Atoi(s) - if err != nil || n < 1 || n > len(options) { - continue - } - selected = append(selected, options[n-1]) - } - return selected, nil -} -``` - -**Step 2: Rewrite cmd_wizard.go to use simple prompts** - -Remove huh import, replace form calls with cli.Prompt/Confirm/Select/MultiSelect. - -**Step 3: Verify build** - -Run: `go build ./pkg/cli/... ./pkg/setup/...` -Expected: PASS - -**Step 4: Commit** - -```bash -git add pkg/cli/prompt.go pkg/setup/cmd_wizard.go -git commit -m "refactor(setup): replace huh with simple stdin prompts - -Removes ~30 transitive dependencies. -Previous wizard at 96eaed5 if needed. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task -1.8: Remove Charmbracelet from go.mod - -**Step 1: Run go mod tidy** - -```bash -go mod tidy -``` - -**Step 2: Verify no charmbracelet deps remain** - -Run: `grep charmbracelet go.mod` -Expected: No output - -**Step 3: Check binary size reduction** - -```bash -go build -o /tmp/core-new ./cmd/core-cli -ls -lh /tmp/core-new -``` - -**Step 4: Commit** - -```bash -git add go.mod go.sum -git commit -m "chore: remove charmbracelet dependencies - -Zero external dependencies for CLI output. -Binary size reduced. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -## Phase 0: HLCRF Layout System - -### Task 0.1: Layout Parser - -**Files:** -- Create: `pkg/cli/layout.go` - -**Step 1: Create layout.go** - -```go -package cli - -import "fmt" - -// Region represents one of the 5 HLCRF regions. -type Region rune - -const ( - RegionHeader Region = 'H' - RegionLeft Region = 'L' - RegionContent Region = 'C' - RegionRight Region = 'R' - RegionFooter Region = 'F' -) - -// Composite represents an HLCRF layout node. -type Composite struct { - variant string - path string - regions map[Region]*Slot - parent *Composite -} - -// Slot holds content for a region. -type Slot struct { - region Region - path string - blocks []Renderable - child *Composite -} - -// Renderable is anything that can be rendered to terminal. -type Renderable interface { - Render() string -} - -// StringBlock is a simple string that implements Renderable. -type StringBlock string - -func (s StringBlock) Render() string { return string(s) } - -// Layout creates a new layout from a variant string. -func Layout(variant string) *Composite { - c, err := ParseVariant(variant) - if err != nil { - return &Composite{variant: variant, regions: make(map[Region]*Slot)} - } - return c -} - -// ParseVariant parses a variant string like "H[LC]C[HCF]F". -func ParseVariant(variant string) (*Composite, error) { - c := &Composite{ - variant: variant, - path: "", - regions: make(map[Region]*Slot), - } - - i := 0 - for i < len(variant) { - r := Region(variant[i]) - if !isValidRegion(r) { - return nil, fmt.Errorf("invalid region: %c", r) - } - - slot := &Slot{region: r, path: string(r)} - c.regions[r] = slot - i++ - - if i < len(variant) && variant[i] == '[' { - end := findMatchingBracket(variant, i) - if end == -1 { - return nil, fmt.Errorf("unmatched bracket at %d", i) - } - nested, err := ParseVariant(variant[i+1 : end]) - if err != nil { - return nil, err - } - nested.path = string(r) + "-" - nested.parent = c - slot.child = nested - i = end + 1 - } - } - return c, nil -} - -func isValidRegion(r Region) bool { - return r == 'H' || r == 'L' || r == 'C' || r == 'R' || r == 'F' -} - -func findMatchingBracket(s string, start int) int { - depth := 0 - for i := start; i < len(s); i++ { - if s[i] == '[' { - depth++ - } else if s[i] == ']' { - depth-- - if depth == 0 { - return i - } - } - } - return -1 -} - -// H adds content to Header region. -func (c *Composite) H(items ...any) *Composite { c.addToRegion(RegionHeader, items...); return c } - -// L adds content to Left region. -func (c *Composite) L(items ...any) *Composite { c.addToRegion(RegionLeft, items...); return c } - -// C adds content to Content region. -func (c *Composite) C(items ...any) *Composite { c.addToRegion(RegionContent, items...); return c } - -// R adds content to Right region. -func (c *Composite) R(items ...any) *Composite { c.addToRegion(RegionRight, items...); return c } - -// F adds content to Footer region. -func (c *Composite) F(items ...any) *Composite { c.addToRegion(RegionFooter, items...); return c } - -func (c *Composite) addToRegion(r Region, items ...any) { - slot, ok := c.regions[r] - if !ok { - return - } - for _, item := range items { - slot.blocks = append(slot.blocks, toRenderable(item)) - } -} - -func toRenderable(item any) Renderable { - switch v := item.(type) { - case Renderable: - return v - case string: - return StringBlock(v) - default: - return StringBlock(fmt.Sprint(v)) - } -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/layout.go -git commit -m "feat(cli): add HLCRF layout parser - -Implements RFC-001 compositor pattern for terminal output. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task 0.2: Terminal Renderer - -**Files:** -- Create: `pkg/cli/render.go` - -**Step 1: Create render.go** - -```go -package cli - -import ( - "fmt" - "strings" -) - -// RenderStyle controls how layouts are rendered. -type RenderStyle int - -const ( - RenderFlat RenderStyle = iota // No borders - RenderSimple // --- separators - RenderBoxed // Unicode box drawing -) - -var currentRenderStyle = RenderFlat - -func UseRenderFlat() { currentRenderStyle = RenderFlat } -func UseRenderSimple() { currentRenderStyle = RenderSimple } -func UseRenderBoxed() { currentRenderStyle = RenderBoxed } - -// Render outputs the layout to terminal. -func (c *Composite) Render() { - fmt.Print(c.String()) -} - -// String returns the rendered layout. -func (c *Composite) String() string { - var sb strings.Builder - c.renderTo(&sb, 0) - return sb.String() -} - -func (c *Composite) renderTo(sb *strings.Builder, depth int) { - order := []Region{RegionHeader, RegionLeft, RegionContent, RegionRight, RegionFooter} - - var active []Region - for _, r := range order { - if slot, ok := c.regions[r]; ok { - if len(slot.blocks) > 0 || slot.child != nil { - active = append(active, r) - } - } - } - - for i, r := range active { - slot := c.regions[r] - if i > 0 && currentRenderStyle != RenderFlat { - c.renderSeparator(sb, depth) - } - c.renderSlot(sb, slot, depth) - } -} - -func (c *Composite) renderSeparator(sb *strings.Builder, depth int) { - indent := strings.Repeat(" ", depth) - switch currentRenderStyle { - case RenderBoxed: - sb.WriteString(indent + "├" + strings.Repeat("─", 40) + "┤\n") - case RenderSimple: - sb.WriteString(indent + strings.Repeat("─", 40) + "\n") - } -} - -func (c *Composite) renderSlot(sb *strings.Builder, slot *Slot, depth int) { - indent := strings.Repeat(" ", depth) - for _, block := range slot.blocks { - for _, line := range strings.Split(block.Render(), "\n") { - if line != "" { - sb.WriteString(indent + line + "\n") - } - } - } - if slot.child != nil { - slot.child.renderTo(sb, depth+1) - } -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/render.go -git commit -m "feat(cli): add HLCRF terminal renderer - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -## Phase 1: Glyph System - -### Task 1.1: Glyph Core - -**Files:** -- Create: `pkg/cli/glyph.go` - -**Step 1: Create glyph.go** - -```go -package cli - -import ( - "bytes" - "unicode" -) - -// GlyphTheme defines which symbols to use. -type GlyphTheme int - -const ( - ThemeUnicode GlyphTheme = iota - ThemeEmoji - ThemeASCII -) - -var currentTheme = ThemeUnicode - -func UseUnicode() { currentTheme = ThemeUnicode } -func UseEmoji() { currentTheme = ThemeEmoji } -func UseASCII() { currentTheme = ThemeASCII } - -func glyphMap() map[string]string { - switch currentTheme { - case ThemeEmoji: - return glyphMapEmoji - case ThemeASCII: - return glyphMapASCII - default: - return glyphMapUnicode - } -} - -// Glyph converts a shortcode to its symbol. -func Glyph(code string) string { - if sym, ok := glyphMap()[code]; ok { - return sym - } - return code -} - -func compileGlyphs(x string) string { - if x == "" { - return "" - } - input := bytes.NewBufferString(x) - output := bytes.NewBufferString("") - - for { - r, _, err := input.ReadRune() - if err != nil { - break - } - if r == ':' { - output.WriteString(replaceGlyph(input)) - } else { - output.WriteRune(r) - } - } - return output.String() -} - -func replaceGlyph(input *bytes.Buffer) string { - code := bytes.NewBufferString(":") - for { - r, _, err := input.ReadRune() - if err != nil { - return code.String() - } - if r == ':' && code.Len() == 1 { - return code.String() + replaceGlyph(input) - } - code.WriteRune(r) - if unicode.IsSpace(r) { - return code.String() - } - if r == ':' { - return Glyph(code.String()) - } - } -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/glyph.go -git commit -m "feat(cli): add glyph shortcode system - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task 1.2: Glyph Maps - -**Files:** -- Create: `pkg/cli/glyph_maps.go` - -**Step 1: Create glyph_maps.go** - -```go -package cli - -var glyphMapUnicode = map[string]string{ - ":check:": "✓", ":cross:": "✗", ":warn:": "⚠", ":info:": "ℹ", - ":question:": "?", ":skip:": "○", ":dot:": "●", ":circle:": "◯", - ":arrow_right:": "→", ":arrow_left:": "←", ":arrow_up:": "↑", ":arrow_down:": "↓", - ":pointer:": "▶", ":bullet:": "•", ":dash:": "─", ":pipe:": "│", - ":corner:": "└", ":tee:": "├", ":pending:": "…", ":spinner:": "⠋", -} - -var glyphMapEmoji = map[string]string{ - ":check:": "✅", ":cross:": "❌", ":warn:": "⚠️", ":info:": "ℹ️", - ":question:": "❓", ":skip:": "⏭️", ":dot:": "🔵", ":circle:": "⚪", - ":arrow_right:": "➡️", ":arrow_left:": "⬅️", ":arrow_up:": "⬆️", ":arrow_down:": "⬇️", - ":pointer:": "▶️", ":bullet:": "•", ":dash:": "─", ":pipe:": "│", - ":corner:": "└", ":tee:": "├", ":pending:": "⏳", ":spinner:": "🔄", -} - -var glyphMapASCII = map[string]string{ - ":check:": "[OK]", ":cross:": "[FAIL]", ":warn:": "[WARN]", ":info:": "[INFO]", - ":question:": "[?]", ":skip:": "[SKIP]", ":dot:": "[*]", ":circle:": "[ ]", - ":arrow_right:": "->", ":arrow_left:": "<-", ":arrow_up:": "^", ":arrow_down:": "v", - ":pointer:": ">", ":bullet:": "*", ":dash:": "-", ":pipe:": "|", - ":corner:": "`", ":tee:": "+", ":pending:": "...", ":spinner:": "-", -} -``` - -**Step 2: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/glyph_maps.go -git commit -m "feat(cli): add glyph maps for unicode/emoji/ascii - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -## Phase 2: DX-Focused Semantic Output - -### Task 2.0: Semantic Patterns for Consuming Packages - -**Files:** -- Create: `pkg/cli/check.go` -- Modify: `pkg/cli/output.go` - -**Goal:** Eliminate display logic from consuming packages. Only `cli` knows about styling. - -**Step 1: Create check.go with fluent Check builder** - -```go -package cli - -import "fmt" - -// CheckBuilder provides fluent API for check results. -type CheckBuilder struct { - name string - status string - style *AnsiStyle - icon string - duration string -} - -// Check starts building a check result line. -// -// cli.Check("audit").Pass() -// cli.Check("fmt").Fail().Duration("2.3s") -// cli.Check("test").Skip() -func Check(name string) *CheckBuilder { - return &CheckBuilder{name: name} -} - -// Pass marks the check as passed. -func (c *CheckBuilder) Pass() *CheckBuilder { - c.status = "passed" - c.style = SuccessStyle - c.icon = Glyph(":check:") - return c -} - -// Fail marks the check as failed. -func (c *CheckBuilder) Fail() *CheckBuilder { - c.status = "failed" - c.style = ErrorStyle - c.icon = Glyph(":cross:") - return c -} - -// Skip marks the check as skipped. -func (c *CheckBuilder) Skip() *CheckBuilder { - c.status = "skipped" - c.style = DimStyle - c.icon = "-" - return c -} - -// Warn marks the check as warning. -func (c *CheckBuilder) Warn() *CheckBuilder { - c.status = "warning" - c.style = WarningStyle - c.icon = Glyph(":warn:") - return c -} - -// Duration adds duration to the check result. -func (c *CheckBuilder) Duration(d string) *CheckBuilder { - c.duration = d - return c -} - -// Message adds a custom message instead of status. -func (c *CheckBuilder) Message(msg string) *CheckBuilder { - c.status = msg - return c -} - -// String returns the formatted check line. -func (c *CheckBuilder) String() string { - icon := c.icon - if c.style != nil { - icon = c.style.Render(c.icon) - } - - status := c.status - if c.style != nil && c.status != "" { - status = c.style.Render(c.status) - } - - if c.duration != "" { - return fmt.Sprintf(" %s %-20s %-10s %s", icon, c.name, status, DimStyle.Render(c.duration)) - } - if status != "" { - return fmt.Sprintf(" %s %s %s", icon, c.name, status) - } - return fmt.Sprintf(" %s %s", icon, c.name) -} - -// Print outputs the check result. -func (c *CheckBuilder) Print() { - fmt.Println(c.String()) -} -``` - -**Step 2: Add semantic output functions to output.go** - -```go -// Task prints a task header: "[label] message" -// -// cli.Task("php", "Running tests...") // [php] Running tests... -// cli.Task("go", i18n.Progress("build")) // [go] Building... -func Task(label, message string) { - fmt.Printf("%s %s\n\n", DimStyle.Render("["+label+"]"), message) -} - -// Section prints a section header: "── SECTION ──" -// -// cli.Section("audit") // ── AUDIT ── -func Section(name string) { - header := "── " + strings.ToUpper(name) + " ──" - fmt.Println(AccentStyle.Render(header)) -} - -// Hint prints a labelled hint: "label: message" -// -// cli.Hint("install", "composer require vimeo/psalm") -// cli.Hint("fix", "core php fmt --fix") -func Hint(label, message string) { - fmt.Printf(" %s %s\n", DimStyle.Render(label+":"), message) -} - -// Severity prints a severity-styled message. -// -// cli.Severity("critical", "SQL injection") // red, bold -// cli.Severity("high", "XSS vulnerability") // orange -// cli.Severity("medium", "Missing CSRF") // amber -// cli.Severity("low", "Debug enabled") // gray -func Severity(level, message string) { - var style *AnsiStyle - switch strings.ToLower(level) { - case "critical": - style = NewStyle().Bold().Foreground(ColourRed500) - case "high": - style = NewStyle().Bold().Foreground(ColourOrange500) - case "medium": - style = NewStyle().Foreground(ColourAmber500) - case "low": - style = NewStyle().Foreground(ColourGray500) - default: - style = DimStyle - } - fmt.Printf(" %s %s\n", style.Render("["+level+"]"), message) -} - -// Result prints a result line: "✓ message" or "✗ message" -// -// cli.Result(passed, "All tests passed") -// cli.Result(false, "3 tests failed") -func Result(passed bool, message string) { - if passed { - Success(message) - } else { - Error(message) - } -} -``` - -**Step 3: Add strings import to output.go** - -```go -import ( - "fmt" - "strings" - - "github.com/host-uk/core/pkg/i18n" -) -``` - -**Step 4: Verify build** - -Run: `go build ./pkg/cli/...` -Expected: PASS - -**Step 5: Commit** - -```bash -git add pkg/cli/check.go pkg/cli/output.go -git commit -m "feat(cli): add DX-focused semantic output patterns - -- Check() fluent builder for check results -- Task() for task headers -- Section() for section headers -- Hint() for labelled hints -- Severity() for severity-styled output -- Result() for pass/fail results - -Consuming packages now have zero display logic. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -## Phase 3: Full Migration - -### Task 3.1: Migrate All pkg/* Files - -**Files:** All files in pkg/ that use: -- `i18n.T()` directly (should use `cli.Echo()`) -- `lipgloss.*` (should use `cli.*Style`) -- `fmt.Printf/Println` for output (should use `cli.Print/Println`) - -**Step 1: Find all files needing migration** - -```bash -grep -r "i18n\.T\|lipgloss\|fmt\.Print" pkg/ --include="*.go" | grep -v "pkg/cli/" | grep -v "_test.go" -``` - -**Step 2: Migrate each file** - -Pattern replacements: -- `fmt.Printf(...)` → `cli.Print(...)` -- `fmt.Println(...)` → `cli.Println(...)` -- `i18n.T("key")` → `cli.Echo("key")` or keep for values -- `successStyle.Render(...)` → `cli.SuccessStyle.Render(...)` - -**Step 3: Verify build** - -Run: `go build ./...` -Expected: PASS - -**Step 4: Commit** - -```bash -git add pkg/ -git commit -m "refactor: migrate all pkg/* to cli abstraction - -No direct fmt/i18n/lipgloss imports outside pkg/cli. - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task 3.2: Tests - -**Files:** -- Create: `pkg/cli/ansi_test.go` -- Create: `pkg/cli/glyph_test.go` -- Create: `pkg/cli/layout_test.go` - -**Step 1: Write tests** - -```go -// ansi_test.go -package cli - -import "testing" - -func TestAnsiStyle_Render(t *testing.T) { - s := NewStyle().Bold().Foreground("#ff0000") - got := s.Render("test") - if got == "test" { - t.Error("Expected styled output") - } - if !contains(got, "test") { - t.Error("Output should contain text") - } -} - -func contains(s, sub string) bool { - return len(s) >= len(sub) && s[len(s)-len(sub)-4:len(s)-4] == sub -} -``` - -**Step 2: Run tests** - -Run: `go test ./pkg/cli/... -v` -Expected: PASS - -**Step 3: Commit** - -```bash -git add pkg/cli/*_test.go -git commit -m "test(cli): add unit tests for ANSI, glyph, layout - -Co-Authored-By: Claude Opus 4.5 " -``` - ---- - -### Task 3.3: Final Verification - -**Step 1: Full build** - -Run: `go build ./...` -Expected: PASS - -**Step 2: All tests** - -Run: `go test ./...` -Expected: PASS - -**Step 3: Verify zero charmbracelet** - -Run: `grep charmbracelet go.mod` -Expected: No output - -**Step 4: Binary test** - -Run: `./bin/core dev health` -Expected: Output displays correctly - ---- - -## Summary of New API - -| Function | Purpose | -|----------|---------| -| `cli.Blank()` | Empty line | -| `cli.Echo(key, args...)` | Translate + print | -| `cli.Print(fmt, args...)` | Printf with glyphs | -| `cli.Println(fmt, args...)` | Println with glyphs | -| `cli.Success(msg)` | ✓ green | -| `cli.Error(msg)` | ✗ red | -| `cli.Warn(msg)` | ⚠ amber | -| `cli.Info(msg)` | ℹ blue | -| `cli.Dim(msg)` | Dimmed text | -| `cli.Progress(verb, n, total)` | Overwriting progress | -| `cli.ProgressDone()` | Clear progress | -| `cli.Label(word, value)` | "Label: value" | -| `cli.Prompt(label, default)` | Text input | -| `cli.Confirm(label)` | y/n | -| `cli.Select(label, opts)` | Numbered list | -| `cli.MultiSelect(label, opts)` | Multi-select | -| `cli.Glyph(code)` | Get symbol | -| `cli.UseUnicode/Emoji/ASCII()` | Set theme | -| `cli.Layout(variant)` | HLCRF layout | -| `cli.NewTable(headers...)` | Create table | -| `cli.FormatAge(time)` | "2h ago" | -| `cli.Truncate(s, max)` | Ellipsis truncation | -| `cli.Pad(s, width)` | Right-pad string | -| **DX Patterns** | | -| `cli.Task(label, msg)` | `[php] Running...` | -| `cli.Section(name)` | `── AUDIT ──` | -| `cli.Check(name).Pass/Fail/Skip()` | Fluent check result | -| `cli.Hint(label, msg)` | `install: composer...` | -| `cli.Severity(level, msg)` | Critical/high/med/low | -| `cli.Result(ok, msg)` | Pass/fail result | diff --git a/go.mod b/go.mod index 04725dc..908a27c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,17 @@ go 1.25.5 require ( github.com/Snider/Borg v0.1.0 github.com/getkin/kin-openapi v0.133.0 + github.com/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1 github.com/leaanthony/debme v1.2.1 github.com/leaanthony/gosod v1.0.4 github.com/minio/selfupdate v0.6.0 github.com/modelcontextprotocol/go-sdk v1.2.0 github.com/oasdiff/oasdiff v1.11.8 + github.com/ollama/ollama v0.15.4 + github.com/qdrant/go-client v1.16.2 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.47.0 golang.org/x/mod v0.32.0 golang.org/x/net v0.49.0 golang.org/x/oauth2 v0.34.0 @@ -27,6 +31,8 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/TwiN/go-color v1.4.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -36,8 +42,10 @@ require ( github.com/go-git/go-git/v5 v5.16.4 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -51,6 +59,7 @@ require ( github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sergi/go-diff v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tidwall/gjson v1.18.0 // indirect @@ -58,13 +67,18 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/wI2L/jsondiff v0.7.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yargevad/filepathx v1.0.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 1402b11..b29d4f7 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,10 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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/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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -43,6 +47,10 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj 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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= @@ -51,14 +59,22 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6 github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -69,6 +85,8 @@ github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PW 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/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1 h1:x1cSEj4Ug5mpuZgUHLvUmlc5r//KHFn6iYiRSrRcVy4= +github.com/kluctl/go-embed-python v0.0.0-3.13.1-20241219-1/go.mod h1:3ebNU9QBrNpUO+Hj6bHaGpkh5pymDHQ+wwVPHTE4mCE= 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= @@ -100,6 +118,8 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/ollama/ollama v0.15.4 h1:y841GH5lsi5j5BTFyX/E+UOC3Yiw+JBfdjBVRGw+I0M= +github.com/ollama/ollama v0.15.4/go.mod h1:4Yn3jw2hZ4VqyJ1XciYawDRE8bzv4RT3JiVZR1kCfwE= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -111,12 +131,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/qdrant/go-client v1.16.2 h1:UUMJJfvXTByhwhH1DwWdbkhZ2cTdvSqVkXSIfBrVWSg= +github.com/qdrant/go-client v1.16.2/go.mod h1:I+EL3h4HRoRTeHtbfOd/4kDXwCukZfkd41j/9wryGkw= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -127,6 +151,7 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -142,8 +167,12 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= +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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -152,6 +181,18 @@ github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5 github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= 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= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -169,6 +210,8 @@ 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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -190,6 +233,14 @@ 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= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= @@ -198,5 +249,6 @@ 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/install.bat b/install.bat new file mode 100644 index 0000000..8f8a4ee --- /dev/null +++ b/install.bat @@ -0,0 +1,170 @@ +@echo off +REM Core CLI unified installer (Windows) +REM Served via *.core.help with BunnyCDN edge transformation +REM +REM Usage: +REM curl -fsSL setup.core.help -o install.bat && install.bat # Interactive (default) +REM curl -fsSL ci.core.help -o install.bat && install.bat # CI/CD +REM curl -fsSL dev.core.help -o install.bat && install.bat # Full development +REM curl -fsSL go.core.help -o install.bat && install.bat # Go variant +REM curl -fsSL php.core.help -o install.bat && install.bat # PHP variant +REM curl -fsSL agent.core.help -o install.bat && install.bat # Agent variant +REM +setlocal enabledelayedexpansion + +REM === BunnyCDN Edge Variables (transformed at edge based on subdomain) === +set "MODE={{CORE_MODE}}" +set "VARIANT={{CORE_VARIANT}}" + +REM === Fallback for local testing === +if "!MODE!"=="{{CORE_MODE}}" ( + if defined CORE_MODE (set "MODE=!CORE_MODE!") else (set "MODE=setup") +) +if "!VARIANT!"=="{{CORE_VARIANT}}" ( + if defined CORE_VARIANT (set "VARIANT=!CORE_VARIANT!") else (set "VARIANT=") +) + +REM === Configuration === +set "VERSION=%~1" +if "%VERSION%"=="" set "VERSION=latest" +set "REPO=host-uk/core" +set "BINARY=core" +set "INSTALL_DIR=%LOCALAPPDATA%\Programs\core" + +REM === Resolve Version === +if "%VERSION%"=="latest" ( + for /f "tokens=2 delims=:" %%a in ('curl -fsSL --max-time 10 "https://api.github.com/repos/%REPO%/releases/latest" ^| findstr "tag_name"') do ( + set "VERSION=%%a" + set "VERSION=!VERSION:"=!" + set "VERSION=!VERSION: =!" + set "VERSION=!VERSION:,=!" + ) + if "!VERSION!"=="" ( + echo ERROR: Failed to fetch latest version + exit /b 1 + ) + if "!VERSION!"=="latest" ( + echo ERROR: Failed to resolve version + exit /b 1 + ) +) + +REM === Create install directory === +if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" + +REM === Mode dispatch === +if "%MODE%"=="ci" goto :install_ci +if "%MODE%"=="dev" goto :install_dev +if "%MODE%"=="variant" goto :install_variant +goto :install_setup + +:install_setup +echo Installing %BINARY% !VERSION! for Windows... +call :find_archive "" ARCHIVE +if errorlevel 1 exit /b 1 +call :download_and_extract +if errorlevel 1 exit /b 1 +call :install_binary +if errorlevel 1 exit /b 1 +call :verify_install +if errorlevel 1 exit /b 1 +goto :done + +:install_ci +echo Installing %BINARY% !VERSION! (CI)... +call :find_archive "" ARCHIVE +if errorlevel 1 exit /b 1 +call :download_and_extract +if errorlevel 1 exit /b 1 +call :install_binary +if errorlevel 1 exit /b 1 + +%BINARY% --version +if errorlevel 1 exit /b 1 +goto :done + +:install_dev +echo Installing %BINARY% !VERSION! (full) for Windows... +call :find_archive "" ARCHIVE +if errorlevel 1 exit /b 1 +call :download_and_extract +if errorlevel 1 exit /b 1 +call :install_binary +if errorlevel 1 exit /b 1 +call :verify_install +if errorlevel 1 exit /b 1 +echo. +echo Full development variant installed. Available commands: +echo core dev - Multi-repo workflows +echo core build - Cross-platform builds +echo core release - Build and publish releases +goto :done + +:install_variant +echo Installing %BINARY% !VERSION! (%VARIANT% variant) for Windows... +call :find_archive "%VARIANT%" ARCHIVE +if errorlevel 1 exit /b 1 +call :download_and_extract +if errorlevel 1 exit /b 1 +call :install_binary +if errorlevel 1 exit /b 1 +call :verify_install +if errorlevel 1 exit /b 1 +goto :done + +REM === Helper Functions === + +:find_archive +set "_variant=%~1" +set "_result=%~2" + +REM Try variant-specific first, then full +if not "%_variant%"=="" ( + set "_try=%BINARY%-%_variant%-windows-amd64.zip" + curl -fsSLI --max-time 10 "https://github.com/%REPO%/releases/download/!VERSION!/!_try!" 2>nul | findstr /r "HTTP/[12].* [23][0-9][0-9]" >nul + if not errorlevel 1 ( + set "%_result%=!_try!" + exit /b 0 + ) + echo Using full variant ^(%_variant% variant not available^) +) + +set "%_result%=%BINARY%-windows-amd64.zip" +exit /b 0 + +:download_and_extract +curl -fsSL --connect-timeout 10 "https://github.com/%REPO%/releases/download/!VERSION!/!ARCHIVE!" -o "%TEMP%\!ARCHIVE!" +if errorlevel 1 ( + echo ERROR: Failed to download !ARCHIVE! + exit /b 1 +) + +powershell -Command "try { Expand-Archive -Force '%TEMP%\!ARCHIVE!' '%INSTALL_DIR%' } catch { exit 1 }" +if errorlevel 1 ( + echo ERROR: Failed to extract archive + del "%TEMP%\!ARCHIVE!" 2>nul + exit /b 1 +) +del "%TEMP%\!ARCHIVE!" 2>nul +exit /b 0 + +:install_binary +REM Add to PATH using PowerShell (avoids setx 1024 char limit) +echo %PATH% | findstr /i /c:"%INSTALL_DIR%" >nul +if errorlevel 1 ( + powershell -Command "[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', 'User') + ';%INSTALL_DIR%', 'User')" + set "PATH=%PATH%;%INSTALL_DIR%" +) +exit /b 0 + +:verify_install +if not exist "%INSTALL_DIR%\%BINARY%.exe" ( + echo ERROR: Installation failed - binary not found + exit /b 1 +) +"%INSTALL_DIR%\%BINARY%.exe" --version +if errorlevel 1 exit /b 1 +exit /b 0 + +:done +endlocal \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ecb879f --- /dev/null +++ b/install.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# Core CLI unified installer +# Served via *.core.help with BunnyCDN edge transformation +# +# Usage: +# curl -fsSL setup.core.help | bash # Interactive setup (default) +# curl -fsSL ci.core.help | bash # CI/CD (minimal, fast) +# curl -fsSL dev.core.help | bash # Full development +# curl -fsSL go.core.help | bash # Go development variant +# curl -fsSL php.core.help | bash # PHP/Laravel variant +# curl -fsSL agent.core.help | bash # AI agent variant +# +# Version override: +# curl -fsSL setup.core.help | bash -s -- v1.0.0 +# +set -eo pipefail + +# === BunnyCDN Edge Variables (transformed at edge based on subdomain) === +MODE="{{CORE_MODE}}" # setup, ci, dev, variant +VARIANT="{{CORE_VARIANT}}" # go, php, agent (when MODE=variant) + +# === User overrides (fallback for local testing) === +[[ "$MODE" == "{{CORE_MODE}}" ]] && MODE="${CORE_MODE:-setup}" +[[ "$VARIANT" == "{{CORE_VARIANT}}" ]] && VARIANT="${CORE_VARIANT:-}" + +# === Configuration === +VERSION="${1:-latest}" +REPO="host-uk/core" +BINARY="core" + +# === Colours === +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +DIM='\033[2m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${BLUE}>>>${NC} $1"; } +success() { echo -e "${GREEN}>>>${NC} $1"; } +error() { echo -e "${RED}>>>${NC} $1" >&2; exit 1; } +dim() { echo -e "${DIM}$1${NC}"; } + +# === Platform Detection === +detect_platform() { + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + ARCH="$(uname -m)" + + case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH" ;; + esac + + case "$OS" in + darwin|linux) ;; + *) error "Unsupported OS: $OS (use Windows installer for Windows)" ;; + esac +} + +# === Version Resolution === +resolve_version() { + if [ "$VERSION" = "latest" ]; then + info "Fetching latest version..." + VERSION=$(curl -fsSL --max-time 10 "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$VERSION" ]; then + error "Failed to fetch latest version from GitHub API" + fi + fi +} + +# === Download Helpers === +url_exists() { + curl -fsSLI "$1" 2>/dev/null | grep -qE "HTTP/.* [23][0-9][0-9]" +} + +find_archive() { + local base="$1" + local variant="$2" + + # Build candidate list (prefer xz over gz, variant over full) + local candidates=() + if [ -n "$variant" ]; then + candidates+=("${base}-${variant}-${OS}-${ARCH}.tar.xz") + candidates+=("${base}-${variant}-${OS}-${ARCH}.tar.gz") + fi + candidates+=("${base}-${OS}-${ARCH}.tar.xz") + candidates+=("${base}-${OS}-${ARCH}.tar.gz") + + for archive in "${candidates[@]}"; do + local url="https://github.com/${REPO}/releases/download/${VERSION}/${archive}" + if url_exists "$url"; then + ARCHIVE="$archive" + DOWNLOAD_URL="$url" + return 0 + fi + done + + error "No compatible archive found for ${OS}/${ARCH}" +} + +download_and_extract() { + WORK_DIR=$(mktemp -d) + trap 'rm -rf "$WORK_DIR"' EXIT + + info "Downloading ${ARCHIVE}..." + if ! curl -fsSL --max-time 120 "$DOWNLOAD_URL" -o "$WORK_DIR/$ARCHIVE"; then + error "Failed to download ${DOWNLOAD_URL}" + fi + + info "Extracting..." + case "$ARCHIVE" in + *.tar.xz) tar -xJf "$WORK_DIR/$ARCHIVE" -C "$WORK_DIR" || error "Failed to extract archive" ;; + *.tar.gz) tar -xzf "$WORK_DIR/$ARCHIVE" -C "$WORK_DIR" || error "Failed to extract archive" ;; + *) error "Unknown archive format: $ARCHIVE" ;; + esac +} + +install_binary() { + local install_dir="${1:-/usr/local/bin}" + + info "Installing to ${install_dir}..." + chmod +x "$WORK_DIR/${BINARY}" + if [ -w "$install_dir" ]; then + mv "$WORK_DIR/${BINARY}" "${install_dir}/${BINARY}" + else + sudo mv "$WORK_DIR/${BINARY}" "${install_dir}/${BINARY}" + fi +} + +verify_install() { + if command -v "$BINARY" &>/dev/null; then + success "Installed successfully!" + dim "$($BINARY --version)" + else + success "Installed to ${1:-/usr/local/bin}/${BINARY}" + dim "Add the directory to your PATH if not already present" + fi +} + +# === Installation Modes === + +install_setup() { + echo -e "${BOLD}Core CLI Installer${NC}" + echo "" + + detect_platform + resolve_version + + local install_dir="/usr/local/bin" + info "Installing ${BINARY} ${VERSION} for ${OS}/${ARCH}..." + find_archive "$BINARY" "" + download_and_extract + install_binary "$install_dir" + verify_install "$install_dir" +} + +install_ci() { + detect_platform + resolve_version + + echo "Installing ${BINARY} ${VERSION} (${OS}/${ARCH})..." + find_archive "$BINARY" "" + download_and_extract + + # CI: prefer /usr/local/bin, no sudo prompts + chmod +x "$WORK_DIR/${BINARY}" + if [ -w /usr/local/bin ]; then + mv "$WORK_DIR/${BINARY}" /usr/local/bin/ + else + sudo mv "$WORK_DIR/${BINARY}" /usr/local/bin/ + fi + + /usr/local/bin/${BINARY} --version +} + +install_dev() { + detect_platform + resolve_version + + local install_dir="/usr/local/bin" + info "Installing ${BINARY} ${VERSION} (full) for ${OS}/${ARCH}..." + find_archive "$BINARY" "" + download_and_extract + install_binary "$install_dir" + verify_install "$install_dir" + + echo "" + echo "Full development variant installed. Available commands:" + echo " core dev - Multi-repo workflows" + echo " core build - Cross-platform builds" + echo " core release - Build and publish releases" +} + +install_variant() { + local variant="$1" + + detect_platform + resolve_version + + local install_dir="/usr/local/bin" + info "Installing ${BINARY} ${VERSION} (${variant} variant) for ${OS}/${ARCH}..." + find_archive "$BINARY" "$variant" + + if [[ "$ARCHIVE" == "${BINARY}-${OS}-${ARCH}"* ]]; then + dim "Using full variant (${variant} variant not available for ${VERSION})" + fi + + download_and_extract + install_binary "$install_dir" + verify_install "$install_dir" +} + +# === Main === +case "$MODE" in + setup) install_setup ;; + ci) install_ci ;; + dev) install_dev ;; + variant) + [ -z "$VARIANT" ] && error "VARIANT must be specified when MODE=variant" + install_variant "$VARIANT" + ;; + *) error "Unknown mode: $MODE" ;; +esac diff --git a/internal/cmd/ai/cmd_commands.go b/internal/cmd/ai/cmd_commands.go index 45e5aaf..1cf7dad 100644 --- a/internal/cmd/ai/cmd_commands.go +++ b/internal/cmd/ai/cmd_commands.go @@ -8,9 +8,12 @@ // - task:commit: Create commits with task references // - task:pr: Create pull requests linked to tasks // - claude: Claude Code CLI integration (planned) +// - rag: RAG tools (ingest, query, collections) +// - metrics: View AI/security event metrics package ai import ( + ragcmd "github.com/host-uk/core/internal/cmd/rag" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" ) @@ -57,6 +60,12 @@ func initCommands() { // Add agentic task commands AddAgenticCommands(aiCmd) + + // Add RAG subcommands (core ai rag ...) + ragcmd.AddRAGSubcommands(aiCmd) + + // Add metrics subcommand (core ai metrics) + addMetricsCommand(aiCmd) } // AddAICommands registers the 'ai' command and all subcommands. diff --git a/internal/cmd/ai/cmd_metrics.go b/internal/cmd/ai/cmd_metrics.go new file mode 100644 index 0000000..376e990 --- /dev/null +++ b/internal/cmd/ai/cmd_metrics.go @@ -0,0 +1,131 @@ +// cmd_metrics.go implements the metrics viewing command. + +package ai + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/host-uk/core/pkg/ai" + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/i18n" +) + +var ( + metricsSince string + metricsJSON bool +) + +var metricsCmd = &cli.Command{ + Use: "metrics", + Short: i18n.T("cmd.ai.metrics.short"), + Long: i18n.T("cmd.ai.metrics.long"), + RunE: func(cmd *cli.Command, args []string) error { + return runMetrics() + }, +} + +func initMetricsFlags() { + metricsCmd.Flags().StringVar(&metricsSince, "since", "7d", i18n.T("cmd.ai.metrics.flag.since")) + metricsCmd.Flags().BoolVar(&metricsJSON, "json", false, i18n.T("common.flag.json")) +} + +func addMetricsCommand(parent *cli.Command) { + initMetricsFlags() + parent.AddCommand(metricsCmd) +} + +func runMetrics() error { + since, err := parseDuration(metricsSince) + if err != nil { + return cli.Err("invalid --since value %q: %v", metricsSince, err) + } + + sinceTime := time.Now().Add(-since) + events, err := ai.ReadEvents(sinceTime) + if err != nil { + return cli.WrapVerb(err, "read", "metrics") + } + + if metricsJSON { + summary := ai.Summary(events) + output, err := json.MarshalIndent(summary, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") + } + cli.Text(string(output)) + return nil + } + + summary := ai.Summary(events) + + cli.Blank() + cli.Print("%s %s\n", dimStyle.Render("Period:"), metricsSince) + total, _ := summary["total"].(int) + cli.Print("%s %d\n", dimStyle.Render("Total events:"), total) + cli.Blank() + + // By type + if byType, ok := summary["by_type"].([]map[string]any); ok && len(byType) > 0 { + cli.Print("%s\n", dimStyle.Render("By type:")) + for _, entry := range byType { + cli.Print(" %-30s %v\n", entry["key"], entry["count"]) + } + cli.Blank() + } + + // By repo + if byRepo, ok := summary["by_repo"].([]map[string]any); ok && len(byRepo) > 0 { + cli.Print("%s\n", dimStyle.Render("By repo:")) + for _, entry := range byRepo { + cli.Print(" %-30s %v\n", entry["key"], entry["count"]) + } + cli.Blank() + } + + // By agent + if byAgent, ok := summary["by_agent"].([]map[string]any); ok && len(byAgent) > 0 { + cli.Print("%s\n", dimStyle.Render("By contributor:")) + for _, entry := range byAgent { + cli.Print(" %-30s %v\n", entry["key"], entry["count"]) + } + cli.Blank() + } + + if len(events) == 0 { + cli.Text(i18n.T("cmd.ai.metrics.none_found")) + } + + return nil +} + +// parseDuration parses a human-friendly duration like "7d", "24h", "30d". +func parseDuration(s string) (time.Duration, error) { + if len(s) < 2 { + return 0, fmt.Errorf("invalid duration: %s", s) + } + + unit := s[len(s)-1] + value := s[:len(s)-1] + + var n int + if _, err := fmt.Sscanf(value, "%d", &n); err != nil { + return 0, fmt.Errorf("invalid duration: %s", s) + } + + if n <= 0 { + return 0, fmt.Errorf("duration must be positive: %s", s) + } + + switch unit { + case 'd': + return time.Duration(n) * 24 * time.Hour, nil + case 'h': + return time.Duration(n) * time.Hour, nil + case 'm': + return time.Duration(n) * time.Minute, nil + default: + return 0, fmt.Errorf("unknown unit %c in duration: %s", unit, s) + } +} diff --git a/internal/cmd/ai/cmd_tasks.go b/internal/cmd/ai/cmd_tasks.go index 3eb665e..d0a2196 100644 --- a/internal/cmd/ai/cmd_tasks.go +++ b/internal/cmd/ai/cmd_tasks.go @@ -10,6 +10,7 @@ import ( "time" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/ai" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" ) @@ -165,6 +166,13 @@ var taskCmd = &cli.Command{ return cli.WrapVerb(err, "claim", "task") } + // Record task claim event + _ = ai.Record(ai.Event{ + Type: "task.claimed", + AgentID: cfg.AgentID, + Data: map[string]any{"task_id": task.ID, "title": task.Title}, + }) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.claim", "task")) cli.Print(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status)) } diff --git a/internal/cmd/ai/cmd_updates.go b/internal/cmd/ai/cmd_updates.go index 91fd7ad..0344d41 100644 --- a/internal/cmd/ai/cmd_updates.go +++ b/internal/cmd/ai/cmd_updates.go @@ -7,6 +7,7 @@ import ( "time" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/ai" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" ) @@ -92,6 +93,13 @@ var taskCompleteCmd = &cli.Command{ return cli.WrapVerb(err, "complete", "task") } + // Record task completion event + _ = ai.Record(ai.Event{ + Type: "task.completed", + AgentID: cfg.AgentID, + Data: map[string]any{"task_id": taskID, "success": !taskCompleteFailed}, + }) + if taskCompleteFailed { cli.Print("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID})) } else { diff --git a/internal/cmd/deploy/cmd_ansible.go b/internal/cmd/deploy/cmd_ansible.go new file mode 100644 index 0000000..8d0b682 --- /dev/null +++ b/internal/cmd/deploy/cmd_ansible.go @@ -0,0 +1,312 @@ +package deploy + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/host-uk/core/pkg/ansible" + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) + +var ( + ansibleInventory string + ansibleLimit string + ansibleTags string + ansibleSkipTags string + ansibleVars []string + ansibleVerbose int + ansibleCheck bool +) + +var ansibleCmd = &cobra.Command{ + Use: "ansible ", + Short: "Run Ansible playbooks natively (no Python required)", + Long: `Execute Ansible playbooks using a pure Go implementation. + +This command parses Ansible YAML playbooks and executes them natively, +without requiring Python or ansible-playbook to be installed. + +Supported modules: + - shell, command, raw, script + - copy, template, file, lineinfile, stat, slurp, fetch, get_url + - apt, apt_key, apt_repository, package, pip + - service, systemd + - user, group + - uri, wait_for, git, unarchive + - debug, fail, assert, set_fact, pause + +Examples: + core deploy ansible playbooks/coolify/create.yml -i inventory/ + core deploy ansible site.yml -l production + core deploy ansible deploy.yml -e "version=1.2.3" -e "env=prod"`, + Args: cobra.ExactArgs(1), + RunE: runAnsible, +} + +var ansibleTestCmd = &cobra.Command{ + Use: "test ", + Short: "Test SSH connectivity to a host", + Long: `Test SSH connection and gather facts from a host. + +Examples: + core deploy ansible test linux.snider.dev -u claude -p claude + core deploy ansible test server.example.com -i ~/.ssh/id_rsa`, + Args: cobra.ExactArgs(1), + RunE: runAnsibleTest, +} + +var ( + testUser string + testPassword string + testKeyFile string + testPort int +) + +func init() { + // ansible command flags + ansibleCmd.Flags().StringVarP(&ansibleInventory, "inventory", "i", "", "Inventory file or directory") + ansibleCmd.Flags().StringVarP(&ansibleLimit, "limit", "l", "", "Limit to specific hosts") + ansibleCmd.Flags().StringVarP(&ansibleTags, "tags", "t", "", "Only run plays and tasks tagged with these values") + ansibleCmd.Flags().StringVar(&ansibleSkipTags, "skip-tags", "", "Skip plays and tasks tagged with these values") + ansibleCmd.Flags().StringArrayVarP(&ansibleVars, "extra-vars", "e", nil, "Set additional variables (key=value)") + ansibleCmd.Flags().CountVarP(&ansibleVerbose, "verbose", "v", "Increase verbosity") + ansibleCmd.Flags().BoolVar(&ansibleCheck, "check", false, "Don't make any changes (dry run)") + + // test command flags + ansibleTestCmd.Flags().StringVarP(&testUser, "user", "u", "root", "SSH user") + ansibleTestCmd.Flags().StringVarP(&testPassword, "password", "p", "", "SSH password") + ansibleTestCmd.Flags().StringVarP(&testKeyFile, "key", "i", "", "SSH private key file") + ansibleTestCmd.Flags().IntVar(&testPort, "port", 22, "SSH port") + + // Add subcommands + ansibleCmd.AddCommand(ansibleTestCmd) + Cmd.AddCommand(ansibleCmd) +} + +func runAnsible(cmd *cobra.Command, args []string) error { + playbookPath := args[0] + + // Resolve playbook path + if !filepath.IsAbs(playbookPath) { + cwd, _ := os.Getwd() + playbookPath = filepath.Join(cwd, playbookPath) + } + + if _, err := os.Stat(playbookPath); os.IsNotExist(err) { + return fmt.Errorf("playbook not found: %s", playbookPath) + } + + // Create executor + basePath := filepath.Dir(playbookPath) + executor := ansible.NewExecutor(basePath) + defer executor.Close() + + // Set options + executor.Limit = ansibleLimit + executor.CheckMode = ansibleCheck + executor.Verbose = ansibleVerbose + + if ansibleTags != "" { + executor.Tags = strings.Split(ansibleTags, ",") + } + if ansibleSkipTags != "" { + executor.SkipTags = strings.Split(ansibleSkipTags, ",") + } + + // Parse extra vars + for _, v := range ansibleVars { + parts := strings.SplitN(v, "=", 2) + if len(parts) == 2 { + executor.SetVar(parts[0], parts[1]) + } + } + + // Load inventory + if ansibleInventory != "" { + invPath := ansibleInventory + if !filepath.IsAbs(invPath) { + cwd, _ := os.Getwd() + invPath = filepath.Join(cwd, invPath) + } + + // Check if it's a directory + info, err := os.Stat(invPath) + if err != nil { + return fmt.Errorf("inventory not found: %s", invPath) + } + + if info.IsDir() { + // Look for inventory.yml or hosts.yml + for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} { + p := filepath.Join(invPath, name) + if _, err := os.Stat(p); err == nil { + invPath = p + break + } + } + } + + if err := executor.SetInventory(invPath); err != nil { + return fmt.Errorf("load inventory: %w", err) + } + } + + // Set up callbacks + executor.OnPlayStart = func(play *ansible.Play) { + fmt.Printf("\n%s %s\n", cli.TitleStyle.Render("PLAY"), cli.BoldStyle.Render("["+play.Name+"]")) + fmt.Println(strings.Repeat("*", 70)) + } + + executor.OnTaskStart = func(host string, task *ansible.Task) { + taskName := task.Name + if taskName == "" { + taskName = task.Module + } + fmt.Printf("\n%s %s\n", cli.TitleStyle.Render("TASK"), cli.BoldStyle.Render("["+taskName+"]")) + if ansibleVerbose > 0 { + fmt.Printf("%s\n", cli.DimStyle.Render("host: "+host)) + } + } + + executor.OnTaskEnd = func(host string, task *ansible.Task, result *ansible.TaskResult) { + status := "ok" + style := cli.SuccessStyle + + if result.Failed { + status = "failed" + style = cli.ErrorStyle + } else if result.Skipped { + status = "skipping" + style = cli.DimStyle + } else if result.Changed { + status = "changed" + style = cli.WarningStyle + } + + fmt.Printf("%s: [%s]", style.Render(status), host) + if result.Msg != "" && ansibleVerbose > 0 { + fmt.Printf(" => %s", result.Msg) + } + if result.Duration > 0 && ansibleVerbose > 1 { + fmt.Printf(" (%s)", result.Duration.Round(time.Millisecond)) + } + fmt.Println() + + if result.Failed && result.Stderr != "" { + fmt.Printf("%s\n", cli.ErrorStyle.Render(result.Stderr)) + } + + if ansibleVerbose > 1 { + if result.Stdout != "" { + fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout)) + } + } + } + + executor.OnPlayEnd = func(play *ansible.Play) { + fmt.Println() + } + + // Run playbook + ctx := context.Background() + start := time.Now() + + fmt.Printf("%s Running playbook: %s\n", cli.BoldStyle.Render("▶"), playbookPath) + + if err := executor.Run(ctx, playbookPath); err != nil { + return fmt.Errorf("playbook failed: %w", err) + } + + fmt.Printf("\n%s Playbook completed in %s\n", + cli.SuccessStyle.Render("✓"), + time.Since(start).Round(time.Millisecond)) + + return nil +} + +func runAnsibleTest(cmd *cobra.Command, args []string) error { + host := args[0] + + fmt.Printf("Testing SSH connection to %s...\n", cli.BoldStyle.Render(host)) + + cfg := ansible.SSHConfig{ + Host: host, + Port: testPort, + User: testUser, + Password: testPassword, + KeyFile: testKeyFile, + Timeout: 30 * time.Second, + } + + client, err := ansible.NewSSHClient(cfg) + if err != nil { + return fmt.Errorf("create client: %w", err) + } + defer func() { _ = client.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Test connection + start := time.Now() + if err := client.Connect(ctx); err != nil { + return fmt.Errorf("connect failed: %w", err) + } + connectTime := time.Since(start) + + fmt.Printf("%s Connected in %s\n", cli.SuccessStyle.Render("✓"), connectTime.Round(time.Millisecond)) + + // Gather facts + fmt.Println("\nGathering facts...") + + // Hostname + stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname") + fmt.Printf(" Hostname: %s\n", cli.BoldStyle.Render(strings.TrimSpace(stdout))) + + // OS + stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2") + if stdout != "" { + fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout)) + } + + // Kernel + stdout, _, _, _ = client.Run(ctx, "uname -r") + fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout)) + + // Architecture + stdout, _, _, _ = client.Run(ctx, "uname -m") + fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout)) + + // Memory + stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'") + fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout)) + + // Disk + stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'") + fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout)) + + // Docker + stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null") + if err == nil { + fmt.Printf(" Docker: %s\n", cli.SuccessStyle.Render(strings.TrimSpace(stdout))) + } else { + fmt.Printf(" Docker: %s\n", cli.DimStyle.Render("not installed")) + } + + // Check if Coolify is running + stdout, _, _, _ = client.Run(ctx, "docker ps 2>/dev/null | grep -q coolify && echo 'running' || echo 'not running'") + if strings.TrimSpace(stdout) == "running" { + fmt.Printf(" Coolify: %s\n", cli.SuccessStyle.Render("running")) + } else { + fmt.Printf(" Coolify: %s\n", cli.DimStyle.Render("not installed")) + } + + fmt.Printf("\n%s SSH test passed\n", cli.SuccessStyle.Render("✓")) + + return nil +} diff --git a/internal/cmd/deploy/cmd_commands.go b/internal/cmd/deploy/cmd_commands.go new file mode 100644 index 0000000..bc61688 --- /dev/null +++ b/internal/cmd/deploy/cmd_commands.go @@ -0,0 +1,15 @@ +package deploy + +import ( + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) + +func init() { + cli.RegisterCommands(AddDeployCommands) +} + +// AddDeployCommands registers the 'deploy' command and all subcommands. +func AddDeployCommands(root *cobra.Command) { + root.AddCommand(Cmd) +} diff --git a/internal/cmd/deploy/cmd_deploy.go b/internal/cmd/deploy/cmd_deploy.go new file mode 100644 index 0000000..4f92657 --- /dev/null +++ b/internal/cmd/deploy/cmd_deploy.go @@ -0,0 +1,280 @@ +package deploy + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/deploy/coolify" + "github.com/host-uk/core/pkg/i18n" + "github.com/spf13/cobra" +) + +var ( + coolifyURL string + coolifyToken string + outputJSON bool +) + +// Cmd is the root deploy command. +var Cmd = &cobra.Command{ + Use: "deploy", + Short: i18n.T("cmd.deploy.short"), + Long: i18n.T("cmd.deploy.long"), +} + +var serversCmd = &cobra.Command{ + Use: "servers", + Short: "List Coolify servers", + RunE: runListServers, +} + +var projectsCmd = &cobra.Command{ + Use: "projects", + Short: "List Coolify projects", + RunE: runListProjects, +} + +var appsCmd = &cobra.Command{ + Use: "apps", + Short: "List Coolify applications", + RunE: runListApps, +} + +var dbsCmd = &cobra.Command{ + Use: "databases", + Short: "List Coolify databases", + Aliases: []string{"dbs", "db"}, + RunE: runListDatabases, +} + +var servicesCmd = &cobra.Command{ + Use: "services", + Short: "List Coolify services", + RunE: runListServices, +} + +var teamCmd = &cobra.Command{ + Use: "team", + Short: "Show current team info", + RunE: runTeam, +} + +var callCmd = &cobra.Command{ + Use: "call [params-json]", + Short: "Call any Coolify API operation", + Args: cobra.RangeArgs(1, 2), + RunE: runCall, +} + +func init() { + // Global flags + Cmd.PersistentFlags().StringVar(&coolifyURL, "url", os.Getenv("COOLIFY_URL"), "Coolify API URL") + Cmd.PersistentFlags().StringVar(&coolifyToken, "token", os.Getenv("COOLIFY_TOKEN"), "Coolify API token") + Cmd.PersistentFlags().BoolVar(&outputJSON, "json", false, "Output as JSON") + + // Add subcommands + Cmd.AddCommand(serversCmd) + Cmd.AddCommand(projectsCmd) + Cmd.AddCommand(appsCmd) + Cmd.AddCommand(dbsCmd) + Cmd.AddCommand(servicesCmd) + Cmd.AddCommand(teamCmd) + Cmd.AddCommand(callCmd) +} + +func getClient() (*coolify.Client, error) { + cfg := coolify.Config{ + BaseURL: coolifyURL, + APIToken: coolifyToken, + Timeout: 30, + VerifySSL: true, + } + + if cfg.BaseURL == "" { + cfg.BaseURL = os.Getenv("COOLIFY_URL") + } + if cfg.APIToken == "" { + cfg.APIToken = os.Getenv("COOLIFY_TOKEN") + } + + return coolify.NewClient(cfg) +} + +func outputResult(data any) error { + if outputJSON { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(data) + } + + // Pretty print based on type + switch v := data.(type) { + case []map[string]any: + for _, item := range v { + printItem(item) + } + case map[string]any: + printItem(v) + default: + fmt.Printf("%v\n", data) + } + return nil +} + +func printItem(item map[string]any) { + // Common fields to display + if uuid, ok := item["uuid"].(string); ok { + fmt.Printf("%s ", cli.DimStyle.Render(uuid[:8])) + } + if name, ok := item["name"].(string); ok { + fmt.Printf("%s", cli.TitleStyle.Render(name)) + } + if desc, ok := item["description"].(string); ok && desc != "" { + fmt.Printf(" %s", cli.DimStyle.Render(desc)) + } + if status, ok := item["status"].(string); ok { + switch status { + case "running": + fmt.Printf(" %s", cli.SuccessStyle.Render("●")) + case "stopped": + fmt.Printf(" %s", cli.ErrorStyle.Render("○")) + default: + fmt.Printf(" %s", cli.DimStyle.Render(status)) + } + } + fmt.Println() +} + +func runListServers(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + servers, err := client.ListServers(context.Background()) + if err != nil { + return err + } + + if len(servers) == 0 { + fmt.Println("No servers found") + return nil + } + + return outputResult(servers) +} + +func runListProjects(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + projects, err := client.ListProjects(context.Background()) + if err != nil { + return err + } + + if len(projects) == 0 { + fmt.Println("No projects found") + return nil + } + + return outputResult(projects) +} + +func runListApps(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + apps, err := client.ListApplications(context.Background()) + if err != nil { + return err + } + + if len(apps) == 0 { + fmt.Println("No applications found") + return nil + } + + return outputResult(apps) +} + +func runListDatabases(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + dbs, err := client.ListDatabases(context.Background()) + if err != nil { + return err + } + + if len(dbs) == 0 { + fmt.Println("No databases found") + return nil + } + + return outputResult(dbs) +} + +func runListServices(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + services, err := client.ListServices(context.Background()) + if err != nil { + return err + } + + if len(services) == 0 { + fmt.Println("No services found") + return nil + } + + return outputResult(services) +} + +func runTeam(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + team, err := client.GetTeam(context.Background()) + if err != nil { + return err + } + + return outputResult(team) +} + +func runCall(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return cli.WrapVerb(err, "initialize", "client") + } + + operation := args[0] + var params map[string]any + if len(args) > 1 { + if err := json.Unmarshal([]byte(args[1]), ¶ms); err != nil { + return fmt.Errorf("invalid JSON params: %w", err) + } + } + + result, err := client.Call(context.Background(), operation, params) + if err != nil { + return err + } + + return outputResult(result) +} diff --git a/internal/cmd/rag/cmd_collections.go b/internal/cmd/rag/cmd_collections.go new file mode 100644 index 0000000..b21d45c --- /dev/null +++ b/internal/cmd/rag/cmd_collections.go @@ -0,0 +1,86 @@ +package rag + +import ( + "context" + "fmt" + + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/rag" + "github.com/spf13/cobra" +) + +var ( + listCollections bool + showStats bool + deleteCollection string +) + +var collectionsCmd = &cobra.Command{ + Use: "collections", + Short: i18n.T("cmd.rag.collections.short"), + Long: i18n.T("cmd.rag.collections.long"), + RunE: runCollections, +} + +func runCollections(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + // Connect to Qdrant + qdrantClient, err := rag.NewQdrantClient(rag.QdrantConfig{ + Host: qdrantHost, + Port: qdrantPort, + UseTLS: false, + }) + if err != nil { + return fmt.Errorf("failed to connect to Qdrant: %w", err) + } + defer qdrantClient.Close() + + // Handle delete + if deleteCollection != "" { + exists, err := qdrantClient.CollectionExists(ctx, deleteCollection) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("collection not found: %s", deleteCollection) + } + if err := qdrantClient.DeleteCollection(ctx, deleteCollection); err != nil { + return err + } + fmt.Printf("Deleted collection: %s\n", deleteCollection) + return nil + } + + // List collections + collections, err := qdrantClient.ListCollections(ctx) + if err != nil { + return err + } + + if len(collections) == 0 { + fmt.Println("No collections found.") + return nil + } + + fmt.Printf("%s\n\n", cli.TitleStyle.Render("Collections")) + + for _, name := range collections { + if showStats { + info, err := qdrantClient.CollectionInfo(ctx, name) + if err != nil { + fmt.Printf(" %s (error: %v)\n", name, err) + continue + } + fmt.Printf(" %s\n", cli.ValueStyle.Render(name)) + fmt.Printf(" Points: %d\n", info.PointsCount) + fmt.Printf(" Status: %s\n", info.Status.String()) + fmt.Println() + } else { + fmt.Printf(" %s\n", name) + } + } + + return nil +} diff --git a/internal/cmd/rag/cmd_commands.go b/internal/cmd/rag/cmd_commands.go new file mode 100644 index 0000000..ba8b6fb --- /dev/null +++ b/internal/cmd/rag/cmd_commands.go @@ -0,0 +1,21 @@ +// Package rag provides RAG (Retrieval Augmented Generation) commands. +// +// Commands: +// - core ai rag ingest: Ingest markdown files into Qdrant +// - core ai rag query: Query the vector database +// - core ai rag collections: List and manage collections +package rag + +import ( + "github.com/spf13/cobra" +) + +// AddRAGSubcommands registers the 'rag' command as a subcommand of parent. +// Called from the ai command package to mount under "core ai rag". +func AddRAGSubcommands(parent *cobra.Command) { + initFlags() + ragCmd.AddCommand(ingestCmd) + ragCmd.AddCommand(queryCmd) + ragCmd.AddCommand(collectionsCmd) + parent.AddCommand(ragCmd) +} diff --git a/internal/cmd/rag/cmd_ingest.go b/internal/cmd/rag/cmd_ingest.go new file mode 100644 index 0000000..b1c7e9f --- /dev/null +++ b/internal/cmd/rag/cmd_ingest.go @@ -0,0 +1,173 @@ +package rag + +import ( + "context" + "fmt" + + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/rag" + "github.com/spf13/cobra" +) + +var ( + collection string + recreate bool + chunkSize int + chunkOverlap int +) + +var ingestCmd = &cobra.Command{ + Use: "ingest [directory]", + Short: i18n.T("cmd.rag.ingest.short"), + Long: i18n.T("cmd.rag.ingest.long"), + Args: cobra.MaximumNArgs(1), + RunE: runIngest, +} + +func runIngest(cmd *cobra.Command, args []string) error { + directory := "." + if len(args) > 0 { + directory = args[0] + } + + ctx := context.Background() + + // Connect to Qdrant + fmt.Printf("Connecting to Qdrant at %s:%d...\n", qdrantHost, qdrantPort) + qdrantClient, err := rag.NewQdrantClient(rag.QdrantConfig{ + Host: qdrantHost, + Port: qdrantPort, + UseTLS: false, + }) + if err != nil { + return fmt.Errorf("failed to connect to Qdrant: %w", err) + } + defer qdrantClient.Close() + + if err := qdrantClient.HealthCheck(ctx); err != nil { + return fmt.Errorf("Qdrant health check failed: %w", err) + } + + // Connect to Ollama + fmt.Printf("Using embedding model: %s (via %s:%d)\n", model, ollamaHost, ollamaPort) + ollamaClient, err := rag.NewOllamaClient(rag.OllamaConfig{ + Host: ollamaHost, + Port: ollamaPort, + Model: model, + }) + if err != nil { + return fmt.Errorf("failed to connect to Ollama: %w", err) + } + + if err := ollamaClient.VerifyModel(ctx); err != nil { + return err + } + + // Configure ingestion + if chunkSize <= 0 { + return fmt.Errorf("chunk-size must be > 0") + } + if chunkOverlap < 0 || chunkOverlap >= chunkSize { + return fmt.Errorf("chunk-overlap must be >= 0 and < chunk-size") + } + + cfg := rag.IngestConfig{ + Directory: directory, + Collection: collection, + Recreate: recreate, + Verbose: verbose, + BatchSize: 100, + Chunk: rag.ChunkConfig{ + Size: chunkSize, + Overlap: chunkOverlap, + }, + } + + // Progress callback + progress := func(file string, chunks int, total int) { + if verbose { + fmt.Printf(" Processed: %s (%d chunks total)\n", file, chunks) + } else { + fmt.Printf("\r %s (%d chunks) ", cli.DimStyle.Render(file), chunks) + } + } + + // Run ingestion + fmt.Printf("\nIngesting from: %s\n", directory) + if recreate { + fmt.Printf(" (recreating collection: %s)\n", collection) + } + + stats, err := rag.Ingest(ctx, qdrantClient, ollamaClient, cfg, progress) + if err != nil { + return err + } + + // Summary + fmt.Printf("\n\n%s\n", cli.TitleStyle.Render("Ingestion complete!")) + fmt.Printf(" Files processed: %d\n", stats.Files) + fmt.Printf(" Chunks created: %d\n", stats.Chunks) + if stats.Errors > 0 { + fmt.Printf(" Errors: %s\n", cli.ErrorStyle.Render(fmt.Sprintf("%d", stats.Errors))) + } + fmt.Printf(" Collection: %s\n", collection) + + return nil +} + +// IngestDirectory is exported for use by other packages (e.g., MCP). +func IngestDirectory(ctx context.Context, directory, collectionName string, recreateCollection bool) error { + qdrantClient, err := rag.NewQdrantClient(rag.DefaultQdrantConfig()) + if err != nil { + return err + } + defer qdrantClient.Close() + + if err := qdrantClient.HealthCheck(ctx); err != nil { + return fmt.Errorf("Qdrant health check failed: %w", err) + } + + ollamaClient, err := rag.NewOllamaClient(rag.DefaultOllamaConfig()) + if err != nil { + return err + } + + if err := ollamaClient.VerifyModel(ctx); err != nil { + return err + } + + cfg := rag.DefaultIngestConfig() + cfg.Directory = directory + cfg.Collection = collectionName + cfg.Recreate = recreateCollection + + _, err = rag.Ingest(ctx, qdrantClient, ollamaClient, cfg, nil) + return err +} + +// IngestFile is exported for use by other packages (e.g., MCP). +func IngestFile(ctx context.Context, filePath, collectionName string) (int, error) { + qdrantClient, err := rag.NewQdrantClient(rag.DefaultQdrantConfig()) + if err != nil { + return 0, err + } + defer qdrantClient.Close() + + if err := qdrantClient.HealthCheck(ctx); err != nil { + return 0, fmt.Errorf("Qdrant health check failed: %w", err) + } + + ollamaClient, err := rag.NewOllamaClient(rag.DefaultOllamaConfig()) + if err != nil { + return 0, err + } + + if err := ollamaClient.VerifyModel(ctx); err != nil { + return 0, err + } + + return rag.IngestFile(ctx, qdrantClient, ollamaClient, collectionName, filePath, rag.DefaultChunkConfig()) +} + + diff --git a/internal/cmd/rag/cmd_query.go b/internal/cmd/rag/cmd_query.go new file mode 100644 index 0000000..076f264 --- /dev/null +++ b/internal/cmd/rag/cmd_query.go @@ -0,0 +1,110 @@ +package rag + +import ( + "context" + "fmt" + + "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/rag" + "github.com/spf13/cobra" +) + +var ( + queryCollection string + limit int + threshold float32 + category string + format string +) + +var queryCmd = &cobra.Command{ + Use: "query [question]", + Short: i18n.T("cmd.rag.query.short"), + Long: i18n.T("cmd.rag.query.long"), + Args: cobra.ExactArgs(1), + RunE: runQuery, +} + +func runQuery(cmd *cobra.Command, args []string) error { + question := args[0] + ctx := context.Background() + + // Connect to Qdrant + qdrantClient, err := rag.NewQdrantClient(rag.QdrantConfig{ + Host: qdrantHost, + Port: qdrantPort, + UseTLS: false, + }) + if err != nil { + return fmt.Errorf("failed to connect to Qdrant: %w", err) + } + defer qdrantClient.Close() + + // Connect to Ollama + ollamaClient, err := rag.NewOllamaClient(rag.OllamaConfig{ + Host: ollamaHost, + Port: ollamaPort, + Model: model, + }) + if err != nil { + return fmt.Errorf("failed to connect to Ollama: %w", err) + } + + // Configure query + if limit < 0 { + limit = 0 + } + cfg := rag.QueryConfig{ + Collection: queryCollection, + Limit: uint64(limit), + Threshold: threshold, + Category: category, + } + + // Run query + results, err := rag.Query(ctx, qdrantClient, ollamaClient, question, cfg) + if err != nil { + return err + } + + // Format output + switch format { + case "json": + fmt.Println(rag.FormatResultsJSON(results)) + case "context": + fmt.Println(rag.FormatResultsContext(results)) + default: + fmt.Println(rag.FormatResultsText(results)) + } + + return nil +} + +// QueryDocs is exported for use by other packages (e.g., MCP). +func QueryDocs(ctx context.Context, question, collectionName string, topK int) ([]rag.QueryResult, error) { + qdrantClient, err := rag.NewQdrantClient(rag.DefaultQdrantConfig()) + if err != nil { + return nil, err + } + defer qdrantClient.Close() + + ollamaClient, err := rag.NewOllamaClient(rag.DefaultOllamaConfig()) + if err != nil { + return nil, err + } + + cfg := rag.DefaultQueryConfig() + cfg.Collection = collectionName + cfg.Limit = uint64(topK) + + return rag.Query(ctx, qdrantClient, ollamaClient, question, cfg) +} + +// QueryDocsContext is exported and returns context-formatted results. +func QueryDocsContext(ctx context.Context, question, collectionName string, topK int) (string, error) { + results, err := QueryDocs(ctx, question, collectionName, topK) + if err != nil { + return "", err + } + return rag.FormatResultsContext(results), nil +} diff --git a/internal/cmd/rag/cmd_rag.go b/internal/cmd/rag/cmd_rag.go new file mode 100644 index 0000000..02e37f2 --- /dev/null +++ b/internal/cmd/rag/cmd_rag.go @@ -0,0 +1,84 @@ +package rag + +import ( + "os" + "strconv" + + "github.com/host-uk/core/pkg/i18n" + "github.com/spf13/cobra" +) + +// Shared flags +var ( + qdrantHost string + qdrantPort int + ollamaHost string + ollamaPort int + model string + verbose bool +) + +var ragCmd = &cobra.Command{ + Use: "rag", + Short: i18n.T("cmd.rag.short"), + Long: i18n.T("cmd.rag.long"), +} + +func initFlags() { + // Qdrant connection flags (persistent) - defaults to localhost for local development + qHost := "localhost" + if v := os.Getenv("QDRANT_HOST"); v != "" { + qHost = v + } + ragCmd.PersistentFlags().StringVar(&qdrantHost, "qdrant-host", qHost, i18n.T("cmd.rag.flag.qdrant_host")) + + qPort := 6334 + if v := os.Getenv("QDRANT_PORT"); v != "" { + if p, err := strconv.Atoi(v); err == nil { + qPort = p + } + } + ragCmd.PersistentFlags().IntVar(&qdrantPort, "qdrant-port", qPort, i18n.T("cmd.rag.flag.qdrant_port")) + + // Ollama connection flags (persistent) - defaults to localhost for local development + oHost := "localhost" + if v := os.Getenv("OLLAMA_HOST"); v != "" { + oHost = v + } + ragCmd.PersistentFlags().StringVar(&ollamaHost, "ollama-host", oHost, i18n.T("cmd.rag.flag.ollama_host")) + + oPort := 11434 + if v := os.Getenv("OLLAMA_PORT"); v != "" { + if p, err := strconv.Atoi(v); err == nil { + oPort = p + } + } + ragCmd.PersistentFlags().IntVar(&ollamaPort, "ollama-port", oPort, i18n.T("cmd.rag.flag.ollama_port")) + + m := "nomic-embed-text" + if v := os.Getenv("EMBEDDING_MODEL"); v != "" { + m = v + } + ragCmd.PersistentFlags().StringVar(&model, "model", m, i18n.T("cmd.rag.flag.model")) + + // Verbose flag (persistent) + ragCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, i18n.T("common.flag.verbose")) + + // Ingest command flags + ingestCmd.Flags().StringVar(&collection, "collection", "hostuk-docs", i18n.T("cmd.rag.ingest.flag.collection")) + ingestCmd.Flags().BoolVar(&recreate, "recreate", false, i18n.T("cmd.rag.ingest.flag.recreate")) + ingestCmd.Flags().IntVar(&chunkSize, "chunk-size", 500, i18n.T("cmd.rag.ingest.flag.chunk_size")) + ingestCmd.Flags().IntVar(&chunkOverlap, "chunk-overlap", 50, i18n.T("cmd.rag.ingest.flag.chunk_overlap")) + + // Query command flags + queryCmd.Flags().StringVar(&queryCollection, "collection", "hostuk-docs", i18n.T("cmd.rag.query.flag.collection")) + queryCmd.Flags().IntVar(&limit, "top", 5, i18n.T("cmd.rag.query.flag.top")) + queryCmd.Flags().Float32Var(&threshold, "threshold", 0.5, i18n.T("cmd.rag.query.flag.threshold")) + queryCmd.Flags().StringVar(&category, "category", "", i18n.T("cmd.rag.query.flag.category")) + queryCmd.Flags().StringVar(&format, "format", "text", i18n.T("cmd.rag.query.flag.format")) + + // Collections command flags + collectionsCmd.Flags().BoolVar(&listCollections, "list", false, i18n.T("cmd.rag.collections.flag.list")) + collectionsCmd.Flags().BoolVar(&showStats, "stats", false, i18n.T("cmd.rag.collections.flag.stats")) + collectionsCmd.Flags().StringVar(&deleteCollection, "delete", "", i18n.T("cmd.rag.collections.flag.delete")) +} diff --git a/internal/cmd/security/cmd_alerts.go b/internal/cmd/security/cmd_alerts.go index 62b205d..2b0795c 100644 --- a/internal/cmd/security/cmd_alerts.go +++ b/internal/cmd/security/cmd_alerts.go @@ -22,6 +22,7 @@ func addAlertsCommand(parent *cli.Command) { cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) parent.AddCommand(cmd) } @@ -43,6 +44,11 @@ func runAlerts() error { return err } + // External target mode: bypass registry entirely + if securityTarget != "" { + return runAlertsForTarget(securityTarget) + } + reg, err := loadRegistry(securityRegistryPath) if err != nil { return err @@ -173,6 +179,124 @@ func runAlerts() error { return nil } +// runAlertsForTarget runs unified alert checks against an external repo target. +func runAlertsForTarget(target string) error { + repo, fullName := buildTargetRepo(target) + if repo == nil { + return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)") + } + + var allAlerts []AlertOutput + summary := &AlertSummary{} + + // Fetch Dependabot alerts + depAlerts, err := fetchDependabotAlerts(fullName) + if err == nil { + for _, alert := range depAlerts { + if alert.State != "open" { + continue + } + severity := alert.Advisory.Severity + if !filterBySeverity(severity, securitySeverity) { + continue + } + summary.Add(severity) + allAlerts = append(allAlerts, AlertOutput{ + Repo: repo.Name, + Severity: severity, + ID: alert.Advisory.CVEID, + Package: alert.Dependency.Package.Name, + Version: alert.SecurityVulnerability.VulnerableVersionRange, + Type: "dependabot", + Message: alert.Advisory.Summary, + }) + } + } + + // Fetch code scanning alerts + codeAlerts, err := fetchCodeScanningAlerts(fullName) + if err == nil { + for _, alert := range codeAlerts { + if alert.State != "open" { + continue + } + severity := alert.Rule.Severity + if !filterBySeverity(severity, securitySeverity) { + continue + } + summary.Add(severity) + location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine) + allAlerts = append(allAlerts, AlertOutput{ + Repo: repo.Name, + Severity: severity, + ID: alert.Rule.ID, + Location: location, + Type: alert.Tool.Name, + Message: alert.Rule.Description, + }) + } + } + + // Fetch secret scanning alerts + secretAlerts, err := fetchSecretScanningAlerts(fullName) + if err == nil { + for _, alert := range secretAlerts { + if alert.State != "open" { + continue + } + if !filterBySeverity("high", securitySeverity) { + continue + } + summary.Add("high") + allAlerts = append(allAlerts, AlertOutput{ + Repo: repo.Name, + Severity: "high", + ID: fmt.Sprintf("secret-%d", alert.Number), + Type: "secret-scanning", + Message: alert.SecretType, + }) + } + } + + if securityJSON { + output, err := json.MarshalIndent(allAlerts, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") + } + cli.Text(string(output)) + return nil + } + + cli.Blank() + cli.Print("%s %s\n", cli.DimStyle.Render("Alerts ("+fullName+"):"), summary.String()) + cli.Blank() + + if len(allAlerts) == 0 { + return nil + } + + for _, alert := range allAlerts { + sevStyle := severityStyle(alert.Severity) + location := alert.Package + if location == "" { + location = alert.Location + } + if alert.Version != "" { + location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version)) + } + cli.Print("%-20s %s %-16s %-40s %s\n", + cli.ValueStyle.Render(alert.Repo), + sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + alert.ID, + location, + cli.DimStyle.Render(alert.Type), + ) + } + cli.Blank() + + return nil +} + func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) { endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName) output, err := runGHAPI(endpoint) diff --git a/internal/cmd/security/cmd_deps.go b/internal/cmd/security/cmd_deps.go index 9637ba6..d55fca1 100644 --- a/internal/cmd/security/cmd_deps.go +++ b/internal/cmd/security/cmd_deps.go @@ -22,6 +22,7 @@ func addDepsCommand(parent *cli.Command) { cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) parent.AddCommand(cmd) } @@ -44,6 +45,11 @@ func runDeps() error { return err } + // External target mode: bypass registry entirely + if securityTarget != "" { + return runDepsForTarget(securityTarget) + } + reg, err := loadRegistry(securityRegistryPath) if err != nil { return err @@ -62,6 +68,7 @@ func runDeps() error { alerts, err := fetchDependabotAlerts(repoFullName) if err != nil { + cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err) continue } @@ -132,3 +139,72 @@ func runDeps() error { return nil } + +// runDepsForTarget runs dependency checks against an external repo target. +func runDepsForTarget(target string) error { + repo, fullName := buildTargetRepo(target) + if repo == nil { + return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)") + } + + var allAlerts []DepAlert + summary := &AlertSummary{} + + alerts, err := fetchDependabotAlerts(fullName) + if err != nil { + return cli.Wrap(err, "fetch dependabot alerts for "+fullName) + } + + for _, alert := range alerts { + if alert.State != "open" { + continue + } + severity := alert.Advisory.Severity + if !filterBySeverity(severity, securitySeverity) { + continue + } + summary.Add(severity) + allAlerts = append(allAlerts, DepAlert{ + Repo: repo.Name, + Severity: severity, + CVE: alert.Advisory.CVEID, + Package: alert.Dependency.Package.Name, + Ecosystem: alert.Dependency.Package.Ecosystem, + Vulnerable: alert.SecurityVulnerability.VulnerableVersionRange, + PatchedVersion: alert.SecurityVulnerability.FirstPatchedVersion.Identifier, + Manifest: alert.Dependency.ManifestPath, + Summary: alert.Advisory.Summary, + }) + } + + if securityJSON { + output, err := json.MarshalIndent(allAlerts, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") + } + cli.Text(string(output)) + return nil + } + + cli.Blank() + cli.Print("%s %s\n", cli.DimStyle.Render("Dependabot ("+fullName+"):"), summary.String()) + cli.Blank() + + for _, alert := range allAlerts { + sevStyle := severityStyle(alert.Severity) + upgrade := alert.Vulnerable + if alert.PatchedVersion != "" { + upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion)) + } + cli.Print("%-16s %s %-16s %-30s %s\n", + cli.ValueStyle.Render(alert.Repo), + sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + alert.CVE, + alert.Package, + upgrade, + ) + } + cli.Blank() + + return nil +} diff --git a/internal/cmd/security/cmd_jobs.go b/internal/cmd/security/cmd_jobs.go new file mode 100644 index 0000000..df655e8 --- /dev/null +++ b/internal/cmd/security/cmd_jobs.go @@ -0,0 +1,229 @@ +package security + +import ( + "fmt" + "os/exec" + "strings" + "time" + + "github.com/host-uk/core/pkg/ai" + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/i18n" +) + +var ( + jobsTargets []string + jobsIssueRepo string + jobsDryRun bool + jobsCopies int +) + +func addJobsCommand(parent *cli.Command) { + cmd := &cli.Command{ + Use: "jobs", + Short: i18n.T("cmd.security.jobs.short"), + Long: i18n.T("cmd.security.jobs.long"), + RunE: func(c *cli.Command, args []string) error { + return runJobs() + }, + } + + cmd.Flags().StringSliceVar(&jobsTargets, "targets", nil, i18n.T("cmd.security.jobs.flag.targets")) + cmd.Flags().StringVar(&jobsIssueRepo, "issue-repo", "host-uk/core", i18n.T("cmd.security.jobs.flag.issue_repo")) + cmd.Flags().BoolVar(&jobsDryRun, "dry-run", false, i18n.T("cmd.security.jobs.flag.dry_run")) + cmd.Flags().IntVar(&jobsCopies, "copies", 1, i18n.T("cmd.security.jobs.flag.copies")) + + parent.AddCommand(cmd) +} + +func runJobs() error { + if err := checkGH(); err != nil { + return err + } + + if len(jobsTargets) == 0 { + return cli.Err("at least one --targets value required (e.g. --targets wailsapp/wails)") + } + + if jobsCopies < 1 { + return cli.Err("--copies must be at least 1") + } + + var failedCount int + for _, target := range jobsTargets { + if err := createJobForTarget(target); err != nil { + cli.Print("%s %s: %v\n", cli.ErrorStyle.Render(">>"), target, err) + failedCount++ + continue + } + } + + if failedCount == len(jobsTargets) { + return cli.Err("all targets failed to process") + } + + return nil +} + +func createJobForTarget(target string) error { + parts := strings.SplitN(target, "/", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid target format: use owner/repo") + } + + // Gather findings + summary := &AlertSummary{} + var findings []string + var fetchErrors int + + // Code scanning + codeAlerts, err := fetchCodeScanningAlerts(target) + if err != nil { + cli.Print("%s %s: failed to fetch code scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) + fetchErrors++ + } + if err == nil { + for _, alert := range codeAlerts { + if alert.State != "open" { + continue + } + severity := alert.Rule.Severity + if severity == "" { + severity = "medium" + } + summary.Add(severity) + findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s:%d)", + strings.ToUpper(severity), alert.Tool.Name, alert.Rule.Description, + alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)) + } + } + + // Dependabot + depAlerts, err := fetchDependabotAlerts(target) + if err != nil { + cli.Print("%s %s: failed to fetch dependabot alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) + fetchErrors++ + } + if err == nil { + for _, alert := range depAlerts { + if alert.State != "open" { + continue + } + summary.Add(alert.Advisory.Severity) + findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s)", + strings.ToUpper(alert.Advisory.Severity), alert.Dependency.Package.Name, + alert.Advisory.Summary, alert.Advisory.CVEID)) + } + } + + // Secret scanning + secretAlerts, err := fetchSecretScanningAlerts(target) + if err != nil { + cli.Print("%s %s: failed to fetch secret scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err) + fetchErrors++ + } + if err == nil { + for _, alert := range secretAlerts { + if alert.State != "open" { + continue + } + summary.Add("high") + findings = append(findings, fmt.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number)) + } + } + + if fetchErrors == 3 { + return fmt.Errorf("failed to fetch any alerts for %s", target) + } + + if summary.Total == 0 { + cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), target, "No open findings") + return nil + } + + // Build issue body + title := fmt.Sprintf("Security scan: %s", target) + body := buildJobIssueBody(target, summary, findings) + + for i := range jobsCopies { + issueTitle := title + if jobsCopies > 1 { + issueTitle = fmt.Sprintf("%s (#%d)", title, i+1) + } + + if jobsDryRun { + cli.Blank() + cli.Print("%s %s\n", cli.DimStyle.Render("[dry-run] Would create issue:"), issueTitle) + cli.Print("%s %s\n", cli.DimStyle.Render(" Repo:"), jobsIssueRepo) + cli.Print("%s %s\n", cli.DimStyle.Render(" Labels:"), "type:security-scan,repo:"+target) + cli.Print("%s %d findings\n", cli.DimStyle.Render(" Findings:"), summary.Total) + continue + } + + // Create issue via gh CLI + cmd := exec.Command("gh", "issue", "create", + "--repo", jobsIssueRepo, + "--title", issueTitle, + "--body", body, + "--label", "type:security-scan,repo:"+target, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output))) + } + + issueURL := strings.TrimSpace(string(output)) + cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), issueTitle, issueURL) + + // Record metrics + _ = ai.Record(ai.Event{ + Type: "security.job_created", + Timestamp: time.Now(), + Repo: target, + Data: map[string]any{ + "issue_repo": jobsIssueRepo, + "issue_url": issueURL, + "total": summary.Total, + "critical": summary.Critical, + "high": summary.High, + }, + }) + } + + return nil +} + +func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string { + var sb strings.Builder + + fmt.Fprintf(&sb, "## Security Scan: %s\n\n", target) + fmt.Fprintf(&sb, "**Summary:** %s\n\n", summary.String()) + + sb.WriteString("### Findings\n\n") + if len(findings) > 50 { + // Truncate long lists + for _, f := range findings[:50] { + sb.WriteString(f + "\n") + } + fmt.Fprintf(&sb, "\n... and %d more\n", len(findings)-50) + } else { + for _, f := range findings { + sb.WriteString(f + "\n") + } + } + + sb.WriteString("\n### Checklist\n\n") + sb.WriteString("- [ ] Review findings above\n") + sb.WriteString("- [ ] Triage by severity (critical/high first)\n") + sb.WriteString("- [ ] Create PRs for fixes\n") + sb.WriteString("- [ ] Verify fixes resolve alerts\n") + + sb.WriteString("\n### Instructions\n\n") + sb.WriteString("1. Claim this issue by assigning yourself\n") + fmt.Fprintf(&sb, "2. Run `core security alerts --target %s` for the latest findings\n", target) + sb.WriteString("3. Work through the checklist above\n") + sb.WriteString("4. Close this issue when all findings are addressed\n") + + return sb.String() +} diff --git a/internal/cmd/security/cmd_scan.go b/internal/cmd/security/cmd_scan.go index a11e2ad..1cd732a 100644 --- a/internal/cmd/security/cmd_scan.go +++ b/internal/cmd/security/cmd_scan.go @@ -3,7 +3,9 @@ package security import ( "encoding/json" "fmt" + "time" + "github.com/host-uk/core/pkg/ai" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" ) @@ -27,6 +29,7 @@ func addScanCommand(parent *cli.Command) { cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) cmd.Flags().StringVar(&scanTool, "tool", "", i18n.T("cmd.security.scan.flag.tool")) cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) parent.AddCommand(cmd) } @@ -48,6 +51,11 @@ func runScan() error { return err } + // External target mode: bypass registry entirely + if securityTarget != "" { + return runScanForTarget(securityTarget) + } + reg, err := loadRegistry(securityRegistryPath) if err != nil { return err @@ -66,6 +74,7 @@ func runScan() error { alerts, err := fetchCodeScanningAlerts(repoFullName) if err != nil { + cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err) continue } @@ -104,6 +113,19 @@ func runScan() error { } } + // Record metrics + _ = ai.Record(ai.Event{ + Type: "security.scan", + Timestamp: time.Now(), + Data: map[string]any{ + "total": summary.Total, + "critical": summary.Critical, + "high": summary.High, + "medium": summary.Medium, + "low": summary.Low, + }, + }) + if securityJSON { output, err := json.MarshalIndent(allAlerts, "", " ") if err != nil { @@ -140,3 +162,93 @@ func runScan() error { return nil } + +// runScanForTarget runs a code scanning check against an external repo target. +func runScanForTarget(target string) error { + repo, fullName := buildTargetRepo(target) + if repo == nil { + return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)") + } + + var allAlerts []ScanAlert + summary := &AlertSummary{} + + alerts, err := fetchCodeScanningAlerts(fullName) + if err != nil { + return cli.Wrap(err, "fetch code-scanning alerts for "+fullName) + } + + for _, alert := range alerts { + if alert.State != "open" { + continue + } + if scanTool != "" && alert.Tool.Name != scanTool { + continue + } + severity := alert.Rule.Severity + if severity == "" { + severity = "medium" + } + if !filterBySeverity(severity, securitySeverity) { + continue + } + summary.Add(severity) + allAlerts = append(allAlerts, ScanAlert{ + Repo: repo.Name, + Severity: severity, + RuleID: alert.Rule.ID, + Tool: alert.Tool.Name, + Path: alert.MostRecentInstance.Location.Path, + Line: alert.MostRecentInstance.Location.StartLine, + Description: alert.Rule.Description, + Message: alert.MostRecentInstance.Message.Text, + }) + } + + // Record metrics + _ = ai.Record(ai.Event{ + Type: "security.scan", + Timestamp: time.Now(), + Repo: fullName, + Data: map[string]any{ + "target": fullName, + "total": summary.Total, + "critical": summary.Critical, + "high": summary.High, + "medium": summary.Medium, + "low": summary.Low, + }, + }) + + if securityJSON { + output, err := json.MarshalIndent(allAlerts, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") + } + cli.Text(string(output)) + return nil + } + + cli.Blank() + cli.Print("%s %s\n", cli.DimStyle.Render("Code Scanning ("+fullName+"):"), summary.String()) + cli.Blank() + + if len(allAlerts) == 0 { + return nil + } + + for _, alert := range allAlerts { + sevStyle := severityStyle(alert.Severity) + location := fmt.Sprintf("%s:%d", alert.Path, alert.Line) + cli.Print("%-16s %s %-20s %-40s %s\n", + cli.ValueStyle.Render(alert.Repo), + sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)), + alert.RuleID, + location, + cli.DimStyle.Render(alert.Tool), + ) + } + cli.Blank() + + return nil +} diff --git a/internal/cmd/security/cmd_secrets.go b/internal/cmd/security/cmd_secrets.go index 87549db..7878cd9 100644 --- a/internal/cmd/security/cmd_secrets.go +++ b/internal/cmd/security/cmd_secrets.go @@ -21,6 +21,7 @@ func addSecretsCommand(parent *cli.Command) { cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) + cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target")) parent.AddCommand(cmd) } @@ -40,6 +41,11 @@ func runSecrets() error { return err } + // External target mode: bypass registry entirely + if securityTarget != "" { + return runSecretsForTarget(securityTarget) + } + reg, err := loadRegistry(securityRegistryPath) if err != nil { return err @@ -119,3 +125,67 @@ func runSecrets() error { return nil } + +// runSecretsForTarget runs secret scanning checks against an external repo target. +func runSecretsForTarget(target string) error { + repo, fullName := buildTargetRepo(target) + if repo == nil { + return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)") + } + + var allAlerts []SecretAlert + openCount := 0 + + alerts, err := fetchSecretScanningAlerts(fullName) + if err != nil { + return cli.Wrap(err, "fetch secret-scanning alerts for "+fullName) + } + + for _, alert := range alerts { + if alert.State != "open" { + continue + } + openCount++ + allAlerts = append(allAlerts, SecretAlert{ + Repo: repo.Name, + Number: alert.Number, + SecretType: alert.SecretType, + State: alert.State, + Resolution: alert.Resolution, + PushProtection: alert.PushProtection, + }) + } + + if securityJSON { + output, err := json.MarshalIndent(allAlerts, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") + } + cli.Text(string(output)) + return nil + } + + cli.Blank() + if openCount > 0 { + cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount))) + } else { + cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.SuccessStyle.Render("No exposed secrets")) + } + cli.Blank() + + for _, alert := range allAlerts { + bypassed := "" + if alert.PushProtection { + bypassed = cli.WarningStyle.Render(" (push protection bypassed)") + } + cli.Print("%-16s %-6d %-30s%s\n", + cli.ValueStyle.Render(alert.Repo), + alert.Number, + cli.ErrorStyle.Render(alert.SecretType), + bypassed, + ) + } + cli.Blank() + + return nil +} diff --git a/internal/cmd/security/cmd_security.go b/internal/cmd/security/cmd_security.go index c37d4a0..68f1b4e 100644 --- a/internal/cmd/security/cmd_security.go +++ b/internal/cmd/security/cmd_security.go @@ -17,6 +17,7 @@ var ( securityRepo string securitySeverity string securityJSON bool + securityTarget string // External repo target (e.g. "wailsapp/wails") ) // AddSecurityCommands adds the 'security' command to the root. @@ -31,6 +32,7 @@ func AddSecurityCommands(root *cli.Command) { addDepsCommand(secCmd) addScanCommand(secCmd) addSecretsCommand(secCmd) + addJobsCommand(secCmd) root.AddCommand(secCmd) } @@ -192,6 +194,16 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo { return reg.List() } +// buildTargetRepo creates a synthetic Repo entry for an external target (e.g. "wailsapp/wails"). +func buildTargetRepo(target string) (*repos.Repo, string) { + parts := strings.SplitN(target, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, "" + } + return &repos.Repo{Name: parts[1]}, target +} + + // AlertSummary holds aggregated alert counts. type AlertSummary struct { Critical int diff --git a/internal/core-ide/.gitignore b/internal/core-ide/.gitignore new file mode 100644 index 0000000..88adc8c --- /dev/null +++ b/internal/core-ide/.gitignore @@ -0,0 +1,7 @@ +.task +.idea +bin +frontend/dist +frontend/node_modules +build/linux/appimage/build +build/windows/nsis/MicrosoftEdgeWebview2Setup.exe \ No newline at end of file diff --git a/internal/core-ide/README.md b/internal/core-ide/README.md new file mode 100644 index 0000000..8a4cf3f --- /dev/null +++ b/internal/core-ide/README.md @@ -0,0 +1,71 @@ +# Wails3 Angular Template + +- Angular 20 +- Wails3 + +![](wails3-angular-template.jpg) + +Includes all Angular CLI guidelines, Web Awesome, and Font Awesome. + +## Getting Started + +1. Navigate to your project directory in the terminal. + +make a new project using Wails3: + + ``` + wails3 init -n MyWailsApp -t https://github.com/Snider/wails-angular-template@v0.0.1 + cd MyWailsApp + ``` + +2. To run your application in development mode, use the following command: + + ``` + wails3 dev + ``` + + This will start your application and enable hot-reloading for both frontend and backend changes. + +3. To build your application for production, use: + + ``` + wails3 build + ``` + + This will create a production-ready executable in the `build` directory. + +## Exploring Wails3 Features + +Now that you have your project set up, it's time to explore the features that Wails3 offers: + +1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications. + +2. **Run an example**: To run any of the examples, navigate to the example's directory and use: + + ``` + go run . + ``` + + Note: Some examples may be under development during the alpha phase. + +3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references. + +4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions). + +## Project Structure + +Take a moment to familiarize yourself with your project structure: + +- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript) +- `main.go`: The entry point of your Go backend +- `app.go`: Define your application structure and methods here +- `wails.json`: Configuration file for your Wails project + +## Next Steps + +1. Modify the frontend in the `frontend/` directory to create your desired UI. +2. Add backend functionality in `main.go`. +3. Use `wails3 dev` to see your changes in real-time. +4. When ready, build your application with `wails3 build`. + +Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community. diff --git a/internal/core-ide/Taskfile.yml b/internal/core-ide/Taskfile.yml new file mode 100644 index 0000000..4eff589 --- /dev/null +++ b/internal/core-ide/Taskfile.yml @@ -0,0 +1,34 @@ +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: "core-ide" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +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}} + diff --git a/internal/core-ide/build/Taskfile.yml b/internal/core-ide/build/Taskfile.yml new file mode 100644 index 0000000..a060314 --- /dev/null +++ b/internal/core-ide/build/Taskfile.yml @@ -0,0 +1,91 @@ +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/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.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/internal/core-ide/build/appicon.png b/internal/core-ide/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/internal/core-ide/build/appicon.png differ diff --git a/internal/core-ide/build/config.yml b/internal/core-ide/build/config.yml new file mode 100644 index 0000000..b803219 --- /dev/null +++ b/internal/core-ide/build/config.yml @@ -0,0 +1,62 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "Lethean Community Interest Company" # The name of the company + productName: "Core IDE" # The name of the application + productIdentifier: "com.lethean.core-ide" # The unique product identifier + description: "Core IDE - Development Environment" # The application description + copyright: "(c) 2026, Lethean Community Interest Company. EUPL-1.2" # Copyright text + comments: "Host UK Core IDE" # Comments + version: "0.0.1" # The application version + +# 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: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/internal/core-ide/build/darwin/Info.dev.plist b/internal/core-ide/build/darwin/Info.dev.plist new file mode 100644 index 0000000..9ccb628 --- /dev/null +++ b/internal/core-ide/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + Core IDE (Dev) + CFBundleExecutable + core-ide + CFBundleIdentifier + com.lethean.core-ide.dev + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + Core IDE Development Build + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + + NSHumanReadableCopyright + © 2026 Lethean Community Interest Company. EUPL-1.2 + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/internal/core-ide/build/darwin/Info.plist b/internal/core-ide/build/darwin/Info.plist new file mode 100644 index 0000000..5896c2f --- /dev/null +++ b/internal/core-ide/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + Core IDE + CFBundleExecutable + core-ide + CFBundleIdentifier + com.lethean.core-ide + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + Core IDE - Development Environment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + + NSHumanReadableCopyright + © 2026 Lethean Community Interest Company. EUPL-1.2 + + \ No newline at end of file diff --git a/internal/core-ide/build/darwin/Taskfile.yml b/internal/core-ide/build/darwin/Taskfile.yml new file mode 100644 index 0000000..47d6a4a --- /dev/null +++ b/internal/core-ide/build/darwin/Taskfile.yml @@ -0,0 +1,85 @@ +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/internal/core-ide/build/darwin/icons.icns b/internal/core-ide/build/darwin/icons.icns new file mode 100644 index 0000000..1b5bd4c Binary files /dev/null and b/internal/core-ide/build/darwin/icons.icns differ diff --git a/internal/core-ide/build/linux/Taskfile.yml b/internal/core-ide/build/linux/Taskfile.yml new file mode 100644 index 0000000..7ddf9f3 --- /dev/null +++ b/internal/core-ide/build/linux/Taskfile.yml @@ -0,0 +1,119 @@ +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 + - task: create:aur + + 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 + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + 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:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -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: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: '{{.APP_NAME}}' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/internal/core-ide/build/linux/appimage/build.sh b/internal/core-ide/build/linux/appimage/build.sh new file mode 100644 index 0000000..d881342 --- /dev/null +++ b/internal/core-ide/build/linux/appimage/build.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +ARCH=$(uname -m) +case "${ARCH}" in + x86_64) + DEPLOY_ARCH="x86_64" + ;; + aarch64|arm64) + DEPLOY_ARCH="aarch64" + ;; + *) + echo "Unsupported architecture: ${ARCH}" >&2 + exit 1 + ;; +esac + +# Download linuxdeploy and make it executable +wget -q -4 -N "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${DEPLOY_ARCH}.AppImage" +chmod +x "linuxdeploy-${DEPLOY_ARCH}.AppImage" + +# Run linuxdeploy to bundle the application +"./linuxdeploy-${DEPLOY_ARCH}.AppImage" --appdir "${APP_DIR}" --output appimage + +# Rename the generated AppImage (glob must be unquoted) +mv ${APP_NAME}*.AppImage "${APP_NAME}.AppImage" + diff --git a/internal/core-ide/build/linux/desktop b/internal/core-ide/build/linux/desktop new file mode 100644 index 0000000..ca928b9 --- /dev/null +++ b/internal/core-ide/build/linux/desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=My Product +Comment=My Product Description +# The Exec line includes %u to pass the URL to the application +Exec=/usr/local/bin/wails-angular-template %u +Terminal=false +Type=Application +Icon=wails-angular-template +Categories=Utility; +StartupWMClass=wails-angular-template + + diff --git a/internal/core-ide/build/linux/nfpm/nfpm.yaml b/internal/core-ide/build/linux/nfpm/nfpm.yaml new file mode 100644 index 0000000..c993b68 --- /dev/null +++ b/internal/core-ide/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,67 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "core-ide" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "Core IDE - Development Environment" +vendor: "Lethean Community Interest Company" +homepage: "https://host.uk.com" +license: "EUPL-1.2" +release: "1" + +contents: + - src: "./bin/core-ide" + dst: "/usr/local/bin/core-ide" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/core-ide.png" + - src: "./build/linux/core-ide.desktop" + dst: "/usr/share/applications/core-ide.desktop" + +# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 +depends: + - libgtk-3-0 + - libwebkit2gtk-4.1-0 + +# Distribution-specific overrides for different package formats and WebKit versions +overrides: + # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.1) + rpm: + depends: + - gtk3 + - webkit2gtk4.1 + + # Arch Linux packages (WebKit 4.1) + archlinux: + depends: + - gtk3 + - webkit2gtk-4.1 + +# scripts section to ensure desktop database is updated after install +scripts: + postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + # You can also add preremove, postremove if needed + # preremove: "./build/linux/nfpm/scripts/preremove.sh" + # postremove: "./build/linux/nfpm/scripts/postremove.sh" + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" diff --git a/internal/core-ide/build/linux/nfpm/scripts/postinstall.sh b/internal/core-ide/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 0000000..4bbb815 --- /dev/null +++ b/internal/core-ide/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Update desktop database for .desktop file changes +# This makes the application appear in application menus and registers its capabilities. +if command -v update-desktop-database >/dev/null 2>&1; then + echo "Updating desktop database..." + update-desktop-database -q /usr/share/applications +else + echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2 +fi + +# Update MIME database for custom URL schemes (x-scheme-handler) +# This ensures the system knows how to handle your custom protocols. +if command -v update-mime-database >/dev/null 2>&1; then + echo "Updating MIME database..." + update-mime-database -n /usr/share/mime +else + echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 +fi + +exit 0 diff --git a/internal/core-ide/build/linux/nfpm/scripts/postremove.sh b/internal/core-ide/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/internal/core-ide/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/internal/core-ide/build/linux/nfpm/scripts/preinstall.sh b/internal/core-ide/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/internal/core-ide/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/internal/core-ide/build/linux/nfpm/scripts/preremove.sh b/internal/core-ide/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/internal/core-ide/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/internal/core-ide/build/windows/Taskfile.yml b/internal/core-ide/build/windows/Taskfile.yml new file mode 100644 index 0000000..12ec591 --- /dev/null +++ b/internal/core-ide/build/windows/Taskfile.yml @@ -0,0 +1,98 @@ +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: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + 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: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application + cmds: + - |- + if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then + task create:msix:package + else + task create:nsis:installer + fi + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/internal/core-ide/build/windows/icon.ico b/internal/core-ide/build/windows/icon.ico new file mode 100644 index 0000000..bfa0690 Binary files /dev/null and b/internal/core-ide/build/windows/icon.ico differ diff --git a/internal/core-ide/build/windows/info.json b/internal/core-ide/build/windows/info.json new file mode 100644 index 0000000..a27cf52 --- /dev/null +++ b/internal/core-ide/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "Lethean Community Interest Company", + "FileDescription": "Core IDE — Desktop development environment", + "LegalCopyright": "© 2026 Lethean Community Interest Company. EUPL-1.2", + "ProductName": "Core IDE", + "Comments": "Built with Wails v3 and Angular" + } + } +} \ No newline at end of file diff --git a/internal/core-ide/build/windows/msix/app_manifest.xml b/internal/core-ide/build/windows/msix/app_manifest.xml new file mode 100644 index 0000000..f03f88b --- /dev/null +++ b/internal/core-ide/build/windows/msix/app_manifest.xml @@ -0,0 +1,52 @@ + + + + + + + Core IDE + Lethean Community Interest Company + Core IDE - Development Environment + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/internal/core-ide/build/windows/msix/template.xml b/internal/core-ide/build/windows/msix/template.xml new file mode 100644 index 0000000..6d10ade --- /dev/null +++ b/internal/core-ide/build/windows/msix/template.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + false + My Product + My Company + My Product Description + Assets\AppIcon.png + + + + + + + diff --git a/internal/core-ide/build/windows/nsis/project.nsi b/internal/core-ide/build/windows/nsis/project.nsi new file mode 100644 index 0000000..a9b6aef --- /dev/null +++ b/internal/core-ide/build/windows/nsis/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "wails-angular-template" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$APPDATA\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/internal/core-ide/build/windows/nsis/wails_tools.nsh b/internal/core-ide/build/windows/nsis/wails_tools.nsh new file mode 100644 index 0000000..16ff5e7 --- /dev/null +++ b/internal/core-ide/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,236 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "core-ide" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "Lethean Community Interest Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "Core IDE" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "© 2026 Lethean Community Interest Company. EUPL-1.2" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + +!macroend \ No newline at end of file diff --git a/internal/core-ide/build/windows/wails.exe.manifest b/internal/core-ide/build/windows/wails.exe.manifest new file mode 100644 index 0000000..322a696 --- /dev/null +++ b/internal/core-ide/build/windows/wails.exe.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + + + + + + + \ No newline at end of file diff --git a/internal/core-ide/claude_bridge.go b/internal/core-ide/claude_bridge.go new file mode 100644 index 0000000..279c745 --- /dev/null +++ b/internal/core-ide/claude_bridge.go @@ -0,0 +1,183 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +var wsUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true // Allow requests with no Origin header (same-origin) + } + host := r.Host + return origin == "http://"+host || origin == "https://"+host || + strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") + }, +} + +// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket. +type ClaudeBridge struct { + mcpConn *websocket.Conn + mcpURL string + clients map[*websocket.Conn]bool + clientsMu sync.RWMutex + broadcast chan []byte + reconnectMu sync.Mutex +} + +// NewClaudeBridge creates a new bridge to the MCP core WebSocket. +func NewClaudeBridge(mcpURL string) *ClaudeBridge { + return &ClaudeBridge{ + mcpURL: mcpURL, + clients: make(map[*websocket.Conn]bool), + broadcast: make(chan []byte, 256), + } +} + +// Start connects to the MCP WebSocket and starts the bridge. +func (cb *ClaudeBridge) Start() { + go cb.connectToMCP() + go cb.broadcastLoop() +} + +// connectToMCP establishes connection to the MCP core WebSocket. +func (cb *ClaudeBridge) connectToMCP() { + for { + cb.reconnectMu.Lock() + if cb.mcpConn != nil { + cb.mcpConn.Close() + } + + log.Printf("Claude bridge connecting to MCP at %s", cb.mcpURL) + conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil) + if err != nil { + log.Printf("Claude bridge failed to connect to MCP: %v", err) + cb.reconnectMu.Unlock() + time.Sleep(5 * time.Second) + continue + } + + cb.mcpConn = conn + cb.reconnectMu.Unlock() + log.Printf("Claude bridge connected to MCP") + + // Read messages from MCP and broadcast to clients + for { + _, message, err := conn.ReadMessage() + if err != nil { + log.Printf("Claude bridge MCP read error: %v", err) + break + } + select { + case cb.broadcast <- message: + default: + log.Printf("Claude bridge: broadcast channel full, dropping message") + } + } + + // Connection lost, retry + time.Sleep(2 * time.Second) + } +} + +// broadcastLoop sends messages from MCP to all connected clients. +func (cb *ClaudeBridge) broadcastLoop() { + for message := range cb.broadcast { + var failedClients []*websocket.Conn + cb.clientsMu.RLock() + for client := range cb.clients { + err := client.WriteMessage(websocket.TextMessage, message) + if err != nil { + log.Printf("Claude bridge client write error: %v", err) + failedClients = append(failedClients, client) + } + } + cb.clientsMu.RUnlock() + + if len(failedClients) > 0 { + cb.clientsMu.Lock() + for _, client := range failedClients { + delete(cb.clients, client) + client.Close() + } + cb.clientsMu.Unlock() + } + } +} + +// HandleWebSocket handles WebSocket connections from GUI clients. +func (cb *ClaudeBridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := wsUpgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Claude bridge upgrade error: %v", err) + return + } + + // Send connected message before registering to avoid concurrent writes + connMsg, _ := json.Marshal(map[string]any{ + "type": "system", + "data": "Connected to Claude bridge", + "timestamp": time.Now(), + }) + if err := conn.WriteMessage(websocket.TextMessage, connMsg); err != nil { + log.Printf("Claude bridge initial write error: %v", err) + conn.Close() + return + } + + cb.clientsMu.Lock() + cb.clients[conn] = true + cb.clientsMu.Unlock() + + defer func() { + cb.clientsMu.Lock() + delete(cb.clients, conn) + cb.clientsMu.Unlock() + conn.Close() + }() + + // Read messages from client and forward to MCP + for { + _, message, err := conn.ReadMessage() + if err != nil { + break + } + + // Parse the message to check type + var msg map[string]any + if err := json.Unmarshal(message, &msg); err != nil { + continue + } + + // Forward claude_message to MCP + if msgType, ok := msg["type"].(string); ok && msgType == "claude_message" { + cb.sendToMCP(message) + } + } +} + +// sendToMCP sends a message to the MCP WebSocket. +func (cb *ClaudeBridge) sendToMCP(message []byte) { + cb.reconnectMu.Lock() + defer cb.reconnectMu.Unlock() + + if cb.mcpConn == nil { + log.Printf("Claude bridge: MCP not connected") + return + } + + err := cb.mcpConn.WriteMessage(websocket.TextMessage, message) + if err != nil { + log.Printf("Claude bridge MCP write error: %v", err) + } +} diff --git a/internal/core-ide/frontend/.editorconfig b/internal/core-ide/frontend/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/internal/core-ide/frontend/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/internal/core-ide/frontend/.gitignore b/internal/core-ide/frontend/.gitignore new file mode 100644 index 0000000..b1d225e --- /dev/null +++ b/internal/core-ide/frontend/.gitignore @@ -0,0 +1,43 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/internal/core-ide/frontend/README.md b/internal/core-ide/frontend/README.md new file mode 100644 index 0000000..d0648c8 --- /dev/null +++ b/internal/core-ide/frontend/README.md @@ -0,0 +1,59 @@ +# WailsAngularTemplate + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.6. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/internal/core-ide/frontend/angular.json b/internal/core-ide/frontend/angular.json new file mode 100644 index 0000000..cbf7b58 --- /dev/null +++ b/internal/core-ide/frontend/angular.json @@ -0,0 +1,98 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "wails-angular-template": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "wails-angular-template:build:production" + }, + "development": { + "buildTarget": "wails-angular-template:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ] + } + } + } + } + } +} diff --git a/internal/core-ide/frontend/bindings/changeme/greetservice.ts b/internal/core-ide/frontend/bindings/changeme/greetservice.ts new file mode 100644 index 0000000..760195a --- /dev/null +++ b/internal/core-ide/frontend/bindings/changeme/greetservice.ts @@ -0,0 +1,10 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime"; + +export function Greet(name: string): $CancellablePromise { + return $Call.ByID(1411160069, name); +} diff --git a/internal/core-ide/frontend/bindings/changeme/index.ts b/internal/core-ide/frontend/bindings/changeme/index.ts new file mode 100644 index 0000000..50e3f04 --- /dev/null +++ b/internal/core-ide/frontend/bindings/changeme/index.ts @@ -0,0 +1,7 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as GreetService from "./greetservice.js"; +export { + GreetService +}; diff --git a/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/greetservice.ts b/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/greetservice.ts new file mode 100644 index 0000000..760195a --- /dev/null +++ b/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/greetservice.ts @@ -0,0 +1,10 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime"; + +export function Greet(name: string): $CancellablePromise { + return $Call.ByID(1411160069, name); +} diff --git a/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/index.ts b/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/index.ts new file mode 100644 index 0000000..50e3f04 --- /dev/null +++ b/internal/core-ide/frontend/bindings/github.com/host-uk/core/internal/core-ide/index.ts @@ -0,0 +1,7 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as GreetService from "./greetservice.js"; +export { + GreetService +}; diff --git a/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts new file mode 100644 index 0000000..1ea1058 --- /dev/null +++ b/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..3dd1807 --- /dev/null +++ b/internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/internal/core-ide/frontend/package-lock.json b/internal/core-ide/frontend/package-lock.json new file mode 100644 index 0000000..b8a6569 --- /dev/null +++ b/internal/core-ide/frontend/package-lock.json @@ -0,0 +1,9759 @@ +{ + "name": "wails-angular-template", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wails-angular-template", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^20.3.0", + "@angular/compiler": "^20.3.0", + "@angular/core": "^20.3.0", + "@angular/forms": "^20.3.0", + "@angular/platform-browser": "^20.3.0", + "@angular/platform-server": "^20.3.0", + "@angular/router": "^20.3.0", + "@angular/ssr": "^20.3.6", + "@wailsio/runtime": "latest", + "express": "^5.1.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular/build": "^20.3.6", + "@angular/cli": "^20.3.6", + "@angular/compiler-cli": "^20.3.0", + "@types/express": "^5.0.1", + "@types/jasmine": "~5.1.0", + "@types/node": "^20.17.19", + "jasmine-core": "~5.9.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.9.2" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.1.0.tgz", + "integrity": "sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.35.0.tgz", + "integrity": "sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.35.0.tgz", + "integrity": "sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.35.0.tgz", + "integrity": "sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.35.0.tgz", + "integrity": "sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.35.0.tgz", + "integrity": "sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.35.0.tgz", + "integrity": "sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.35.0.tgz", + "integrity": "sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.35.0.tgz", + "integrity": "sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.35.0.tgz", + "integrity": "sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.35.0.tgz", + "integrity": "sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.35.0.tgz", + "integrity": "sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.35.0.tgz", + "integrity": "sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.35.0.tgz", + "integrity": "sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.35.0" + }, + "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, + "license": "Apache-2.0", + "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.2003.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.8.tgz", + "integrity": "sha512-pbXQ2NlZQwzjsSIEoRQMGB1WrgZFCyM0zoD9h+rDjyR8PEB1Evl4evZ4Q5CJzjEBxC8IEG61PHKHjh8GdLb+sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.8", + "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/core": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.8.tgz", + "integrity": "sha512-+YFpJdvlL4gxnMm/++8rseE7ZNRHlYPmOqpoiXSuP5eGPSmdklEoQGTQvpMw42S3bll1g6/029DmV2FCZ/dtEQ==", + "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": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.8.tgz", + "integrity": "sha512-Ymv7nWLTDB1gBh2laRveO912eUpQ/rUIzKRr8VQFMVG/wNipL88vzyrlKhJa7WhQ3CdKxLD7kplFIjdev7XUVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.8", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "8.2.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/build": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.8.tgz", + "integrity": "sha512-wE6/T1FIjDSXljyNPh7KEwK5ysH3/uq2h8ZB5UCAAUkPHcQ/Y1unk27TUYePO7++KjkYXUX6XwwYZksXCZFJjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2003.8", + "@babel/core": "7.28.3", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.14", + "@vitejs/plugin-basic-ssl": "2.1.0", + "beasties": "0.3.5", + "browserslist": "^4.23.0", + "esbuild": "0.25.9", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.1", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.3", + "piscina": "5.1.3", + "rollup": "4.52.3", + "sass": "1.90.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.14", + "vite": "7.1.11", + "watchpack": "2.4.4" + }, + "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" + }, + "optionalDependencies": { + "lmdb": "3.4.2" + }, + "peerDependencies": { + "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/localize": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/service-worker": "^20.0.0", + "@angular/ssr": "^20.3.8", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^20.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.8 <6.0", + "vitest": "^3.1.1" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "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 + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.8.tgz", + "integrity": "sha512-UUNmwDCrRknE+50Gwwt66o4T/l0KfLWOzxlYdLn9l2PIVNhpspg+5CUkO0juRyRyCxCnojic1s9pPTD1Eq4rtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2003.8", + "@angular-devkit/core": "20.3.8", + "@angular-devkit/schematics": "20.3.8", + "@inquirer/prompts": "7.8.2", + "@listr2/prompt-adapter-inquirer": "3.0.1", + "@modelcontextprotocol/sdk": "1.17.3", + "@schematics/angular": "20.3.8", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.35.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.1", + "npm-package-arg": "13.0.0", + "pacote": "21.0.0", + "resolve": "1.22.10", + "semver": "7.7.2", + "yargs": "18.0.0", + "zod": "3.25.76" + }, + "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/common": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.9.tgz", + "integrity": "sha512-PgKEnv30TxvpfTJ3d4h5LEjUHpKSYcs3Rc4OvK7p5A7waBkXzfqCBmy54nomzfcf4dlEjb6wSoXxlJbR7Y34Iw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "20.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.9.tgz", + "integrity": "sha512-nfzR/JpI77Yr4opRimnnTys//taZiibEco1ihV1C02eM4FDCQMOEp8WB+DT/yUESb6MRBlZe1MjeelwSfHlB7g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.9.tgz", + "integrity": "sha512-Fe7MIg2NWXoK+M4GtclxaYNoTdZX2U8f/Fd3N8zxtEMcRsvliJOnJ4oQtpx5kqMAuZVO4zY3wuIY1wAGXYCUbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.28.3", + "@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": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.3.9", + "typescript": ">=5.8 <6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.9.tgz", + "integrity": "sha512-zZb7wUexBIIUojr1helzXsL25ilAoASm8aPOjBNHPLYr4ndDjMD/wogmH/dA7EzuCdmZf30ZmZZpuX149WdrpA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.3.9", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.9.tgz", + "integrity": "sha512-jSlhU1IyuxxSYNN5Gg3oBb0nAqIl5Mwf1hywtkbyMay+3sENYGvBRseWp00R308isKe+n8bKi6hF54A1lhozzg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.3.9", + "@angular/core": "20.3.9", + "@angular/platform-browser": "20.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.9.tgz", + "integrity": "sha512-q9uyNIKto3PmIh3q9/OX0HYN/SMYqCJ7MyQHBuF9Rel0vXi0gWyk2dgsWAl/tSTLlqHWtGZZ3rvJyxYQmxFo4w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "20.3.9", + "@angular/common": "20.3.9", + "@angular/core": "20.3.9" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-server": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.9.tgz", + "integrity": "sha512-rLE3hFxEs2D0wmKcrNiVLUajEyHBZvHN/YDt7ujaZNR0gVSj45CJOWn2/V2+AnP/73RjmvZgukh15sqFR2j6LQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.3.9", + "@angular/compiler": "20.3.9", + "@angular/core": "20.3.9", + "@angular/platform-browser": "20.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/router": { + "version": "20.3.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.9.tgz", + "integrity": "sha512-wsilSrTtR85OFd6XP0b9rMakx1pEw5sHEYBrfoSQc+NfYCsP5a5qFBJ5CWOQKgWjKlfPgpkaheD6JdqN9WpFoQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.3.9", + "@angular/core": "20.3.9", + "@angular/platform-browser": "20.3.9", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/ssr": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-20.3.8.tgz", + "integrity": "sha512-7xPDwF6uyHSo1cLJO4YJZiNPtuuK5Ujz4B17NCSvYaEFGYbaZa/K9OXdUyrY56C6r4iU9V1gfEHXBuhCajMN0Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/router": "^20.0.0" + }, + "peerDependenciesMeta": { + "@angular/platform-server": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.0.tgz", + "integrity": "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.21.tgz", + "integrity": "sha512-+mScLhIcbPFmuvU3tAGBed78XvYHSvCl6dBiYMlzCLhpr0bzGzd8tfivMMeqND6XZiaZ1tgusbUHJEfc6YzOdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.5.tgz", + "integrity": "sha512-7GoWev7P6s7t0oJbenH0eQ0ThNdDJbEAEtVt9vsrYZ9FulIokvd823yLyhQlWHJPGce1wzP53ttfdCZmonMHyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.21.tgz", + "integrity": "sha512-5QWs0KGaNMlhbdhOSCFfKsW+/dcAVC2g4wT/z2MCiZM47uLgatC5N20kpkDQf7dHx+XFct/MJvvNGy6aYJn4Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.21.tgz", + "integrity": "sha512-xxeW1V5SbNFNig2pLfetsDb0svWlKuhmr7MPJZMYuDnCTkpVBI+X/doudg4pznc1/U+yYmWFFOi4hNvGgUo7EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.2.tgz", + "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.1", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.17", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.9.tgz", + "integrity": "sha512-AWpxB7MuJrRiSfTKGJ7Y68imYt8P9N3Gaa7ySdkFj1iWjr6WfbGAhdZvw/UnhFXTHITJzxGUI9k8IX7akAEBCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.0.tgz", + "integrity": "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.0.tgz", + "integrity": "sha512-kaC3FHsJZvVyIjYBs5Ih8y8Bj4P/QItQWrZW22WJax7zTN+ZPXVGuOM55vzbdCP9zKUiBd9iEJVdesujfF+cAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "dev": true, + "license": "MIT", + "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.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz", + "integrity": "sha512-3XFmGwm3u6ioREG+ynAQB7FoxfajgQnMhIu8wC5eo/Lsih4aKDg0VuIMGaOsYn7hJSJagSeaD4K8yfpkEoDEmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.1" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.2.tgz", + "integrity": "sha512-NK80WwDoODyPaSazKbzd3NEJ3ygePrkERilZshxBViBARNz21rmediktGHExoj9n5t9+ChlgLlxecdFKLCuCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.2.tgz", + "integrity": "sha512-zevaowQNmrp3U7Fz1s9pls5aIgpKRsKb3dZWDINtLiozh3jZI9fBrI19lYYBxqdyiIyNdlyiidPnwPShj4aK+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.2.tgz", + "integrity": "sha512-OmHCULY17rkx/RoCoXlzU7LyR8xqrksgdYWwtYa14l/sseezZ8seKWXcogHcjulBddER5NnEFV4L/Jtr2nyxeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.2.tgz", + "integrity": "sha512-ZBEfbNZdkneebvZs98Lq30jMY8V9IJzckVeigGivV7nTHJc+89Ctomp1kAIWKlwIG0ovCDrFI448GzFPORANYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.2.tgz", + "integrity": "sha512-vL9nM17C77lohPYE4YaAQvfZCSVJSryE4fXdi8M7uWPBnU+9DJabgKVAeyDb84ZM2vcFseoBE4/AagVtJeRE7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.2.tgz", + "integrity": "sha512-SXWjdBfNDze4ZPeLtYIzsIeDJDJ/SdsA0pEXcUBayUIMO0FQBHfVZZyHXQjjHr4cvOAzANBgIiqaXRwfMhzmLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.2.tgz", + "integrity": "sha512-IY+r3bxKW6Q6sIPiMC0L533DEfRJSXibjSI3Ft/w9Q8KQBNqEIvUFXt+09wV8S5BRk0a8uSF19YWxuRwEfI90g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", + "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "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", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", + "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "license": "MIT", + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.8.tgz", + "integrity": "sha512-lmdh1JywRl0BK1VcYwGDrNre78OpduNhsV4N5afELvrNPKSk/ixCb3iZq4MCY3yBZ3RV5Uso+vrJwwEeqe02JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "20.3.8", + "@angular-devkit/schematics": "20.3.8", + "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/@sigstore/bundle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", + "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", + "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", + "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "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, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@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, + "license": "MIT", + "dependencies": { + "@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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@types/jasmine": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.12.tgz", + "integrity": "sha512-1BzPxNsFDLDfj9InVR3IeY0ZVf4o9XV+4mDqoCfyPkbsA7dYyKAPAb2co6wLFlHcvxPlt1wShm7zQdV7uTfLGA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@wailsio/runtime": { + "version": "3.0.0-alpha.72", + "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.72.tgz", + "integrity": "sha512-VJjDa0GBG7tp7WBMlytzLvsZ4gBQVBftIwiJ+dSg2C4e11N6JonJZp9iHT2xgK35rewKdwbX1vMDyrcBcyZYoA==", + "license": "MIT" + }, + "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, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", + "integrity": "sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.1.0", + "@algolia/client-abtesting": "5.35.0", + "@algolia/client-analytics": "5.35.0", + "@algolia/client-common": "5.35.0", + "@algolia/client-insights": "5.35.0", + "@algolia/client-personalization": "5.35.0", + "@algolia/client-query-suggestions": "5.35.0", + "@algolia/client-search": "5.35.0", + "@algolia/ingestion": "1.35.0", + "@algolia/monitoring": "1.35.0", + "@algolia/recommend": "5.35.0", + "@algolia/requester-browser-xhr": "5.35.0", + "@algolia/requester-fetch": "5.35.0", + "@algolia/requester-node-http": "5.35.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/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/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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", + "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/beasties": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", + "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.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/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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "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": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/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/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "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/cacache/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/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/caniuse-lite": { + "version": "1.0.30001752", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz", + "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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/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, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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/cliui/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/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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/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, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "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/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, + "license": "MIT" + }, + "node_modules/connect/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, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "BSD-2-Clause" + }, + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "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.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/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, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "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, + "license": "MIT" + }, + "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": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.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/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/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "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" + } + ], + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "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": ">= 0.8" + } + }, + "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, + "license": "ISC" + }, + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "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, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/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/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-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "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/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.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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, + "license": "MIT", + "engines": { + "node": ">=0.12.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==", + "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, + "license": "MIT", + "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": "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/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", + "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/karma/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "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.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/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, + "license": "MIT", + "dependencies": { + "ms": "2.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, + "license": "MIT" + }, + "node_modules/karma/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, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/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, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/karma/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/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, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/listr2": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", + "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", + "dev": true, + "license": "MIT", + "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": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "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, + "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/lmdb": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.2.tgz", + "integrity": "sha512-nwVGUfTBUwJKXd6lRV8pFNfnrCC1+l49ESJRM19t/tFb/97QfJEixe5DYRvug5JO7DSFKoKaVy7oGMt5rVqZvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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.4.2", + "@lmdb/lmdb-darwin-x64": "3.4.2", + "@lmdb/lmdb-linux-arm": "3.4.2", + "@lmdb/lmdb-linux-arm64": "3.4.2", + "@lmdb/lmdb-linux-x64": "3.4.2", + "@lmdb/lmdb-win32-arm64": "3.4.2", + "@lmdb/lmdb-win32-x64": "3.4.2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "MIT", + "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/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/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, + "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/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, + "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/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "optional": 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, + "license": "MIT", + "optional": 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, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.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, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/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-sized/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/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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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/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, + "license": "ISC", + "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" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT", + "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/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/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/tar": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "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/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/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/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, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", + "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.0.tgz", + "integrity": "sha512-+t2etZAGcB7TbbLHfDwooV9ppB2LhhcT6A+L9cahsf9mEUAoQ6CktLEVvEnpD0N5CkX7zJqnPGaFtoQDy9EkHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.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-packlist/node_modules/proc-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", + "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest/node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-pick-manifest/node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-registry-fetch/node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "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==", + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", + "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.0.tgz", + "integrity": "sha512-lcqexq73AMv6QNLo7SOpz0JJoaGdS3rBFgF122NZVl1bApo2mfu+XzUBU/X/XsiJu+iUmKpekRayqQYAs+PhkA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^10.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pacote/node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/pacote/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pacote/node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/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/parse5-html-rewriting-stream/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/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/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, + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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, + "license": "ISC" + }, + "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/piscina": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.3.tgz", + "integrity": "sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT", + "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, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "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.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "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==", + "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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz", + "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", + "dev": true, + "license": "MIT", + "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/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT", + "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/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.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "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.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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.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/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, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.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, + "license": "MIT", + "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/string-width-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/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/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", + "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "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" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "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/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "license": "MIT", + "optional": true + }, + "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/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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "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-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "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/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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/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/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, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "license": "MIT" + } + } +} diff --git a/internal/core-ide/frontend/package.json b/internal/core-ide/frontend/package.json new file mode 100644 index 0000000..885d402 --- /dev/null +++ b/internal/core-ide/frontend/package.json @@ -0,0 +1,57 @@ +{ + "name": "wails-angular-template", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "dev": "ng serve --configuration development", + "build": "ng build", + "build:dev": "ng build --configuration development", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "serve:ssr:wails-angular-template": "node dist/wails-angular-template/server/server.mjs" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] + }, + "private": true, + "dependencies": { + "@angular/common": "^20.3.0", + "@angular/compiler": "^20.3.0", + "@angular/core": "^20.3.0", + "@angular/forms": "^20.3.0", + "@angular/platform-browser": "^20.3.0", + "@angular/platform-server": "^20.3.0", + "@angular/router": "^20.3.0", + "@angular/ssr": "^20.3.6", + "@wailsio/runtime": "3.0.0-alpha.72", + "express": "^5.1.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular/build": "^20.3.6", + "@angular/cli": "^20.3.6", + "@angular/compiler-cli": "^20.3.0", + "@types/express": "^5.0.1", + "@types/jasmine": "~5.1.0", + "@types/node": "^20.17.19", + "jasmine-core": "~5.9.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.9.2" + } +} diff --git a/internal/core-ide/frontend/public/Inter Font License.txt b/internal/core-ide/frontend/public/Inter Font License.txt new file mode 100644 index 0000000..b525cbf --- /dev/null +++ b/internal/core-ide/frontend/public/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/internal/core-ide/frontend/public/Inter-Medium.ttf b/internal/core-ide/frontend/public/Inter-Medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/internal/core-ide/frontend/public/Inter-Medium.ttf differ diff --git a/internal/core-ide/frontend/public/angular.png b/internal/core-ide/frontend/public/angular.png new file mode 100644 index 0000000..2f17323 Binary files /dev/null and b/internal/core-ide/frontend/public/angular.png differ diff --git a/internal/core-ide/frontend/public/javascript.svg b/internal/core-ide/frontend/public/javascript.svg new file mode 100644 index 0000000..f9abb2b --- /dev/null +++ b/internal/core-ide/frontend/public/javascript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/core-ide/frontend/public/style.css b/internal/core-ide/frontend/public/style.css new file mode 100644 index 0000000..0b9c582 --- /dev/null +++ b/internal/core-ide/frontend/public/style.css @@ -0,0 +1,157 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/internal/core-ide/frontend/public/wails.png b/internal/core-ide/frontend/public/wails.png new file mode 100644 index 0000000..8bdf424 Binary files /dev/null and b/internal/core-ide/frontend/public/wails.png differ diff --git a/internal/core-ide/frontend/src/app/app.config.server.ts b/internal/core-ide/frontend/src/app/app.config.server.ts new file mode 100644 index 0000000..41031f1 --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.config.server.ts @@ -0,0 +1,12 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering, withRoutes } from '@angular/ssr'; +import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering(withRoutes(serverRoutes)) + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/internal/core-ide/frontend/src/app/app.config.ts b/internal/core-ide/frontend/src/app/app.config.ts new file mode 100644 index 0000000..969812a --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.config.ts @@ -0,0 +1,13 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), provideClientHydration(withEventReplay()) + ] +}; diff --git a/internal/core-ide/frontend/src/app/app.html b/internal/core-ide/frontend/src/app/app.html new file mode 100644 index 0000000..7ba5c24 --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.html @@ -0,0 +1,23 @@ +
+ +

Wails + Angular v20

+
+
{{ result() }}
+
+ + +
+
+ +
+ diff --git a/internal/core-ide/frontend/src/app/app.routes.server.ts b/internal/core-ide/frontend/src/app/app.routes.server.ts new file mode 100644 index 0000000..62e9bd5 --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.routes.server.ts @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Client + } +]; diff --git a/internal/core-ide/frontend/src/app/app.routes.ts b/internal/core-ide/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..3d7b89d --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.routes.ts @@ -0,0 +1,17 @@ +import { Routes } from '@angular/router'; +import { TrayComponent } from './pages/tray/tray.component'; +import { IdeComponent } from './pages/ide/ide.component'; + +export const routes: Routes = [ + // System tray panel - standalone compact UI + { path: 'tray', component: TrayComponent }, + + // Full IDE interface + { path: 'ide', component: IdeComponent }, + + // Default to tray for the root (tray panel is the default view) + { path: '', redirectTo: 'tray', pathMatch: 'full' }, + + // Catch-all + { path: '**', redirectTo: 'tray' }, +]; diff --git a/internal/core-ide/frontend/src/app/app.scss b/internal/core-ide/frontend/src/app/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/internal/core-ide/frontend/src/app/app.spec.ts b/internal/core-ide/frontend/src/app/app.spec.ts new file mode 100644 index 0000000..a2ca84b --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; +import { App } from './app'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, wails-angular-template'); + }); +}); diff --git a/internal/core-ide/frontend/src/app/app.ts b/internal/core-ide/frontend/src/app/app.ts new file mode 100644 index 0000000..01fd1c1 --- /dev/null +++ b/internal/core-ide/frontend/src/app/app.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, + styles: [` + :host { + display: block; + width: 100%; + height: 100%; + } + `] +}) +export class App {} diff --git a/internal/core-ide/frontend/src/app/components/sidebar/sidebar.component.ts b/internal/core-ide/frontend/src/app/components/sidebar/sidebar.component.ts new file mode 100644 index 0000000..0071c59 --- /dev/null +++ b/internal/core-ide/frontend/src/app/components/sidebar/sidebar.component.ts @@ -0,0 +1,201 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +interface NavItem { + id: string; + label: string; + icon: SafeHtml; +} + +@Component({ + selector: 'app-sidebar', + standalone: true, + imports: [CommonModule], + template: ` + + `, + styles: [` + .sidebar { + display: flex; + flex-direction: column; + width: 56px; + background: #16161e; + border-right: 1px solid #24283b; + } + + .sidebar-header { + display: flex; + align-items: center; + justify-content: center; + height: 56px; + border-bottom: 1px solid #24283b; + } + + .logo { + width: 28px; + height: 28px; + color: #7aa2f7; + } + + .logo svg { + width: 100%; + height: 100%; + } + + .nav-items { + flex: 1; + display: flex; + flex-direction: column; + padding: 0.5rem 0; + gap: 0.25rem; + } + + .nav-item { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + background: transparent; + border: none; + color: #565f89; + cursor: pointer; + transition: all 0.15s ease; + position: relative; + } + + .nav-item:hover { + color: #a9b1d6; + background: rgba(122, 162, 247, 0.1); + } + + .nav-item.active { + color: #7aa2f7; + background: rgba(122, 162, 247, 0.15); + } + + .nav-item.active::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 2px; + background: #7aa2f7; + border-radius: 0 2px 2px 0; + } + + .nav-icon { + width: 22px; + height: 22px; + } + + .nav-icon svg { + width: 100%; + height: 100%; + } + + .sidebar-footer { + border-top: 1px solid #24283b; + padding: 0.5rem 0; + } + `] +}) +export class SidebarComponent { + @Input() currentRoute = 'dashboard'; + @Output() routeChange = new EventEmitter(); + + constructor(private sanitizer: DomSanitizer) { + this.navItems = this.createNavItems(); + } + + navItems: NavItem[]; + + private createNavItems(): NavItem[] { + return [ + { + id: 'dashboard', + label: 'Dashboard', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + { + id: 'explorer', + label: 'Explorer', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + { + id: 'search', + label: 'Search', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + { + id: 'git', + label: 'Source Control', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + { + id: 'debug', + label: 'Debug', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + { + id: 'terminal', + label: 'Terminal', + icon: this.sanitizer.bypassSecurityTrustHtml(` + + `) + }, + ]; + } +} diff --git a/internal/core-ide/frontend/src/app/pages/ide/ide.component.ts b/internal/core-ide/frontend/src/app/pages/ide/ide.component.ts new file mode 100644 index 0000000..04c5596 --- /dev/null +++ b/internal/core-ide/frontend/src/app/pages/ide/ide.component.ts @@ -0,0 +1,506 @@ +import { Component, signal, OnInit, OnDestroy, PLATFORM_ID, Inject } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { SidebarComponent } from '../../components/sidebar/sidebar.component'; + +@Component({ + selector: 'app-ide', + standalone: true, + imports: [CommonModule, SidebarComponent], + template: ` +
+ + +
+ +
+ +
+ {{ currentTime() }} +
+
+ + +
+ @switch (currentRoute()) { + @case ('dashboard') { +
+

Welcome to Core IDE

+

Your development environment is ready.

+ +
+
+
+ + + +
+
+ {{ projectCount() }} + Projects +
+
+ +
+
+ + + +
+
+ {{ taskCount() }} + Tasks +
+
+ +
+
+ + + +
+
+ {{ gitChanges() }} + Changes +
+
+ +
+
+ + + +
+
+ OK + Status +
+
+
+ +
+

Quick Actions

+
+ + + + +
+
+
+ } + @case ('explorer') { +
+

File Explorer

+

Browse and manage your project files.

+
+ } + @case ('search') { +
+

Search

+

Search across all files in your workspace.

+
+ } + @case ('git') { +
+

Source Control

+

Manage your Git repositories and commits.

+
+ } + @case ('debug') { +
+

Debug

+

Debug your applications.

+
+ } + @case ('terminal') { +
+

Terminal

+
+
$ core dev health
+18 repos | clean | synced
+
+$ _
+
+
+ } + @case ('settings') { +
+

Settings

+

Configure your IDE preferences.

+
+ } + @default { +
+

{{ currentRoute() }}

+
+ } + } +
+ + +
+
+ + + + + main + + UTF-8 +
+
+ Core IDE v0.1.0 +
+
+
+
+ `, + styles: [` + :host { + display: block; + width: 100%; + height: 100%; + } + + .ide-layout { + display: flex; + height: 100%; + background: #1a1b26; + color: #a9b1d6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + .ide-main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + } + + .top-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + padding: 0 1rem; + background: #16161e; + border-bottom: 1px solid #24283b; + } + + .breadcrumb { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + } + + .breadcrumb-item { + color: #565f89; + } + + .breadcrumb-item.active { + color: #c0caf5; + text-transform: capitalize; + } + + .breadcrumb-sep { + color: #414868; + } + + .top-bar-actions { + display: flex; + align-items: center; + gap: 1rem; + } + + .time { + font-size: 0.75rem; + color: #565f89; + font-family: 'JetBrains Mono', monospace; + } + + .ide-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + } + + .dashboard-view h1 { + font-size: 1.75rem; + font-weight: 600; + color: #c0caf5; + margin: 0 0 0.5rem 0; + } + + .subtitle { + color: #565f89; + margin: 0 0 2rem 0; + } + + .stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + margin-bottom: 2rem; + } + + .stat-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem; + background: #16161e; + border: 1px solid #24283b; + border-radius: 8px; + } + + .stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 8px; + } + + .stat-icon svg { + width: 24px; + height: 24px; + } + + .stat-icon.projects { + background: rgba(122, 162, 247, 0.15); + color: #7aa2f7; + } + + .stat-icon.tasks { + background: rgba(158, 206, 106, 0.15); + color: #9ece6a; + } + + .stat-icon.git { + background: rgba(247, 118, 142, 0.15); + color: #f7768e; + } + + .stat-icon.status { + background: rgba(158, 206, 106, 0.15); + color: #9ece6a; + } + + .stat-info { + display: flex; + flex-direction: column; + } + + .stat-value { + font-size: 1.5rem; + font-weight: 600; + color: #c0caf5; + } + + .stat-value.status-ok { + color: #9ece6a; + } + + .stat-label { + font-size: 0.8125rem; + color: #565f89; + } + + .quick-actions h2 { + font-size: 1.125rem; + font-weight: 600; + color: #c0caf5; + margin: 0 0 1rem 0; + } + + .actions-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + } + + .action-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1.5rem; + background: #16161e; + border: 1px solid #24283b; + border-radius: 8px; + color: #a9b1d6; + cursor: pointer; + transition: all 0.15s ease; + } + + .action-card:hover { + background: #1f2335; + border-color: #7aa2f7; + color: #c0caf5; + } + + .action-card svg { + width: 32px; + height: 32px; + color: #7aa2f7; + } + + .action-card span { + font-size: 0.875rem; + font-weight: 500; + } + + .panel-view { + padding: 1rem; + } + + .panel-view h2 { + font-size: 1.25rem; + font-weight: 600; + color: #c0caf5; + margin: 0 0 0.5rem 0; + } + + .panel-view p { + color: #565f89; + } + + .panel-view.terminal { + display: flex; + flex-direction: column; + height: 100%; + } + + .terminal-output { + flex: 1; + background: #16161e; + border: 1px solid #24283b; + border-radius: 8px; + padding: 1rem; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 0.875rem; + overflow: auto; + } + + .terminal-output pre { + margin: 0; + color: #9ece6a; + } + + .status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 24px; + padding: 0 0.75rem; + background: #7aa2f7; + color: #1a1b26; + font-size: 0.6875rem; + font-weight: 500; + } + + .status-left, .status-right { + display: flex; + align-items: center; + gap: 1rem; + } + + .status-item { + display: flex; + align-items: center; + gap: 0.25rem; + } + + .status-item svg { + width: 12px; + height: 12px; + } + + @media (max-width: 1024px) { + .stats-grid, .actions-grid { + grid-template-columns: repeat(2, 1fr); + } + } + + @media (max-width: 640px) { + .stats-grid, .actions-grid { + grid-template-columns: 1fr; + } + } + `] +}) +export class IdeComponent implements OnInit, OnDestroy { + private isBrowser: boolean; + private timeEventCleanup?: () => void; + currentRoute = signal('dashboard'); + currentTime = signal(''); + projectCount = signal(18); + taskCount = signal(5); + gitChanges = signal(12); + + constructor(@Inject(PLATFORM_ID) platformId: Object) { + this.isBrowser = isPlatformBrowser(platformId); + } + + ngOnInit() { + if (!this.isBrowser) return; + + import('@wailsio/runtime').then(({ Events }) => { + this.timeEventCleanup = Events.On('time', (time: { data: string }) => { + this.currentTime.set(time.data); + }); + }); + } + + ngOnDestroy() { + this.timeEventCleanup?.(); + } + + onRouteChange(route: string) { + this.currentRoute.set(route); + } + + emitAction(action: string) { + if (!this.isBrowser) return; + import('@wailsio/runtime').then(({ Events }) => { + Events.Emit('action', action); + }); + } +} diff --git a/internal/core-ide/frontend/src/app/pages/tray/tray.component.ts b/internal/core-ide/frontend/src/app/pages/tray/tray.component.ts new file mode 100644 index 0000000..a809203 --- /dev/null +++ b/internal/core-ide/frontend/src/app/pages/tray/tray.component.ts @@ -0,0 +1,444 @@ +import { Component, signal, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Events } from '@wailsio/runtime'; + +@Component({ + selector: 'app-tray', + standalone: true, + imports: [CommonModule], + template: ` +
+ +
+ +
+ +
+
+ + +
+
+ Status + + {{ isActive() ? 'Running' : 'Idle' }} + +
+
+ Projects + {{ projectCount() }} +
+
+ Active Tasks + {{ taskCount() }} +
+
+ Time + {{ currentTime() }} +
+
+ + +
+
Quick Actions
+
+ + + + +
+
+ + +
+
Recent Projects
+
+ @for (project of recentProjects(); track project.name) { + + } @empty { +
No recent projects
+ } +
+
+ + + +
+ `, + styles: [` + :host { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + } + + .tray-container { + display: flex; + flex-direction: column; + height: 100%; + background: #1a1b26; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + color: #a9b1d6; + } + + .tray-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: #16161e; + border-bottom: 1px solid #24283b; + } + + .tray-logo { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 600; + color: #c0caf5; + } + + .logo-icon { + width: 20px; + height: 20px; + color: #7aa2f7; + } + + .tray-controls { + display: flex; + gap: 0.5rem; + } + + .control-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: transparent; + border: 1px solid #24283b; + border-radius: 6px; + color: #7aa2f7; + cursor: pointer; + transition: all 0.15s ease; + } + + .control-btn:hover { + background: #24283b; + border-color: #7aa2f7; + } + + .control-btn svg { + width: 16px; + height: 16px; + } + + .tray-stats { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.875rem 1rem; + background: #16161e; + border-bottom: 1px solid #24283b; + } + + .stat-row { + display: flex; + align-items: center; + justify-content: space-between; + } + + .stat-label { + font-size: 0.8125rem; + color: #565f89; + } + + .stat-value { + font-size: 0.875rem; + font-weight: 600; + color: #c0caf5; + } + + .stat-value.active { + color: #9ece6a; + } + + .stat-value.mono { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 0.8125rem; + } + + .actions-section, .projects-section { + display: flex; + flex-direction: column; + } + + .section-header { + padding: 0.625rem 1rem; + font-size: 0.6875rem; + font-weight: 600; + color: #565f89; + text-transform: uppercase; + letter-spacing: 0.05em; + background: #1a1b26; + border-bottom: 1px solid #24283b; + } + + .actions-list { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + padding: 0.75rem; + } + + .action-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 0.75rem; + background: #16161e; + border: 1px solid #24283b; + border-radius: 6px; + color: #a9b1d6; + font-size: 0.8125rem; + cursor: pointer; + transition: all 0.15s ease; + } + + .action-btn:hover { + background: #24283b; + border-color: #414868; + } + + .action-btn.danger { + color: #f7768e; + } + + .action-btn.danger:hover { + border-color: #f7768e; + background: rgba(247, 118, 142, 0.1); + } + + .action-btn svg { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + .projects-section { + flex: 1; + min-height: 0; + } + + .projects-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; + } + + .project-item { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.625rem 0.75rem; + background: transparent; + border: none; + border-radius: 6px; + color: #a9b1d6; + text-align: left; + cursor: pointer; + transition: background 0.15s ease; + } + + .project-item:hover { + background: #24283b; + } + + .project-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: #24283b; + border-radius: 6px; + color: #7aa2f7; + } + + .project-icon svg { + width: 16px; + height: 16px; + } + + .project-info { + display: flex; + flex-direction: column; + min-width: 0; + } + + .project-name { + font-size: 0.8125rem; + font-weight: 500; + color: #c0caf5; + } + + .project-path { + font-size: 0.6875rem; + color: #565f89; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .no-projects { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: #565f89; + font-size: 0.8125rem; + } + + .tray-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 1rem; + background: #16161e; + border-top: 1px solid #24283b; + } + + .connection-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: #565f89; + } + + .status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #565f89; + } + + .connection-status.connected .status-dot { + background: #9ece6a; + box-shadow: 0 0 4px #9ece6a; + } + + .connection-status.connected { + color: #9ece6a; + } + + .footer-btn { + padding: 0.375rem 0.75rem; + background: #7aa2f7; + border: none; + border-radius: 4px; + color: #1a1b26; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease; + } + + .footer-btn:hover { + background: #89b4fa; + } + `] +}) +export class TrayComponent implements OnInit, OnDestroy { + currentTime = signal(''); + isActive = signal(false); + projectCount = signal(3); + taskCount = signal(0); + recentProjects = signal([ + { name: 'core', path: '~/Code/host-uk/core' }, + { name: 'core-gui', path: '~/Code/host-uk/core-gui' }, + { name: 'core-php', path: '~/Code/host-uk/core-php' }, + ]); + private timeEventCleanup?: () => void; + + ngOnInit() { + this.timeEventCleanup = Events.On('time', (time: { data: string }) => { + this.currentTime.set(time.data); + }); + } + + ngOnDestroy() { + this.timeEventCleanup?.(); + } + + openIDE() { + Events.Emit('action', 'open-ide'); + } + + emitAction(action: string) { + Events.Emit('action', action); + } + + openProject(path: string) { + Events.Emit('open-project', path); + } +} diff --git a/internal/core-ide/frontend/src/index.html b/internal/core-ide/frontend/src/index.html new file mode 100644 index 0000000..2883185 --- /dev/null +++ b/internal/core-ide/frontend/src/index.html @@ -0,0 +1,13 @@ + + + + + Core IDE + + + + + + + + diff --git a/internal/core-ide/frontend/src/main.server.ts b/internal/core-ide/frontend/src/main.server.ts new file mode 100644 index 0000000..723e001 --- /dev/null +++ b/internal/core-ide/frontend/src/main.server.ts @@ -0,0 +1,8 @@ +import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; +import { App } from './app/app'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(App, config, context); + +export default bootstrap; diff --git a/internal/core-ide/frontend/src/main.ts b/internal/core-ide/frontend/src/main.ts new file mode 100644 index 0000000..5df75f9 --- /dev/null +++ b/internal/core-ide/frontend/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/internal/core-ide/frontend/src/server.ts b/internal/core-ide/frontend/src/server.ts new file mode 100644 index 0000000..43d1e99 --- /dev/null +++ b/internal/core-ide/frontend/src/server.ts @@ -0,0 +1,68 @@ +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; +import express from 'express'; +import { join } from 'node:path'; + +const browserDistFolder = join(import.meta.dirname, '../browser'); + +const app = express(); +const angularApp = new AngularNodeAppEngine(); + +/** + * Example Express Rest API endpoints can be defined here. + * Uncomment and define endpoints as necessary. + * + * Example: + * ```ts + * app.get('/api/{*splat}', (req, res) => { + * // Handle API request + * }); + * ``` + */ + +/** + * Serve static files from /browser + */ +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }), +); + +/** + * Handle all other requests by rendering the Angular application. + */ +app.use((req, res, next) => { + angularApp + .handle(req) + .then((response) => + response ? writeResponseToNodeResponse(response, res) : next(), + ) + .catch(next); +}); + +/** + * Start the server if this module is the main entry point, or it is ran via PM2. + * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. + */ +if (isMainModule(import.meta.url) || process.env['pm_id']) { + const port = process.env['PORT'] || 4000; + const server = app.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); + server.on('error', (error: Error) => { + console.error('Failed to start server:', error); + process.exit(1); + }); +} + +/** + * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. + */ +export const reqHandler = createNodeRequestHandler(app); diff --git a/internal/core-ide/frontend/src/styles.scss b/internal/core-ide/frontend/src/styles.scss new file mode 100644 index 0000000..1e4c80a --- /dev/null +++ b/internal/core-ide/frontend/src/styles.scss @@ -0,0 +1,63 @@ +/* Global styles for Core IDE */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + width: 100%; + height: 100%; + overflow: hidden; + background: #1a1b26; + color: #a9b1d6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +app-root { + display: block; + width: 100%; + height: 100%; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #16161e; +} + +::-webkit-scrollbar-thumb { + background: #414868; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #565f89; +} + +/* Focus styles */ +:focus-visible { + outline: 2px solid #7aa2f7; + outline-offset: 2px; +} + +/* Button reset */ +button { + font-family: inherit; + font-size: inherit; + border: none; + background: none; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/internal/core-ide/frontend/tsconfig.app.json b/internal/core-ide/frontend/tsconfig.app.json new file mode 100644 index 0000000..ef19921 --- /dev/null +++ b/internal/core-ide/frontend/tsconfig.app.json @@ -0,0 +1,17 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/internal/core-ide/frontend/tsconfig.json b/internal/core-ide/frontend/tsconfig.json new file mode 100644 index 0000000..c06cfbb --- /dev/null +++ b/internal/core-ide/frontend/tsconfig.json @@ -0,0 +1,41 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "allowJs": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve", + "baseUrl": "./", + "paths": { + "@bindings/*": [ + "bindings/*" + ] + } + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/internal/core-ide/frontend/tsconfig.spec.json b/internal/core-ide/frontend/tsconfig.spec.json new file mode 100644 index 0000000..04df34c --- /dev/null +++ b/internal/core-ide/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/internal/core-ide/go.mod b/internal/core-ide/go.mod new file mode 100644 index 0000000..2aa108c --- /dev/null +++ b/internal/core-ide/go.mod @@ -0,0 +1,54 @@ +module github.com/host-uk/core/internal/core-ide + +go 1.25.5 + +require github.com/wailsapp/wails/v3 v3.0.0-alpha.64 + +require ( + github.com/coder/websocket v1.8.14 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // 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/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/gorilla/websocket v1.5.3 + github.com/host-uk/core-gui v0.0.0 + 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/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/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // 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/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/wailsapp/go-webview2 v1.0.23 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.47.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 +) + +replace github.com/host-uk/core-gui => ../../../core-gui diff --git a/internal/core-ide/go.sum b/internal/core-ide/go.sum new file mode 100644 index 0000000..10a11dd --- /dev/null +++ b/internal/core-ide/go.sum @@ -0,0 +1,151 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +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/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/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +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/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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/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/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/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/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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +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/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/internal/core-ide/greetservice.go b/internal/core-ide/greetservice.go new file mode 100644 index 0000000..8972c39 --- /dev/null +++ b/internal/core-ide/greetservice.go @@ -0,0 +1,7 @@ +package main + +type GreetService struct{} + +func (g *GreetService) Greet(name string) string { + return "Hello " + name + "!" +} diff --git a/internal/core-ide/icons/apptray.png b/internal/core-ide/icons/apptray.png new file mode 100644 index 0000000..0778fc6 Binary files /dev/null and b/internal/core-ide/icons/apptray.png differ diff --git a/internal/core-ide/icons/icons.go b/internal/core-ide/icons/icons.go new file mode 100644 index 0000000..c3adf1d --- /dev/null +++ b/internal/core-ide/icons/icons.go @@ -0,0 +1,19 @@ +package icons + +import _ "embed" + +// AppTray is the main application tray icon. +// +//go:embed apptray.png +var AppTray []byte + +// SystrayMacTemplate is the template icon for macOS systray (22x22 PNG, black on transparent). +// Template icons automatically adapt to light/dark mode on macOS. +// +//go:embed systray-mac-template.png +var SystrayMacTemplate []byte + +// SystrayDefault is the default icon for Windows/Linux systray. +// +//go:embed systray-default.png +var SystrayDefault []byte diff --git a/internal/core-ide/icons/systray-default.png b/internal/core-ide/icons/systray-default.png new file mode 100644 index 0000000..4d6eda7 Binary files /dev/null and b/internal/core-ide/icons/systray-default.png differ diff --git a/internal/core-ide/icons/systray-mac-template.png b/internal/core-ide/icons/systray-mac-template.png new file mode 100644 index 0000000..e98822c Binary files /dev/null and b/internal/core-ide/icons/systray-mac-template.png differ diff --git a/internal/core-ide/main.go b/internal/core-ide/main.go new file mode 100644 index 0000000..646bd70 --- /dev/null +++ b/internal/core-ide/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "embed" + "io/fs" + "log" + "runtime" + + "github.com/host-uk/core/internal/core-ide/icons" + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed all:frontend/dist/wails-angular-template/browser +var assets embed.FS + +// Default MCP port for the embedded server +const mcpPort = 9877 + +func main() { + // Strip the embed path prefix so files are served from root + staticAssets, err := fs.Sub(assets, "frontend/dist/wails-angular-template/browser") + if err != nil { + log.Fatal(err) + } + + // Create the MCP bridge for Claude Code integration + mcpBridge := NewMCPBridge(mcpPort) + + app := application.New(application.Options{ + Name: "Core IDE", + Description: "Host UK Core IDE - Development Environment", + Services: []application.Service{ + application.NewService(&GreetService{}), + application.NewService(mcpBridge), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(staticAssets), + }, + Mac: application.MacOptions{ + ActivationPolicy: application.ActivationPolicyAccessory, + }, + }) + + // System tray + systray := app.SystemTray.New() + systray.SetTooltip("Core IDE") + + if runtime.GOOS == "darwin" { + systray.SetTemplateIcon(icons.AppTray) + } else { + systray.SetDarkModeIcon(icons.AppTray) + systray.SetIcon(icons.AppTray) + } + + // Tray panel window + trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "tray-panel", + Title: "Core IDE", + Width: 380, + Height: 480, + URL: "/tray", + Hidden: true, + Frameless: true, + BackgroundColour: application.NewRGB(26, 27, 38), + }) + systray.AttachWindow(trayWindow).WindowOffset(5) + + // Tray menu + trayMenu := app.Menu.New() + trayMenu.Add("Quit").OnClick(func(ctx *application.Context) { + app.Quit() + }) + systray.SetMenu(trayMenu) + + log.Println("Starting Core IDE...") + + if err := app.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/internal/core-ide/mcp_bridge.go b/internal/core-ide/mcp_bridge.go new file mode 100644 index 0000000..3d6ae6e --- /dev/null +++ b/internal/core-ide/mcp_bridge.go @@ -0,0 +1,520 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "sync" + "time" + + "github.com/host-uk/core-gui/pkg/webview" + "github.com/host-uk/core-gui/pkg/ws" + "github.com/wailsapp/wails/v3/pkg/application" +) + +// MCPBridge wires together WebView and WebSocket services +// and starts the MCP HTTP server after Wails initializes. +type MCPBridge struct { + webview *webview.Service + wsHub *ws.Hub + claudeBridge *ClaudeBridge + app *application.App + port int + running bool + mu sync.Mutex +} + +// NewMCPBridge creates a new MCP bridge with all services wired up. +func NewMCPBridge(port int) *MCPBridge { + wv := webview.New() + hub := ws.NewHub() + + // Create Claude bridge to forward messages to MCP core on port 9876 + claudeBridge := NewClaudeBridge("ws://localhost:9876/ws") + + return &MCPBridge{ + webview: wv, + wsHub: hub, + claudeBridge: claudeBridge, + port: port, + } +} + +// ServiceStartup is called by Wails when the app starts. +// This wires up the app reference and starts the HTTP server. +func (b *MCPBridge) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + b.mu.Lock() + defer b.mu.Unlock() + + // Get the Wails app reference + b.app = application.Get() + if b.app == nil { + return fmt.Errorf("failed to get Wails app reference") + } + + // Wire up the WebView service with the app + b.webview.SetApp(b.app) + + // Set up console listener + b.webview.SetupConsoleListener() + + // Inject console capture into all windows after a short delay + // (windows may not be created yet) + go b.injectConsoleCapture() + + // Start the HTTP server for MCP + go b.startHTTPServer() + + log.Printf("MCP Bridge started on port %d", b.port) + return nil +} + +// injectConsoleCapture injects the console capture script into windows. +func (b *MCPBridge) injectConsoleCapture() { + // Wait for windows to be created (poll with timeout) + var windows []webview.WindowInfo + for i := 0; i < 10; i++ { + time.Sleep(500 * time.Millisecond) + windows = b.webview.ListWindows() + if len(windows) > 0 { + break + } + } + if len(windows) == 0 { + log.Printf("MCP Bridge: no windows found after waiting") + return + } + for _, w := range windows { + if err := b.webview.InjectConsoleCapture(w.Name); err != nil { + log.Printf("Failed to inject console capture in %s: %v", w.Name, err) + } + } +} + +// startHTTPServer starts the HTTP server for MCP and WebSocket. +func (b *MCPBridge) startHTTPServer() { + b.mu.Lock() + b.running = true + b.mu.Unlock() + + // Start the WebSocket hub + hubCtx := context.Background() + go b.wsHub.Run(hubCtx) + + // Claude bridge disabled - port 9876 is not an MCP WebSocket server + // b.claudeBridge.Start() + + mux := http.NewServeMux() + + // WebSocket endpoint for GUI clients + mux.HandleFunc("/ws", b.wsHub.HandleWebSocket) + + // MCP info endpoint + mux.HandleFunc("/mcp", b.handleMCPInfo) + + // MCP tools endpoint + mux.HandleFunc("/mcp/tools", b.handleMCPTools) + mux.HandleFunc("/mcp/call", b.handleMCPCall) + + // Health check + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", + "mcp": true, + "webview": b.webview != nil, + }) + }) + + addr := fmt.Sprintf("127.0.0.1:%d", b.port) + log.Printf("MCP HTTP server listening on %s", addr) + + if err := http.ListenAndServe(addr, mux); err != nil { + log.Printf("MCP HTTP server error: %v", err) + } +} + +// handleMCPInfo returns MCP server information. +func (b *MCPBridge) handleMCPInfo(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost") + + info := map[string]any{ + "name": "core-ide", + "version": "0.1.0", + "capabilities": map[string]any{ + "webview": true, + "websocket": fmt.Sprintf("ws://localhost:%d/ws", b.port), + }, + } + json.NewEncoder(w).Encode(info) +} + +// handleMCPTools returns the list of available tools. +func (b *MCPBridge) handleMCPTools(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost") + + tools := []map[string]string{ + // WebView interaction (JS runtime, console, DOM) + {"name": "webview_list", "description": "List windows"}, + {"name": "webview_eval", "description": "Execute JavaScript"}, + {"name": "webview_console", "description": "Get console messages"}, + {"name": "webview_console_clear", "description": "Clear console buffer"}, + {"name": "webview_click", "description": "Click element"}, + {"name": "webview_type", "description": "Type into element"}, + {"name": "webview_query", "description": "Query DOM elements"}, + {"name": "webview_navigate", "description": "Navigate to URL"}, + {"name": "webview_source", "description": "Get page source"}, + {"name": "webview_url", "description": "Get current page URL"}, + {"name": "webview_title", "description": "Get current page title"}, + {"name": "webview_screenshot", "description": "Capture page as base64 PNG"}, + {"name": "webview_screenshot_element", "description": "Capture specific element as PNG"}, + {"name": "webview_scroll", "description": "Scroll to element or position"}, + {"name": "webview_hover", "description": "Hover over element"}, + {"name": "webview_select", "description": "Select option in dropdown"}, + {"name": "webview_check", "description": "Check/uncheck checkbox or radio"}, + {"name": "webview_element_info", "description": "Get detailed info about element"}, + {"name": "webview_computed_style", "description": "Get computed styles for element"}, + {"name": "webview_highlight", "description": "Visually highlight element"}, + {"name": "webview_dom_tree", "description": "Get DOM tree structure"}, + {"name": "webview_errors", "description": "Get captured error messages"}, + {"name": "webview_performance", "description": "Get performance metrics"}, + {"name": "webview_resources", "description": "List loaded resources"}, + {"name": "webview_network", "description": "Get network requests log"}, + {"name": "webview_network_clear", "description": "Clear network request log"}, + {"name": "webview_network_inject", "description": "Inject network interceptor for detailed logging"}, + {"name": "webview_pdf", "description": "Export page as PDF (base64 data URI)"}, + {"name": "webview_print", "description": "Open print dialog for window"}, + } + json.NewEncoder(w).Encode(map[string]any{"tools": tools}) +} + +// handleMCPCall handles tool calls via HTTP POST. +func (b *MCPBridge) handleMCPCall(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Tool string `json:"tool"` + Params map[string]any `json:"params"` + } + + // Limit request body to 1MB + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + result := b.executeWebviewTool(req.Tool, req.Params) + json.NewEncoder(w).Encode(result) +} + +// executeWebviewTool handles webview/JS tool execution. +func (b *MCPBridge) executeWebviewTool(tool string, params map[string]any) map[string]any { + if b.webview == nil { + return map[string]any{"error": "webview service not available"} + } + + switch tool { + case "webview_list": + windows := b.webview.ListWindows() + return map[string]any{"windows": windows} + + case "webview_eval": + windowName := getStringParam(params, "window") + code := getStringParam(params, "code") + result, err := b.webview.ExecJS(windowName, code) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"result": result} + + case "webview_console": + level := getStringParam(params, "level") + limit := getIntParam(params, "limit") + if limit == 0 { + limit = 100 + } + messages := b.webview.GetConsoleMessages(level, limit) + return map[string]any{"messages": messages} + + case "webview_console_clear": + b.webview.ClearConsole() + return map[string]any{"success": true} + + case "webview_click": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + err := b.webview.Click(windowName, selector) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_type": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + text := getStringParam(params, "text") + err := b.webview.Type(windowName, selector, text) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_query": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + result, err := b.webview.QuerySelector(windowName, selector) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"elements": result} + + case "webview_navigate": + windowName := getStringParam(params, "window") + rawURL := getStringParam(params, "url") + parsed, err := url.Parse(rawURL) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") { + return map[string]any{"error": "only http/https URLs are allowed"} + } + err = b.webview.Navigate(windowName, rawURL) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_source": + windowName := getStringParam(params, "window") + result, err := b.webview.GetPageSource(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"source": result} + + case "webview_url": + windowName := getStringParam(params, "window") + result, err := b.webview.GetURL(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"url": result} + + case "webview_title": + windowName := getStringParam(params, "window") + result, err := b.webview.GetTitle(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"title": result} + + case "webview_screenshot": + windowName := getStringParam(params, "window") + data, err := b.webview.Screenshot(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"data": data} + + case "webview_screenshot_element": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + data, err := b.webview.ScreenshotElement(windowName, selector) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"data": data} + + case "webview_scroll": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + x := getIntParam(params, "x") + y := getIntParam(params, "y") + err := b.webview.Scroll(windowName, selector, x, y) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_hover": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + err := b.webview.Hover(windowName, selector) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_select": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + value := getStringParam(params, "value") + err := b.webview.Select(windowName, selector, value) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_check": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + checked, _ := params["checked"].(bool) + err := b.webview.Check(windowName, selector, checked) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_element_info": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + result, err := b.webview.GetElementInfo(windowName, selector) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"element": result} + + case "webview_computed_style": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + var properties []string + if props, ok := params["properties"].([]any); ok { + for _, p := range props { + if s, ok := p.(string); ok { + properties = append(properties, s) + } + } + } + result, err := b.webview.GetComputedStyle(windowName, selector, properties) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"styles": result} + + case "webview_highlight": + windowName := getStringParam(params, "window") + selector := getStringParam(params, "selector") + duration := getIntParam(params, "duration") + err := b.webview.Highlight(windowName, selector, duration) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_dom_tree": + windowName := getStringParam(params, "window") + maxDepth := getIntParam(params, "maxDepth") + result, err := b.webview.GetDOMTree(windowName, maxDepth) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"tree": result} + + case "webview_errors": + limit := getIntParam(params, "limit") + if limit == 0 { + limit = 50 + } + errors := b.webview.GetErrors(limit) + return map[string]any{"errors": errors} + + case "webview_performance": + windowName := getStringParam(params, "window") + result, err := b.webview.GetPerformance(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"performance": result} + + case "webview_resources": + windowName := getStringParam(params, "window") + result, err := b.webview.GetResources(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"resources": result} + + case "webview_network": + windowName := getStringParam(params, "window") + limit := getIntParam(params, "limit") + result, err := b.webview.GetNetworkRequests(windowName, limit) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"requests": result} + + case "webview_network_clear": + windowName := getStringParam(params, "window") + err := b.webview.ClearNetworkRequests(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_network_inject": + windowName := getStringParam(params, "window") + err := b.webview.InjectNetworkInterceptor(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + case "webview_pdf": + windowName := getStringParam(params, "window") + options := make(map[string]any) + if filename := getStringParam(params, "filename"); filename != "" { + options["filename"] = filename + } + if margin, ok := params["margin"].(float64); ok { + options["margin"] = margin + } + data, err := b.webview.ExportToPDF(windowName, options) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"data": data} + + case "webview_print": + windowName := getStringParam(params, "window") + err := b.webview.PrintToPDF(windowName) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"success": true} + + default: + return map[string]any{"error": "unknown tool", "tool": tool} + } +} + +// Helper functions for parameter extraction +func getStringParam(params map[string]any, key string) string { + if v, ok := params[key].(string); ok { + return v + } + return "" +} + +func getIntParam(params map[string]any, key string) int { + if v, ok := params[key].(float64); ok { + return int(v) + } + return 0 +} diff --git a/internal/core-ide/wails3-angular-template.jpg b/internal/core-ide/wails3-angular-template.jpg new file mode 100644 index 0000000..6be4655 Binary files /dev/null and b/internal/core-ide/wails3-angular-template.jpg differ diff --git a/internal/variants/core_ide.go b/internal/variants/core_ide.go new file mode 100644 index 0000000..089f38d --- /dev/null +++ b/internal/variants/core_ide.go @@ -0,0 +1,25 @@ +//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 ( + // Core IDE GUI + _ "github.com/host-uk/core/internal/core-ide" + + // CLI commands available via IPC + _ "github.com/host-uk/core/internal/cmd/ai" + _ "github.com/host-uk/core/internal/cmd/dev" + _ "github.com/host-uk/core/internal/cmd/deploy" + _ "github.com/host-uk/core/internal/cmd/php" + _ "github.com/host-uk/core/internal/cmd/rag" +) diff --git a/internal/variants/full.go b/internal/variants/full.go index ebecd16..720c456 100644 --- a/internal/variants/full.go +++ b/internal/variants/full.go @@ -6,7 +6,7 @@ // // This is the default build variant with all development tools: // - dev: Multi-repo git workflows (commit, push, pull, sync) -// - ai: AI agent task management +// - ai: AI agent task management + RAG + metrics // - go: Go module and build tools // - php: Laravel/Composer development tools // - build: Cross-platform compilation @@ -19,6 +19,7 @@ // - doctor: Environment health checks // - test: Test runner with coverage // - qa: Quality assurance workflows +// - monitor: Security monitoring aggregation package variants @@ -26,12 +27,14 @@ import ( // Commands via self-registration _ "github.com/host-uk/core/internal/cmd/ai" _ "github.com/host-uk/core/internal/cmd/ci" + _ "github.com/host-uk/core/internal/cmd/deploy" _ "github.com/host-uk/core/internal/cmd/dev" _ "github.com/host-uk/core/internal/cmd/docs" _ "github.com/host-uk/core/internal/cmd/doctor" _ "github.com/host-uk/core/internal/cmd/gitcmd" _ "github.com/host-uk/core/internal/cmd/go" _ "github.com/host-uk/core/internal/cmd/help" + _ "github.com/host-uk/core/internal/cmd/monitor" _ "github.com/host-uk/core/internal/cmd/php" _ "github.com/host-uk/core/internal/cmd/pkgcmd" _ "github.com/host-uk/core/internal/cmd/qa" diff --git a/pkg/agentic/context.go b/pkg/agentic/context.go index 0095132..80cc962 100644 --- a/pkg/agentic/context.go +++ b/pkg/agentic/context.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" + "github.com/host-uk/core/pkg/ai" "github.com/host-uk/core/pkg/log" ) @@ -34,6 +35,8 @@ type TaskContext struct { RecentCommits string `json:"recent_commits"` // RelatedCode contains code snippets related to the task. RelatedCode []FileContent `json:"related_code"` + // RAGContext contains relevant documentation from the vector database. + RAGContext string `json:"rag_context,omitempty"` } // BuildTaskContext gathers context for AI collaboration on a task. @@ -79,6 +82,13 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) { } ctx.RelatedCode = relatedCode + // Query RAG for relevant documentation (graceful degradation) + ragCtx := ai.QueryRAGForTask(ai.TaskInfo{ + Title: task.Title, + Description: task.Description, + }) + ctx.RAGContext = ragCtx + return ctx, nil } @@ -331,5 +341,12 @@ func (tc *TaskContext) FormatContext() string { } } + // Relevant documentation from RAG + if tc.RAGContext != "" { + sb.WriteString("## Relevant Documentation\n") + sb.WriteString(tc.RAGContext) + sb.WriteString("\n\n") + } + return sb.String() } diff --git a/pkg/ai/ai.go b/pkg/ai/ai.go new file mode 100644 index 0000000..29cc20e --- /dev/null +++ b/pkg/ai/ai.go @@ -0,0 +1,11 @@ +// Package ai provides the unified AI package for the core CLI. +// +// It composes functionality from pkg/rag (vector search) and pkg/agentic +// (task management) into a single public API surface. New AI features +// should be added here; existing packages remain importable but pkg/ai +// is the canonical entry point. +// +// Sub-packages composed: +// - pkg/rag: Qdrant vector database + Ollama embeddings +// - pkg/agentic: Task queue client and context building +package ai diff --git a/pkg/ai/metrics.go b/pkg/ai/metrics.go new file mode 100644 index 0000000..830fc12 --- /dev/null +++ b/pkg/ai/metrics.go @@ -0,0 +1,171 @@ +package ai + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "time" +) + +// Event represents a recorded AI/security metric event. +type Event struct { + Type string `json:"type"` + Timestamp time.Time `json:"timestamp"` + AgentID string `json:"agent_id,omitempty"` + Repo string `json:"repo,omitempty"` + Duration time.Duration `json:"duration,omitempty"` + Data map[string]any `json:"data,omitempty"` +} + +// metricsDir returns the base directory for metrics storage. +func metricsDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("get home directory: %w", err) + } + return filepath.Join(home, ".core", "ai", "metrics"), nil +} + +// metricsFilePath returns the JSONL file path for the given date. +func metricsFilePath(dir string, t time.Time) string { + return filepath.Join(dir, t.Format("2006-01-02")+".jsonl") +} + +// Record appends an event to the daily JSONL file at +// ~/.core/ai/metrics/YYYY-MM-DD.jsonl. +func Record(event Event) (err error) { + if event.Timestamp.IsZero() { + event.Timestamp = time.Now() + } + + dir, err := metricsDir() + if err != nil { + return err + } + + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create metrics directory: %w", err) + } + + path := metricsFilePath(dir, event.Timestamp) + + f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("open metrics file: %w", err) + } + defer func() { + if cerr := f.Close(); cerr != nil && err == nil { + err = fmt.Errorf("close metrics file: %w", cerr) + } + }() + + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("marshal event: %w", err) + } + + if _, err := f.Write(append(data, '\n')); err != nil { + return fmt.Errorf("write event: %w", err) + } + + return nil +} + +// ReadEvents reads events from JSONL files within the given time range. +func ReadEvents(since time.Time) ([]Event, error) { + dir, err := metricsDir() + if err != nil { + return nil, err + } + + var events []Event + now := time.Now() + + // Iterate each day from since to now. + for d := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, time.Local); !d.After(now); d = d.AddDate(0, 0, 1) { + path := metricsFilePath(dir, d) + + dayEvents, err := readMetricsFile(path, since) + if err != nil { + return nil, err + } + events = append(events, dayEvents...) + } + + return events, nil +} + +// readMetricsFile reads events from a single JSONL file, returning only those at or after since. +func readMetricsFile(path string, since time.Time) ([]Event, error) { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("open metrics file %s: %w", path, err) + } + defer f.Close() + + var events []Event + scanner := bufio.NewScanner(f) + for scanner.Scan() { + var ev Event + if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil { + continue // skip malformed lines + } + if !ev.Timestamp.Before(since) { + events = append(events, ev) + } + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("read metrics file %s: %w", path, err) + } + return events, nil +} + +// Summary aggregates events into counts by type, repo, and agent. +func Summary(events []Event) map[string]any { + byType := make(map[string]int) + byRepo := make(map[string]int) + byAgent := make(map[string]int) + + for _, ev := range events { + byType[ev.Type]++ + if ev.Repo != "" { + byRepo[ev.Repo]++ + } + if ev.AgentID != "" { + byAgent[ev.AgentID]++ + } + } + + return map[string]any{ + "total": len(events), + "by_type": sortedMap(byType), + "by_repo": sortedMap(byRepo), + "by_agent": sortedMap(byAgent), + } +} + +// sortedMap returns a slice of key-count pairs sorted by count descending. +func sortedMap(m map[string]int) []map[string]any { + type entry struct { + key string + count int + } + entries := make([]entry, 0, len(m)) + for k, v := range m { + entries = append(entries, entry{k, v}) + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].count > entries[j].count + }) + result := make([]map[string]any, len(entries)) + for i, e := range entries { + result[i] = map[string]any{"key": e.key, "count": e.count} + } + return result +} diff --git a/pkg/ai/rag.go b/pkg/ai/rag.go new file mode 100644 index 0000000..6df906b --- /dev/null +++ b/pkg/ai/rag.go @@ -0,0 +1,58 @@ +package ai + +import ( + "context" + "time" + + "github.com/host-uk/core/pkg/rag" +) + +// TaskInfo carries the minimal task data needed for RAG queries, +// avoiding a direct dependency on pkg/agentic (which imports pkg/ai). +type TaskInfo struct { + Title string + Description string +} + +// QueryRAGForTask queries Qdrant for documentation relevant to a task. +// It builds a query from the task title and description, queries with +// sensible defaults, and returns formatted context. Returns "" on any +// error (e.g. Qdrant/Ollama not running) for graceful degradation. +func QueryRAGForTask(task TaskInfo) string { + query := task.Title + " " + task.Description + + // Truncate to 500 runes to keep the embedding focused. + runes := []rune(query) + if len(runes) > 500 { + query = string(runes[:500]) + } + + qdrantCfg := rag.DefaultQdrantConfig() + qdrantClient, err := rag.NewQdrantClient(qdrantCfg) + if err != nil { + return "" + } + defer qdrantClient.Close() + + ollamaCfg := rag.DefaultOllamaConfig() + ollamaClient, err := rag.NewOllamaClient(ollamaCfg) + if err != nil { + return "" + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + queryCfg := rag.QueryConfig{ + Collection: "hostuk-docs", + Limit: 3, + Threshold: 0.5, + } + + results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryCfg) + if err != nil { + return "" + } + + return rag.FormatResultsContext(results) +} diff --git a/pkg/ansible/executor.go b/pkg/ansible/executor.go new file mode 100644 index 0000000..f7e2d48 --- /dev/null +++ b/pkg/ansible/executor.go @@ -0,0 +1,1021 @@ +package ansible + +import ( + "context" + "fmt" + "os" + "regexp" + "strings" + "sync" + "text/template" + "time" + + "github.com/host-uk/core/pkg/log" +) + +// Executor runs Ansible playbooks. +type Executor struct { + parser *Parser + inventory *Inventory + vars map[string]any + facts map[string]*Facts + results map[string]map[string]*TaskResult // host -> register_name -> result + handlers map[string][]Task + notified map[string]bool + clients map[string]*SSHClient + mu sync.RWMutex + + // Callbacks + OnPlayStart func(play *Play) + OnTaskStart func(host string, task *Task) + OnTaskEnd func(host string, task *Task, result *TaskResult) + OnPlayEnd func(play *Play) + + // Options + Limit string + Tags []string + SkipTags []string + CheckMode bool + Diff bool + Verbose int +} + +// NewExecutor creates a new playbook executor. +func NewExecutor(basePath string) *Executor { + return &Executor{ + parser: NewParser(basePath), + vars: make(map[string]any), + facts: make(map[string]*Facts), + results: make(map[string]map[string]*TaskResult), + handlers: make(map[string][]Task), + notified: make(map[string]bool), + clients: make(map[string]*SSHClient), + } +} + +// SetInventory loads inventory from a file. +func (e *Executor) SetInventory(path string) error { + inv, err := e.parser.ParseInventory(path) + if err != nil { + return err + } + e.inventory = inv + return nil +} + +// SetInventoryDirect sets inventory directly. +func (e *Executor) SetInventoryDirect(inv *Inventory) { + e.inventory = inv +} + +// SetVar sets a variable. +func (e *Executor) SetVar(key string, value any) { + e.mu.Lock() + defer e.mu.Unlock() + e.vars[key] = value +} + +// Run executes a playbook. +func (e *Executor) Run(ctx context.Context, playbookPath string) error { + plays, err := e.parser.ParsePlaybook(playbookPath) + if err != nil { + return fmt.Errorf("parse playbook: %w", err) + } + + for i := range plays { + if err := e.runPlay(ctx, &plays[i]); err != nil { + return fmt.Errorf("play %d (%s): %w", i, plays[i].Name, err) + } + } + + return nil +} + +// runPlay executes a single play. +func (e *Executor) runPlay(ctx context.Context, play *Play) error { + if e.OnPlayStart != nil { + e.OnPlayStart(play) + } + defer func() { + if e.OnPlayEnd != nil { + e.OnPlayEnd(play) + } + }() + + // Get target hosts + hosts := e.getHosts(play.Hosts) + if len(hosts) == 0 { + return nil // No hosts matched + } + + // Merge play vars + for k, v := range play.Vars { + e.vars[k] = v + } + + // Gather facts if needed + gatherFacts := play.GatherFacts == nil || *play.GatherFacts + if gatherFacts { + for _, host := range hosts { + if err := e.gatherFacts(ctx, host, play); err != nil { + // Non-fatal + if e.Verbose > 0 { + fmt.Fprintf(os.Stderr, "Warning: gather facts failed for %s: %v\n", host, err) + } + } + } + } + + // Execute pre_tasks + for _, task := range play.PreTasks { + if err := e.runTaskOnHosts(ctx, hosts, &task, play); err != nil { + return err + } + } + + // Execute roles + for _, roleRef := range play.Roles { + if err := e.runRole(ctx, hosts, &roleRef, play); err != nil { + return err + } + } + + // Execute tasks + for _, task := range play.Tasks { + if err := e.runTaskOnHosts(ctx, hosts, &task, play); err != nil { + return err + } + } + + // Execute post_tasks + for _, task := range play.PostTasks { + if err := e.runTaskOnHosts(ctx, hosts, &task, play); err != nil { + return err + } + } + + // Run notified handlers + for _, handler := range play.Handlers { + if e.notified[handler.Name] { + if err := e.runTaskOnHosts(ctx, hosts, &handler, play); err != nil { + return err + } + } + } + + return nil +} + +// runRole executes a role on hosts. +func (e *Executor) runRole(ctx context.Context, hosts []string, roleRef *RoleRef, play *Play) error { + // Check when condition + if roleRef.When != nil { + if !e.evaluateWhen(roleRef.When, "", nil) { + return nil + } + } + + // Parse role tasks + tasks, err := e.parser.ParseRole(roleRef.Role, roleRef.TasksFrom) + if err != nil { + return log.E("executor.runRole", fmt.Sprintf("parse role %s", roleRef.Role), err) + } + + // Merge role vars + oldVars := make(map[string]any) + for k, v := range e.vars { + oldVars[k] = v + } + for k, v := range roleRef.Vars { + e.vars[k] = v + } + + // Execute tasks + for _, task := range tasks { + if err := e.runTaskOnHosts(ctx, hosts, &task, play); err != nil { + // Restore vars + e.vars = oldVars + return err + } + } + + // Restore vars + e.vars = oldVars + return nil +} + +// runTaskOnHosts runs a task on all hosts. +func (e *Executor) runTaskOnHosts(ctx context.Context, hosts []string, task *Task, play *Play) error { + // Check tags + if !e.matchesTags(task.Tags) { + return nil + } + + // Handle block tasks + if len(task.Block) > 0 { + return e.runBlock(ctx, hosts, task, play) + } + + // Handle include/import + if task.IncludeTasks != "" || task.ImportTasks != "" { + return e.runIncludeTasks(ctx, hosts, task, play) + } + if task.IncludeRole != nil || task.ImportRole != nil { + return e.runIncludeRole(ctx, hosts, task, play) + } + + for _, host := range hosts { + if err := e.runTaskOnHost(ctx, host, task, play); err != nil { + if !task.IgnoreErrors { + return err + } + } + } + + return nil +} + +// runTaskOnHost runs a task on a single host. +func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, play *Play) error { + start := time.Now() + + if e.OnTaskStart != nil { + e.OnTaskStart(host, task) + } + + // Initialize host results + if e.results[host] == nil { + e.results[host] = make(map[string]*TaskResult) + } + + // Check when condition + if task.When != nil { + if !e.evaluateWhen(task.When, host, task) { + result := &TaskResult{Skipped: true, Msg: "Skipped due to when condition"} + if task.Register != "" { + e.results[host][task.Register] = result + } + if e.OnTaskEnd != nil { + e.OnTaskEnd(host, task, result) + } + return nil + } + } + + // Get SSH client + client, err := e.getClient(host, play) + if err != nil { + return fmt.Errorf("get client for %s: %w", host, err) + } + + // Handle loops + if task.Loop != nil { + return e.runLoop(ctx, host, client, task, play) + } + + // Execute the task + result, err := e.executeModule(ctx, host, client, task, play) + if err != nil { + result = &TaskResult{Failed: true, Msg: err.Error()} + } + result.Duration = time.Since(start) + + // Store result + if task.Register != "" { + e.results[host][task.Register] = result + } + + // Handle notify + if result.Changed && task.Notify != nil { + e.handleNotify(task.Notify) + } + + if e.OnTaskEnd != nil { + e.OnTaskEnd(host, task, result) + } + + if result.Failed && !task.IgnoreErrors { + return fmt.Errorf("task failed: %s", result.Msg) + } + + return nil +} + +// runLoop handles task loops. +func (e *Executor) runLoop(ctx context.Context, host string, client *SSHClient, task *Task, play *Play) error { + items := e.resolveLoop(task.Loop, host) + + loopVar := "item" + if task.LoopControl != nil && task.LoopControl.LoopVar != "" { + loopVar = task.LoopControl.LoopVar + } + + // Save loop state to restore after loop + savedVars := make(map[string]any) + if v, ok := e.vars[loopVar]; ok { + savedVars[loopVar] = v + } + indexVar := "" + if task.LoopControl != nil && task.LoopControl.IndexVar != "" { + indexVar = task.LoopControl.IndexVar + if v, ok := e.vars[indexVar]; ok { + savedVars[indexVar] = v + } + } + + var results []TaskResult + for i, item := range items { + // Set loop variables + e.vars[loopVar] = item + if indexVar != "" { + e.vars[indexVar] = i + } + + result, err := e.executeModule(ctx, host, client, task, play) + if err != nil { + result = &TaskResult{Failed: true, Msg: err.Error()} + } + results = append(results, *result) + + if result.Failed && !task.IgnoreErrors { + break + } + } + + // Restore loop variables + if v, ok := savedVars[loopVar]; ok { + e.vars[loopVar] = v + } else { + delete(e.vars, loopVar) + } + if indexVar != "" { + if v, ok := savedVars[indexVar]; ok { + e.vars[indexVar] = v + } else { + delete(e.vars, indexVar) + } + } + + // Store combined result + if task.Register != "" { + combined := &TaskResult{ + Results: results, + Changed: false, + } + for _, r := range results { + if r.Changed { + combined.Changed = true + } + if r.Failed { + combined.Failed = true + } + } + e.results[host][task.Register] = combined + } + + return nil +} + +// runBlock handles block/rescue/always. +func (e *Executor) runBlock(ctx context.Context, hosts []string, task *Task, play *Play) error { + var blockErr error + + // Try block + for _, t := range task.Block { + if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + blockErr = err + break + } + } + + // Run rescue if block failed + if blockErr != nil && len(task.Rescue) > 0 { + for _, t := range task.Rescue { + if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + // Rescue also failed + break + } + } + } + + // Always run always block + for _, t := range task.Always { + if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + if blockErr == nil { + blockErr = err + } + } + } + + if blockErr != nil && len(task.Rescue) == 0 { + return blockErr + } + + return nil +} + +// runIncludeTasks handles include_tasks/import_tasks. +func (e *Executor) runIncludeTasks(ctx context.Context, hosts []string, task *Task, play *Play) error { + path := task.IncludeTasks + if path == "" { + path = task.ImportTasks + } + + // Resolve path relative to playbook + path = e.templateString(path, "", nil) + + tasks, err := e.parser.ParseTasks(path) + if err != nil { + return fmt.Errorf("include_tasks %s: %w", path, err) + } + + for _, t := range tasks { + if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + return err + } + } + + return nil +} + +// runIncludeRole handles include_role/import_role. +func (e *Executor) runIncludeRole(ctx context.Context, hosts []string, task *Task, play *Play) error { + var roleName, tasksFrom string + var roleVars map[string]any + + if task.IncludeRole != nil { + roleName = task.IncludeRole.Name + tasksFrom = task.IncludeRole.TasksFrom + roleVars = task.IncludeRole.Vars + } else { + roleName = task.ImportRole.Name + tasksFrom = task.ImportRole.TasksFrom + roleVars = task.ImportRole.Vars + } + + roleRef := &RoleRef{ + Role: roleName, + TasksFrom: tasksFrom, + Vars: roleVars, + } + + return e.runRole(ctx, hosts, roleRef, play) +} + +// getHosts returns hosts matching the pattern. +func (e *Executor) getHosts(pattern string) []string { + if e.inventory == nil { + if pattern == "localhost" { + return []string{"localhost"} + } + return nil + } + + hosts := GetHosts(e.inventory, pattern) + + // Apply limit - filter to hosts that are also in the limit group + if e.Limit != "" { + limitHosts := GetHosts(e.inventory, e.Limit) + limitSet := make(map[string]bool) + for _, h := range limitHosts { + limitSet[h] = true + } + + var filtered []string + for _, h := range hosts { + if limitSet[h] || h == e.Limit || strings.Contains(h, e.Limit) { + filtered = append(filtered, h) + } + } + hosts = filtered + } + + return hosts +} + +// getClient returns or creates an SSH client for a host. +func (e *Executor) getClient(host string, play *Play) (*SSHClient, error) { + e.mu.Lock() + defer e.mu.Unlock() + + if client, ok := e.clients[host]; ok { + return client, nil + } + + // Get host vars + vars := make(map[string]any) + if e.inventory != nil { + vars = GetHostVars(e.inventory, host) + } + + // Merge with play vars + for k, v := range e.vars { + if _, exists := vars[k]; !exists { + vars[k] = v + } + } + + // Build SSH config + cfg := SSHConfig{ + Host: host, + Port: 22, + User: "root", + } + + if h, ok := vars["ansible_host"].(string); ok { + cfg.Host = h + } + if p, ok := vars["ansible_port"].(int); ok { + cfg.Port = p + } + if u, ok := vars["ansible_user"].(string); ok { + cfg.User = u + } + if p, ok := vars["ansible_password"].(string); ok { + cfg.Password = p + } + if k, ok := vars["ansible_ssh_private_key_file"].(string); ok { + cfg.KeyFile = k + } + + // Apply play become settings + if play.Become { + cfg.Become = true + cfg.BecomeUser = play.BecomeUser + if bp, ok := vars["ansible_become_password"].(string); ok { + cfg.BecomePass = bp + } else if cfg.Password != "" { + // Use SSH password for sudo if no become password specified + cfg.BecomePass = cfg.Password + } + } + + client, err := NewSSHClient(cfg) + if err != nil { + return nil, err + } + + e.clients[host] = client + return client, nil +} + +// gatherFacts collects facts from a host. +func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) error { + if play.Connection == "local" || host == "localhost" { + // Local facts + e.facts[host] = &Facts{ + Hostname: "localhost", + } + return nil + } + + client, err := e.getClient(host, play) + if err != nil { + return err + } + + // Gather basic facts + facts := &Facts{} + + // Hostname + stdout, _, _, err := client.Run(ctx, "hostname -f 2>/dev/null || hostname") + if err == nil { + facts.FQDN = strings.TrimSpace(stdout) + } + + stdout, _, _, err = client.Run(ctx, "hostname -s 2>/dev/null || hostname") + if err == nil { + facts.Hostname = strings.TrimSpace(stdout) + } + + // OS info + stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2") + for _, line := range strings.Split(stdout, "\n") { + if strings.HasPrefix(line, "ID=") { + facts.Distribution = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") + } + if strings.HasPrefix(line, "VERSION_ID=") { + facts.Version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") + } + } + + // Architecture + stdout, _, _, _ = client.Run(ctx, "uname -m") + facts.Architecture = strings.TrimSpace(stdout) + + // Kernel + stdout, _, _, _ = client.Run(ctx, "uname -r") + facts.Kernel = strings.TrimSpace(stdout) + + e.mu.Lock() + e.facts[host] = facts + e.mu.Unlock() + + return nil +} + +// evaluateWhen evaluates a when condition. +func (e *Executor) evaluateWhen(when any, host string, task *Task) bool { + conditions := normalizeConditions(when) + + for _, cond := range conditions { + cond = e.templateString(cond, host, task) + if !e.evalCondition(cond, host) { + return false + } + } + + return true +} + +func normalizeConditions(when any) []string { + switch v := when.(type) { + case string: + return []string{v} + case []any: + var conds []string + for _, c := range v { + if s, ok := c.(string); ok { + conds = append(conds, s) + } + } + return conds + case []string: + return v + } + return nil +} + +// evalCondition evaluates a single condition. +func (e *Executor) evalCondition(cond string, host string) bool { + cond = strings.TrimSpace(cond) + + // Handle negation + if strings.HasPrefix(cond, "not ") { + return !e.evalCondition(strings.TrimPrefix(cond, "not "), host) + } + + // Handle boolean literals + if cond == "true" || cond == "True" { + return true + } + if cond == "false" || cond == "False" { + return false + } + + // Handle registered variable checks + // e.g., "result is success", "result.rc == 0" + if strings.Contains(cond, " is ") { + parts := strings.SplitN(cond, " is ", 2) + varName := strings.TrimSpace(parts[0]) + check := strings.TrimSpace(parts[1]) + + result := e.getRegisteredVar(host, varName) + if result == nil { + return check == "not defined" || check == "undefined" + } + + switch check { + case "defined": + return true + case "not defined", "undefined": + return false + case "success", "succeeded": + return !result.Failed + case "failed": + return result.Failed + case "changed": + return result.Changed + case "skipped": + return result.Skipped + } + } + + // Handle simple var checks + if strings.Contains(cond, " | default(") { + // Extract var name and check if defined + re := regexp.MustCompile(`(\w+)\s*\|\s*default\([^)]*\)`) + if match := re.FindStringSubmatch(cond); len(match) > 1 { + // Has default, so condition is satisfied + return true + } + } + + // Check if it's a variable that should be truthy + if result := e.getRegisteredVar(host, cond); result != nil { + return !result.Failed && !result.Skipped + } + + // Check vars + if val, ok := e.vars[cond]; ok { + switch v := val.(type) { + case bool: + return v + case string: + return v != "" && v != "false" && v != "False" + case int: + return v != 0 + } + } + + // Default to true for unknown conditions (be permissive) + return true +} + +// getRegisteredVar gets a registered task result. +func (e *Executor) getRegisteredVar(host string, name string) *TaskResult { + e.mu.RLock() + defer e.mu.RUnlock() + + // Handle dotted access (e.g., "result.stdout") + parts := strings.SplitN(name, ".", 2) + varName := parts[0] + + if hostResults, ok := e.results[host]; ok { + if result, ok := hostResults[varName]; ok { + return result + } + } + + return nil +} + +// templateString applies Jinja2-like templating. +func (e *Executor) templateString(s string, host string, task *Task) string { + // Handle {{ var }} syntax + re := regexp.MustCompile(`\{\{\s*([^}]+)\s*\}\}`) + + return re.ReplaceAllStringFunc(s, func(match string) string { + expr := strings.TrimSpace(match[2 : len(match)-2]) + return e.resolveExpr(expr, host, task) + }) +} + +// resolveExpr resolves a template expression. +func (e *Executor) resolveExpr(expr string, host string, task *Task) string { + // Handle filters + if strings.Contains(expr, " | ") { + parts := strings.SplitN(expr, " | ", 2) + value := e.resolveExpr(parts[0], host, task) + return e.applyFilter(value, parts[1]) + } + + // Handle lookups + if strings.HasPrefix(expr, "lookup(") { + return e.handleLookup(expr) + } + + // Handle registered vars + if strings.Contains(expr, ".") { + parts := strings.SplitN(expr, ".", 2) + if result := e.getRegisteredVar(host, parts[0]); result != nil { + switch parts[1] { + case "stdout": + return result.Stdout + case "stderr": + return result.Stderr + case "rc": + return fmt.Sprintf("%d", result.RC) + case "changed": + return fmt.Sprintf("%t", result.Changed) + case "failed": + return fmt.Sprintf("%t", result.Failed) + } + } + } + + // Check vars + if val, ok := e.vars[expr]; ok { + return fmt.Sprintf("%v", val) + } + + // Check task vars + if task != nil { + if val, ok := task.Vars[expr]; ok { + return fmt.Sprintf("%v", val) + } + } + + // Check host vars + if e.inventory != nil { + hostVars := GetHostVars(e.inventory, host) + if val, ok := hostVars[expr]; ok { + return fmt.Sprintf("%v", val) + } + } + + // Check facts + if facts, ok := e.facts[host]; ok { + switch expr { + case "ansible_hostname": + return facts.Hostname + case "ansible_fqdn": + return facts.FQDN + case "ansible_distribution": + return facts.Distribution + case "ansible_distribution_version": + return facts.Version + case "ansible_architecture": + return facts.Architecture + case "ansible_kernel": + return facts.Kernel + } + } + + return "{{ " + expr + " }}" // Return as-is if unresolved +} + +// applyFilter applies a Jinja2 filter. +func (e *Executor) applyFilter(value, filter string) string { + filter = strings.TrimSpace(filter) + + // Handle default filter + if strings.HasPrefix(filter, "default(") { + if value == "" || value == "{{ "+filter+" }}" { + // Extract default value + re := regexp.MustCompile(`default\(([^)]*)\)`) + if match := re.FindStringSubmatch(filter); len(match) > 1 { + return strings.Trim(match[1], "'\"") + } + } + return value + } + + // Handle bool filter + if filter == "bool" { + lower := strings.ToLower(value) + if lower == "true" || lower == "yes" || lower == "1" { + return "true" + } + return "false" + } + + // Handle trim + if filter == "trim" { + return strings.TrimSpace(value) + } + + // Handle b64decode + if filter == "b64decode" { + // Would need base64 decode + return value + } + + return value +} + +// handleLookup handles lookup() expressions. +func (e *Executor) handleLookup(expr string) string { + // Parse lookup('type', 'arg') + re := regexp.MustCompile(`lookup\s*\(\s*['"](\w+)['"]\s*,\s*['"]([^'"]+)['"]\s*`) + match := re.FindStringSubmatch(expr) + if len(match) < 3 { + return "" + } + + lookupType := match[1] + arg := match[2] + + switch lookupType { + case "env": + return os.Getenv(arg) + case "file": + if data, err := os.ReadFile(arg); err == nil { + return string(data) + } + } + + return "" +} + +// resolveLoop resolves loop items. +func (e *Executor) resolveLoop(loop any, host string) []any { + switch v := loop.(type) { + case []any: + return v + case []string: + items := make([]any, len(v)) + for i, s := range v { + items[i] = s + } + return items + case string: + // Template the string and see if it's a var reference + resolved := e.templateString(v, host, nil) + if val, ok := e.vars[resolved]; ok { + if items, ok := val.([]any); ok { + return items + } + } + } + return nil +} + +// matchesTags checks if task tags match execution tags. +func (e *Executor) matchesTags(taskTags []string) bool { + // If no tags specified, run all + if len(e.Tags) == 0 && len(e.SkipTags) == 0 { + return true + } + + // Check skip tags + for _, skip := range e.SkipTags { + for _, tt := range taskTags { + if skip == tt { + return false + } + } + } + + // Check include tags + if len(e.Tags) > 0 { + for _, tag := range e.Tags { + for _, tt := range taskTags { + if tag == tt || tag == "all" { + return true + } + } + } + return false + } + + return true +} + +// handleNotify marks handlers as notified. +func (e *Executor) handleNotify(notify any) { + switch v := notify.(type) { + case string: + e.notified[v] = true + case []any: + for _, n := range v { + if s, ok := n.(string); ok { + e.notified[s] = true + } + } + case []string: + for _, s := range v { + e.notified[s] = true + } + } +} + +// Close closes all SSH connections. +func (e *Executor) Close() { + e.mu.Lock() + defer e.mu.Unlock() + + for _, client := range e.clients { + _ = client.Close() + } + e.clients = make(map[string]*SSHClient) +} + +// TemplateFile processes a template file. +func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) { + content, err := os.ReadFile(src) + if err != nil { + return "", err + } + + // Convert Jinja2 to Go template syntax (basic conversion) + tmplContent := string(content) + tmplContent = strings.ReplaceAll(tmplContent, "{{", "{{ .") + tmplContent = strings.ReplaceAll(tmplContent, "{%", "{{") + tmplContent = strings.ReplaceAll(tmplContent, "%}", "}}") + + tmpl, err := template.New("template").Parse(tmplContent) + if err != nil { + // Fall back to simple replacement + return e.templateString(string(content), host, task), nil + } + + // Build context map + context := make(map[string]any) + for k, v := range e.vars { + context[k] = v + } + // Add host vars + if e.inventory != nil { + hostVars := GetHostVars(e.inventory, host) + for k, v := range hostVars { + context[k] = v + } + } + // Add facts + if facts, ok := e.facts[host]; ok { + context["ansible_hostname"] = facts.Hostname + context["ansible_fqdn"] = facts.FQDN + context["ansible_distribution"] = facts.Distribution + context["ansible_distribution_version"] = facts.Version + context["ansible_architecture"] = facts.Architecture + context["ansible_kernel"] = facts.Kernel + } + + var buf strings.Builder + if err := tmpl.Execute(&buf, context); err != nil { + return e.templateString(string(content), host, task), nil + } + + return buf.String(), nil +} diff --git a/pkg/ansible/modules.go b/pkg/ansible/modules.go new file mode 100644 index 0000000..200aaa1 --- /dev/null +++ b/pkg/ansible/modules.go @@ -0,0 +1,1437 @@ +package ansible + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// executeModule dispatches to the appropriate module handler. +func (e *Executor) executeModule(ctx context.Context, host string, client *SSHClient, task *Task, play *Play) (*TaskResult, error) { + module := NormalizeModule(task.Module) + + // Apply task-level become + if task.Become != nil && *task.Become { + // Save old state to restore + oldBecome := client.become + oldUser := client.becomeUser + oldPass := client.becomePass + + client.SetBecome(true, task.BecomeUser, "") + + defer client.SetBecome(oldBecome, oldUser, oldPass) + } + + // Template the args + args := e.templateArgs(task.Args, host, task) + + switch module { + // Command execution + case "ansible.builtin.shell": + return e.moduleShell(ctx, client, args) + case "ansible.builtin.command": + return e.moduleCommand(ctx, client, args) + case "ansible.builtin.raw": + return e.moduleRaw(ctx, client, args) + case "ansible.builtin.script": + return e.moduleScript(ctx, client, args) + + // File operations + case "ansible.builtin.copy": + return e.moduleCopy(ctx, client, args, host, task) + case "ansible.builtin.template": + return e.moduleTemplate(ctx, client, args, host, task) + case "ansible.builtin.file": + return e.moduleFile(ctx, client, args) + case "ansible.builtin.lineinfile": + return e.moduleLineinfile(ctx, client, args) + case "ansible.builtin.stat": + return e.moduleStat(ctx, client, args) + case "ansible.builtin.slurp": + return e.moduleSlurp(ctx, client, args) + case "ansible.builtin.fetch": + return e.moduleFetch(ctx, client, args) + case "ansible.builtin.get_url": + return e.moduleGetURL(ctx, client, args) + + // Package management + case "ansible.builtin.apt": + return e.moduleApt(ctx, client, args) + case "ansible.builtin.apt_key": + return e.moduleAptKey(ctx, client, args) + case "ansible.builtin.apt_repository": + return e.moduleAptRepository(ctx, client, args) + case "ansible.builtin.package": + return e.modulePackage(ctx, client, args) + case "ansible.builtin.pip": + return e.modulePip(ctx, client, args) + + // Service management + case "ansible.builtin.service": + return e.moduleService(ctx, client, args) + case "ansible.builtin.systemd": + return e.moduleSystemd(ctx, client, args) + + // User/Group + case "ansible.builtin.user": + return e.moduleUser(ctx, client, args) + case "ansible.builtin.group": + return e.moduleGroup(ctx, client, args) + + // HTTP + case "ansible.builtin.uri": + return e.moduleURI(ctx, client, args) + + // Misc + case "ansible.builtin.debug": + return e.moduleDebug(args) + case "ansible.builtin.fail": + return e.moduleFail(args) + case "ansible.builtin.assert": + return e.moduleAssert(args, host) + case "ansible.builtin.set_fact": + return e.moduleSetFact(args) + case "ansible.builtin.pause": + return e.modulePause(ctx, args) + case "ansible.builtin.wait_for": + return e.moduleWaitFor(ctx, client, args) + case "ansible.builtin.git": + return e.moduleGit(ctx, client, args) + case "ansible.builtin.unarchive": + return e.moduleUnarchive(ctx, client, args) + + // Additional modules + case "ansible.builtin.hostname": + return e.moduleHostname(ctx, client, args) + case "ansible.builtin.sysctl": + return e.moduleSysctl(ctx, client, args) + case "ansible.builtin.cron": + return e.moduleCron(ctx, client, args) + case "ansible.builtin.blockinfile": + return e.moduleBlockinfile(ctx, client, args) + case "ansible.builtin.include_vars": + return e.moduleIncludeVars(args) + case "ansible.builtin.meta": + return e.moduleMeta(args) + case "ansible.builtin.setup": + return e.moduleSetup(ctx, client) + case "ansible.builtin.reboot": + return e.moduleReboot(ctx, client, args) + + // Community modules (basic support) + case "community.general.ufw": + return e.moduleUFW(ctx, client, args) + case "ansible.posix.authorized_key": + return e.moduleAuthorizedKey(ctx, client, args) + case "community.docker.docker_compose": + return e.moduleDockerCompose(ctx, client, args) + + default: + // For unknown modules, try to execute as shell if it looks like a command + if strings.Contains(task.Module, " ") || task.Module == "" { + return e.moduleShell(ctx, client, args) + } + return nil, fmt.Errorf("unsupported module: %s", module) + } +} + +// templateArgs templates all string values in args. +func (e *Executor) templateArgs(args map[string]any, host string, task *Task) map[string]any { + // Set inventory_hostname for templating + e.vars["inventory_hostname"] = host + + result := make(map[string]any) + for k, v := range args { + switch val := v.(type) { + case string: + result[k] = e.templateString(val, host, task) + case map[string]any: + // Recurse for nested maps + result[k] = e.templateArgs(val, host, task) + case []any: + // Template strings in arrays + templated := make([]any, len(val)) + for i, item := range val { + if s, ok := item.(string); ok { + templated[i] = e.templateString(s, host, task) + } else { + templated[i] = item + } + } + result[k] = templated + default: + result[k] = v + } + } + return result +} + +// --- Command Modules --- + +func (e *Executor) moduleShell(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + cmd := getStringArg(args, "_raw_params", "") + if cmd == "" { + cmd = getStringArg(args, "cmd", "") + } + if cmd == "" { + return nil, fmt.Errorf("shell: no command specified") + } + + // Handle chdir + if chdir := getStringArg(args, "chdir", ""); chdir != "" { + cmd = fmt.Sprintf("cd %q && %s", chdir, cmd) + } + + stdout, stderr, rc, err := client.RunScript(ctx, cmd) + if err != nil { + return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil + } + + return &TaskResult{ + Changed: true, + Stdout: stdout, + Stderr: stderr, + RC: rc, + Failed: rc != 0, + }, nil +} + +func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + cmd := getStringArg(args, "_raw_params", "") + if cmd == "" { + cmd = getStringArg(args, "cmd", "") + } + if cmd == "" { + return nil, fmt.Errorf("command: no command specified") + } + + // Handle chdir + if chdir := getStringArg(args, "chdir", ""); chdir != "" { + cmd = fmt.Sprintf("cd %q && %s", chdir, cmd) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil { + return &TaskResult{Failed: true, Msg: err.Error()}, nil + } + + return &TaskResult{ + Changed: true, + Stdout: stdout, + Stderr: stderr, + RC: rc, + Failed: rc != 0, + }, nil +} + +func (e *Executor) moduleRaw(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + cmd := getStringArg(args, "_raw_params", "") + if cmd == "" { + return nil, fmt.Errorf("raw: no command specified") + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil { + return &TaskResult{Failed: true, Msg: err.Error()}, nil + } + + return &TaskResult{ + Changed: true, + Stdout: stdout, + Stderr: stderr, + RC: rc, + }, nil +} + +func (e *Executor) moduleScript(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + script := getStringArg(args, "_raw_params", "") + if script == "" { + return nil, fmt.Errorf("script: no script specified") + } + + // Read local script + content, err := os.ReadFile(script) + if err != nil { + return nil, fmt.Errorf("read script: %w", err) + } + + stdout, stderr, rc, err := client.RunScript(ctx, string(content)) + if err != nil { + return &TaskResult{Failed: true, Msg: err.Error()}, nil + } + + return &TaskResult{ + Changed: true, + Stdout: stdout, + Stderr: stderr, + RC: rc, + Failed: rc != 0, + }, nil +} + +// --- File Modules --- + +func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) { + dest := getStringArg(args, "dest", "") + if dest == "" { + return nil, fmt.Errorf("copy: dest required") + } + + var content []byte + var err error + + if src := getStringArg(args, "src", ""); src != "" { + content, err = os.ReadFile(src) + if err != nil { + return nil, fmt.Errorf("read src: %w", err) + } + } else if c := getStringArg(args, "content", ""); c != "" { + content = []byte(c) + } else { + return nil, fmt.Errorf("copy: src or content required") + } + + mode := os.FileMode(0644) + if m := getStringArg(args, "mode", ""); m != "" { + if parsed, err := strconv.ParseInt(m, 8, 32); err == nil { + mode = os.FileMode(parsed) + } + } + + err = client.Upload(ctx, strings.NewReader(string(content)), dest, mode) + if err != nil { + return nil, err + } + + // Handle owner/group (best-effort, errors ignored) + if owner := getStringArg(args, "owner", ""); owner != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, dest)) + } + if group := getStringArg(args, "group", ""); group != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, dest)) + } + + return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil +} + +func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) { + src := getStringArg(args, "src", "") + dest := getStringArg(args, "dest", "") + if src == "" || dest == "" { + return nil, fmt.Errorf("template: src and dest required") + } + + // Process template + content, err := e.TemplateFile(src, host, task) + if err != nil { + return nil, fmt.Errorf("template: %w", err) + } + + mode := os.FileMode(0644) + if m := getStringArg(args, "mode", ""); m != "" { + if parsed, err := strconv.ParseInt(m, 8, 32); err == nil { + mode = os.FileMode(parsed) + } + } + + err = client.Upload(ctx, strings.NewReader(content), dest, mode) + if err != nil { + return nil, err + } + + return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil +} + +func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + path := getStringArg(args, "path", "") + if path == "" { + path = getStringArg(args, "dest", "") + } + if path == "" { + return nil, fmt.Errorf("file: path required") + } + + state := getStringArg(args, "state", "file") + + switch state { + case "directory": + mode := getStringArg(args, "mode", "0755") + cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + case "absent": + cmd := fmt.Sprintf("rm -rf %q", path) + _, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + + case "touch": + cmd := fmt.Sprintf("touch %q", path) + _, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + + case "link": + src := getStringArg(args, "src", "") + if src == "" { + return nil, fmt.Errorf("file: src required for link state") + } + cmd := fmt.Sprintf("ln -sf %q %q", src, path) + _, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + + case "file": + // Ensure file exists and set permissions + if mode := getStringArg(args, "mode", ""); mode != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, path)) + } + } + + // Handle owner/group (best-effort, errors ignored) + if owner := getStringArg(args, "owner", ""); owner != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, path)) + } + if group := getStringArg(args, "group", ""); group != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, path)) + } + if recurse := getBoolArg(args, "recurse", false); recurse { + if owner := getStringArg(args, "owner", ""); owner != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown -R %s %q", owner, path)) + } + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + path := getStringArg(args, "path", "") + if path == "" { + path = getStringArg(args, "dest", "") + } + if path == "" { + return nil, fmt.Errorf("lineinfile: path required") + } + + line := getStringArg(args, "line", "") + regexp := getStringArg(args, "regexp", "") + state := getStringArg(args, "state", "present") + + if state == "absent" { + if regexp != "" { + cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexp, path) + _, stderr, rc, _ := client.Run(ctx, cmd) + if rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + } + } else { + // state == present + if regexp != "" { + // Replace line matching regexp + escapedLine := strings.ReplaceAll(line, "/", "\\/") + cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path) + _, _, rc, _ := client.Run(ctx, cmd) + if rc != 0 { + // Line not found, append + cmd = fmt.Sprintf("echo %q >> %q", line, path) + _, _, _, _ = client.Run(ctx, cmd) + } + } else if line != "" { + // Ensure line is present + cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path) + _, _, _, _ = client.Run(ctx, cmd) + } + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleStat(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + path := getStringArg(args, "path", "") + if path == "" { + return nil, fmt.Errorf("stat: path required") + } + + stat, err := client.Stat(ctx, path) + if err != nil { + return nil, err + } + + return &TaskResult{ + Changed: false, + Data: map[string]any{"stat": stat}, + }, nil +} + +func (e *Executor) moduleSlurp(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + path := getStringArg(args, "path", "") + if path == "" { + path = getStringArg(args, "src", "") + } + if path == "" { + return nil, fmt.Errorf("slurp: path required") + } + + content, err := client.Download(ctx, path) + if err != nil { + return nil, err + } + + encoded := base64.StdEncoding.EncodeToString(content) + + return &TaskResult{ + Changed: false, + Data: map[string]any{"content": encoded, "encoding": "base64"}, + }, nil +} + +func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + src := getStringArg(args, "src", "") + dest := getStringArg(args, "dest", "") + if src == "" || dest == "" { + return nil, fmt.Errorf("fetch: src and dest required") + } + + content, err := client.Download(ctx, src) + if err != nil { + return nil, err + } + + // Create dest directory + if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { + return nil, err + } + + if err := os.WriteFile(dest, content, 0644); err != nil { + return nil, err + } + + return &TaskResult{Changed: true, Msg: fmt.Sprintf("fetched %s to %s", src, dest)}, nil +} + +func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + url := getStringArg(args, "url", "") + dest := getStringArg(args, "dest", "") + if url == "" || dest == "" { + return nil, fmt.Errorf("get_url: url and dest required") + } + + // Use curl or wget + cmd := fmt.Sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Set mode if specified (best-effort) + if mode := getStringArg(args, "mode", ""); mode != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, dest)) + } + + return &TaskResult{Changed: true}, nil +} + +// --- Package Modules --- + +func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + state := getStringArg(args, "state", "present") + updateCache := getBoolArg(args, "update_cache", false) + + var cmd string + + if updateCache { + _, _, _, _ = client.Run(ctx, "apt-get update -qq") + } + + switch state { + case "present", "installed": + if name != "" { + cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name) + } + case "absent", "removed": + cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name) + case "latest": + cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name) + } + + if cmd == "" { + return &TaskResult{Changed: false}, nil + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + url := getStringArg(args, "url", "") + keyring := getStringArg(args, "keyring", "") + state := getStringArg(args, "state", "present") + + if state == "absent" { + if keyring != "" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", keyring)) + } + return &TaskResult{Changed: true}, nil + } + + if url == "" { + return nil, fmt.Errorf("apt_key: url required") + } + + var cmd string + if keyring != "" { + cmd = fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring) + } else { + cmd = fmt.Sprintf("curl -fsSL %q | apt-key add -", url) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + repo := getStringArg(args, "repo", "") + filename := getStringArg(args, "filename", "") + state := getStringArg(args, "state", "present") + + if repo == "" { + return nil, fmt.Errorf("apt_repository: repo required") + } + + if filename == "" { + // Generate filename from repo + filename = strings.ReplaceAll(repo, " ", "-") + filename = strings.ReplaceAll(filename, "/", "-") + filename = strings.ReplaceAll(filename, ":", "") + } + + path := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", filename) + + if state == "absent" { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", path)) + return &TaskResult{Changed: true}, nil + } + + cmd := fmt.Sprintf("echo %q > %q", repo, path) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Update apt cache (best-effort) + if getBoolArg(args, "update_cache", true) { + _, _, _, _ = client.Run(ctx, "apt-get update -qq") + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) modulePackage(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + // Detect package manager and delegate + stdout, _, _, _ := client.Run(ctx, "which apt-get yum dnf 2>/dev/null | head -1") + stdout = strings.TrimSpace(stdout) + + if strings.Contains(stdout, "apt") { + return e.moduleApt(ctx, client, args) + } + + // Default to apt + return e.moduleApt(ctx, client, args) +} + +func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + state := getStringArg(args, "state", "present") + executable := getStringArg(args, "executable", "pip3") + + var cmd string + switch state { + case "present", "installed": + cmd = fmt.Sprintf("%s install %s", executable, name) + case "absent", "removed": + cmd = fmt.Sprintf("%s uninstall -y %s", executable, name) + case "latest": + cmd = fmt.Sprintf("%s install --upgrade %s", executable, name) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +// --- Service Modules --- + +func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + state := getStringArg(args, "state", "") + enabled := args["enabled"] + + if name == "" { + return nil, fmt.Errorf("service: name required") + } + + var cmds []string + + if state != "" { + switch state { + case "started": + cmds = append(cmds, fmt.Sprintf("systemctl start %s", name)) + case "stopped": + cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name)) + case "restarted": + cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name)) + case "reloaded": + cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name)) + } + } + + if enabled != nil { + if getBoolArg(args, "enabled", false) { + cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name)) + } else { + cmds = append(cmds, fmt.Sprintf("systemctl disable %s", name)) + } + } + + for _, cmd := range cmds { + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + } + + return &TaskResult{Changed: len(cmds) > 0}, nil +} + +func (e *Executor) moduleSystemd(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + // systemd is similar to service + if getBoolArg(args, "daemon_reload", false) { + _, _, _, _ = client.Run(ctx, "systemctl daemon-reload") + } + + return e.moduleService(ctx, client, args) +} + +// --- User/Group Modules --- + +func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + state := getStringArg(args, "state", "present") + + if name == "" { + return nil, fmt.Errorf("user: name required") + } + + if state == "absent" { + cmd := fmt.Sprintf("userdel -r %s 2>/dev/null || true", name) + _, _, _, _ = client.Run(ctx, cmd) + return &TaskResult{Changed: true}, nil + } + + // Build useradd/usermod command + var opts []string + + if uid := getStringArg(args, "uid", ""); uid != "" { + opts = append(opts, "-u", uid) + } + if group := getStringArg(args, "group", ""); group != "" { + opts = append(opts, "-g", group) + } + if groups := getStringArg(args, "groups", ""); groups != "" { + opts = append(opts, "-G", groups) + } + if home := getStringArg(args, "home", ""); home != "" { + opts = append(opts, "-d", home) + } + if shell := getStringArg(args, "shell", ""); shell != "" { + opts = append(opts, "-s", shell) + } + if getBoolArg(args, "system", false) { + opts = append(opts, "-r") + } + if getBoolArg(args, "create_home", true) { + opts = append(opts, "-m") + } + + // Try usermod first, then useradd + optsStr := strings.Join(opts, " ") + var cmd string + if optsStr == "" { + cmd = fmt.Sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name) + } else { + cmd = fmt.Sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s", + name, optsStr, name, optsStr, name) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + state := getStringArg(args, "state", "present") + + if name == "" { + return nil, fmt.Errorf("group: name required") + } + + if state == "absent" { + cmd := fmt.Sprintf("groupdel %s 2>/dev/null || true", name) + _, _, _, _ = client.Run(ctx, cmd) + return &TaskResult{Changed: true}, nil + } + + var opts []string + if gid := getStringArg(args, "gid", ""); gid != "" { + opts = append(opts, "-g", gid) + } + if getBoolArg(args, "system", false) { + opts = append(opts, "-r") + } + + cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s", + name, strings.Join(opts, " "), name) + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +// --- HTTP Module --- + +func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + url := getStringArg(args, "url", "") + method := getStringArg(args, "method", "GET") + + if url == "" { + return nil, fmt.Errorf("uri: url required") + } + + var curlOpts []string + curlOpts = append(curlOpts, "-s", "-S") + curlOpts = append(curlOpts, "-X", method) + + // Headers + if headers, ok := args["headers"].(map[string]any); ok { + for k, v := range headers { + curlOpts = append(curlOpts, "-H", fmt.Sprintf("%s: %v", k, v)) + } + } + + // Body + if body := getStringArg(args, "body", ""); body != "" { + curlOpts = append(curlOpts, "-d", body) + } + + // Status code + curlOpts = append(curlOpts, "-w", "\\n%{http_code}") + + cmd := fmt.Sprintf("curl %s %q", strings.Join(curlOpts, " "), url) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil { + return &TaskResult{Failed: true, Msg: err.Error()}, nil + } + + // Parse status code from last line + lines := strings.Split(strings.TrimSpace(stdout), "\n") + statusCode := 0 + if len(lines) > 0 { + statusCode, _ = strconv.Atoi(lines[len(lines)-1]) + } + + // Check expected status + expectedStatus := 200 + if s, ok := args["status_code"].(int); ok { + expectedStatus = s + } + + failed := rc != 0 || statusCode != expectedStatus + + return &TaskResult{ + Changed: false, + Failed: failed, + Stdout: stdout, + Stderr: stderr, + RC: statusCode, + Data: map[string]any{"status": statusCode}, + }, nil +} + +// --- Misc Modules --- + +func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) { + msg := getStringArg(args, "msg", "") + if v, ok := args["var"]; ok { + msg = fmt.Sprintf("%v = %v", v, e.vars[fmt.Sprintf("%v", v)]) + } + + return &TaskResult{ + Changed: false, + Msg: msg, + }, nil +} + +func (e *Executor) moduleFail(args map[string]any) (*TaskResult, error) { + msg := getStringArg(args, "msg", "Failed as requested") + return &TaskResult{ + Failed: true, + Msg: msg, + }, nil +} + +func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult, error) { + that, ok := args["that"] + if !ok { + return nil, fmt.Errorf("assert: 'that' required") + } + + conditions := normalizeConditions(that) + for _, cond := range conditions { + if !e.evalCondition(cond, host) { + msg := getStringArg(args, "fail_msg", fmt.Sprintf("Assertion failed: %s", cond)) + return &TaskResult{Failed: true, Msg: msg}, nil + } + } + + return &TaskResult{Changed: false, Msg: "All assertions passed"}, nil +} + +func (e *Executor) moduleSetFact(args map[string]any) (*TaskResult, error) { + for k, v := range args { + if k != "cacheable" { + e.vars[k] = v + } + } + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) modulePause(ctx context.Context, args map[string]any) (*TaskResult, error) { + seconds := 0 + if s, ok := args["seconds"].(int); ok { + seconds = s + } + if s, ok := args["seconds"].(string); ok { + seconds, _ = strconv.Atoi(s) + } + + if seconds > 0 { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ctxSleep(ctx, seconds): + } + } + + return &TaskResult{Changed: false}, nil +} + +func ctxSleep(ctx context.Context, seconds int) <-chan struct{} { + ch := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + case <-sleepChan(seconds): + } + close(ch) + }() + return ch +} + +func sleepChan(seconds int) <-chan struct{} { + ch := make(chan struct{}) + go func() { + for i := 0; i < seconds; i++ { + select { + case <-ch: + return + default: + // Sleep 1 second at a time + } + } + close(ch) + }() + return ch +} + +func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + port := 0 + if p, ok := args["port"].(int); ok { + port = p + } + host := getStringArg(args, "host", "127.0.0.1") + state := getStringArg(args, "state", "started") + timeout := 300 + if t, ok := args["timeout"].(int); ok { + timeout = t + } + + if port > 0 && state == "started" { + cmd := fmt.Sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'", + timeout, host, port) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + } + + return &TaskResult{Changed: false}, nil +} + +func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + repo := getStringArg(args, "repo", "") + dest := getStringArg(args, "dest", "") + version := getStringArg(args, "version", "HEAD") + + if repo == "" || dest == "" { + return nil, fmt.Errorf("git: repo and dest required") + } + + // Check if dest exists + exists, _ := client.FileExists(ctx, dest+"/.git") + + var cmd string + if exists { + // Fetch and checkout (force to ensure clean state) + cmd = fmt.Sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version) + } else { + cmd = fmt.Sprintf("git clone %q %q && cd %q && git checkout %q", + repo, dest, dest, version) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + src := getStringArg(args, "src", "") + dest := getStringArg(args, "dest", "") + remote := getBoolArg(args, "remote_src", false) + + if src == "" || dest == "" { + return nil, fmt.Errorf("unarchive: src and dest required") + } + + // Create dest directory (best-effort) + _, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q", dest)) + + var cmd string + if !remote { + // Upload local file first + content, err := os.ReadFile(src) + if err != nil { + return nil, fmt.Errorf("read src: %w", err) + } + tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src) + err = client.Upload(ctx, strings.NewReader(string(content)), tmpPath, 0644) + if err != nil { + return nil, err + } + src = tmpPath + defer func() { _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", tmpPath)) }() + } + + // Detect archive type and extract + if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") { + cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest) + } else if strings.HasSuffix(src, ".tar.xz") { + cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest) + } else if strings.HasSuffix(src, ".tar.bz2") { + cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest) + } else if strings.HasSuffix(src, ".tar") { + cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) + } else if strings.HasSuffix(src, ".zip") { + cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest) + } else { + cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) // Guess tar + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +// --- Helpers --- + +func getStringArg(args map[string]any, key, def string) string { + if v, ok := args[key]; ok { + if s, ok := v.(string); ok { + return s + } + return fmt.Sprintf("%v", v) + } + return def +} + +func getBoolArg(args map[string]any, key string, def bool) bool { + if v, ok := args[key]; ok { + switch b := v.(type) { + case bool: + return b + case string: + lower := strings.ToLower(b) + return lower == "true" || lower == "yes" || lower == "1" + } + } + return def +} + +// --- Additional Modules --- + +func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + if name == "" { + return nil, fmt.Errorf("hostname: name required") + } + + // Set hostname + cmd := fmt.Sprintf("hostnamectl set-hostname %q || hostname %q", name, name) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Update /etc/hosts if needed (best-effort) + _, _, _, _ = client.Run(ctx, fmt.Sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name)) + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + value := getStringArg(args, "value", "") + state := getStringArg(args, "state", "present") + + if name == "" { + return nil, fmt.Errorf("sysctl: name required") + } + + if state == "absent" { + // Remove from sysctl.conf + cmd := fmt.Sprintf("sed -i '/%s/d' /etc/sysctl.conf", name) + _, _, _, _ = client.Run(ctx, cmd) + return &TaskResult{Changed: true}, nil + } + + // Set value + cmd := fmt.Sprintf("sysctl -w %s=%s", name, value) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Persist if requested (best-effort) + if getBoolArg(args, "sysctl_set", true) { + cmd = fmt.Sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf", + name, name, name, value, name, value) + _, _, _, _ = client.Run(ctx, cmd) + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + name := getStringArg(args, "name", "") + job := getStringArg(args, "job", "") + state := getStringArg(args, "state", "present") + user := getStringArg(args, "user", "root") + + minute := getStringArg(args, "minute", "*") + hour := getStringArg(args, "hour", "*") + day := getStringArg(args, "day", "*") + month := getStringArg(args, "month", "*") + weekday := getStringArg(args, "weekday", "*") + + if state == "absent" { + if name != "" { + // Remove by name (comment marker) + cmd := fmt.Sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -", + user, name, job, user) + _, _, _, _ = client.Run(ctx, cmd) + } + return &TaskResult{Changed: true}, nil + } + + // Build cron entry + schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday) + entry := fmt.Sprintf("%s %s # %s", schedule, job, name) + + // Add to crontab + cmd := fmt.Sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -", + user, name, entry, user) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + path := getStringArg(args, "path", "") + if path == "" { + path = getStringArg(args, "dest", "") + } + if path == "" { + return nil, fmt.Errorf("blockinfile: path required") + } + + block := getStringArg(args, "block", "") + marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK") + state := getStringArg(args, "state", "present") + create := getBoolArg(args, "create", false) + + beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1) + endMarker := strings.Replace(marker, "{mark}", "END", 1) + + if state == "absent" { + // Remove block + cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q", + strings.ReplaceAll(beginMarker, "/", "\\/"), + strings.ReplaceAll(endMarker, "/", "\\/"), + path) + _, _, _, _ = client.Run(ctx, cmd) + return &TaskResult{Changed: true}, nil + } + + // Create file if needed (best-effort) + if create { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("touch %q", path)) + } + + // Remove existing block and add new one + escapedBlock := strings.ReplaceAll(block, "'", "'\\''") + cmd := fmt.Sprintf(` +sed -i '/%s/,/%s/d' %q 2>/dev/null || true +cat >> %q << 'BLOCK_EOF' +%s +%s +%s +BLOCK_EOF +`, strings.ReplaceAll(beginMarker, "/", "\\/"), + strings.ReplaceAll(endMarker, "/", "\\/"), + path, path, beginMarker, escapedBlock, endMarker) + + stdout, stderr, rc, err := client.RunScript(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { + file := getStringArg(args, "file", "") + if file == "" { + file = getStringArg(args, "_raw_params", "") + } + + if file != "" { + // Would need to read and parse the vars file + // For now, just acknowledge + return &TaskResult{Changed: false, Msg: "include_vars: " + file}, nil + } + + return &TaskResult{Changed: false}, nil +} + +func (e *Executor) moduleMeta(args map[string]any) (*TaskResult, error) { + // meta module controls play execution + // Most actions are no-ops for us + return &TaskResult{Changed: false}, nil +} + +func (e *Executor) moduleSetup(ctx context.Context, client *SSHClient) (*TaskResult, error) { + // Gather facts - similar to what we do in gatherFacts + return &TaskResult{Changed: false, Msg: "facts gathered"}, nil +} + +func (e *Executor) moduleReboot(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + preRebootDelay := 0 + if d, ok := args["pre_reboot_delay"].(int); ok { + preRebootDelay = d + } + + msg := getStringArg(args, "msg", "Reboot initiated by Ansible") + + if preRebootDelay > 0 { + cmd := fmt.Sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg) + _, _, _, _ = client.Run(ctx, cmd) + } else { + _, _, _, _ = client.Run(ctx, fmt.Sprintf("shutdown -r now '%s' &", msg)) + } + + return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil +} + +func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + rule := getStringArg(args, "rule", "") + port := getStringArg(args, "port", "") + proto := getStringArg(args, "proto", "tcp") + state := getStringArg(args, "state", "") + + var cmd string + + // Handle state (enable/disable) + if state != "" { + switch state { + case "enabled": + cmd = "ufw --force enable" + case "disabled": + cmd = "ufw disable" + case "reloaded": + cmd = "ufw reload" + case "reset": + cmd = "ufw --force reset" + } + if cmd != "" { + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + return &TaskResult{Changed: true}, nil + } + } + + // Handle rule + if rule != "" && port != "" { + switch rule { + case "allow": + cmd = fmt.Sprintf("ufw allow %s/%s", port, proto) + case "deny": + cmd = fmt.Sprintf("ufw deny %s/%s", port, proto) + case "reject": + cmd = fmt.Sprintf("ufw reject %s/%s", port, proto) + case "limit": + cmd = fmt.Sprintf("ufw limit %s/%s", port, proto) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + } + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + user := getStringArg(args, "user", "") + key := getStringArg(args, "key", "") + state := getStringArg(args, "state", "present") + + if user == "" || key == "" { + return nil, fmt.Errorf("authorized_key: user and key required") + } + + // Get user's home directory + stdout, _, _, err := client.Run(ctx, fmt.Sprintf("getent passwd %s | cut -d: -f6", user)) + if err != nil { + return nil, fmt.Errorf("get home dir: %w", err) + } + home := strings.TrimSpace(stdout) + if home == "" { + home = "/root" + if user != "root" { + home = "/home/" + user + } + } + + authKeysPath := filepath.Join(home, ".ssh", "authorized_keys") + + if state == "absent" { + // Remove key + escapedKey := strings.ReplaceAll(key, "/", "\\/") + cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath) + _, _, _, _ = client.Run(ctx, cmd) + return &TaskResult{Changed: true}, nil + } + + // Ensure .ssh directory exists (best-effort) + _, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q", + filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath))) + + // Add key if not present + cmd := fmt.Sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q", + key[:40], authKeysPath, key, authKeysPath) + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Fix permissions (best-effort) + _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod 600 %q && chown %s:%s %q", + authKeysPath, user, user, authKeysPath)) + + return &TaskResult{Changed: true}, nil +} + +func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { + projectSrc := getStringArg(args, "project_src", "") + state := getStringArg(args, "state", "present") + + if projectSrc == "" { + return nil, fmt.Errorf("docker_compose: project_src required") + } + + var cmd string + switch state { + case "present": + cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc) + case "absent": + cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc) + case "restarted": + cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc) + default: + cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc) + } + + stdout, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil + } + + // Heuristic for changed + changed := true + if strings.Contains(stdout, "Up to date") || strings.Contains(stderr, "Up to date") { + changed = false + } + + return &TaskResult{Changed: changed, Stdout: stdout}, nil +} diff --git a/pkg/ansible/parser.go b/pkg/ansible/parser.go new file mode 100644 index 0000000..b8423f6 --- /dev/null +++ b/pkg/ansible/parser.go @@ -0,0 +1,438 @@ +package ansible + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/host-uk/core/pkg/log" + "gopkg.in/yaml.v3" +) + +// Parser handles Ansible YAML parsing. +type Parser struct { + basePath string + vars map[string]any +} + +// NewParser creates a new Ansible parser. +func NewParser(basePath string) *Parser { + return &Parser{ + basePath: basePath, + vars: make(map[string]any), + } +} + +// ParsePlaybook parses an Ansible playbook file. +func (p *Parser) ParsePlaybook(path string) ([]Play, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read playbook: %w", err) + } + + var plays []Play + if err := yaml.Unmarshal(data, &plays); err != nil { + return nil, fmt.Errorf("parse playbook: %w", err) + } + + // Process each play + for i := range plays { + if err := p.processPlay(&plays[i]); err != nil { + return nil, fmt.Errorf("process play %d: %w", i, err) + } + } + + return plays, nil +} + +// ParseInventory parses an Ansible inventory file. +func (p *Parser) ParseInventory(path string) (*Inventory, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read inventory: %w", err) + } + + var inv Inventory + if err := yaml.Unmarshal(data, &inv); err != nil { + return nil, fmt.Errorf("parse inventory: %w", err) + } + + return &inv, nil +} + +// ParseTasks parses a tasks file (used by include_tasks). +func (p *Parser) ParseTasks(path string) ([]Task, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read tasks: %w", err) + } + + var tasks []Task + if err := yaml.Unmarshal(data, &tasks); err != nil { + return nil, fmt.Errorf("parse tasks: %w", err) + } + + for i := range tasks { + if err := p.extractModule(&tasks[i]); err != nil { + return nil, fmt.Errorf("task %d: %w", i, err) + } + } + + return tasks, nil +} + +// ParseRole parses a role and returns its tasks. +func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) { + if tasksFrom == "" { + tasksFrom = "main.yml" + } + + // Search paths for roles (in order of precedence) + searchPaths := []string{ + // Relative to playbook + filepath.Join(p.basePath, "roles", name, "tasks", tasksFrom), + // Parent directory roles + filepath.Join(filepath.Dir(p.basePath), "roles", name, "tasks", tasksFrom), + // Sibling roles directory + filepath.Join(p.basePath, "..", "roles", name, "tasks", tasksFrom), + // playbooks/roles pattern + filepath.Join(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom), + // Common DevOps structure + filepath.Join(filepath.Dir(filepath.Dir(p.basePath)), "roles", name, "tasks", tasksFrom), + } + + var tasksPath string + for _, sp := range searchPaths { + // Clean the path to resolve .. segments + sp = filepath.Clean(sp) + if _, err := os.Stat(sp); err == nil { + tasksPath = sp + break + } + } + + if tasksPath == "" { + return nil, log.E("parser.ParseRole", fmt.Sprintf("role %s not found in search paths: %v", name, searchPaths), nil) + } + + // Load role defaults + defaultsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "defaults", "main.yml") + if data, err := os.ReadFile(defaultsPath); err == nil { + var defaults map[string]any + if yaml.Unmarshal(data, &defaults) == nil { + for k, v := range defaults { + if _, exists := p.vars[k]; !exists { + p.vars[k] = v + } + } + } + } + + // Load role vars + varsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "vars", "main.yml") + if data, err := os.ReadFile(varsPath); err == nil { + var roleVars map[string]any + if yaml.Unmarshal(data, &roleVars) == nil { + for k, v := range roleVars { + p.vars[k] = v + } + } + } + + return p.ParseTasks(tasksPath) +} + +// processPlay processes a play and extracts modules from tasks. +func (p *Parser) processPlay(play *Play) error { + // Merge play vars + for k, v := range play.Vars { + p.vars[k] = v + } + + for i := range play.PreTasks { + if err := p.extractModule(&play.PreTasks[i]); err != nil { + return fmt.Errorf("pre_task %d: %w", i, err) + } + } + + for i := range play.Tasks { + if err := p.extractModule(&play.Tasks[i]); err != nil { + return fmt.Errorf("task %d: %w", i, err) + } + } + + for i := range play.PostTasks { + if err := p.extractModule(&play.PostTasks[i]); err != nil { + return fmt.Errorf("post_task %d: %w", i, err) + } + } + + for i := range play.Handlers { + if err := p.extractModule(&play.Handlers[i]); err != nil { + return fmt.Errorf("handler %d: %w", i, err) + } + } + + return nil +} + +// extractModule extracts the module name and args from a task. +func (p *Parser) extractModule(task *Task) error { + // First, unmarshal the raw YAML to get all keys + // This is a workaround since we need to find the module key dynamically + + // Handle block tasks + for i := range task.Block { + if err := p.extractModule(&task.Block[i]); err != nil { + return err + } + } + for i := range task.Rescue { + if err := p.extractModule(&task.Rescue[i]); err != nil { + return err + } + } + for i := range task.Always { + if err := p.extractModule(&task.Always[i]); err != nil { + return err + } + } + + return nil +} + +// UnmarshalYAML implements custom YAML unmarshaling for Task. +func (t *Task) UnmarshalYAML(node *yaml.Node) error { + // First decode known fields + type rawTask Task + var raw rawTask + + // Create a map to capture all fields + var m map[string]any + if err := node.Decode(&m); err != nil { + return err + } + + // Decode into struct + if err := node.Decode(&raw); err != nil { + return err + } + *t = Task(raw) + t.raw = m + + // Find the module key + knownKeys := map[string]bool{ + "name": true, "register": true, "when": true, "loop": true, + "loop_control": true, "vars": true, "environment": true, + "changed_when": true, "failed_when": true, "ignore_errors": true, + "no_log": true, "become": true, "become_user": true, + "delegate_to": true, "run_once": true, "tags": true, + "block": true, "rescue": true, "always": true, "notify": true, + "retries": true, "delay": true, "until": true, + "include_tasks": true, "import_tasks": true, + "include_role": true, "import_role": true, + "with_items": true, "with_dict": true, "with_file": true, + } + + for key, val := range m { + if knownKeys[key] { + continue + } + + // Check if this is a module + if isModule(key) { + t.Module = key + t.Args = make(map[string]any) + + switch v := val.(type) { + case string: + // Free-form args (e.g., shell: echo hello) + t.Args["_raw_params"] = v + case map[string]any: + t.Args = v + case nil: + // Module with no args + default: + t.Args["_raw_params"] = v + } + break + } + } + + // Handle with_items as loop + if items, ok := m["with_items"]; ok && t.Loop == nil { + t.Loop = items + } + + return nil +} + +// isModule checks if a key is a known module. +func isModule(key string) bool { + for _, m := range KnownModules { + if key == m { + return true + } + // Also check without ansible.builtin. prefix + if strings.HasPrefix(m, "ansible.builtin.") { + if key == strings.TrimPrefix(m, "ansible.builtin.") { + return true + } + } + } + // Accept any key with dots (likely a module) + return strings.Contains(key, ".") +} + +// NormalizeModule normalizes a module name to its canonical form. +func NormalizeModule(name string) string { + // Add ansible.builtin. prefix if missing + if !strings.Contains(name, ".") { + return "ansible.builtin." + name + } + return name +} + +// GetHosts returns hosts matching a pattern from inventory. +func GetHosts(inv *Inventory, pattern string) []string { + if pattern == "all" { + return getAllHosts(inv.All) + } + if pattern == "localhost" { + return []string{"localhost"} + } + + // Check if it's a group name + hosts := getGroupHosts(inv.All, pattern) + if len(hosts) > 0 { + return hosts + } + + // Check if it's a specific host + if hasHost(inv.All, pattern) { + return []string{pattern} + } + + // Handle patterns with : (intersection/union) + // For now, just return empty + return nil +} + +func getAllHosts(group *InventoryGroup) []string { + if group == nil { + return nil + } + + var hosts []string + for name := range group.Hosts { + hosts = append(hosts, name) + } + for _, child := range group.Children { + hosts = append(hosts, getAllHosts(child)...) + } + return hosts +} + +func getGroupHosts(group *InventoryGroup, name string) []string { + if group == nil { + return nil + } + + // Check children for the group name + if child, ok := group.Children[name]; ok { + return getAllHosts(child) + } + + // Recurse + for _, child := range group.Children { + if hosts := getGroupHosts(child, name); len(hosts) > 0 { + return hosts + } + } + + return nil +} + +func hasHost(group *InventoryGroup, name string) bool { + if group == nil { + return false + } + + if _, ok := group.Hosts[name]; ok { + return true + } + + for _, child := range group.Children { + if hasHost(child, name) { + return true + } + } + + return false +} + +// GetHostVars returns variables for a specific host. +func GetHostVars(inv *Inventory, hostname string) map[string]any { + vars := make(map[string]any) + + // Collect vars from all levels + collectHostVars(inv.All, hostname, vars) + + return vars +} + +func collectHostVars(group *InventoryGroup, hostname string, vars map[string]any) bool { + if group == nil { + return false + } + + // Check if host is in this group + found := false + if host, ok := group.Hosts[hostname]; ok { + found = true + // Apply group vars first + for k, v := range group.Vars { + vars[k] = v + } + // Then host vars + if host != nil { + if host.AnsibleHost != "" { + vars["ansible_host"] = host.AnsibleHost + } + if host.AnsiblePort != 0 { + vars["ansible_port"] = host.AnsiblePort + } + if host.AnsibleUser != "" { + vars["ansible_user"] = host.AnsibleUser + } + if host.AnsiblePassword != "" { + vars["ansible_password"] = host.AnsiblePassword + } + if host.AnsibleSSHPrivateKeyFile != "" { + vars["ansible_ssh_private_key_file"] = host.AnsibleSSHPrivateKeyFile + } + if host.AnsibleConnection != "" { + vars["ansible_connection"] = host.AnsibleConnection + } + for k, v := range host.Vars { + vars[k] = v + } + } + } + + // Check children + for _, child := range group.Children { + if collectHostVars(child, hostname, vars) { + // Apply this group's vars (parent vars) + for k, v := range group.Vars { + if _, exists := vars[k]; !exists { + vars[k] = v + } + } + found = true + } + } + + return found +} diff --git a/pkg/ansible/ssh.go b/pkg/ansible/ssh.go new file mode 100644 index 0000000..9678ba6 --- /dev/null +++ b/pkg/ansible/ssh.go @@ -0,0 +1,449 @@ +package ansible + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/host-uk/core/pkg/log" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +// SSHClient handles SSH connections to remote hosts. +type SSHClient struct { + host string + port int + user string + password string + keyFile string + client *ssh.Client + mu sync.Mutex + become bool + becomeUser string + becomePass string + timeout time.Duration + insecure bool +} + +// SSHConfig holds SSH connection configuration. +type SSHConfig struct { + Host string + Port int + User string + Password string + KeyFile string + Become bool + BecomeUser string + BecomePass string + Timeout time.Duration + Insecure bool +} + +// NewSSHClient creates a new SSH client. +func NewSSHClient(cfg SSHConfig) (*SSHClient, error) { + if cfg.Port == 0 { + cfg.Port = 22 + } + if cfg.User == "" { + cfg.User = "root" + } + if cfg.Timeout == 0 { + cfg.Timeout = 30 * time.Second + } + + client := &SSHClient{ + host: cfg.Host, + port: cfg.Port, + user: cfg.User, + password: cfg.Password, + keyFile: cfg.KeyFile, + become: cfg.Become, + becomeUser: cfg.BecomeUser, + becomePass: cfg.BecomePass, + timeout: cfg.Timeout, + insecure: cfg.Insecure, + } + + return client, nil +} + +// Connect establishes the SSH connection. +func (c *SSHClient) Connect(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.client != nil { + return nil + } + + var authMethods []ssh.AuthMethod + + // Try key-based auth first + if c.keyFile != "" { + keyPath := c.keyFile + if strings.HasPrefix(keyPath, "~") { + home, _ := os.UserHomeDir() + keyPath = filepath.Join(home, keyPath[1:]) + } + + if key, err := os.ReadFile(keyPath); err == nil { + if signer, err := ssh.ParsePrivateKey(key); err == nil { + authMethods = append(authMethods, ssh.PublicKeys(signer)) + } + } + } + + // Try default SSH keys + if len(authMethods) == 0 { + home, _ := os.UserHomeDir() + defaultKeys := []string{ + filepath.Join(home, ".ssh", "id_ed25519"), + filepath.Join(home, ".ssh", "id_rsa"), + } + for _, keyPath := range defaultKeys { + if key, err := os.ReadFile(keyPath); err == nil { + if signer, err := ssh.ParsePrivateKey(key); err == nil { + authMethods = append(authMethods, ssh.PublicKeys(signer)) + break + } + } + } + } + + // Fall back to password auth + if c.password != "" { + authMethods = append(authMethods, ssh.Password(c.password)) + authMethods = append(authMethods, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range questions { + answers[i] = c.password + } + return answers, nil + })) + } + + if len(authMethods) == 0 { + return log.E("ssh.Connect", "no authentication method available", nil) + } + + // Host key verification + var hostKeyCallback ssh.HostKeyCallback + + if c.insecure { + hostKeyCallback = ssh.InsecureIgnoreHostKey() + } else { + home, err := os.UserHomeDir() + if err != nil { + return log.E("ssh.Connect", "failed to get user home dir", err) + } + knownHostsPath := filepath.Join(home, ".ssh", "known_hosts") + + cb, err := knownhosts.New(knownHostsPath) + if err != nil { + return log.E("ssh.Connect", "failed to load known_hosts (use Insecure=true to bypass)", err) + } + hostKeyCallback = cb + } + + config := &ssh.ClientConfig{ + User: c.user, + Auth: authMethods, + HostKeyCallback: hostKeyCallback, + Timeout: c.timeout, + } + + addr := fmt.Sprintf("%s:%d", c.host, c.port) + + // Connect with context timeout + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", addr) + if err != nil { + return log.E("ssh.Connect", fmt.Sprintf("dial %s", addr), err) + } + + sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config) + if err != nil { + // conn is closed by NewClientConn on error + return log.E("ssh.Connect", fmt.Sprintf("ssh connect %s", addr), err) + } + + + c.client = ssh.NewClient(sshConn, chans, reqs) + return nil +} + +// Close closes the SSH connection. +func (c *SSHClient) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.client != nil { + err := c.client.Close() + c.client = nil + return err + } + return nil +} + +// Run executes a command on the remote host. +func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string, exitCode int, err error) { + if err := c.Connect(ctx); err != nil { + return "", "", -1, err + } + + session, err := c.client.NewSession() + if err != nil { + return "", "", -1, log.E("ssh.Run", "new session", err) + } + defer func() { _ = session.Close() }() + + var stdoutBuf, stderrBuf bytes.Buffer + session.Stdout = &stdoutBuf + session.Stderr = &stderrBuf + + // Apply become if needed + if c.become { + becomeUser := c.becomeUser + if becomeUser == "" { + becomeUser = "root" + } + // Escape single quotes in the command + escapedCmd := strings.ReplaceAll(cmd, "'", "'\\''") + if c.becomePass != "" { + // Use sudo with password via stdin (-S flag) + // We launch a goroutine to write the password to stdin + cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) + stdin, err := session.StdinPipe() + if err != nil { + return "", "", -1, log.E("ssh.Run", "stdin pipe", err) + } + go func() { + defer stdin.Close() + _, _ = io.WriteString(stdin, c.becomePass+"\n") + }() + } else if c.password != "" { + // Try using connection password for sudo + cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) + stdin, err := session.StdinPipe() + if err != nil { + return "", "", -1, log.E("ssh.Run", "stdin pipe", err) + } + go func() { + defer stdin.Close() + _, _ = io.WriteString(stdin, c.password+"\n") + }() + } else { + // Try passwordless sudo + cmd = fmt.Sprintf("sudo -n -u %s bash -c '%s'", becomeUser, escapedCmd) + } + } + + // Run with context + done := make(chan error, 1) + go func() { + done <- session.Run(cmd) + }() + + select { + case <-ctx.Done(): + _ = session.Signal(ssh.SIGKILL) + return "", "", -1, ctx.Err() + case err := <-done: + exitCode = 0 + if err != nil { + if exitErr, ok := err.(*ssh.ExitError); ok { + exitCode = exitErr.ExitStatus() + } else { + return stdoutBuf.String(), stderrBuf.String(), -1, err + } + } + return stdoutBuf.String(), stderrBuf.String(), exitCode, nil + } +} + +// RunScript runs a script on the remote host. +func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) { + // Escape the script for heredoc + cmd := fmt.Sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script) + return c.Run(ctx, cmd) +} + +// Upload copies a file to the remote host. +func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, mode os.FileMode) error { + if err := c.Connect(ctx); err != nil { + return err + } + + // Read content + content, err := io.ReadAll(local) + if err != nil { + return log.E("ssh.Upload", "read content", err) + } + + // Create parent directory + dir := filepath.Dir(remote) + dirCmd := fmt.Sprintf("mkdir -p %q", dir) + if c.become { + dirCmd = fmt.Sprintf("sudo mkdir -p %q", dir) + } + if _, _, _, err := c.Run(ctx, dirCmd); err != nil { + return log.E("ssh.Upload", "create parent dir", err) + } + + // Use cat to write the file (simpler than SCP) + writeCmd := fmt.Sprintf("cat > %q && chmod %o %q", remote, mode, remote) + + // If become is needed, we construct a command that reads password then content from stdin + // But we need to be careful with handling stdin for sudo + cat. + // We'll use a session with piped stdin. + + session2, err := c.client.NewSession() + if err != nil { + return log.E("ssh.Upload", "new session for write", err) + } + defer func() { _ = session2.Close() }() + + stdin, err := session2.StdinPipe() + if err != nil { + return log.E("ssh.Upload", "stdin pipe", err) + } + + var stderrBuf bytes.Buffer + session2.Stderr = &stderrBuf + + if c.become { + becomeUser := c.becomeUser + if becomeUser == "" { + becomeUser = "root" + } + + pass := c.becomePass + if pass == "" { + pass = c.password + } + + if pass != "" { + // Use sudo -S with password from stdin + writeCmd = fmt.Sprintf("sudo -S -u %s bash -c 'cat > %q && chmod %o %q'", + becomeUser, remote, mode, remote) + } else { + // Use passwordless sudo (sudo -n) to avoid consuming file content as password + writeCmd = fmt.Sprintf("sudo -n -u %s bash -c 'cat > %q && chmod %o %q'", + becomeUser, remote, mode, remote) + } + + if err := session2.Start(writeCmd); err != nil { + return log.E("ssh.Upload", "start write", err) + } + + go func() { + defer stdin.Close() + if pass != "" { + _, _ = io.WriteString(stdin, pass+"\n") + } + _, _ = stdin.Write(content) + }() + } else { + // Normal write + if err := session2.Start(writeCmd); err != nil { + return log.E("ssh.Upload", "start write", err) + } + + go func() { + defer stdin.Close() + _, _ = stdin.Write(content) + }() + } + + if err := session2.Wait(); err != nil { + return log.E("ssh.Upload", fmt.Sprintf("write failed (stderr: %s)", stderrBuf.String()), err) + } + + return nil +} + +// Download copies a file from the remote host. +func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error) { + if err := c.Connect(ctx); err != nil { + return nil, err + } + + cmd := fmt.Sprintf("cat %q", remote) + + stdout, stderr, exitCode, err := c.Run(ctx, cmd) + if err != nil { + return nil, err + } + if exitCode != 0 { + return nil, log.E("ssh.Download", fmt.Sprintf("cat failed: %s", stderr), nil) + } + + return []byte(stdout), nil +} + +// FileExists checks if a file exists on the remote host. +func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) { + cmd := fmt.Sprintf("test -e %q && echo yes || echo no", path) + stdout, _, exitCode, err := c.Run(ctx, cmd) + if err != nil { + return false, err + } + if exitCode != 0 { + // test command failed but didn't error - file doesn't exist + return false, nil + } + return strings.TrimSpace(stdout) == "yes", nil +} + +// Stat returns file info from the remote host. +func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) { + // Simple approach - get basic file info + cmd := fmt.Sprintf(` +if [ -e %q ]; then + if [ -d %q ]; then + echo "exists=true isdir=true" + else + echo "exists=true isdir=false" + fi +else + echo "exists=false" +fi +`, path, path) + + stdout, _, _, err := c.Run(ctx, cmd) + if err != nil { + return nil, err + } + + result := make(map[string]any) + parts := strings.Fields(strings.TrimSpace(stdout)) + for _, part := range parts { + kv := strings.SplitN(part, "=", 2) + if len(kv) == 2 { + result[kv[0]] = kv[1] == "true" + } + } + + return result, nil +} + +// SetBecome enables privilege escalation. +func (c *SSHClient) SetBecome(become bool, user, password string) { + c.mu.Lock() + defer c.mu.Unlock() + c.become = become + if user != "" { + c.becomeUser = user + } + if password != "" { + c.becomePass = password + } +} diff --git a/pkg/ansible/types.go b/pkg/ansible/types.go new file mode 100644 index 0000000..5a6939f --- /dev/null +++ b/pkg/ansible/types.go @@ -0,0 +1,258 @@ +package ansible + +import ( + "time" +) + +// Playbook represents an Ansible playbook. +type Playbook struct { + Plays []Play `yaml:",inline"` +} + +// Play represents a single play in a playbook. +type Play struct { + Name string `yaml:"name"` + Hosts string `yaml:"hosts"` + Connection string `yaml:"connection,omitempty"` + Become bool `yaml:"become,omitempty"` + BecomeUser string `yaml:"become_user,omitempty"` + GatherFacts *bool `yaml:"gather_facts,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + PreTasks []Task `yaml:"pre_tasks,omitempty"` + Tasks []Task `yaml:"tasks,omitempty"` + PostTasks []Task `yaml:"post_tasks,omitempty"` + Roles []RoleRef `yaml:"roles,omitempty"` + Handlers []Task `yaml:"handlers,omitempty"` + Tags []string `yaml:"tags,omitempty"` + Environment map[string]string `yaml:"environment,omitempty"` + Serial any `yaml:"serial,omitempty"` // int or string + MaxFailPercent int `yaml:"max_fail_percentage,omitempty"` +} + +// RoleRef represents a role reference in a play. +type RoleRef struct { + Role string `yaml:"role,omitempty"` + Name string `yaml:"name,omitempty"` // Alternative to role + TasksFrom string `yaml:"tasks_from,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + When any `yaml:"when,omitempty"` + Tags []string `yaml:"tags,omitempty"` +} + +// UnmarshalYAML handles both string and struct role refs. +func (r *RoleRef) UnmarshalYAML(unmarshal func(any) error) error { + // Try string first + var s string + if err := unmarshal(&s); err == nil { + r.Role = s + return nil + } + + // Try struct + type rawRoleRef RoleRef + var raw rawRoleRef + if err := unmarshal(&raw); err != nil { + return err + } + *r = RoleRef(raw) + if r.Role == "" && r.Name != "" { + r.Role = r.Name + } + return nil +} + +// Task represents an Ansible task. +type Task struct { + Name string `yaml:"name,omitempty"` + Module string `yaml:"-"` // Derived from the module key + Args map[string]any `yaml:"-"` // Module arguments + Register string `yaml:"register,omitempty"` + When any `yaml:"when,omitempty"` // string or []string + Loop any `yaml:"loop,omitempty"` // string or []any + LoopControl *LoopControl `yaml:"loop_control,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + Environment map[string]string `yaml:"environment,omitempty"` + ChangedWhen any `yaml:"changed_when,omitempty"` + FailedWhen any `yaml:"failed_when,omitempty"` + IgnoreErrors bool `yaml:"ignore_errors,omitempty"` + NoLog bool `yaml:"no_log,omitempty"` + Become *bool `yaml:"become,omitempty"` + BecomeUser string `yaml:"become_user,omitempty"` + Delegate string `yaml:"delegate_to,omitempty"` + RunOnce bool `yaml:"run_once,omitempty"` + Tags []string `yaml:"tags,omitempty"` + Block []Task `yaml:"block,omitempty"` + Rescue []Task `yaml:"rescue,omitempty"` + Always []Task `yaml:"always,omitempty"` + Notify any `yaml:"notify,omitempty"` // string or []string + Retries int `yaml:"retries,omitempty"` + Delay int `yaml:"delay,omitempty"` + Until string `yaml:"until,omitempty"` + + // Include/import directives + IncludeTasks string `yaml:"include_tasks,omitempty"` + ImportTasks string `yaml:"import_tasks,omitempty"` + IncludeRole *struct { + Name string `yaml:"name"` + TasksFrom string `yaml:"tasks_from,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + } `yaml:"include_role,omitempty"` + ImportRole *struct { + Name string `yaml:"name"` + TasksFrom string `yaml:"tasks_from,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + } `yaml:"import_role,omitempty"` + + // Raw YAML for module extraction + raw map[string]any +} + +// LoopControl controls loop behavior. +type LoopControl struct { + LoopVar string `yaml:"loop_var,omitempty"` + IndexVar string `yaml:"index_var,omitempty"` + Label string `yaml:"label,omitempty"` + Pause int `yaml:"pause,omitempty"` + Extended bool `yaml:"extended,omitempty"` +} + +// TaskResult holds the result of executing a task. +type TaskResult struct { + Changed bool `json:"changed"` + Failed bool `json:"failed"` + Skipped bool `json:"skipped"` + Msg string `json:"msg,omitempty"` + Stdout string `json:"stdout,omitempty"` + Stderr string `json:"stderr,omitempty"` + RC int `json:"rc,omitempty"` + Results []TaskResult `json:"results,omitempty"` // For loops + Data map[string]any `json:"data,omitempty"` // Module-specific data + Duration time.Duration `json:"duration,omitempty"` +} + +// Inventory represents Ansible inventory. +type Inventory struct { + All *InventoryGroup `yaml:"all"` +} + +// InventoryGroup represents a group in inventory. +type InventoryGroup struct { + Hosts map[string]*Host `yaml:"hosts,omitempty"` + Children map[string]*InventoryGroup `yaml:"children,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` +} + +// Host represents a host in inventory. +type Host struct { + AnsibleHost string `yaml:"ansible_host,omitempty"` + AnsiblePort int `yaml:"ansible_port,omitempty"` + AnsibleUser string `yaml:"ansible_user,omitempty"` + AnsiblePassword string `yaml:"ansible_password,omitempty"` + AnsibleSSHPrivateKeyFile string `yaml:"ansible_ssh_private_key_file,omitempty"` + AnsibleConnection string `yaml:"ansible_connection,omitempty"` + AnsibleBecomePassword string `yaml:"ansible_become_password,omitempty"` + + // Custom vars + Vars map[string]any `yaml:",inline"` +} + +// Facts holds gathered facts about a host. +type Facts struct { + Hostname string `json:"ansible_hostname"` + FQDN string `json:"ansible_fqdn"` + OS string `json:"ansible_os_family"` + Distribution string `json:"ansible_distribution"` + Version string `json:"ansible_distribution_version"` + Architecture string `json:"ansible_architecture"` + Kernel string `json:"ansible_kernel"` + Memory int64 `json:"ansible_memtotal_mb"` + CPUs int `json:"ansible_processor_vcpus"` + IPv4 string `json:"ansible_default_ipv4_address"` +} + +// Known Ansible modules +var KnownModules = []string{ + // Builtin + "ansible.builtin.shell", + "ansible.builtin.command", + "ansible.builtin.raw", + "ansible.builtin.script", + "ansible.builtin.copy", + "ansible.builtin.template", + "ansible.builtin.file", + "ansible.builtin.lineinfile", + "ansible.builtin.blockinfile", + "ansible.builtin.stat", + "ansible.builtin.slurp", + "ansible.builtin.fetch", + "ansible.builtin.get_url", + "ansible.builtin.uri", + "ansible.builtin.apt", + "ansible.builtin.apt_key", + "ansible.builtin.apt_repository", + "ansible.builtin.yum", + "ansible.builtin.dnf", + "ansible.builtin.package", + "ansible.builtin.pip", + "ansible.builtin.service", + "ansible.builtin.systemd", + "ansible.builtin.user", + "ansible.builtin.group", + "ansible.builtin.cron", + "ansible.builtin.git", + "ansible.builtin.unarchive", + "ansible.builtin.archive", + "ansible.builtin.debug", + "ansible.builtin.fail", + "ansible.builtin.assert", + "ansible.builtin.pause", + "ansible.builtin.wait_for", + "ansible.builtin.set_fact", + "ansible.builtin.include_vars", + "ansible.builtin.add_host", + "ansible.builtin.group_by", + "ansible.builtin.meta", + "ansible.builtin.setup", + + // Short forms (legacy) + "shell", + "command", + "raw", + "script", + "copy", + "template", + "file", + "lineinfile", + "blockinfile", + "stat", + "slurp", + "fetch", + "get_url", + "uri", + "apt", + "apt_key", + "apt_repository", + "yum", + "dnf", + "package", + "pip", + "service", + "systemd", + "user", + "group", + "cron", + "git", + "unarchive", + "archive", + "debug", + "fail", + "assert", + "pause", + "wait_for", + "set_fact", + "include_vars", + "add_host", + "group_by", + "meta", + "setup", +} diff --git a/pkg/build/archive.go b/pkg/build/archive.go index b0451f2..3e38bac 100644 --- a/pkg/build/archive.go +++ b/pkg/build/archive.go @@ -4,19 +4,49 @@ package build import ( "archive/tar" "archive/zip" + "bytes" "compress/gzip" "fmt" "io" "os" "path/filepath" "strings" + + "github.com/Snider/Borg/pkg/compress" ) -// Archive creates an archive for a single artifact. +// ArchiveFormat specifies the compression format for archives. +type ArchiveFormat string + +const ( + // ArchiveFormatGzip uses tar.gz (gzip compression) - widely compatible. + ArchiveFormatGzip ArchiveFormat = "gz" + // ArchiveFormatXZ uses tar.xz (xz/LZMA2 compression) - better compression ratio. + ArchiveFormatXZ ArchiveFormat = "xz" + // ArchiveFormatZip uses zip - for Windows. + ArchiveFormatZip ArchiveFormat = "zip" +) + +// Archive creates an archive for a single artifact using gzip compression. // Uses tar.gz for linux/darwin and zip for windows. // The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.gz). // Returns a new Artifact with Path pointing to the archive. func Archive(artifact Artifact) (Artifact, error) { + return ArchiveWithFormat(artifact, ArchiveFormatGzip) +} + +// ArchiveXZ creates an archive for a single artifact using xz compression. +// Uses tar.xz for linux/darwin and zip for windows. +// Returns a new Artifact with Path pointing to the archive. +func ArchiveXZ(artifact Artifact) (Artifact, error) { + return ArchiveWithFormat(artifact, ArchiveFormatXZ) +} + +// ArchiveWithFormat creates an archive for a single artifact with the specified format. +// Uses tar.gz or tar.xz for linux/darwin and zip for windows. +// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.xz). +// Returns a new Artifact with Path pointing to the archive. +func ArchiveWithFormat(artifact Artifact, format ArchiveFormat) (Artifact, error) { if artifact.Path == "" { return Artifact{}, fmt.Errorf("build.Archive: artifact path is empty") } @@ -30,7 +60,7 @@ func Archive(artifact Artifact) (Artifact, error) { return Artifact{}, fmt.Errorf("build.Archive: source path is a directory, expected file") } - // Determine archive type based on OS + // Determine archive type based on OS and format var archivePath string var archiveFunc func(src, dst string) error @@ -38,8 +68,14 @@ func Archive(artifact Artifact) (Artifact, error) { archivePath = archiveFilename(artifact, ".zip") archiveFunc = createZipArchive } else { - archivePath = archiveFilename(artifact, ".tar.gz") - archiveFunc = createTarGzArchive + switch format { + case ArchiveFormatXZ: + archivePath = archiveFilename(artifact, ".tar.xz") + archiveFunc = createTarXzArchive + default: + archivePath = archiveFilename(artifact, ".tar.gz") + archiveFunc = createTarGzArchive + } } // Create the archive @@ -55,16 +91,28 @@ func Archive(artifact Artifact) (Artifact, error) { }, nil } -// ArchiveAll archives all artifacts. +// ArchiveAll archives all artifacts using gzip compression. // Returns a slice of new artifacts pointing to the archives. func ArchiveAll(artifacts []Artifact) ([]Artifact, error) { + return ArchiveAllWithFormat(artifacts, ArchiveFormatGzip) +} + +// ArchiveAllXZ archives all artifacts using xz compression. +// Returns a slice of new artifacts pointing to the archives. +func ArchiveAllXZ(artifacts []Artifact) ([]Artifact, error) { + return ArchiveAllWithFormat(artifacts, ArchiveFormatXZ) +} + +// ArchiveAllWithFormat archives all artifacts with the specified format. +// Returns a slice of new artifacts pointing to the archives. +func ArchiveAllWithFormat(artifacts []Artifact, format ArchiveFormat) ([]Artifact, error) { if len(artifacts) == 0 { return nil, nil } var archived []Artifact for _, artifact := range artifacts { - arch, err := Archive(artifact) + arch, err := ArchiveWithFormat(artifact, format) if err != nil { return archived, fmt.Errorf("build.ArchiveAll: failed to archive %s: %w", artifact.Path, err) } @@ -92,6 +140,58 @@ func archiveFilename(artifact Artifact, ext string) string { return filepath.Join(outputDir, archiveName) } +// createTarXzArchive creates a tar.xz archive containing a single file. +// Uses Borg's compress package for xz compression. +func createTarXzArchive(src, dst string) error { + // Open the source file + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %w", err) + } + defer srcFile.Close() + + srcInfo, err := srcFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat source file: %w", err) + } + + // Create tar archive in memory + var tarBuf bytes.Buffer + tarWriter := tar.NewWriter(&tarBuf) + + // Create tar header + header, err := tar.FileInfoHeader(srcInfo, "") + if err != nil { + return fmt.Errorf("failed to create tar header: %w", err) + } + header.Name = filepath.Base(src) + + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write tar header: %w", err) + } + + if _, err := io.Copy(tarWriter, srcFile); err != nil { + return fmt.Errorf("failed to write file content to tar: %w", err) + } + + if err := tarWriter.Close(); err != nil { + return fmt.Errorf("failed to close tar writer: %w", err) + } + + // Compress with xz using Borg + xzData, err := compress.Compress(tarBuf.Bytes(), "xz") + if err != nil { + return fmt.Errorf("failed to compress with xz: %w", err) + } + + // Write to destination file + if err := os.WriteFile(dst, xzData, 0644); err != nil { + return fmt.Errorf("failed to write archive file: %w", err) + } + + return nil +} + // createTarGzArchive creates a tar.gz archive containing a single file. func createTarGzArchive(src, dst string) error { // Open the source file diff --git a/pkg/build/archive_test.go b/pkg/build/archive_test.go index 27d6660..0d3a5c7 100644 --- a/pkg/build/archive_test.go +++ b/pkg/build/archive_test.go @@ -3,12 +3,14 @@ package build import ( "archive/tar" "archive/zip" + "bytes" "compress/gzip" "io" "os" "path/filepath" "testing" + "github.com/Snider/Borg/pkg/compress" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -113,6 +115,64 @@ func TestArchive_Good(t *testing.T) { require.NoError(t, err) assert.Equal(t, "abc123", result.Checksum) }) + + t.Run("creates tar.xz for linux with ArchiveXZ", func(t *testing.T) { + binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64") + + artifact := Artifact{ + Path: binaryPath, + OS: "linux", + Arch: "amd64", + } + + result, err := ArchiveXZ(artifact) + require.NoError(t, err) + + expectedPath := filepath.Join(outputDir, "myapp_linux_amd64.tar.xz") + assert.Equal(t, expectedPath, result.Path) + assert.FileExists(t, result.Path) + + verifyTarXzContent(t, result.Path, "myapp") + }) + + t.Run("creates tar.xz for darwin with ArchiveWithFormat", func(t *testing.T) { + binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "darwin", "arm64") + + artifact := Artifact{ + Path: binaryPath, + OS: "darwin", + Arch: "arm64", + } + + result, err := ArchiveWithFormat(artifact, ArchiveFormatXZ) + require.NoError(t, err) + + expectedPath := filepath.Join(outputDir, "myapp_darwin_arm64.tar.xz") + assert.Equal(t, expectedPath, result.Path) + assert.FileExists(t, result.Path) + + verifyTarXzContent(t, result.Path, "myapp") + }) + + t.Run("windows still uses zip even with xz format", func(t *testing.T) { + binaryPath, outputDir := setupArchiveTestFile(t, "myapp.exe", "windows", "amd64") + + artifact := Artifact{ + Path: binaryPath, + OS: "windows", + Arch: "amd64", + } + + result, err := ArchiveWithFormat(artifact, ArchiveFormatXZ) + require.NoError(t, err) + + // Windows should still get .zip regardless of format + expectedPath := filepath.Join(outputDir, "myapp_windows_amd64.zip") + assert.Equal(t, expectedPath, result.Path) + assert.FileExists(t, result.Path) + + verifyZipContent(t, result.Path, "myapp.exe") + }) } func TestArchive_Bad(t *testing.T) { @@ -306,3 +366,27 @@ func verifyZipContent(t *testing.T, archivePath, expectedName string) { require.Len(t, reader.File, 1) assert.Equal(t, expectedName, reader.File[0].Name) } + +// verifyTarXzContent opens a tar.xz file and verifies it contains the expected file. +func verifyTarXzContent(t *testing.T, archivePath, expectedName string) { + t.Helper() + + // Read the xz-compressed file + xzData, err := os.ReadFile(archivePath) + require.NoError(t, err) + + // Decompress with Borg + tarData, err := compress.Decompress(xzData) + require.NoError(t, err) + + // Read tar archive + tarReader := tar.NewReader(bytes.NewReader(tarData)) + + header, err := tarReader.Next() + require.NoError(t, err) + assert.Equal(t, expectedName, header.Name) + + // Verify there's only one file + _, err = tarReader.Next() + assert.Equal(t, io.EOF, err) +} diff --git a/pkg/build/buildcmd/cmd_build.go b/pkg/build/buildcmd/cmd_build.go index e9bb65a..b391b37 100644 --- a/pkg/build/buildcmd/cmd_build.go +++ b/pkg/build/buildcmd/cmd_build.go @@ -63,7 +63,7 @@ var buildCmd = &cobra.Command{ Short: i18n.T("cmd.build.short"), Long: i18n.T("cmd.build.long"), RunE: func(cmd *cobra.Command, args []string) error { - return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize, verbose) + return runProjectBuild(cmd.Context(), buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize, verbose) }, } @@ -139,5 +139,6 @@ func initBuildFlags() { // AddBuildCommands registers the 'build' command and all subcommands. func AddBuildCommands(root *cobra.Command) { initBuildFlags() + AddReleaseCommand(buildCmd) root.AddCommand(buildCmd) } diff --git a/pkg/build/buildcmd/cmd_project.go b/pkg/build/buildcmd/cmd_project.go index da9abbd..3429b76 100644 --- a/pkg/build/buildcmd/cmd_project.go +++ b/pkg/build/buildcmd/cmd_project.go @@ -21,7 +21,7 @@ import ( ) // runProjectBuild handles the main `core build` command with auto-detection. -func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDir string, doArchive bool, doChecksum bool, configPath string, format string, push bool, imageName string, noSign bool, notarize bool, verbose bool) error { +func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targetsFlag string, outputDir string, doArchive bool, doChecksum bool, configPath string, format string, push bool, imageName string, noSign bool, notarize bool, verbose bool) error { // Get current working directory as project root projectDir, err := os.Getwd() if err != nil { @@ -116,7 +116,6 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Execute build - ctx := context.Background() artifacts, err := builder.Build(ctx, cfg, buildTargets) if err != nil { if !ciMode { diff --git a/pkg/build/buildcmd/cmd_release.go b/pkg/build/buildcmd/cmd_release.go new file mode 100644 index 0000000..8c40a98 --- /dev/null +++ b/pkg/build/buildcmd/cmd_release.go @@ -0,0 +1,111 @@ +// cmd_release.go implements the release command: build + archive + publish in one step. + +package buildcmd + +import ( + "context" + "os" + + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/framework/core" + "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/release" +) + +// Flag variables for release command +var ( + releaseVersion string + releaseDraft bool + releasePrerelease bool + releaseGoForLaunch bool +) + +var releaseCmd = &cli.Command{ + Use: "release", + Short: i18n.T("cmd.build.release.short"), + Long: i18n.T("cmd.build.release.long"), + RunE: func(cmd *cli.Command, args []string) error { + return runRelease(cmd.Context(), !releaseGoForLaunch, releaseVersion, releaseDraft, releasePrerelease) + }, +} + +func init() { + releaseCmd.Flags().BoolVar(&releaseGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.build.release.flag.go_for_launch")) + releaseCmd.Flags().StringVar(&releaseVersion, "version", "", i18n.T("cmd.build.release.flag.version")) + releaseCmd.Flags().BoolVar(&releaseDraft, "draft", false, i18n.T("cmd.build.release.flag.draft")) + releaseCmd.Flags().BoolVar(&releasePrerelease, "prerelease", false, i18n.T("cmd.build.release.flag.prerelease")) +} + +// AddReleaseCommand adds the release subcommand to the build command. +func AddReleaseCommand(buildCmd *cli.Command) { + buildCmd.AddCommand(releaseCmd) +} + +// runRelease executes the full release workflow: build + archive + checksum + publish. +func runRelease(ctx context.Context, dryRun bool, version string, draft, prerelease bool) error { + // Get current directory + projectDir, err := os.Getwd() + if err != nil { + return core.E("release", "get working directory", err) + } + + // Check for release config + if !release.ConfigExists(projectDir) { + cli.Print("%s %s\n", + buildErrorStyle.Render(i18n.Label("error")), + i18n.T("cmd.build.release.error.no_config"), + ) + cli.Print(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.release.hint.create_config"))) + return core.E("release", "config not found", nil) + } + + // Load configuration + cfg, err := release.LoadConfig(projectDir) + if err != nil { + return core.E("release", "load config", err) + } + + // 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", buildHeaderStyle.Render(i18n.T("cmd.build.release.label.release")), i18n.T("cmd.build.release.building_and_publishing")) + if dryRun { + cli.Print(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.release.dry_run_hint"))) + } + cli.Blank() + + // Run full release (build + archive + checksum + publish) + rel, err := release.Run(ctx, cfg, dryRun) + if err != nil { + return err + } + + // Print summary + cli.Blank() + cli.Print("%s %s\n", buildSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.build.release.completed")) + cli.Print(" %s %s\n", i18n.Label("version"), buildTargetStyle.Render(rel.Version)) + cli.Print(" %s %d\n", i18n.T("cmd.build.release.label.artifacts"), len(rel.Artifacts)) + + if !dryRun { + for _, pub := range cfg.Publishers { + cli.Print(" %s %s\n", i18n.T("cmd.build.release.label.published"), buildTargetStyle.Render(pub.Type)) + } + } + + return nil +} diff --git a/pkg/build/config.go b/pkg/build/config.go index 7ff68c8..3a396a0 100644 --- a/pkg/build/config.go +++ b/pkg/build/config.go @@ -108,7 +108,6 @@ func DefaultConfig() *BuildConfig { Targets: []TargetConfig{ {OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, - {OS: "darwin", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}, }, diff --git a/pkg/build/config_test.go b/pkg/build/config_test.go index 2723ce7..f23359c 100644 --- a/pkg/build/config_test.go +++ b/pkg/build/config_test.go @@ -188,7 +188,7 @@ func TestDefaultConfig_Good(t *testing.T) { assert.Empty(t, cfg.Build.Env) // Default targets cover common platforms - assert.Len(t, cfg.Targets, 5) + assert.Len(t, cfg.Targets, 4) hasLinuxAmd64 := false hasDarwinArm64 := false hasWindowsAmd64 := false diff --git a/pkg/deploy/coolify/client.go b/pkg/deploy/coolify/client.go new file mode 100644 index 0000000..35ab8a5 --- /dev/null +++ b/pkg/deploy/coolify/client.go @@ -0,0 +1,219 @@ +package coolify + +import ( + "context" + "encoding/json" + "fmt" + "os" + "sync" + + "github.com/host-uk/core/pkg/deploy/python" +) + +// Client wraps the Python CoolifyClient for Go usage. +type Client struct { + baseURL string + apiToken string + timeout int + verifySSL bool + + mu sync.Mutex +} + +// Config holds Coolify client configuration. +type Config struct { + BaseURL string + APIToken string + Timeout int + VerifySSL bool +} + +// DefaultConfig returns default configuration from environment. +func DefaultConfig() Config { + return Config{ + BaseURL: os.Getenv("COOLIFY_URL"), + APIToken: os.Getenv("COOLIFY_TOKEN"), + Timeout: 30, + VerifySSL: true, + } +} + +// NewClient creates a new Coolify client. +func NewClient(cfg Config) (*Client, error) { + if cfg.BaseURL == "" { + return nil, fmt.Errorf("COOLIFY_URL not set") + } + if cfg.APIToken == "" { + return nil, fmt.Errorf("COOLIFY_TOKEN not set") + } + + // Initialize Python runtime + if err := python.Init(); err != nil { + return nil, fmt.Errorf("failed to initialize Python: %w", err) + } + + return &Client{ + baseURL: cfg.BaseURL, + apiToken: cfg.APIToken, + timeout: cfg.Timeout, + verifySSL: cfg.VerifySSL, + }, nil +} + +// Call invokes a Coolify API operation by operationId. +func (c *Client) Call(ctx context.Context, operationID string, params map[string]any) (map[string]any, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if params == nil { + params = map[string]any{} + } + + // Generate and run Python script + script, err := python.CoolifyScript(c.baseURL, c.apiToken, operationID, params) + if err != nil { + return nil, fmt.Errorf("failed to generate script: %w", err) + } + output, err := python.RunScript(ctx, script) + if err != nil { + return nil, fmt.Errorf("API call %s failed: %w", operationID, err) + } + + // Parse JSON result + var result map[string]any + if err := json.Unmarshal([]byte(output), &result); err != nil { + // Try parsing as array + var arrResult []any + if err2 := json.Unmarshal([]byte(output), &arrResult); err2 == nil { + return map[string]any{"result": arrResult}, nil + } + return nil, fmt.Errorf("failed to parse response: %w (output: %s)", err, output) + } + + return result, nil +} + +// ListServers returns all servers. +func (c *Client) ListServers(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "list-servers", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetServer returns a server by UUID. +func (c *Client) GetServer(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "get-server-by-uuid", map[string]any{"uuid": uuid}) +} + +// ValidateServer validates a server by UUID. +func (c *Client) ValidateServer(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "validate-server-by-uuid", map[string]any{"uuid": uuid}) +} + +// ListProjects returns all projects. +func (c *Client) ListProjects(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "list-projects", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetProject returns a project by UUID. +func (c *Client) GetProject(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "get-project-by-uuid", map[string]any{"uuid": uuid}) +} + +// CreateProject creates a new project. +func (c *Client) CreateProject(ctx context.Context, name, description string) (map[string]any, error) { + return c.Call(ctx, "create-project", map[string]any{ + "name": name, + "description": description, + }) +} + +// ListApplications returns all applications. +func (c *Client) ListApplications(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "list-applications", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetApplication returns an application by UUID. +func (c *Client) GetApplication(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "get-application-by-uuid", map[string]any{"uuid": uuid}) +} + +// DeployApplication triggers deployment of an application. +func (c *Client) DeployApplication(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "deploy-by-tag-or-uuid", map[string]any{"uuid": uuid}) +} + +// ListDatabases returns all databases. +func (c *Client) ListDatabases(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "list-databases", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetDatabase returns a database by UUID. +func (c *Client) GetDatabase(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "get-database-by-uuid", map[string]any{"uuid": uuid}) +} + +// ListServices returns all services. +func (c *Client) ListServices(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "list-services", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetService returns a service by UUID. +func (c *Client) GetService(ctx context.Context, uuid string) (map[string]any, error) { + return c.Call(ctx, "get-service-by-uuid", map[string]any{"uuid": uuid}) +} + +// ListEnvironments returns environments for a project. +func (c *Client) ListEnvironments(ctx context.Context, projectUUID string) ([]map[string]any, error) { + result, err := c.Call(ctx, "get-environments", map[string]any{"project_uuid": projectUUID}) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// GetTeam returns the current team. +func (c *Client) GetTeam(ctx context.Context) (map[string]any, error) { + return c.Call(ctx, "get-current-team", nil) +} + +// GetTeamMembers returns members of the current team. +func (c *Client) GetTeamMembers(ctx context.Context) ([]map[string]any, error) { + result, err := c.Call(ctx, "get-current-team-members", nil) + if err != nil { + return nil, err + } + return extractArray(result) +} + +// extractArray extracts an array from result["result"] or returns empty. +func extractArray(result map[string]any) ([]map[string]any, error) { + if arr, ok := result["result"].([]any); ok { + items := make([]map[string]any, 0, len(arr)) + for _, item := range arr { + if m, ok := item.(map[string]any); ok { + items = append(items, m) + } + } + return items, nil + } + return nil, nil +} diff --git a/pkg/deploy/python/python.go b/pkg/deploy/python/python.go new file mode 100644 index 0000000..b96bef5 --- /dev/null +++ b/pkg/deploy/python/python.go @@ -0,0 +1,147 @@ +package python + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "sync" + + "github.com/host-uk/core/pkg/framework/core" + "github.com/kluctl/go-embed-python/python" +) + +var ( + once sync.Once + ep *python.EmbeddedPython + initErr error +) + +// Init initializes the embedded Python runtime. +func Init() error { + once.Do(func() { + ep, initErr = python.NewEmbeddedPython("core-deploy") + }) + return initErr +} + +// GetPython returns the embedded Python instance. +func GetPython() *python.EmbeddedPython { + return ep +} + +// RunScript runs a Python script with the given code and returns stdout. +func RunScript(ctx context.Context, code string, args ...string) (string, error) { + if err := Init(); err != nil { + return "", err + } + + // Write code to temp file + tmpFile, err := os.CreateTemp("", "core-*.py") + if err != nil { + return "", core.E("python", "create temp file", err) + } + defer func() { _ = os.Remove(tmpFile.Name()) }() + + if _, err := tmpFile.WriteString(code); err != nil { + _ = tmpFile.Close() + return "", core.E("python", "write script", err) + } + _ = tmpFile.Close() + + // Build args: script path + any additional args + cmdArgs := append([]string{tmpFile.Name()}, args...) + + // Get the command + cmd, err := ep.PythonCmd(cmdArgs...) + if err != nil { + return "", core.E("python", "create command", err) + } + + // Run with context + output, err := cmd.Output() + if err != nil { + // Try to get stderr for better error message + if exitErr, ok := err.(*exec.ExitError); ok { + return "", core.E("python", "run script", fmt.Errorf("%w: %s", err, string(exitErr.Stderr))) + } + return "", core.E("python", "run script", err) + } + + return string(output), nil +} + +// RunModule runs a Python module (python -m module_name). +func RunModule(ctx context.Context, module string, args ...string) (string, error) { + if err := Init(); err != nil { + return "", err + } + + cmdArgs := append([]string{"-m", module}, args...) + cmd, err := ep.PythonCmd(cmdArgs...) + if err != nil { + return "", core.E("python", "create command", err) + } + + output, err := cmd.Output() + if err != nil { + return "", core.E("python", fmt.Sprintf("run module %s", module), err) + } + + return string(output), nil +} + +// DevOpsPath returns the path to the DevOps repo. +func DevOpsPath() (string, error) { + if path := os.Getenv("DEVOPS_PATH"); path != "" { + return path, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", core.E("python", "get user home", err) + } + return filepath.Join(home, "Code", "DevOps"), nil +} + +// CoolifyModulePath returns the path to the Coolify module_utils. +func CoolifyModulePath() (string, error) { + path, err := DevOpsPath() + if err != nil { + return "", err + } + return filepath.Join(path, "playbooks", "roles", "coolify", "module_utils"), nil +} + +// CoolifyScript generates Python code to call the Coolify API. +func CoolifyScript(baseURL, apiToken, operation string, params map[string]any) (string, error) { + paramsJSON, err := json.Marshal(params) + if err != nil { + return "", core.E("python", "marshal params", err) + } + + modulePath, err := CoolifyModulePath() + if err != nil { + return "", err + } + + return fmt.Sprintf(` +import sys +import json +sys.path.insert(0, %q) + +from swagger.coolify_api import CoolifyClient + +client = CoolifyClient( + base_url=%q, + api_token=%q, + timeout=30, + verify_ssl=True, +) + +params = json.loads(%q) +result = client._call(%q, params, check_response=False) +print(json.dumps(result)) +`, modulePath, baseURL, apiToken, string(paramsJSON), operation), nil +} diff --git a/pkg/i18n/locales/en_GB.json b/pkg/i18n/locales/en_GB.json index f328acb..4f6d8f4 100644 --- a/pkg/i18n/locales/en_GB.json +++ b/pkg/i18n/locales/en_GB.json @@ -135,7 +135,7 @@ "cmd": { "ai": { "short": "AI agent task management", - "long": "AI agent task management for core-agentic integration. Provides commands to list, claim, update, and complete tasks from the agentic task queue.", + "long": "AI agent task management for core-agentic integration. Provides commands to list, claim, update, and complete tasks from the agentic task queue. Includes RAG tools and metrics.", "claude.short": "Claude Code integration", "claude.long": "Claude Code integration for AI-assisted development workflows.", "claude.config.short": "Configure Claude Code settings", @@ -153,7 +153,11 @@ "task_complete.short": "Mark a task as completed", "task_pr.short": "Create a pull request for a task", "task_pr.branch_error": "cannot create PR from {{.Branch}} branch; create a feature branch first", - "task_update.short": "Update task status or progress" + "task_update.short": "Update task status or progress", + "metrics.short": "View AI and security event metrics", + "metrics.long": "View collected metrics from AI tasks, security scans, and job creation events. Reads JSONL event logs from ~/.core/ai/metrics/.", + "metrics.flag.since": "Time period to show (e.g. 7d, 24h, 30d)", + "metrics.none_found": "No events recorded in this period." }, "build": { "short": "Build projects with auto-detection and cross-compilation", @@ -226,7 +230,21 @@ "sdk.languages_label": "Languages:", "sdk.would_generate": "Would generate SDK", "sdk.generated_label": "Generated:", - "sdk.complete": "SDK generation complete" + "sdk.complete": "SDK generation complete", + "release.short": "Build, archive, and publish a release", + "release.long": "Build all targets, create archives, generate checksums, and publish to configured destinations. Requires .core/release.yaml configuration.", + "release.flag.go_for_launch": "Actually publish to configured targets (default: dry-run only)", + "release.flag.version": "Version to release (overrides config)", + "release.flag.draft": "Create as draft release", + "release.flag.prerelease": "Mark as pre-release", + "release.label.release": "Release", + "release.building_and_publishing": "Building and publishing release", + "release.dry_run_hint": "(dry-run) no artifacts will be published", + "release.completed": "Release completed", + "release.label.artifacts": "Artifacts:", + "release.label.published": "Published to:", + "release.error.no_config": "No .core/release.yaml found", + "release.hint.create_config": "Create .core/release.yaml to configure release settings" }, "ci": { "short": "Publish releases (dry-run by default)", @@ -241,6 +259,10 @@ "changelog.short": "Generate changelog", "version.short": "Show or set version" }, + "deploy": { + "short": "Infrastructure deployment via Coolify", + "long": "Infrastructure deployment tools for managing Coolify servers, projects, applications, databases, and services." + }, "dev": { "short": "Multi-repo development workflow", "long": "Multi-repo development workflow tools for managing federated monorepos. Provides health checks, commit assistance, push/pull operations, and CI status across all repositories.", @@ -406,9 +428,33 @@ "qa.flag.full": "Run all stages including slow checks", "build.short": "Build Docker or LinuxKit image", "deploy.short": "Deploy to Coolify", + "deploy.long": "Deploy the PHP application to Coolify", + "deploy.deploying": "Deploying to {{.Environment}}", + "deploy.warning_status": "Deployment finished with status: {{.Status}}", + "deploy.triggered": "Deployment triggered successfully", + "deploy.flag.staging": "Deploy to staging environment", + "deploy.flag.force": "Force deployment even if no changes detected", + "deploy.flag.wait": "Wait for deployment to complete", "deploy_list.short": "List deployments", + "deploy_list.long": "List recent deployments", + "deploy_list.recent": "Recent deployments for {{.Environment}}", + "deploy_list.none_found": "No deployments found", + "deploy_list.flag.staging": "List staging deployments", + "deploy_list.flag.limit": "Number of deployments to list", "deploy_rollback.short": "Rollback to previous deployment", + "deploy_rollback.long": "Rollback to a previous deployment", + "deploy_rollback.rolling_back": "Rolling back {{.Environment}}", + "deploy_rollback.warning_status": "Rollback finished with status: {{.Status}}", + "deploy_rollback.triggered": "Rollback triggered successfully", + "deploy_rollback.flag.staging": "Rollback staging environment", + "deploy_rollback.flag.id": "Specific deployment ID to rollback to", + "deploy_rollback.flag.wait": "Wait for rollback to complete", "deploy_status.short": "Show deployment status", + "deploy_status.long": "Show the status of a deployment", + "deploy_status.flag.staging": "Check staging deployment", + "deploy_status.flag.id": "Specific deployment ID", + "label.deploy": "Deploy", + "error.deploy_failed": "Deployment failed", "serve.short": "Run production container", "ssl.short": "Setup SSL certificates with mkcert", "packages.short": "Manage local PHP packages", @@ -494,7 +540,14 @@ "secrets.short": "List exposed secrets", "secrets.long": "List secrets detected by GitHub secret scanning.", "flag.repo": "Specific repo to check", - "flag.severity": "Filter by severity (critical,high,medium,low)" + "flag.severity": "Filter by severity (critical,high,medium,low)", + "flag.target": "External repo to scan (e.g. wailsapp/wails)", + "jobs.short": "Create GitHub issues from scan results", + "jobs.long": "Create GitHub issues from security scan results so contributors can claim and work on them. Supports targeting external repositories.", + "jobs.flag.targets": "Target repos to scan (owner/repo format)", + "jobs.flag.issue_repo": "Repository to create issues in", + "jobs.flag.dry_run": "Show what would be created without creating issues", + "jobs.flag.copies": "Number of duplicate issues for parallel work" }, "qa": { "short": "Quality assurance workflows", @@ -594,6 +647,33 @@ "no_findings": "No security findings", "error.no_repos": "No repositories to scan. Use --repo, --all, or run from a git repo", "error.not_git_repo": "Not in a git repository. Use --repo to specify one" + }, + "rag": { + "short": "RAG (Retrieval Augmented Generation) tools", + "long": "RAG tools for storing documentation in Qdrant vector database and querying with semantic search. Eliminates need to repeatedly remind Claude about project specifics.", + "flag.qdrant_host": "Qdrant server hostname", + "flag.qdrant_port": "Qdrant gRPC port", + "flag.ollama_host": "Ollama server hostname", + "flag.ollama_port": "Ollama server port", + "flag.model": "Embedding model name", + "ingest.short": "Ingest markdown files into Qdrant", + "ingest.long": "Ingest markdown files from a directory into Qdrant vector database. Chunks files, generates embeddings via Ollama, and stores for semantic search.", + "ingest.flag.collection": "Qdrant collection name", + "ingest.flag.recreate": "Delete and recreate collection", + "ingest.flag.chunk_size": "Characters per chunk", + "ingest.flag.chunk_overlap": "Overlap between chunks", + "query.short": "Query the vector database", + "query.long": "Search for similar documents using semantic similarity. Returns relevant chunks ranked by score.", + "query.flag.collection": "Qdrant collection name", + "query.flag.top": "Number of results to return", + "query.flag.threshold": "Minimum similarity score (0-1)", + "query.flag.category": "Filter by category", + "query.flag.format": "Output format (text, json, context)", + "collections.short": "List and manage collections", + "collections.long": "List available collections, show statistics, or delete collections from Qdrant.", + "collections.flag.list": "List all collections", + "collections.flag.stats": "Show collection statistics", + "collections.flag.delete": "Delete a collection" } }, "common": { diff --git a/pkg/rag/chunk.go b/pkg/rag/chunk.go new file mode 100644 index 0000000..fbcc3c9 --- /dev/null +++ b/pkg/rag/chunk.go @@ -0,0 +1,204 @@ +package rag + +import ( + "crypto/md5" + "fmt" + "path/filepath" + "slices" + "strings" +) + +// ChunkConfig holds chunking configuration. +type ChunkConfig struct { + Size int // Characters per chunk + Overlap int // Overlap between chunks +} + +// DefaultChunkConfig returns default chunking configuration. +func DefaultChunkConfig() ChunkConfig { + return ChunkConfig{ + Size: 500, + Overlap: 50, + } +} + +// Chunk represents a text chunk with metadata. +type Chunk struct { + Text string + Section string + Index int +} + +// ChunkMarkdown splits markdown text into chunks by sections and paragraphs. +// Preserves context with configurable overlap. +func ChunkMarkdown(text string, cfg ChunkConfig) []Chunk { + if cfg.Size <= 0 { + cfg.Size = 500 + } + if cfg.Overlap < 0 || cfg.Overlap >= cfg.Size { + cfg.Overlap = 0 + } + + var chunks []Chunk + + // Split by ## headers + sections := splitBySections(text) + + chunkIndex := 0 + for _, section := range sections { + section = strings.TrimSpace(section) + if section == "" { + continue + } + + // Extract section title + lines := strings.SplitN(section, "\n", 2) + title := "" + if strings.HasPrefix(lines[0], "#") { + title = strings.TrimLeft(lines[0], "#") + title = strings.TrimSpace(title) + } + + // If section is small enough, yield as-is + if len(section) <= cfg.Size { + chunks = append(chunks, Chunk{ + Text: section, + Section: title, + Index: chunkIndex, + }) + chunkIndex++ + continue + } + + // Otherwise, chunk by paragraphs + paragraphs := splitByParagraphs(section) + currentChunk := "" + + for _, para := range paragraphs { + para = strings.TrimSpace(para) + if para == "" { + continue + } + + if len(currentChunk)+len(para)+2 <= cfg.Size { + if currentChunk != "" { + currentChunk += "\n\n" + para + } else { + currentChunk = para + } + } else { + if currentChunk != "" { + chunks = append(chunks, Chunk{ + Text: strings.TrimSpace(currentChunk), + Section: title, + Index: chunkIndex, + }) + chunkIndex++ + } + // Start new chunk with overlap from previous (rune-safe for UTF-8) + runes := []rune(currentChunk) + if cfg.Overlap > 0 && len(runes) > cfg.Overlap { + overlapText := string(runes[len(runes)-cfg.Overlap:]) + currentChunk = overlapText + "\n\n" + para + } else { + currentChunk = para + } + } + } + + // Don't forget the last chunk + if strings.TrimSpace(currentChunk) != "" { + chunks = append(chunks, Chunk{ + Text: strings.TrimSpace(currentChunk), + Section: title, + Index: chunkIndex, + }) + chunkIndex++ + } + } + + return chunks +} + +// splitBySections splits text by ## headers while preserving the header with its content. +func splitBySections(text string) []string { + var sections []string + lines := strings.Split(text, "\n") + + var currentSection strings.Builder + for _, line := range lines { + // Check if this line is a ## header + if strings.HasPrefix(line, "## ") { + // Save previous section if exists + if currentSection.Len() > 0 { + sections = append(sections, currentSection.String()) + currentSection.Reset() + } + } + currentSection.WriteString(line) + currentSection.WriteString("\n") + } + + // Don't forget the last section + if currentSection.Len() > 0 { + sections = append(sections, currentSection.String()) + } + + return sections +} + +// splitByParagraphs splits text by double newlines. +func splitByParagraphs(text string) []string { + // Replace multiple newlines with a marker, then split + normalized := text + for strings.Contains(normalized, "\n\n\n") { + normalized = strings.ReplaceAll(normalized, "\n\n\n", "\n\n") + } + return strings.Split(normalized, "\n\n") +} + +// Category determines the document category from file path. +func Category(path string) string { + lower := strings.ToLower(path) + + switch { + case strings.Contains(lower, "flux") || strings.Contains(lower, "ui/component"): + return "ui-component" + case strings.Contains(lower, "brand") || strings.Contains(lower, "mascot"): + return "brand" + case strings.Contains(lower, "brief"): + return "product-brief" + case strings.Contains(lower, "help") || strings.Contains(lower, "draft"): + return "help-doc" + case strings.Contains(lower, "task") || strings.Contains(lower, "plan"): + return "task" + case strings.Contains(lower, "architecture") || strings.Contains(lower, "migration"): + return "architecture" + default: + return "documentation" + } +} + +// ChunkID generates a unique ID for a chunk. +func ChunkID(path string, index int, text string) string { + // Use first 100 runes of text for uniqueness (rune-safe for UTF-8) + runes := []rune(text) + if len(runes) > 100 { + runes = runes[:100] + } + textPart := string(runes) + data := fmt.Sprintf("%s:%d:%s", path, index, textPart) + hash := md5.Sum([]byte(data)) + return fmt.Sprintf("%x", hash) +} + +// FileExtensions returns the file extensions to process. +func FileExtensions() []string { + return []string{".md", ".markdown", ".txt"} +} + +// ShouldProcess checks if a file should be processed based on extension. +func ShouldProcess(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + return slices.Contains(FileExtensions(), ext) +} diff --git a/pkg/rag/chunk_test.go b/pkg/rag/chunk_test.go new file mode 100644 index 0000000..87fd5c0 --- /dev/null +++ b/pkg/rag/chunk_test.go @@ -0,0 +1,120 @@ +package rag + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChunkMarkdown_Good_SmallSection(t *testing.T) { + text := `# Title + +This is a small section that fits in one chunk. +` + chunks := ChunkMarkdown(text, DefaultChunkConfig()) + + assert.Len(t, chunks, 1) + assert.Contains(t, chunks[0].Text, "small section") +} + +func TestChunkMarkdown_Good_MultipleSections(t *testing.T) { + text := `# Main Title + +Introduction paragraph. + +## Section One + +Content for section one. + +## Section Two + +Content for section two. +` + chunks := ChunkMarkdown(text, DefaultChunkConfig()) + + assert.GreaterOrEqual(t, len(chunks), 2) +} + +func TestChunkMarkdown_Good_LargeSection(t *testing.T) { + // Create a section larger than chunk size + text := `## Large Section + +` + repeatString("This is a test paragraph with some content. ", 50) + + cfg := ChunkConfig{Size: 200, Overlap: 20} + chunks := ChunkMarkdown(text, cfg) + + assert.Greater(t, len(chunks), 1) + for _, chunk := range chunks { + assert.NotEmpty(t, chunk.Text) + assert.Equal(t, "Large Section", chunk.Section) + } +} + +func TestChunkMarkdown_Good_ExtractsTitle(t *testing.T) { + text := `## My Section Title + +Some content here. +` + chunks := ChunkMarkdown(text, DefaultChunkConfig()) + + assert.Len(t, chunks, 1) + assert.Equal(t, "My Section Title", chunks[0].Section) +} + +func TestCategory_Good_UIComponent(t *testing.T) { + tests := []struct { + path string + expected string + }{ + {"docs/flux/button.md", "ui-component"}, + {"ui/components/modal.md", "ui-component"}, + {"brand/vi-personality.md", "brand"}, + {"mascot/expressions.md", "brand"}, + {"product-brief.md", "product-brief"}, + {"tasks/2024-01-15-feature.md", "task"}, + {"plans/architecture.md", "task"}, + {"architecture/migration.md", "architecture"}, + {"docs/api.md", "documentation"}, + } + + for _, tc := range tests { + t.Run(tc.path, func(t *testing.T) { + assert.Equal(t, tc.expected, Category(tc.path)) + }) + } +} + +func TestChunkID_Good_Deterministic(t *testing.T) { + id1 := ChunkID("test.md", 0, "hello world") + id2 := ChunkID("test.md", 0, "hello world") + + assert.Equal(t, id1, id2) +} + +func TestChunkID_Good_DifferentForDifferentInputs(t *testing.T) { + id1 := ChunkID("test.md", 0, "hello world") + id2 := ChunkID("test.md", 1, "hello world") + id3 := ChunkID("other.md", 0, "hello world") + + assert.NotEqual(t, id1, id2) + assert.NotEqual(t, id1, id3) +} + +func TestShouldProcess_Good_MarkdownFiles(t *testing.T) { + assert.True(t, ShouldProcess("doc.md")) + assert.True(t, ShouldProcess("doc.markdown")) + assert.True(t, ShouldProcess("doc.txt")) + assert.False(t, ShouldProcess("doc.go")) + assert.False(t, ShouldProcess("doc.py")) + assert.False(t, ShouldProcess("doc")) +} + +// Helper function +func repeatString(s string, n int) string { + result := "" + for i := 0; i < n; i++ { + result += s + } + return result +} diff --git a/pkg/rag/ingest.go b/pkg/rag/ingest.go new file mode 100644 index 0000000..33b10b1 --- /dev/null +++ b/pkg/rag/ingest.go @@ -0,0 +1,216 @@ +package rag + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/host-uk/core/pkg/log" +) + +// IngestConfig holds ingestion configuration. +type IngestConfig struct { + Directory string + Collection string + Recreate bool + Verbose bool + BatchSize int + Chunk ChunkConfig +} + +// DefaultIngestConfig returns default ingestion configuration. +func DefaultIngestConfig() IngestConfig { + return IngestConfig{ + Collection: "hostuk-docs", + BatchSize: 100, + Chunk: DefaultChunkConfig(), + } +} + +// IngestStats holds statistics from ingestion. +type IngestStats struct { + Files int + Chunks int + Errors int +} + +// IngestProgress is called during ingestion to report progress. +type IngestProgress func(file string, chunks int, total int) + +// Ingest processes a directory of documents and stores them in Qdrant. +func Ingest(ctx context.Context, qdrant *QdrantClient, ollama *OllamaClient, cfg IngestConfig, progress IngestProgress) (*IngestStats, error) { + stats := &IngestStats{} + + // Validate batch size to prevent infinite loop + if cfg.BatchSize <= 0 { + cfg.BatchSize = 100 // Safe default + } + + // Resolve directory + absDir, err := filepath.Abs(cfg.Directory) + if err != nil { + return nil, log.E("rag.Ingest", "error resolving directory", err) + } + + info, err := os.Stat(absDir) + if err != nil { + return nil, log.E("rag.Ingest", "error accessing directory", err) + } + if !info.IsDir() { + return nil, log.E("rag.Ingest", fmt.Sprintf("not a directory: %s", absDir), nil) + } + + // Check/create collection + exists, err := qdrant.CollectionExists(ctx, cfg.Collection) + if err != nil { + return nil, log.E("rag.Ingest", "error checking collection", err) + } + + if cfg.Recreate && exists { + if err := qdrant.DeleteCollection(ctx, cfg.Collection); err != nil { + return nil, log.E("rag.Ingest", "error deleting collection", err) + } + exists = false + } + + if !exists { + vectorDim := ollama.EmbedDimension() + if err := qdrant.CreateCollection(ctx, cfg.Collection, vectorDim); err != nil { + return nil, log.E("rag.Ingest", "error creating collection", err) + } + } + + // Find markdown files + var files []string + err = filepath.WalkDir(absDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && ShouldProcess(path) { + files = append(files, path) + } + return nil + }) + if err != nil { + return nil, log.E("rag.Ingest", "error walking directory", err) + } + + if len(files) == 0 { + return nil, log.E("rag.Ingest", fmt.Sprintf("no markdown files found in %s", absDir), nil) + } + + // Process files + var points []Point + for _, filePath := range files { + relPath, err := filepath.Rel(absDir, filePath) + if err != nil { + stats.Errors++ + continue + } + + content, err := os.ReadFile(filePath) + if err != nil { + stats.Errors++ + continue + } + + if len(strings.TrimSpace(string(content))) == 0 { + continue + } + + // Chunk the content + category := Category(relPath) + chunks := ChunkMarkdown(string(content), cfg.Chunk) + + for _, chunk := range chunks { + // Generate embedding + embedding, err := ollama.Embed(ctx, chunk.Text) + if err != nil { + stats.Errors++ + if cfg.Verbose { + fmt.Printf(" Error embedding %s chunk %d: %v\n", relPath, chunk.Index, err) + } + continue + } + + // Create point + points = append(points, Point{ + ID: ChunkID(relPath, chunk.Index, chunk.Text), + Vector: embedding, + Payload: map[string]any{ + "text": chunk.Text, + "source": relPath, + "section": chunk.Section, + "category": category, + "chunk_index": chunk.Index, + }, + }) + stats.Chunks++ + } + + stats.Files++ + if progress != nil { + progress(relPath, stats.Chunks, len(files)) + } + } + + // Batch upsert to Qdrant + if len(points) > 0 { + for i := 0; i < len(points); i += cfg.BatchSize { + end := i + cfg.BatchSize + if end > len(points) { + end = len(points) + } + batch := points[i:end] + if err := qdrant.UpsertPoints(ctx, cfg.Collection, batch); err != nil { + return stats, log.E("rag.Ingest", fmt.Sprintf("error upserting batch %d", i/cfg.BatchSize+1), err) + } + } + } + + return stats, nil +} + +// IngestFile processes a single file and stores it in Qdrant. +func IngestFile(ctx context.Context, qdrant *QdrantClient, ollama *OllamaClient, collection string, filePath string, chunkCfg ChunkConfig) (int, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return 0, log.E("rag.IngestFile", "error reading file", err) + } + + if len(strings.TrimSpace(string(content))) == 0 { + return 0, nil + } + + category := Category(filePath) + chunks := ChunkMarkdown(string(content), chunkCfg) + + var points []Point + for _, chunk := range chunks { + embedding, err := ollama.Embed(ctx, chunk.Text) + if err != nil { + return 0, log.E("rag.IngestFile", fmt.Sprintf("error embedding chunk %d", chunk.Index), err) + } + + points = append(points, Point{ + ID: ChunkID(filePath, chunk.Index, chunk.Text), + Vector: embedding, + Payload: map[string]any{ + "text": chunk.Text, + "source": filePath, + "section": chunk.Section, + "category": category, + "chunk_index": chunk.Index, + }, + }) + } + + if err := qdrant.UpsertPoints(ctx, collection, points); err != nil { + return 0, log.E("rag.IngestFile", "error upserting points", err) + } + + return len(points), nil +} \ No newline at end of file diff --git a/pkg/rag/ollama.go b/pkg/rag/ollama.go new file mode 100644 index 0000000..c68ca14 --- /dev/null +++ b/pkg/rag/ollama.go @@ -0,0 +1,120 @@ +package rag + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/host-uk/core/pkg/log" + "github.com/ollama/ollama/api" +) + +// OllamaConfig holds Ollama connection configuration. +type OllamaConfig struct { + Host string + Port int + Model string +} + +// DefaultOllamaConfig returns default Ollama configuration. +// Host defaults to localhost for local development. +func DefaultOllamaConfig() OllamaConfig { + return OllamaConfig{ + Host: "localhost", + Port: 11434, + Model: "nomic-embed-text", + } +} + +// OllamaClient wraps the Ollama API client for embeddings. +type OllamaClient struct { + client *api.Client + config OllamaConfig +} + +// NewOllamaClient creates a new Ollama client. +func NewOllamaClient(cfg OllamaConfig) (*OllamaClient, error) { + baseURL := &url.URL{ + Scheme: "http", + Host: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), + } + + client := api.NewClient(baseURL, &http.Client{ + Timeout: 30 * time.Second, + }) + + return &OllamaClient{ + client: client, + config: cfg, + }, nil +} + +// EmbedDimension returns the embedding dimension for the configured model. +// nomic-embed-text uses 768 dimensions. +func (o *OllamaClient) EmbedDimension() uint64 { + switch o.config.Model { + case "nomic-embed-text": + return 768 + case "mxbai-embed-large": + return 1024 + case "all-minilm": + return 384 + default: + return 768 // Default to nomic-embed-text dimension + } +} + +// Embed generates embeddings for the given text. +func (o *OllamaClient) Embed(ctx context.Context, text string) ([]float32, error) { + req := &api.EmbedRequest{ + Model: o.config.Model, + Input: text, + } + + resp, err := o.client.Embed(ctx, req) + if err != nil { + return nil, log.E("rag.Ollama.Embed", "failed to generate embedding", err) + } + + if len(resp.Embeddings) == 0 || len(resp.Embeddings[0]) == 0 { + return nil, log.E("rag.Ollama.Embed", "empty embedding response", nil) + } + + // Convert float64 to float32 for Qdrant + embedding := resp.Embeddings[0] + result := make([]float32, len(embedding)) + for i, v := range embedding { + result[i] = float32(v) + } + + return result, nil +} + +// EmbedBatch generates embeddings for multiple texts. +func (o *OllamaClient) EmbedBatch(ctx context.Context, texts []string) ([][]float32, error) { + results := make([][]float32, len(texts)) + for i, text := range texts { + embedding, err := o.Embed(ctx, text) + if err != nil { + return nil, log.E("rag.Ollama.EmbedBatch", fmt.Sprintf("failed to embed text %d", i), err) + } + results[i] = embedding + } + return results, nil +} + +// VerifyModel checks if the embedding model is available. +func (o *OllamaClient) VerifyModel(ctx context.Context) error { + _, err := o.Embed(ctx, "test") + if err != nil { + return log.E("rag.Ollama.VerifyModel", fmt.Sprintf("model %s not available (run: ollama pull %s)", o.config.Model, o.config.Model), err) + } + return nil +} + +// Model returns the configured embedding model name. +func (o *OllamaClient) Model() string { + return o.config.Model +} \ No newline at end of file diff --git a/pkg/rag/qdrant.go b/pkg/rag/qdrant.go new file mode 100644 index 0000000..885def4 --- /dev/null +++ b/pkg/rag/qdrant.go @@ -0,0 +1,225 @@ +// Package rag provides RAG (Retrieval Augmented Generation) functionality +// for storing and querying documentation in Qdrant vector database. +package rag + +import ( + "context" + "fmt" + + "github.com/host-uk/core/pkg/log" + "github.com/qdrant/go-client/qdrant" +) + +// QdrantConfig holds Qdrant connection configuration. +type QdrantConfig struct { + Host string + Port int + APIKey string + UseTLS bool +} + +// DefaultQdrantConfig returns default Qdrant configuration. +// Host defaults to localhost for local development. +func DefaultQdrantConfig() QdrantConfig { + return QdrantConfig{ + Host: "localhost", + Port: 6334, // gRPC port + UseTLS: false, + } +} + +// QdrantClient wraps the Qdrant Go client with convenience methods. +type QdrantClient struct { + client *qdrant.Client + config QdrantConfig +} + +// NewQdrantClient creates a new Qdrant client. +func NewQdrantClient(cfg QdrantConfig) (*QdrantClient, error) { + addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + + client, err := qdrant.NewClient(&qdrant.Config{ + Host: cfg.Host, + Port: cfg.Port, + APIKey: cfg.APIKey, + UseTLS: cfg.UseTLS, + }) + if err != nil { + return nil, log.E("rag.Qdrant", fmt.Sprintf("failed to connect to Qdrant at %s", addr), err) + } + + return &QdrantClient{ + client: client, + config: cfg, + }, nil +} + +// Close closes the Qdrant client connection. +func (q *QdrantClient) Close() error { + return q.client.Close() +} + +// HealthCheck verifies the connection to Qdrant. +func (q *QdrantClient) HealthCheck(ctx context.Context) error { + _, err := q.client.HealthCheck(ctx) + return err +} + +// ListCollections returns all collection names. +func (q *QdrantClient) ListCollections(ctx context.Context) ([]string, error) { + resp, err := q.client.ListCollections(ctx) + if err != nil { + return nil, err + } + names := make([]string, len(resp)) + copy(names, resp) + return names, nil +} + +// CollectionExists checks if a collection exists. +func (q *QdrantClient) CollectionExists(ctx context.Context, name string) (bool, error) { + return q.client.CollectionExists(ctx, name) +} + +// CreateCollection creates a new collection with cosine distance. +func (q *QdrantClient) CreateCollection(ctx context.Context, name string, vectorSize uint64) error { + return q.client.CreateCollection(ctx, &qdrant.CreateCollection{ + CollectionName: name, + VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{ + Size: vectorSize, + Distance: qdrant.Distance_Cosine, + }), + }) +} + +// DeleteCollection deletes a collection. +func (q *QdrantClient) DeleteCollection(ctx context.Context, name string) error { + return q.client.DeleteCollection(ctx, name) +} + +// CollectionInfo returns information about a collection. +func (q *QdrantClient) CollectionInfo(ctx context.Context, name string) (*qdrant.CollectionInfo, error) { + return q.client.GetCollectionInfo(ctx, name) +} + +// Point represents a vector point with payload. +type Point struct { + ID string + Vector []float32 + Payload map[string]any +} + +// UpsertPoints inserts or updates points in a collection. +func (q *QdrantClient) UpsertPoints(ctx context.Context, collection string, points []Point) error { + if len(points) == 0 { + return nil + } + + qdrantPoints := make([]*qdrant.PointStruct, len(points)) + for i, p := range points { + qdrantPoints[i] = &qdrant.PointStruct{ + Id: qdrant.NewID(p.ID), + Vectors: qdrant.NewVectors(p.Vector...), + Payload: qdrant.NewValueMap(p.Payload), + } + } + + _, err := q.client.Upsert(ctx, &qdrant.UpsertPoints{ + CollectionName: collection, + Points: qdrantPoints, + }) + return err +} + +// SearchResult represents a search result with score. +type SearchResult struct { + ID string + Score float32 + Payload map[string]any +} + +// Search performs a vector similarity search. +func (q *QdrantClient) Search(ctx context.Context, collection string, vector []float32, limit uint64, filter map[string]string) ([]SearchResult, error) { + query := &qdrant.QueryPoints{ + CollectionName: collection, + Query: qdrant.NewQuery(vector...), + Limit: qdrant.PtrOf(limit), + WithPayload: qdrant.NewWithPayload(true), + } + + // Add filter if provided + if len(filter) > 0 { + conditions := make([]*qdrant.Condition, 0, len(filter)) + for k, v := range filter { + conditions = append(conditions, qdrant.NewMatch(k, v)) + } + query.Filter = &qdrant.Filter{ + Must: conditions, + } + } + + resp, err := q.client.Query(ctx, query) + if err != nil { + return nil, err + } + + results := make([]SearchResult, len(resp)) + for i, p := range resp { + payload := make(map[string]any) + for k, v := range p.Payload { + payload[k] = valueToGo(v) + } + results[i] = SearchResult{ + ID: pointIDToString(p.Id), + Score: p.Score, + Payload: payload, + } + } + return results, nil +} + +// pointIDToString converts a Qdrant point ID to string. +func pointIDToString(id *qdrant.PointId) string { + if id == nil { + return "" + } + switch v := id.PointIdOptions.(type) { + case *qdrant.PointId_Num: + return fmt.Sprintf("%d", v.Num) + case *qdrant.PointId_Uuid: + return v.Uuid + default: + return "" + } +} + +// valueToGo converts a Qdrant value to a Go value. +func valueToGo(v *qdrant.Value) any { + if v == nil { + return nil + } + switch val := v.Kind.(type) { + case *qdrant.Value_StringValue: + return val.StringValue + case *qdrant.Value_IntegerValue: + return val.IntegerValue + case *qdrant.Value_DoubleValue: + return val.DoubleValue + case *qdrant.Value_BoolValue: + return val.BoolValue + case *qdrant.Value_ListValue: + list := make([]any, len(val.ListValue.Values)) + for i, item := range val.ListValue.Values { + list[i] = valueToGo(item) + } + return list + case *qdrant.Value_StructValue: + m := make(map[string]any) + for k, item := range val.StructValue.Fields { + m[k] = valueToGo(item) + } + return m + default: + return nil + } +} \ No newline at end of file diff --git a/pkg/rag/query.go b/pkg/rag/query.go new file mode 100644 index 0000000..ae85af6 --- /dev/null +++ b/pkg/rag/query.go @@ -0,0 +1,163 @@ +package rag + +import ( + "context" + "fmt" + "html" + "strings" + + "github.com/host-uk/core/pkg/log" +) + +// QueryConfig holds query configuration. +type QueryConfig struct { + Collection string + Limit uint64 + Threshold float32 // Minimum similarity score (0-1) + Category string // Filter by category +} + +// DefaultQueryConfig returns default query configuration. +func DefaultQueryConfig() QueryConfig { + return QueryConfig{ + Collection: "hostuk-docs", + Limit: 5, + Threshold: 0.5, + } +} + +// QueryResult represents a query result with metadata. +type QueryResult struct { + Text string + Source string + Section string + Category string + ChunkIndex int + Score float32 +} + +// Query searches for similar documents in Qdrant. +func Query(ctx context.Context, qdrant *QdrantClient, ollama *OllamaClient, query string, cfg QueryConfig) ([]QueryResult, error) { + // Generate embedding for query + embedding, err := ollama.Embed(ctx, query) + if err != nil { + return nil, log.E("rag.Query", "error generating query embedding", err) + } + + // Build filter + var filter map[string]string + if cfg.Category != "" { + filter = map[string]string{"category": cfg.Category} + } + + // Search Qdrant + results, err := qdrant.Search(ctx, cfg.Collection, embedding, cfg.Limit, filter) + if err != nil { + return nil, log.E("rag.Query", "error searching", err) + } + + // Convert and filter by threshold + var queryResults []QueryResult + for _, r := range results { + if r.Score < cfg.Threshold { + continue + } + + qr := QueryResult{ + Score: r.Score, + } + + // Extract payload fields + if text, ok := r.Payload["text"].(string); ok { + qr.Text = text + } + if source, ok := r.Payload["source"].(string); ok { + qr.Source = source + } + if section, ok := r.Payload["section"].(string); ok { + qr.Section = section + } + if category, ok := r.Payload["category"].(string); ok { + qr.Category = category + } + // Handle chunk_index from various types (JSON unmarshaling produces float64) + switch idx := r.Payload["chunk_index"].(type) { + case int64: + qr.ChunkIndex = int(idx) + case float64: + qr.ChunkIndex = int(idx) + case int: + qr.ChunkIndex = idx + } + + queryResults = append(queryResults, qr) + } + + return queryResults, nil +} + +// FormatResultsText formats query results as plain text. +func FormatResultsText(results []QueryResult) string { + if len(results) == 0 { + return "No results found." + } + + var sb strings.Builder + for i, r := range results { + sb.WriteString(fmt.Sprintf("\n--- Result %d (score: %.2f) ---\n", i+1, r.Score)) + sb.WriteString(fmt.Sprintf("Source: %s\n", r.Source)) + if r.Section != "" { + sb.WriteString(fmt.Sprintf("Section: %s\n", r.Section)) + } + sb.WriteString(fmt.Sprintf("Category: %s\n\n", r.Category)) + sb.WriteString(r.Text) + sb.WriteString("\n") + } + return sb.String() +} + +// FormatResultsContext formats query results for LLM context injection. +func FormatResultsContext(results []QueryResult) string { + if len(results) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString("\n") + for _, r := range results { + // Escape XML special characters to prevent malformed output + fmt.Fprintf(&sb, "\n", + html.EscapeString(r.Source), + html.EscapeString(r.Section), + html.EscapeString(r.Category)) + sb.WriteString(html.EscapeString(r.Text)) + sb.WriteString("\n\n\n") + } + sb.WriteString("") + return sb.String() +} + +// FormatResultsJSON formats query results as JSON-like output. +func FormatResultsJSON(results []QueryResult) string { + if len(results) == 0 { + return "[]" + } + + var sb strings.Builder + sb.WriteString("[\n") + for i, r := range results { + sb.WriteString(" {\n") + sb.WriteString(fmt.Sprintf(" \"source\": %q,\n", r.Source)) + sb.WriteString(fmt.Sprintf(" \"section\": %q,\n", r.Section)) + sb.WriteString(fmt.Sprintf(" \"category\": %q,\n", r.Category)) + sb.WriteString(fmt.Sprintf(" \"score\": %.4f,\n", r.Score)) + sb.WriteString(fmt.Sprintf(" \"text\": %q\n", r.Text)) + if i < len(results)-1 { + sb.WriteString(" },\n") + } else { + sb.WriteString(" }\n") + } + } + sb.WriteString("]") + return sb.String() +} \ No newline at end of file diff --git a/pkg/release/config.go b/pkg/release/config.go index 24b035c..2beefbf 100644 --- a/pkg/release/config.go +++ b/pkg/release/config.go @@ -210,7 +210,6 @@ func DefaultConfig() *Config { Targets: []TargetConfig{ {OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, - {OS: "darwin", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}, }, diff --git a/pkg/release/config_test.go b/pkg/release/config_test.go index d214c18..4cc714a 100644 --- a/pkg/release/config_test.go +++ b/pkg/release/config_test.go @@ -161,7 +161,7 @@ func TestDefaultConfig_Good(t *testing.T) { assert.Empty(t, cfg.Project.Repository) // Default targets - assert.Len(t, cfg.Build.Targets, 5) + assert.Len(t, cfg.Build.Targets, 4) hasLinuxAmd64 := false hasDarwinArm64 := false hasWindowsAmd64 := false diff --git a/pkg/release/release.go b/pkg/release/release.go index f5dd53b..46f9d41 100644 --- a/pkg/release/release.go +++ b/pkg/release/release.go @@ -227,7 +227,6 @@ func buildArtifacts(ctx context.Context, cfg *Config, projectDir, version string targets = []build.Target{ {OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, - {OS: "darwin", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}, } diff --git a/tools/rag/README.md b/tools/rag/README.md new file mode 100644 index 0000000..e7a4f5d --- /dev/null +++ b/tools/rag/README.md @@ -0,0 +1,193 @@ +# RAG Pipeline for Host UK Documentation + +Store documentation in a vector database so Claude (and local LLMs) can retrieve relevant context without being reminded every conversation. + +## The Problem This Solves + +> "The amount of times I've had to re-tell you how to make a Flux button is crazy" + +Instead of wasting context window on "remember, Flux buttons work like this...", the RAG system: +1. Stores all documentation in Qdrant +2. Claude queries before answering +3. Relevant docs injected automatically +4. No more re-teaching + +## Prerequisites + +**Already running on your lab:** +- Qdrant: `linux.snider.dev:6333` +- Ollama: `linux.snider.dev:11434` (or local) + +**Install Python deps:** +```bash +pip install -r requirements.txt +``` + +**Ensure embedding model is available:** +```bash +ollama pull nomic-embed-text +``` + +## Quick Start + +### 1. Ingest Documentation + +```bash +# Ingest recovered Host UK docs +python ingest.py /Users/snider/Code/host-uk/core/tasks/recovered-hostuk \ + --collection hostuk-docs \ + --recreate + +# Ingest Flux UI docs separately (higher priority) +python ingest.py /path/to/flux-ui-docs \ + --collection flux-ui-docs \ + --recreate +``` + +### 2. Query the Database + +```bash +# Search for Flux button docs +python query.py "how to create a Flux button component" + +# Filter by category +python query.py "path sandboxing" --category architecture + +# Get more results +python query.py "Vi personality" --top 10 + +# Output as JSON +python query.py "brand voice" --format json + +# Output for LLM context injection +python query.py "Flux modal component" --format context +``` + +### 3. List Collections + +```bash +python query.py --list-collections +python query.py --stats --collection flux-ui-docs +``` + +## Collections Strategy + +| Collection | Content | Priority | +|------------|---------|----------| +| `flux-ui-docs` | Flux Pro component docs | High (UI questions) | +| `hostuk-docs` | Recovered implementation docs | Medium | +| `brand-docs` | Vi, brand voice, visual identity | For content generation | +| `lethean-docs` | SASE/dVPN technical docs | Product-specific | + +## Integration with Claude Code + +### Option 1: MCP Server (Best) + +Create an MCP server that Claude can query: + +```go +// In core CLI +func (s *RagServer) Query(query string) ([]Document, error) { + // Query Qdrant + // Return relevant docs +} +``` + +Then Claude can call `rag.query("Flux button")` and get docs automatically. + +### Option 2: CLAUDE.md Instruction + +Add to project CLAUDE.md: + +```markdown +## Before Answering UI Questions + +When asked about Flux UI components, query the RAG database first: +```bash +python /path/to/query.py "your question" --collection flux-ui-docs --format context +``` + +Include the retrieved context in your response. +``` + +### Option 3: Claude Code Hook + +Create a hook that auto-injects context for certain queries. + +## Category Taxonomy + +The ingestion automatically categorizes files: + +| Category | Matches | +|----------|---------| +| `ui-component` | flux, ui/component | +| `brand` | brand, mascot | +| `product-brief` | brief | +| `help-doc` | help, draft | +| `task` | task, plan | +| `architecture` | architecture, migration | +| `documentation` | default | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `QDRANT_HOST` | linux.snider.dev | Qdrant server | +| `QDRANT_PORT` | 6333 | Qdrant port | +| `EMBEDDING_MODEL` | nomic-embed-text | Ollama model | +| `CHUNK_SIZE` | 500 | Characters per chunk | +| `CHUNK_OVERLAP` | 50 | Overlap between chunks | + +## Training a Model vs RAG + +**RAG** (what this does): +- Model weights unchanged +- Documents retrieved at query time +- Knowledge updates instantly (re-ingest) +- Good for: facts, API docs, current information + +**Fine-tuning** (separate process): +- Model weights updated +- Knowledge baked into model +- Requires retraining to update +- Good for: style, patterns, conventions + +**For Flux UI**: RAG is perfect. The docs change, API changes, you want current info. + +**For Vi's voice**: Fine-tuning is better. Style doesn't change often, should be "baked in". + +## Vector Math (For Understanding) + +```text +"How do I make a Flux button?" + ↓ Embedding +[0.12, -0.45, 0.78, ...768 floats...] + ↓ Cosine similarity search +Find chunks with similar vectors + ↓ Results +1. doc/ui/flux/components/button.md (score: 0.89) +2. doc/ui/flux/forms.md (score: 0.76) +3. doc/ui/flux/components/input.md (score: 0.71) +``` + +The embedding model converts text to "meaning vectors". Similar meanings = similar vectors = found by search. + +## Troubleshooting + +**"No results found"** +- Lower threshold: `--threshold 0.3` +- Check collection has data: `--stats` +- Verify Ollama is running: `ollama list` + +**"Connection refused"** +- Check Qdrant is running: `curl http://linux.snider.dev:6333/collections` +- Check firewall/network + +**"Embedding model not available"** +```bash +ollama pull nomic-embed-text +``` + +--- + +*Part of the Host UK Core CLI tooling* diff --git a/tools/rag/ingest.py b/tools/rag/ingest.py new file mode 100644 index 0000000..7755bc2 --- /dev/null +++ b/tools/rag/ingest.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +RAG Ingestion Pipeline for Host UK Documentation + +Chunks markdown files, generates embeddings via Ollama, stores in Qdrant. + +Usage: + python ingest.py /path/to/docs --collection hostuk-docs + python ingest.py /path/to/flux-ui --collection flux-ui-docs + +Requirements: + pip install qdrant-client ollama markdown +""" + +import argparse +import hashlib +import json +import os +import re +import sys +from pathlib import Path +from typing import Generator + +try: + from qdrant_client import QdrantClient + from qdrant_client.models import Distance, VectorParams, PointStruct + import ollama +except ImportError: + print("Install dependencies: pip install qdrant-client ollama") + sys.exit(1) + + +# Configuration +QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost") +QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333")) +EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text") +CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "500")) # chars +CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "50")) # chars +VECTOR_DIM = 768 # nomic-embed-text dimension + + +def chunk_markdown(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> Generator[dict, None, None]: + """ + Chunk markdown by sections (## headers), then by paragraphs if too long. + Preserves context with overlap. + """ + # Split by ## headers first + sections = re.split(r'\n(?=## )', text) + + for section in sections: + if not section.strip(): + continue + + # Extract section title + lines = section.strip().split('\n') + title = lines[0].lstrip('#').strip() if lines[0].startswith('#') else "" + + # If section is small enough, yield as-is + if len(section) <= chunk_size: + yield { + "text": section.strip(), + "section": title, + } + continue + + # Otherwise, chunk by paragraphs + paragraphs = re.split(r'\n\n+', section) + current_chunk = "" + + for para in paragraphs: + if len(current_chunk) + len(para) <= chunk_size: + current_chunk += "\n\n" + para if current_chunk else para + else: + if current_chunk: + yield { + "text": current_chunk.strip(), + "section": title, + } + # Start new chunk with overlap from previous + if overlap and current_chunk: + overlap_text = current_chunk[-overlap:] + current_chunk = overlap_text + "\n\n" + para + else: + current_chunk = para + + # Don't forget the last chunk + if current_chunk.strip(): + yield { + "text": current_chunk.strip(), + "section": title, + } + + +def generate_embedding(text: str) -> list[float]: + """Generate embedding using Ollama.""" + response = ollama.embeddings(model=EMBEDDING_MODEL, prompt=text) + return response["embedding"] + + +def get_file_category(path: str) -> str: + """Determine category from file path.""" + path_lower = path.lower() + + if "flux" in path_lower or "ui/component" in path_lower: + return "ui-component" + elif "brand" in path_lower or "mascot" in path_lower: + return "brand" + elif "brief" in path_lower: + return "product-brief" + elif "help" in path_lower or "draft" in path_lower: + return "help-doc" + elif "task" in path_lower or "plan" in path_lower: + return "task" + elif "architecture" in path_lower or "migration" in path_lower: + return "architecture" + else: + return "documentation" + + +def ingest_directory( + directory: Path, + client: QdrantClient, + collection: str, + verbose: bool = False +) -> dict: + """Ingest all markdown files from directory into Qdrant.""" + + stats = {"files": 0, "chunks": 0, "errors": 0} + points = [] + + # Find all markdown files + md_files = list(directory.rglob("*.md")) + print(f"Found {len(md_files)} markdown files") + + for file_path in md_files: + try: + rel_path = str(file_path.relative_to(directory)) + + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + if not content.strip(): + continue + + # Extract metadata + category = get_file_category(rel_path) + + # Chunk the content + for i, chunk in enumerate(chunk_markdown(content)): + chunk_id = hashlib.md5( + f"{rel_path}:{i}:{chunk['text'][:100]}".encode() + ).hexdigest() + + # Generate embedding + embedding = generate_embedding(chunk["text"]) + + # Create point + point = PointStruct( + id=chunk_id, + vector=embedding, + payload={ + "text": chunk["text"], + "source": rel_path, + "section": chunk["section"], + "category": category, + "chunk_index": i, + } + ) + points.append(point) + stats["chunks"] += 1 + + if verbose: + print(f" [{category}] {rel_path} chunk {i}: {len(chunk['text'])} chars") + + stats["files"] += 1 + if not verbose: + print(f" Processed: {rel_path} ({stats['chunks']} chunks total)") + + except Exception as e: + print(f" Error processing {file_path}: {e}") + stats["errors"] += 1 + + # Batch upsert to Qdrant + if points: + print(f"\nUpserting {len(points)} vectors to Qdrant...") + + # Upsert in batches of 100 + batch_size = 100 + for i in range(0, len(points), batch_size): + batch = points[i:i + batch_size] + client.upsert(collection_name=collection, points=batch) + print(f" Uploaded batch {i // batch_size + 1}/{(len(points) - 1) // batch_size + 1}") + + return stats + + +def main(): + parser = argparse.ArgumentParser(description="Ingest markdown docs into Qdrant") + parser.add_argument("directory", type=Path, help="Directory containing markdown files") + parser.add_argument("--collection", default="hostuk-docs", help="Qdrant collection name") + parser.add_argument("--recreate", action="store_true", help="Delete and recreate collection") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + parser.add_argument("--qdrant-host", default=QDRANT_HOST, help="Qdrant host") + parser.add_argument("--qdrant-port", type=int, default=QDRANT_PORT, help="Qdrant port") + + args = parser.parse_args() + + if not args.directory.exists(): + print(f"Error: Directory not found: {args.directory}") + sys.exit(1) + + # Connect to Qdrant + print(f"Connecting to Qdrant at {args.qdrant_host}:{args.qdrant_port}...") + client = QdrantClient(host=args.qdrant_host, port=args.qdrant_port) + + # Create or recreate collection + collections = [c.name for c in client.get_collections().collections] + + if args.recreate and args.collection in collections: + print(f"Deleting existing collection: {args.collection}") + client.delete_collection(args.collection) + collections.remove(args.collection) + + if args.collection not in collections: + print(f"Creating collection: {args.collection}") + client.create_collection( + collection_name=args.collection, + vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE) + ) + + # Verify Ollama model is available + print(f"Using embedding model: {EMBEDDING_MODEL}") + try: + ollama.embeddings(model=EMBEDDING_MODEL, prompt="test") + except Exception as e: + print(f"Error: Embedding model not available. Run: ollama pull {EMBEDDING_MODEL}") + sys.exit(1) + + # Ingest files + print(f"\nIngesting from: {args.directory}") + stats = ingest_directory(args.directory, client, args.collection, args.verbose) + + # Summary + print(f"\n{'=' * 50}") + print(f"Ingestion complete!") + print(f" Files processed: {stats['files']}") + print(f" Chunks created: {stats['chunks']}") + print(f" Errors: {stats['errors']}") + print(f" Collection: {args.collection}") + print(f"{'=' * 50}") + + +if __name__ == "__main__": + main() diff --git a/tools/rag/query.py b/tools/rag/query.py new file mode 100644 index 0000000..24846d5 --- /dev/null +++ b/tools/rag/query.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +RAG Query Tool for Host UK Documentation + +Query the vector database and retrieve relevant documentation chunks. + +Usage: + python query.py "how do I create a Flux button" + python query.py "what is Vi's personality" --collection hostuk-docs + python query.py "path sandboxing" --top 10 --category architecture + +Requirements: + pip install qdrant-client ollama +""" + +import argparse +import html +import json +import os +import sys +from typing import Optional + +try: + from qdrant_client import QdrantClient + from qdrant_client.models import Filter, FieldCondition, MatchValue + import ollama +except ImportError: + print("Install dependencies: pip install qdrant-client ollama") + sys.exit(1) + + +# Configuration +QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost") +QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333")) +EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text") + + +def generate_embedding(text: str) -> list[float]: + """Generate embedding using Ollama.""" + response = ollama.embeddings(model=EMBEDDING_MODEL, prompt=text) + return response["embedding"] + + +def query_rag( + query: str, + client: QdrantClient, + collection: str, + top_k: int = 5, + category: Optional[str] = None, + score_threshold: float = 0.5, +) -> list[dict]: + """Query the RAG database and return relevant chunks.""" + + # Generate query embedding + query_embedding = generate_embedding(query) + + # Build filter if category specified + query_filter = None + if category: + query_filter = Filter( + must=[ + FieldCondition(key="category", match=MatchValue(value=category)) + ] + ) + + # Search + results = client.query_points( + collection_name=collection, + query=query_embedding, + query_filter=query_filter, + limit=top_k, + score_threshold=score_threshold, + ).points + + return [ + { + "score": hit.score, + "text": hit.payload["text"], + "source": hit.payload["source"], + "section": hit.payload.get("section", ""), + "category": hit.payload.get("category", ""), + } + for hit in results + ] + + +def format_results(results: list[dict], query: str, format: str = "text") -> str: + """Format results for display.""" + + if format == "json": + return json.dumps(results, indent=2) + + if not results: + return f"No results found for: {query}" + + output = [] + output.append(f"Query: {query}") + output.append(f"Results: {len(results)}") + output.append("=" * 60) + + for i, r in enumerate(results, 1): + output.append(f"\n[{i}] {r['source']} (score: {r['score']:.3f})") + if r['section']: + output.append(f" Section: {r['section']}") + output.append(f" Category: {r['category']}") + output.append("-" * 40) + # Truncate long text for display + text = r['text'] + if len(text) > 500: + text = text[:500] + "..." + output.append(text) + output.append("") + + return "\n".join(output) + + +def format_for_context(results: list[dict], query: str) -> str: + """Format results as context for LLM injection.""" + + if not results: + return "" + + output = [] + output.append(f'') + + for r in results: + output.append(f'\n') + output.append(html.escape(r['text'])) + output.append("") + + output.append("\n") + + return "\n".join(output) + +def main(): + parser = argparse.ArgumentParser(description="Query RAG documentation") + parser.add_argument("query", nargs="?", help="Search query") + parser.add_argument("--collection", default="hostuk-docs", help="Qdrant collection name") + parser.add_argument("--top", "-k", type=int, default=5, help="Number of results") + parser.add_argument("--category", "-c", help="Filter by category") + parser.add_argument("--threshold", "-t", type=float, default=0.5, help="Score threshold") + parser.add_argument("--format", "-f", choices=["text", "json", "context"], default="text") + parser.add_argument("--qdrant-host", default=QDRANT_HOST) + parser.add_argument("--qdrant-port", type=int, default=QDRANT_PORT) + parser.add_argument("--list-collections", action="store_true", help="List available collections") + parser.add_argument("--stats", action="store_true", help="Show collection stats") + + args = parser.parse_args() + + # Connect to Qdrant + client = QdrantClient(host=args.qdrant_host, port=args.qdrant_port) + + # List collections + if args.list_collections: + collections = client.get_collections().collections + print("Available collections:") + for c in collections: + info = client.get_collection(c.name) + print(f" - {c.name}: {info.points_count} vectors") + return + + # Show stats + if args.stats: + try: + info = client.get_collection(args.collection) + print(f"Collection: {args.collection}") + print(f" Vectors: {info.points_count}") + print(f" Status: {info.status}") + except Exception as e: + print(f"Collection not found: {args.collection}") + return + + # Query required + if not args.query: + parser.print_help() + return + + # Execute query + results = query_rag( + query=args.query, + client=client, + collection=args.collection, + top_k=args.top, + category=args.category, + score_threshold=args.threshold, + ) + + # Format output + if args.format == "context": + print(format_for_context(results, args.query)) + else: + print(format_results(results, args.query, args.format)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/rag/requirements.txt b/tools/rag/requirements.txt new file mode 100644 index 0000000..cd4cc3e --- /dev/null +++ b/tools/rag/requirements.txt @@ -0,0 +1,2 @@ +qdrant-client>=1.12.0,<2.0.0 +ollama>=0.1.0 \ No newline at end of file