feat: wire release command, add tar.xz support, unified installers (#277)

* feat(cli): wire release command and add installer scripts

- Wire up `core build release` subcommand (was orphaned)
- Wire up `core monitor` command (missing import in full variant)
- Add installer scripts for Unix (.sh) and Windows (.bat)
  - setup: Interactive with variant selection
  - ci: Minimal for CI/CD environments
  - dev: Full development variant
  - go/php/agent: Targeted development variants
- All scripts include security hardening:
  - Secure temp directories (mktemp -d)
  - Architecture validation
  - Version validation after GitHub API call
  - Proper cleanup on exit
  - PowerShell PATH updates on Windows (avoids setx truncation)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(build): add tar.xz support and unified installer scripts

- Add tar.xz archive support using Borg's compress package
  - ArchiveXZ() and ArchiveWithFormat() for configurable compression
  - Better compression ratio than gzip for release artifacts
- Consolidate 12 installer scripts into 2 unified scripts
  - install.sh and install.bat with BunnyCDN edge variable support
  - Subdomains: setup.core.help, ci.core.help, dev.core.help, etc.
  - MODE and VARIANT transformed at edge based on subdomain
- Installers prefer tar.xz with automatic fallback to tar.gz
- Fixed CodeRabbit issues: HTTP status patterns, tar error handling,
  verify_install params, VARIANT validation, CI PATH persistence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: add build and release config files

- .core/build.yaml - cross-platform build configuration
- .core/release.yaml - release workflow configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: move plans from docs/ to tasks/

Consolidate planning documents in tasks/plans/ directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(install): address CodeRabbit review feedback

- Add curl timeout (--max-time) to prevent hanging on slow networks
- Rename TMPDIR to WORK_DIR to avoid clobbering system env var
- Add chmod +x to ensure binary has execute permissions
- Add error propagation after subroutine calls in batch file
- Remove System32 install attempt in CI mode (use consistent INSTALL_DIR)
- Fix HTTP status regex for HTTP/2 compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(rag): add Go RAG implementation with Qdrant + Ollama

Add RAG (Retrieval Augmented Generation) tools for storing documentation
in Qdrant vector database and querying with semantic search. This replaces
the Python tools/rag implementation with a native Go solution.

New commands:
- core rag ingest [directory] - Ingest markdown files into Qdrant
- core rag query [question] - Query vector database with semantic search
- core rag collections - List and manage Qdrant collections

Features:
- Markdown chunking by sections and paragraphs with overlap
- UTF-8 safe text handling for international content
- Automatic category detection from file paths
- Multiple output formats: text, JSON, LLM context injection
- Environment variable support for host configuration

Dependencies:
- github.com/qdrant/go-client (gRPC client)
- github.com/ollama/ollama/api (embeddings API)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(deploy): add pure-Go Ansible executor and Coolify API integration

Implement infrastructure deployment system with:

- pkg/ansible: Pure Go Ansible executor
  - Playbook/inventory parsing (types.go, parser.go)
  - Full execution engine with variable templating, loops, blocks,
    conditionals, handlers, and fact gathering (executor.go)
  - SSH client with key/password auth and privilege escalation (ssh.go)
  - 35+ module implementations: shell, command, copy, template, file,
    apt, service, systemd, user, group, git, docker_compose, etc. (modules.go)

- pkg/deploy/coolify: Coolify API client wrapping Python swagger client
  - List/get servers, projects, applications, databases, services
  - Generic Call() for any OpenAPI operation

- pkg/deploy/python: Embedded Python runtime for swagger client integration

- internal/cmd/deploy: CLI commands
  - core deploy servers/projects/apps/databases/services/team
  - core deploy call <operation> [params-json]

This enables Docker-free infrastructure deployment with Ansible-compatible
playbooks executed natively in Go.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(deploy): address linter warnings and build errors

- Fix fmt.Sprintf format verb error in ssh.go (remove unused stat command)
- Fix errcheck warnings by explicitly ignoring best-effort operations
- Fix ineffassign warning in cmd_ansible.go

All golangci-lint checks now pass for deploy packages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style(deploy): fix gofmt formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(deploy): use known_hosts for SSH host key verification

Address CodeQL security alert by using the user's known_hosts file
for SSH host key verification when available. Falls back to accepting
any key only when known_hosts doesn't exist (common in containerized
or ephemeral environments).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(ai,security,ide): add agentic MVP, security jobs, and Core IDE desktop app

