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>
32
.core/build.yaml
Normal 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
|
|
@ -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
|
||||||
|
|
@ -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`
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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?
|
|
||||||
16
go.mod
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
131
internal/cmd/ai/cmd_metrics.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
312
internal/cmd/deploy/cmd_ansible.go
Normal 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
|
||||||
|
}
|
||||||
15
internal/cmd/deploy/cmd_commands.go
Normal 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)
|
||||||
|
}
|
||||||
280
internal/cmd/deploy/cmd_deploy.go
Normal 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]), ¶ms); err != nil {
|
||||||
|
return fmt.Errorf("invalid JSON params: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := client.Call(context.Background(), operation, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputResult(result)
|
||||||
|
}
|
||||||
86
internal/cmd/rag/cmd_collections.go
Normal 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
|
||||||
|
}
|
||||||
21
internal/cmd/rag/cmd_commands.go
Normal 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)
|
||||||
|
}
|
||||||
173
internal/cmd/rag/cmd_ingest.go
Normal 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
110
internal/cmd/rag/cmd_query.go
Normal 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
|
||||||
|
}
|
||||||
84
internal/cmd/rag/cmd_rag.go
Normal 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"))
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
229
internal/cmd/security/cmd_jobs.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
||||||
|
.task
|
||||||
|
.idea
|
||||||
|
bin
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
build/linux/appimage/build
|
||||||
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
71
internal/core-ide/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Wails3 Angular Template
|
||||||
|
|
||||||
|
- Angular 20
|
||||||
|
- Wails3
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
34
internal/core-ide/Taskfile.yml
Normal 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}}
|
||||||
|
|
||||||
91
internal/core-ide/build/Taskfile.yml
Normal 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 .
|
||||||
BIN
internal/core-ide/build/appicon.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
62
internal/core-ide/build/config.yml
Normal 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
|
||||||
32
internal/core-ide/build/darwin/Info.dev.plist
Normal 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>
|
||||||
27
internal/core-ide/build/darwin/Info.plist
Normal 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>
|
||||||
85
internal/core-ide/build/darwin/Taskfile.yml
Normal 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}}'
|
||||||
BIN
internal/core-ide/build/darwin/icons.icns
Normal file
119
internal/core-ide/build/linux/Taskfile.yml
Normal 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}}'
|
||||||
40
internal/core-ide/build/linux/appimage/build.sh
Normal 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"
|
||||||
|
|
||||||
13
internal/core-ide/build/linux/desktop
Normal 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
|
||||||
|
|
||||||
|
|
||||||
67
internal/core-ide/build/linux/nfpm/nfpm.yaml
Normal 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"
|
||||||
21
internal/core-ide/build/linux/nfpm/scripts/postinstall.sh
Normal 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
|
||||||
1
internal/core-ide/build/linux/nfpm/scripts/postremove.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
||||||
1
internal/core-ide/build/linux/nfpm/scripts/preinstall.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
||||||
1
internal/core-ide/build/linux/nfpm/scripts/preremove.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
||||||
98
internal/core-ide/build/windows/Taskfile.yml
Normal 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'
|
||||||
BIN
internal/core-ide/build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
15
internal/core-ide/build/windows/info.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
internal/core-ide/build/windows/msix/app_manifest.xml
Normal 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>
|
||||||
54
internal/core-ide/build/windows/msix/template.xml
Normal 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>
|
||||||
114
internal/core-ide/build/windows/nsis/project.nsi
Normal 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
|
||||||
236
internal/core-ide/build/windows/nsis/wails_tools.nsh
Normal 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
|
||||||
22
internal/core-ide/build/windows/wails.exe.manifest
Normal 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>
|
||||||
183
internal/core-ide/claude_bridge.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
internal/core-ide/frontend/.editorconfig
Normal 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
|
|
@ -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
|
||||||
59
internal/core-ide/frontend/README.md
Normal 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.
|
||||||
98
internal/core-ide/frontend/angular.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
internal/core-ide/frontend/bindings/changeme/greetservice.ts
Normal 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);
|
||||||
|
}
|
||||||
7
internal/core-ide/frontend/bindings/changeme/index.ts
Normal 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
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
2
internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
9759
internal/core-ide/frontend/package-lock.json
generated
Normal file
57
internal/core-ide/frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
internal/core-ide/frontend/public/Inter Font License.txt
Normal 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.
|
||||||
BIN
internal/core-ide/frontend/public/Inter-Medium.ttf
Normal file
BIN
internal/core-ide/frontend/public/angular.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
1
internal/core-ide/frontend/public/javascript.svg
Normal 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 |
157
internal/core-ide/frontend/public/style.css
Normal 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);
|
||||||
|
}
|
||||||
BIN
internal/core-ide/frontend/public/wails.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
12
internal/core-ide/frontend/src/app/app.config.server.ts
Normal 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);
|
||||||
13
internal/core-ide/frontend/src/app/app.config.ts
Normal 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())
|
||||||
|
]
|
||||||
|
};
|
||||||
23
internal/core-ide/frontend/src/app/app.html
Normal 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 />
|
||||||
8
internal/core-ide/frontend/src/app/app.routes.server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||||
|
|
||||||
|
export const serverRoutes: ServerRoute[] = [
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
renderMode: RenderMode.Client
|
||||||
|
}
|
||||||
|
];
|
||||||
17
internal/core-ide/frontend/src/app/app.routes.ts
Normal 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' },
|
||||||
|
];
|
||||||
0
internal/core-ide/frontend/src/app/app.scss
Normal file
23
internal/core-ide/frontend/src/app/app.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
17
internal/core-ide/frontend/src/app/app.ts
Normal 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 {}
|
||||||
|
|
@ -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>`)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
506
internal/core-ide/frontend/src/app/pages/ide/ide.component.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
444
internal/core-ide/frontend/src/app/pages/tray/tray.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
internal/core-ide/frontend/src/index.html
Normal 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>
|
||||||
8
internal/core-ide/frontend/src/main.server.ts
Normal 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;
|
||||||
6
internal/core-ide/frontend/src/main.ts
Normal 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));
|
||||||
68
internal/core-ide/frontend/src/server.ts
Normal 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);
|
||||||
63
internal/core-ide/frontend/src/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
17
internal/core-ide/frontend/tsconfig.app.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
41
internal/core-ide/frontend/tsconfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
14
internal/core-ide/frontend/tsconfig.spec.json
Normal 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
|
|
@ -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
|
|
@ -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=
|
||||||
7
internal/core-ide/greetservice.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type GreetService struct{}
|
||||||
|
|
||||||
|
func (g *GreetService) Greet(name string) string {
|
||||||
|
return "Hello " + name + "!"
|
||||||
|
}
|
||||||
BIN
internal/core-ide/icons/apptray.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
19
internal/core-ide/icons/icons.go
Normal 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
|
||||||
BIN
internal/core-ide/icons/systray-default.png
Normal file
|
After Width: | Height: | Size: 228 B |
BIN
internal/core-ide/icons/systray-mac-template.png
Normal file
|
After Width: | Height: | Size: 167 B |