Wire up AI infrastructure with unified pkg/ai package (metrics JSONL,
RAG integration), move RAG under `core ai rag`, add `core ai metrics`
command, and enrich task context with Qdrant documentation.

Add `--target` flag to all security commands for external repo scanning,
`core security jobs` for distributing findings as GitHub Issues, and
consistent error logging across scan/deps/alerts/secrets commands.

Add Core IDE Wails v3 desktop app with Angular 20 frontend, MCP bridge
(loopback-only HTTP server), WebSocket hub, and Claude Code bridge.
Production-ready with Lethean CIC branding, macOS code signing support,
and security hardening (origin validation, body size limits, URL scheme
checks, memory leak prevention, XSS mitigation).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review comments from CodeRabbit, Copilot, and Gemini

Fixes across 25 files addressing 46+ review comments:

- pkg/ai/metrics.go: handle error from Close() on writable file handle
- pkg/ansible: restore loop vars after loop, restore become settings,
  fix Upload with become=true and no password (use sudo -n), honour
  SSH timeout config, use E() helper for contextual errors, quote git
  refs in checkout commands
- pkg/rag: validate chunk config, guard negative-to-uint64 conversion,
  use E() helper for errors, add context timeout to Ollama HTTP calls
- pkg/deploy/python: fix exec.ExitError type assertion (was os.PathError),
  handle os.UserHomeDir() error
- pkg/build/buildcmd: use cmd.Context() instead of context.Background()
  for proper Ctrl+C cancellation
- install.bat: add curl timeouts, CRLF line endings, use --connect-timeout
  for archive downloads
- install.sh: use absolute path for version check in CI mode
- tools/rag: fix broken ingest.py function def, escape HTML in query.py,
  pin qdrant-client version, add markdown code block languages
- internal/cmd/rag: add chunk size validation, env override handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(build): make release dry-run by default and remove darwin/amd64 target

Replace --dry-run (default false) with --we-are-go-for-launch (default
false) so `core build release` is safe by default. Remove darwin/amd64
from default build targets (arm64 only for macOS). Fix cmd_project.go
to use command context instead of context.Background().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-04 00:49:57 +00:00 committed by GitHub
parent 2ec4d300ee
commit 989b7e1e65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
137 changed files with 22830 additions and 2883 deletions

32
.core/build.yaml Normal file
View file

@ -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

39
.core/release.yaml Normal file
View file

@ -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

View file

@ -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`

View file

@ -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.

View file

@ -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

View file

@ -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?

File diff suppressed because it is too large Load diff

16
go.mod
View file

@ -5,13 +5,17 @@ go 1.25.5
require ( require (
github.com/Snider/Borg v0.1.0 github.com/Snider/Borg v0.1.0
github.com/getkin/kin-openapi v0.133.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/debme v1.2.1
github.com/leaanthony/gosod v1.0.4 github.com/leaanthony/gosod v1.0.4
github.com/minio/selfupdate v0.6.0 github.com/minio/selfupdate v0.6.0
github.com/modelcontextprotocol/go-sdk v1.2.0 github.com/modelcontextprotocol/go-sdk v1.2.0
github.com/oasdiff/oasdiff v1.11.8 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/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.47.0
golang.org/x/mod v0.32.0 golang.org/x/mod v0.32.0
golang.org/x/net v0.49.0 golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0 golang.org/x/oauth2 v0.34.0
@ -27,6 +31,8 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/TwiN/go-color v1.4.1 // 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/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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-git/go-git/v5 v5.16.4 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/jsonschema-go v0.4.2 // 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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
@ -51,6 +59,7 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sergi/go-diff v1.4.0 // 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/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/tidwall/gjson v1.18.0 // 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/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/ugorji/go/codec v1.3.0 // 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/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/woodsbury/decimal128 v1.4.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // 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/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.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 gopkg.in/warnings.v0 v0.1.2 // indirect
) )

52
go.sum
View file

@ -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/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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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= 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-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 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= 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 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= 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= 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-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 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 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 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 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/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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/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 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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.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 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 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/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 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 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 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= 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 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 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/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 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 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= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 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/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 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.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 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= 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 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-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 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/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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

170
install.bat Normal file
View file

@ -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

224
install.sh Normal file
View file

@ -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

View file

@ -8,9 +8,12 @@
// - task:commit: Create commits with task references // - task:commit: Create commits with task references
// - task:pr: Create pull requests linked to tasks // - task:pr: Create pull requests linked to tasks
// - claude: Claude Code CLI integration (planned) // - claude: Claude Code CLI integration (planned)
// - rag: RAG tools (ingest, query, collections)
// - metrics: View AI/security event metrics
package ai package ai
import ( import (
ragcmd "github.com/host-uk/core/internal/cmd/rag"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
) )
@ -57,6 +60,12 @@ func initCommands() {
// Add agentic task commands // Add agentic task commands
AddAgenticCommands(aiCmd) 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. // AddAICommands registers the 'ai' command and all subcommands.

View file

@ -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)
}
}

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "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/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
) )
@ -165,6 +166,13 @@ var taskCmd = &cli.Command{
return cli.WrapVerb(err, "claim", "task") 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", successStyle.Render(">>"), i18n.T("i18n.done.claim", "task"))
cli.Print(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status)) cli.Print(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status))
} }

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "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/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
) )
@ -92,6 +93,13 @@ var taskCompleteCmd = &cli.Command{
return cli.WrapVerb(err, "complete", "task") 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 { if taskCompleteFailed {
cli.Print("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID})) cli.Print("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID}))
} else { } else {

View file

@ -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 <playbook>",
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 <host>",
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
}

View file

@ -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)
}

View file

@ -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 <operation> [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]), &params); 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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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
}

View file

@ -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"))
}

View file

@ -22,6 +22,7 @@ func addAlertsCommand(parent *cli.Command) {
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) 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) parent.AddCommand(cmd)
} }
@ -43,6 +44,11 @@ func runAlerts() error {
return err return err
} }
// External target mode: bypass registry entirely
if securityTarget != "" {
return runAlertsForTarget(securityTarget)
}
reg, err := loadRegistry(securityRegistryPath) reg, err := loadRegistry(securityRegistryPath)
if err != nil { if err != nil {
return err return err
@ -173,6 +179,124 @@ func runAlerts() error {
return nil 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) { func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName) endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint) output, err := runGHAPI(endpoint)

View file

@ -22,6 +22,7 @@ func addDepsCommand(parent *cli.Command) {
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity")) cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) 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) parent.AddCommand(cmd)
} }
@ -44,6 +45,11 @@ func runDeps() error {
return err return err
} }
// External target mode: bypass registry entirely
if securityTarget != "" {
return runDepsForTarget(securityTarget)
}
reg, err := loadRegistry(securityRegistryPath) reg, err := loadRegistry(securityRegistryPath)
if err != nil { if err != nil {
return err return err
@ -62,6 +68,7 @@ func runDeps() error {
alerts, err := fetchDependabotAlerts(repoFullName) alerts, err := fetchDependabotAlerts(repoFullName)
if err != nil { if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
continue continue
} }
@ -132,3 +139,72 @@ func runDeps() error {
return nil 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
}

View file

@ -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()
}

View file

@ -3,7 +3,9 @@ package security
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"github.com/host-uk/core/pkg/ai"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "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(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
cmd.Flags().StringVar(&scanTool, "tool", "", i18n.T("cmd.security.scan.flag.tool")) 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().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json"))
cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target"))
parent.AddCommand(cmd) parent.AddCommand(cmd)
} }
@ -48,6 +51,11 @@ func runScan() error {
return err return err
} }
// External target mode: bypass registry entirely
if securityTarget != "" {
return runScanForTarget(securityTarget)
}
reg, err := loadRegistry(securityRegistryPath) reg, err := loadRegistry(securityRegistryPath)
if err != nil { if err != nil {
return err return err
@ -66,6 +74,7 @@ func runScan() error {
alerts, err := fetchCodeScanningAlerts(repoFullName) alerts, err := fetchCodeScanningAlerts(repoFullName)
if err != nil { if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
continue 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 { if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ") output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil { if err != nil {
@ -140,3 +162,93 @@ func runScan() error {
return nil 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
}

View file

@ -21,6 +21,7 @@ func addSecretsCommand(parent *cli.Command) {
cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry")) cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry"))
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo")) cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json")) 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) parent.AddCommand(cmd)
} }
@ -40,6 +41,11 @@ func runSecrets() error {
return err return err
} }
// External target mode: bypass registry entirely
if securityTarget != "" {
return runSecretsForTarget(securityTarget)
}
reg, err := loadRegistry(securityRegistryPath) reg, err := loadRegistry(securityRegistryPath)
if err != nil { if err != nil {
return err return err
@ -119,3 +125,67 @@ func runSecrets() error {
return nil 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
}

View file

@ -17,6 +17,7 @@ var (
securityRepo string securityRepo string
securitySeverity string securitySeverity string
securityJSON bool securityJSON bool
securityTarget string // External repo target (e.g. "wailsapp/wails")
) )
// AddSecurityCommands adds the 'security' command to the root. // AddSecurityCommands adds the 'security' command to the root.
@ -31,6 +32,7 @@ func AddSecurityCommands(root *cli.Command) {
addDepsCommand(secCmd) addDepsCommand(secCmd)
addScanCommand(secCmd) addScanCommand(secCmd)
addSecretsCommand(secCmd) addSecretsCommand(secCmd)
addJobsCommand(secCmd)
root.AddCommand(secCmd) root.AddCommand(secCmd)
} }
@ -192,6 +194,16 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
return reg.List() 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. // AlertSummary holds aggregated alert counts.
type AlertSummary struct { type AlertSummary struct {
Critical int Critical int

7
internal/core-ide/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.task
.idea
bin
frontend/dist
frontend/node_modules
build/linux/appimage/build
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe

View file

@ -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.

View file

@ -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}}

View file

@ -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 .

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -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

View file

@ -0,0 +1,32 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Core IDE (Dev)</string>
<key>CFBundleExecutable</key>
<string>core-ide</string>
<key>CFBundleIdentifier</key>
<string>com.lethean.core-ide.dev</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Core IDE Development Build</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 2026 Lethean Community Interest Company. EUPL-1.2</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Core IDE</string>
<key>CFBundleExecutable</key>
<string>core-ide</string>
<key>CFBundleIdentifier</key>
<string>com.lethean.core-ide</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Core IDE - Development Environment</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 2026 Lethean Community Interest Company. EUPL-1.2</string>
</dict>
</plist>

View file

@ -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}}'

Binary file not shown.

View file

@ -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}}'

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -0,0 +1 @@
#!/bin/bash

View file

@ -0,0 +1 @@
#!/bin/bash

View file

@ -0,0 +1 @@
#!/bin/bash

View file

@ -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'

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -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"
}
}
}

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
<Identity
Name="com.lethean.core-ide"
Publisher="CN=Lethean Community Interest Company"
Version="0.1.0.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>Core IDE</DisplayName>
<PublisherDisplayName>Lethean Community Interest Company</PublisherDisplayName>
<Description>Core IDE - Development Environment</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="CoreIDE" Executable="core-ide.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="Core IDE"
Description="Core IDE - Development Environment"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="core-ide.exe" />
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<MsixPackagingToolTemplate
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
<Settings
AllowTelemetry="false"
ApplyACLsToPackageFiles="true"
GenerateCommandLineFile="true"
AllowPromptForPassword="false">
</Settings>
<Installer
Path="wails-angular-template"
Arguments=""
InstallLocation="C:\Program Files\My Company\My Product">
</Installer>
<PackageInformation
PackageName="My Product"
PackageDisplayName="My Product"
PublisherName="CN=My Company"
PublisherDisplayName="My Company"
Version="0.1.0.0"
PackageDescription="My Product Description">
<Capabilities>
<Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application
Id="com.wails.wails-angular-template"
Description="My Product Description"
DisplayName="My Product"
ExecutableName="wails-angular-template"
EntryPoint="Windows.FullTrustApplication">
</Application>
</Applications>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Properties>
<Framework>false</Framework>
<DisplayName>My Product</DisplayName>
<PublisherDisplayName>My Company</PublisherDisplayName>
<Description>My Product Description</Description>
<Logo>Assets\AppIcon.png</Logo>
</Properties>
</PackageInformation>
<SaveLocation PackagePath="wails-angular-template.msix" />
<PackageIntegrity>
<CertificatePath></CertificatePath>
</PackageIntegrity>
</MsixPackagingToolTemplate>

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.wails-angular-template" version="0.1.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View file

@ -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)
}
}

View file

@ -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

43
internal/core-ide/frontend/.gitignore vendored Normal file
View file

@ -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

View file

@ -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.

View file

@ -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"
]
}
}
}
}
}
}

View file

@ -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<string> {
return $Call.ByID(1411160069, name);
}

View file

@ -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
};

View file

@ -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<string> {
return $Call.ByID(1411160069, name);
}

View file

@ -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
};

View file

@ -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);

View file

@ -0,0 +1,2 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>

After

Width:  |  Height:  |  Size: 995 B

View file

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -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);

View file

@ -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())
]
};

View file

@ -0,0 +1,23 @@
<div class="container">
<div>
<a (click)='openLink("https://v3alpha.wails.io")'>
<img src="/wails.png" class="logo" alt="Wails logo"/>
</a>
<a (click)='openLink("https://angular.io")'>
<img src="/angular.png" class="logo vanilla" alt="Angular logo"/>
</a>
</div>
<h1>Wails + Angular v20</h1>
<div class="card">
<div class="result">{{ result() }}</div>
<div class="input-box">
<input class="input" type="text" autocomplete="off" [value]="name()" (input)="name.set($event.target.value)"/>
<button class="btn" (click)="doGreet()">Greet</button>
</div>
</div>
<div class="footer">
<div><p>Click on the Wails logo to learn more</p></div>
<div><p>{{ time() }}</p></div>
</div>
</div>
<router-outlet />

View file

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Client
}
];

View file

@ -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' },
];

View file

@ -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');
});
});

View file

@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`,
styles: [`
:host {
display: block;
width: 100%;
height: 100%;
}
`]
})
export class App {}

View file

@ -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: `
<nav class="sidebar">
<div class="sidebar-header">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
</svg>
</div>
</div>
<div class="nav-items">
@for (item of navItems; track item.id) {
<button
class="nav-item"
[class.active]="currentRoute === item.id"
(click)="routeChange.emit(item.id)"
[title]="item.label">
<div class="nav-icon" [innerHTML]="item.icon"></div>
</button>
}
</div>
<div class="sidebar-footer">
<button class="nav-item" (click)="routeChange.emit('settings')" title="Settings">
<div class="nav-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
</button>
</div>
</nav>
`,
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<string>();
constructor(private sanitizer: DomSanitizer) {
this.navItems = this.createNavItems();
}
navItems: NavItem[];
private createNavItems(): NavItem[] {
return [
{
id: 'dashboard',
label: 'Dashboard',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
</svg>`)
},
{
id: 'explorer',
label: 'Explorer',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>`)
},
{
id: 'search',
label: 'Search',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>`)
},
{
id: 'git',
label: 'Source Control',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
</svg>`)
},
{
id: 'debug',
label: 'Debug',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>`)
},
{
id: 'terminal',
label: 'Terminal',
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>`)
},
];
}
}

View file

@ -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: `
<div class="ide-layout">
<app-sidebar [currentRoute]="currentRoute()" (routeChange)="onRouteChange($event)"></app-sidebar>
<div class="ide-main">
<!-- Top Bar -->
<div class="top-bar">
<div class="breadcrumb">
<span class="breadcrumb-item">Core IDE</span>
<span class="breadcrumb-sep">/</span>
<span class="breadcrumb-item active">{{ currentRoute() }}</span>
</div>
<div class="top-bar-actions">
<span class="time">{{ currentTime() }}</span>
</div>
</div>
<!-- Content Area -->
<div class="ide-content">
@switch (currentRoute()) {
@case ('dashboard') {
<div class="dashboard-view">
<h1>Welcome to Core IDE</h1>
<p class="subtitle">Your development environment is ready.</p>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon projects">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
</div>
<div class="stat-info">
<span class="stat-value">{{ projectCount() }}</span>
<span class="stat-label">Projects</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon tasks">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
</div>
<div class="stat-info">
<span class="stat-value">{{ taskCount() }}</span>
<span class="stat-label">Tasks</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon git">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
</svg>
</div>
<div class="stat-info">
<span class="stat-value">{{ gitChanges() }}</span>
<span class="stat-label">Changes</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon status">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="stat-info">
<span class="stat-value status-ok">OK</span>
<span class="stat-label">Status</span>
</div>
</div>
</div>
<div class="quick-actions">
<h2>Quick Actions</h2>
<div class="actions-grid">
<button class="action-card" (click)="emitAction('new-project')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
<span>New Project</span>
</button>
<button class="action-card" (click)="emitAction('open-project')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/>
</svg>
<span>Open Project</span>
</button>
<button class="action-card" (click)="emitAction('run-dev')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Run Dev Server</span>
</button>
<button class="action-card" (click)="onRouteChange('terminal')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Open Terminal</span>
</button>
</div>
</div>
</div>
}
@case ('explorer') {
<div class="panel-view">
<h2>File Explorer</h2>
<p>Browse and manage your project files.</p>
</div>
}
@case ('search') {
<div class="panel-view">
<h2>Search</h2>
<p>Search across all files in your workspace.</p>
</div>
}
@case ('git') {
<div class="panel-view">
<h2>Source Control</h2>
<p>Manage your Git repositories and commits.</p>
</div>
}
@case ('debug') {
<div class="panel-view">
<h2>Debug</h2>
<p>Debug your applications.</p>
</div>
}
@case ('terminal') {
<div class="panel-view terminal">
<h2>Terminal</h2>
<div class="terminal-output">
<pre>$ core dev health
18 repos | clean | synced
$ _</pre>
</div>
</div>
}
@case ('settings') {
<div class="panel-view">
<h2>Settings</h2>
<p>Configure your IDE preferences.</p>
</div>
}
@default {
<div class="panel-view">
<h2>{{ currentRoute() }}</h2>
</div>
}
}
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-left">
<span class="status-item branch">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
</svg>
main
</span>
<span class="status-item">UTF-8</span>
</div>
<div class="status-right">
<span class="status-item">Core IDE v0.1.0</span>
</div>
</div>
</div>
</div>
`,
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);
});
}
}

View file

@ -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: `
<div class="tray-container">
<!-- Header -->
<div class="tray-header">
<div class="tray-logo">
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
</svg>
<span>Core IDE</span>
</div>
<div class="tray-controls">
<button class="control-btn" (click)="openIDE()" title="Open IDE">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</button>
</div>
</div>
<!-- Quick Stats -->
<div class="tray-stats">
<div class="stat-row">
<span class="stat-label">Status</span>
<span class="stat-value" [class.active]="isActive()">
{{ isActive() ? 'Running' : 'Idle' }}
</span>
</div>
<div class="stat-row">
<span class="stat-label">Projects</span>
<span class="stat-value">{{ projectCount() }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active Tasks</span>
<span class="stat-value">{{ taskCount() }}</span>
</div>
<div class="stat-row">
<span class="stat-label">Time</span>
<span class="stat-value mono">{{ currentTime() }}</span>
</div>
</div>
<!-- Quick Actions -->
<div class="actions-section">
<div class="section-header">Quick Actions</div>
<div class="actions-list">
<button class="action-btn" (click)="emitAction('new-project')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
<span>New Project</span>
</button>
<button class="action-btn" (click)="emitAction('open-project')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/>
</svg>
<span>Open Project</span>
</button>
<button class="action-btn" (click)="emitAction('run-dev')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Run Dev Server</span>
</button>
<button class="action-btn danger" (click)="emitAction('stop-all')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>
<span>Stop All</span>
</button>
</div>
</div>
<!-- Recent Projects -->
<div class="projects-section">
<div class="section-header">Recent Projects</div>
<div class="projects-list">
@for (project of recentProjects(); track project.name) {
<button class="project-item" (click)="openProject(project.path)">
<div class="project-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
</div>
<div class="project-info">
<span class="project-name">{{ project.name }}</span>
<span class="project-path">{{ project.path }}</span>
</div>
</button>
} @empty {
<div class="no-projects">No recent projects</div>
}
</div>
</div>
<!-- Footer -->
<div class="tray-footer">
<div class="connection-status" [class.connected]="isActive()">
<div class="status-dot"></div>
<span>{{ isActive() ? 'Services Running' : 'Ready' }}</span>
</div>
<button class="footer-btn" (click)="openIDE()">Open IDE</button>
</div>
</div>
`,
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);
}
}

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Core IDE</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/assets/wails.png"/>
</head>
<body>
<app-root></app-root>
</body>
</html>

View file

@ -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;

View file

@ -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));

View file

@ -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);

View file

@ -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;
}

View file

@ -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"
]
}

View file

@ -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"
}
]
}

View file

@ -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"
]
}

54
internal/core-ide/go.mod Normal file
View file

@ -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

151
internal/core-ide/go.sum Normal file
View file

@ -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=

View file

@ -0,0 +1,7 @@
package main
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello " + name + "!"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Some files were not shown because too many files have changed in this diff Show more