Compare commits

...

2 commits

Author SHA1 Message Date
Claude
47e11e7861
refactor: flatten commands, extract php/ci variants to own repos
- Remove internal/cmd/php/ (now core/php repo)
- Remove internal/cmd/ci/ and internal/cmd/sdk/ (now core/ci repo)
- Remove internal/variants/ build tag system entirely
- Move all 30 remaining command packages from internal/cmd/ to cmd/
- Rewrite main.go with direct imports (no more variant selection)
- Update all cross-references from internal/cmd/ to cmd/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:44:28 +00:00
197ee400b1 refactor: split CLI from monorepo, import core/go as library (#1)
Some checks are pending
Security Scan / Go Vulnerability Check (push) Waiting to run
Security Scan / Secret Detection (push) Waiting to run
Security Scan / Dependency & Config Scan (push) Waiting to run
- Change module from forge.lthn.ai/core/go to forge.lthn.ai/core/cli
- Remove pkg/ directory (now served from core/go)
- Add require + replace for forge.lthn.ai/core/go => ../go
- Update go.work to include ../go workspace module
- Fix all internal/cmd/* imports: pkg/ refs → forge.lthn.ai/core/go/pkg/
- Rename internal/cmd/sdk package to sdkcmd (avoids conflict with pkg/sdk)
- Remove SDK library files from internal/cmd/sdk/ (now in core/go/pkg/sdk/)
- Remove duplicate RAG helper functions from internal/cmd/rag/
- Remove stale cmd/core-ide/ (now in core/ide repo)
- Update IDE variant to remove core-ide import
- Fix test assertion for new module name
- Run go mod tidy to sync dependencies

core/cli is now a pure CLI application importing core/go for packages.

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

Co-authored-by: Claude <developers@lethean.io>
Reviewed-on: #1
2026-02-16 14:24:37 +00:00
997 changed files with 535 additions and 197510 deletions

BIN
cli Executable file

Binary file not shown.

View file

@ -7,9 +7,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/agentci" "forge.lthn.ai/core/go/pkg/agentci"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/config" "forge.lthn.ai/core/go/pkg/config"
) )
// AddAgentCommands registers the 'agent' subcommand group under 'ai'. // AddAgentCommands registers the 'agent' subcommand group under 'ai'.

View file

@ -3,7 +3,7 @@
package ai package ai
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
) )
// Style aliases from shared package // Style aliases from shared package

View file

@ -13,9 +13,9 @@
package ai package ai
import ( import (
ragcmd "forge.lthn.ai/core/cli/internal/cmd/rag" ragcmd "forge.lthn.ai/core/cli/cmd/rag"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
func init() { func init() {

View file

@ -16,8 +16,8 @@ import (
"syscall" "syscall"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/log" "forge.lthn.ai/core/go/pkg/log"
) )
// AddDispatchCommands registers the 'dispatch' subcommand group under 'ai'. // AddDispatchCommands registers the 'dispatch' subcommand group under 'ai'.

View file

@ -10,9 +10,9 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// task:commit command flags // task:commit command flags

View file

@ -7,9 +7,9 @@ import (
"fmt" "fmt"
"time" "time"
"forge.lthn.ai/core/cli/pkg/ai" "forge.lthn.ai/core/go/pkg/ai"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
var ( var (

View file

@ -7,9 +7,9 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/config" "forge.lthn.ai/core/go/pkg/config"
"forge.lthn.ai/core/cli/pkg/ratelimit" "forge.lthn.ai/core/go/pkg/ratelimit"
) )
// AddRateLimitCommands registers the 'ratelimits' subcommand group under 'ai'. // AddRateLimitCommands registers the 'ratelimits' subcommand group under 'ai'.

View file

@ -9,10 +9,10 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/ai" "forge.lthn.ai/core/go/pkg/ai"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// tasks command flags // tasks command flags

View file

@ -6,10 +6,10 @@ import (
"context" "context"
"time" "time"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/ai" "forge.lthn.ai/core/go/pkg/ai"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// task:update command flags // task:update command flags

View file

@ -3,8 +3,8 @@ package ai
import ( import (
"context" "context"
"forge.lthn.ai/core/cli/pkg/log" "forge.lthn.ai/core/go/pkg/log"
"forge.lthn.ai/core/cli/pkg/ratelimit" "forge.lthn.ai/core/go/pkg/ratelimit"
) )
// executeWithRateLimit wraps an agent execution with rate limiting logic. // executeWithRateLimit wraps an agent execution with rate limiting logic.

View file

@ -17,8 +17,8 @@ import (
"strings" "strings"
"forge.lthn.ai/core/go/cmd/bugseti/icons" "forge.lthn.ai/core/go/cmd/bugseti/icons"
"forge.lthn.ai/core/go/internal/bugseti" "forge.lthn.ai/core/cli/internal/bugseti"
"forge.lthn.ai/core/go/internal/bugseti/updater" "forge.lthn.ai/core/cli/internal/bugseti/updater"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/events"
) )

View file

@ -5,7 +5,7 @@ import (
"context" "context"
"log" "log"
"forge.lthn.ai/core/go/internal/bugseti" "forge.lthn.ai/core/cli/internal/bugseti"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )

View file

@ -11,7 +11,7 @@ import (
"sync" "sync"
"time" "time"
"forge.lthn.ai/core/go/internal/bugseti" "forge.lthn.ai/core/cli/internal/bugseti"
"forge.lthn.ai/core/go/pkg/io/datanode" "forge.lthn.ai/core/go/pkg/io/datanode"
"github.com/Snider/Borg/pkg/tim" "github.com/Snider/Borg/pkg/tim"
) )

View file

@ -3,10 +3,10 @@ package collect
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
) )
func init() { func init() {

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// BitcoinTalk command flags // BitcoinTalk command flags

View file

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
collectpkg "forge.lthn.ai/core/cli/pkg/collect" collectpkg "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// addDispatchCommand adds the 'dispatch' subcommand to the collect parent. // addDispatchCommand adds the 'dispatch' subcommand to the collect parent.

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Excavate command flags // Excavate command flags

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// GitHub command flags // GitHub command flags

View file

@ -3,9 +3,9 @@ package collect
import ( import (
"context" "context"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Market command flags // Market command flags

View file

@ -3,9 +3,9 @@ package collect
import ( import (
"context" "context"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Papers command flags // Papers command flags

View file

@ -3,9 +3,9 @@ package collect
import ( import (
"context" "context"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/collect" "forge.lthn.ai/core/go/pkg/collect"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// addProcessCommand adds the 'process' subcommand to the collect parent. // addProcessCommand adds the 'process' subcommand to the collect parent.

View file

@ -1,6 +1,6 @@
package config package config
import "forge.lthn.ai/core/cli/pkg/cli" import "forge.lthn.ai/core/go/pkg/cli"
func init() { func init() {
cli.RegisterCommands(AddConfigCommands) cli.RegisterCommands(AddConfigCommands)

View file

@ -3,8 +3,8 @@ package config
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/config" "forge.lthn.ai/core/go/pkg/config"
) )
func addGetCommand(parent *cli.Command) { func addGetCommand(parent *cli.Command) {

View file

@ -3,7 +3,7 @@ package config
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View file

@ -3,7 +3,7 @@ package config
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
) )
func addPathCommand(parent *cli.Command) { func addPathCommand(parent *cli.Command) {

View file

@ -1,7 +1,7 @@
package config package config
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
) )
func addSetCommand(parent *cli.Command) { func addSetCommand(parent *cli.Command) {

View file

@ -1,71 +0,0 @@
package main
import (
"context"
"log"
"time"
"forge.lthn.ai/core/go/pkg/mcp/ide"
"github.com/wailsapp/wails/v3/pkg/application"
)
// BuildService provides build monitoring bindings for the frontend.
type BuildService struct {
ideSub *ide.Subsystem
}
// NewBuildService creates a new BuildService.
func NewBuildService(ideSub *ide.Subsystem) *BuildService {
return &BuildService{ideSub: ideSub}
}
// ServiceName returns the service name for Wails.
func (s *BuildService) ServiceName() string { return "BuildService" }
// ServiceStartup is called when the Wails application starts.
func (s *BuildService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
log.Println("BuildService started")
return nil
}
// ServiceShutdown is called when the Wails application shuts down.
func (s *BuildService) ServiceShutdown() error {
log.Println("BuildService shutdown")
return nil
}
// BuildDTO is a build for the frontend.
type BuildDTO struct {
ID string `json:"id"`
Repo string `json:"repo"`
Branch string `json:"branch"`
Status string `json:"status"`
Duration string `json:"duration,omitempty"`
StartedAt time.Time `json:"startedAt"`
}
// GetBuilds returns recent builds.
func (s *BuildService) GetBuilds(repo string) []BuildDTO {
bridge := s.ideSub.Bridge()
if bridge == nil {
return []BuildDTO{}
}
_ = bridge.Send(ide.BridgeMessage{
Type: "build_list",
Data: map[string]any{"repo": repo},
})
return []BuildDTO{}
}
// GetBuildLogs returns log output for a specific build.
func (s *BuildService) GetBuildLogs(buildID string) []string {
bridge := s.ideSub.Bridge()
if bridge == nil {
return []string{}
}
_ = bridge.Send(ide.BridgeMessage{
Type: "build_logs",
Data: map[string]any{"buildId": buildID},
})
return []string{}
}

View file

@ -1,135 +0,0 @@
package main
import (
"context"
"log"
"time"
"forge.lthn.ai/core/go/pkg/mcp/ide"
"github.com/wailsapp/wails/v3/pkg/application"
)
// ChatService provides chat bindings for the frontend.
type ChatService struct {
ideSub *ide.Subsystem
}
// NewChatService creates a new ChatService.
func NewChatService(ideSub *ide.Subsystem) *ChatService {
return &ChatService{ideSub: ideSub}
}
// ServiceName returns the service name for Wails.
func (s *ChatService) ServiceName() string { return "ChatService" }
// ServiceStartup is called when the Wails application starts.
func (s *ChatService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
log.Println("ChatService started")
return nil
}
// ServiceShutdown is called when the Wails application shuts down.
func (s *ChatService) ServiceShutdown() error {
log.Println("ChatService shutdown")
return nil
}
// ChatMessageDTO is a message for the frontend.
type ChatMessageDTO struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
// SessionDTO is a session for the frontend.
type SessionDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
}
// PlanStepDTO is a plan step for the frontend.
type PlanStepDTO struct {
Name string `json:"name"`
Status string `json:"status"`
}
// PlanDTO is a plan for the frontend.
type PlanDTO struct {
SessionID string `json:"sessionId"`
Status string `json:"status"`
Steps []PlanStepDTO `json:"steps"`
}
// SendMessage sends a message to an agent session via the bridge.
func (s *ChatService) SendMessage(sessionID string, message string) (bool, error) {
bridge := s.ideSub.Bridge()
if bridge == nil {
return false, nil
}
err := bridge.Send(ide.BridgeMessage{
Type: "chat_send",
Channel: "chat:" + sessionID,
SessionID: sessionID,
Data: message,
})
return err == nil, err
}
// GetHistory retrieves message history for a session.
func (s *ChatService) GetHistory(sessionID string) []ChatMessageDTO {
bridge := s.ideSub.Bridge()
if bridge == nil {
return []ChatMessageDTO{}
}
_ = bridge.Send(ide.BridgeMessage{
Type: "chat_history",
SessionID: sessionID,
})
return []ChatMessageDTO{}
}
// ListSessions returns active agent sessions.
func (s *ChatService) ListSessions() []SessionDTO {
bridge := s.ideSub.Bridge()
if bridge == nil {
return []SessionDTO{}
}
_ = bridge.Send(ide.BridgeMessage{Type: "session_list"})
return []SessionDTO{}
}
// CreateSession creates a new agent session.
func (s *ChatService) CreateSession(name string) SessionDTO {
bridge := s.ideSub.Bridge()
if bridge == nil {
return SessionDTO{Name: name, Status: "offline"}
}
_ = bridge.Send(ide.BridgeMessage{
Type: "session_create",
Data: map[string]any{"name": name},
})
return SessionDTO{
Name: name,
Status: "creating",
CreatedAt: time.Now(),
}
}
// GetPlanStatus returns the plan status for a session.
func (s *ChatService) GetPlanStatus(sessionID string) PlanDTO {
bridge := s.ideSub.Bridge()
if bridge == nil {
return PlanDTO{SessionID: sessionID, Status: "offline"}
}
_ = bridge.Send(ide.BridgeMessage{
Type: "plan_status",
SessionID: sessionID,
})
return PlanDTO{
SessionID: sessionID,
Status: "unknown",
Steps: []PlanStepDTO{},
}
}

View file

@ -1,171 +0,0 @@
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket.
// This is the CLIENT bridge — it connects to the MCP core process on port 9876
// and relays messages bidirectionally with connected GUI WebSocket clients.
type ClaudeBridge struct {
mcpConn *websocket.Conn
mcpURL string
clients map[*websocket.Conn]bool
clientsMu sync.RWMutex
broadcast chan []byte
reconnectMu sync.Mutex
connected bool
}
// 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),
}
}
// Connected reports whether the bridge is connected to MCP core.
func (cb *ClaudeBridge) Connected() bool {
cb.reconnectMu.Lock()
defer cb.reconnectMu.Unlock()
return cb.connected
}
// 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("ide bridge: connect to MCP at %s", cb.mcpURL)
conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil)
if err != nil {
log.Printf("ide bridge: connect failed: %v", err)
cb.connected = false
cb.reconnectMu.Unlock()
time.Sleep(5 * time.Second)
continue
}
cb.mcpConn = conn
cb.connected = true
cb.reconnectMu.Unlock()
log.Println("ide bridge: connected to MCP core")
// Read messages from MCP and broadcast to GUI clients
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("ide bridge: MCP read error: %v", err)
break
}
cb.broadcast <- message
}
cb.reconnectMu.Lock()
cb.connected = false
cb.reconnectMu.Unlock()
// Connection lost, retry after delay
time.Sleep(2 * time.Second)
}
}
// broadcastLoop sends messages from MCP core to all connected GUI clients.
func (cb *ClaudeBridge) broadcastLoop() {
for message := range cb.broadcast {
cb.clientsMu.RLock()
for client := range cb.clients {
if err := client.WriteMessage(websocket.TextMessage, message); err != nil {
log.Printf("ide bridge: client write error: %v", err)
}
}
cb.clientsMu.RUnlock()
}
}
// 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("ide bridge: upgrade error: %v", err)
return
}
cb.clientsMu.Lock()
cb.clients[conn] = true
cb.clientsMu.Unlock()
// Send connected message
connMsg, _ := json.Marshal(map[string]any{
"type": "system",
"data": "Connected to Claude bridge",
"timestamp": time.Now(),
})
conn.WriteMessage(websocket.TextMessage, connMsg)
defer func() {
cb.clientsMu.Lock()
delete(cb.clients, conn)
cb.clientsMu.Unlock()
conn.Close()
}()
// Read messages from GUI client and forward to MCP core
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 core
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.Println("ide bridge: MCP not connected, dropping message")
return
}
if err := cb.mcpConn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Printf("ide bridge: MCP write error: %v", err)
}
}

View file

@ -1,91 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"core-ide": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"standalone": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/core-ide",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "core-ide:build:production"
},
"development": {
"buildTarget": "core-ide:build:development"
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
{
"name": "core-ide",
"version": "0.1.0",
"private": true,
"scripts": {
"ng": "ng",
"start": "ng serve",
"dev": "ng serve --configuration development",
"build": "ng build --configuration production",
"build:dev": "ng build --configuration development",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint"
},
"dependencies": {
"@angular/animations": "^19.1.0",
"@angular/common": "^19.1.0",
"@angular/compiler": "^19.1.0",
"@angular/core": "^19.1.0",
"@angular/forms": "^19.1.0",
"@angular/platform-browser": "^19.1.0",
"@angular/platform-browser-dynamic": "^19.1.0",
"@angular/router": "^19.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.0",
"@angular/cli": "^21.1.2",
"@angular/compiler-cli": "^19.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.5.2"
}
}

View file

@ -1,18 +0,0 @@
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;
height: 100%;
}
`]
})
export class AppComponent {
title = 'Core IDE';
}

View file

@ -1,9 +0,0 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
]
};

View file

@ -1,25 +0,0 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: 'tray',
pathMatch: 'full'
},
{
path: 'tray',
loadComponent: () => import('./tray/tray.component').then(m => m.TrayComponent)
},
{
path: 'main',
loadComponent: () => import('./main/main.component').then(m => m.MainComponent)
},
{
path: 'settings',
loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent)
},
{
path: 'jellyfin',
loadComponent: () => import('./jellyfin/jellyfin.component').then(m => m.JellyfinComponent)
}
];

View file

@ -1,184 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WailsService, Build } from '@shared/wails.service';
import { WebSocketService, WSMessage } from '@shared/ws.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-build',
standalone: true,
imports: [CommonModule],
template: `
<div class="builds">
<div class="builds__header">
<h2>Builds</h2>
<button class="btn btn--secondary" (click)="refresh()">Refresh</button>
</div>
<div class="builds__list">
<div
*ngFor="let build of builds; trackBy: trackBuild"
class="build-card"
[class.build-card--expanded]="expandedId === build.id"
(click)="toggle(build.id)"
>
<div class="build-card__header">
<div class="build-card__info">
<span class="build-card__repo">{{ build.repo }}</span>
<span class="build-card__branch text-muted">{{ build.branch }}</span>
</div>
<span class="badge" [class]="statusBadge(build.status)">{{ build.status }}</span>
</div>
<div class="build-card__meta text-muted">
{{ build.startedAt | date:'medium' }}
<span *ngIf="build.duration"> &middot; {{ build.duration }}</span>
</div>
<div *ngIf="expandedId === build.id" class="build-card__logs">
<pre *ngIf="logs.length > 0">{{ logs.join('\\n') }}</pre>
<p *ngIf="logs.length === 0" class="text-muted">No logs available</p>
</div>
</div>
<div *ngIf="builds.length === 0" class="builds__empty text-muted">
No builds found. Builds will appear here from Forgejo CI.
</div>
</div>
</div>
`,
styles: [`
.builds {
padding: var(--spacing-md);
}
.builds__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.builds__list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.build-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-md);
cursor: pointer;
transition: border-color 0.15s;
&:hover {
border-color: var(--text-muted);
}
}
.build-card__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
}
.build-card__info {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.build-card__repo {
font-weight: 600;
}
.build-card__branch {
font-size: 12px;
}
.build-card__meta {
font-size: 12px;
}
.build-card__logs {
margin-top: var(--spacing-md);
border-top: 1px solid var(--border-color);
padding-top: var(--spacing-md);
}
.build-card__logs pre {
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.builds__empty {
text-align: center;
padding: var(--spacing-xl);
}
`]
})
export class BuildComponent implements OnInit, OnDestroy {
builds: Build[] = [];
expandedId = '';
logs: string[] = [];
private sub: Subscription | null = null;
constructor(
private wails: WailsService,
private wsService: WebSocketService
) {}
ngOnInit(): void {
this.refresh();
this.wsService.connect();
this.sub = this.wsService.subscribe('build:status').subscribe(
(msg: WSMessage) => {
if (msg.data && typeof msg.data === 'object') {
const update = msg.data as Build;
const idx = this.builds.findIndex(b => b.id === update.id);
if (idx >= 0) {
this.builds[idx] = { ...this.builds[idx], ...update };
} else {
this.builds.unshift(update);
}
}
}
);
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
async refresh(): Promise<void> {
this.builds = await this.wails.getBuilds();
}
async toggle(buildId: string): Promise<void> {
if (this.expandedId === buildId) {
this.expandedId = '';
this.logs = [];
return;
}
this.expandedId = buildId;
this.logs = await this.wails.getBuildLogs(buildId);
}
trackBuild(_: number, build: Build): string {
return build.id;
}
statusBadge(status: string): string {
switch (status) {
case 'success': return 'badge--success';
case 'running': return 'badge--info';
case 'failed': return 'badge--danger';
default: return 'badge--warning';
}
}
}

View file

@ -1,242 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { WailsService, ChatMessage, Session, PlanStatus } from '@shared/wails.service';
import { WebSocketService, WSMessage } from '@shared/ws.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="chat">
<div class="chat__header">
<div class="chat__session-picker">
<select class="form-select" [(ngModel)]="activeSessionId" (ngModelChange)="onSessionChange()">
<option *ngFor="let s of sessions" [value]="s.id">{{ s.name }} ({{ s.status }})</option>
</select>
<button class="btn btn--ghost" (click)="createSession()">+ New</button>
</div>
</div>
<div class="chat__body">
<div class="chat__messages">
<div
*ngFor="let msg of messages"
class="chat__msg"
[class.chat__msg--user]="msg.role === 'user'"
[class.chat__msg--agent]="msg.role === 'agent'"
>
<div class="chat__msg-role">{{ msg.role }}</div>
<div class="chat__msg-content">{{ msg.content }}</div>
</div>
<div *ngIf="messages.length === 0" class="chat__empty text-muted">
No messages yet. Start a conversation with an agent.
</div>
</div>
<div *ngIf="plan.steps.length > 0" class="chat__plan">
<h4>Plan: {{ plan.status }}</h4>
<ul>
<li *ngFor="let step of plan.steps" [class]="'plan-step plan-step--' + step.status">
{{ step.name }}
<span class="badge badge--info">{{ step.status }}</span>
</li>
</ul>
</div>
</div>
<div class="chat__input">
<textarea
class="form-textarea"
[(ngModel)]="draft"
(keydown.enter)="sendMessage($any($event))"
placeholder="Type a message... (Enter to send)"
rows="2"
></textarea>
<button class="btn btn--primary" (click)="sendMessage()" [disabled]="!draft.trim()">Send</button>
</div>
</div>
`,
styles: [`
.chat {
display: flex;
flex-direction: column;
height: 100%;
}
.chat__header {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.chat__session-picker {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.chat__session-picker select {
flex: 1;
}
.chat__body {
flex: 1;
display: flex;
overflow: hidden;
}
.chat__messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.chat__msg {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
max-width: 80%;
}
.chat__msg--user {
align-self: flex-end;
background: rgba(57, 208, 216, 0.12);
border: 1px solid rgba(57, 208, 216, 0.2);
}
.chat__msg--agent {
align-self: flex-start;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.chat__msg-role {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 2px;
}
.chat__msg-content {
white-space: pre-wrap;
word-break: break-word;
}
.chat__empty {
margin: auto;
text-align: center;
}
.chat__plan {
width: 260px;
border-left: 1px solid var(--border-color);
padding: var(--spacing-md);
overflow-y: auto;
}
.chat__plan ul {
list-style: none;
margin-top: var(--spacing-sm);
}
.chat__plan li {
padding: var(--spacing-xs) 0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.chat__input {
padding: var(--spacing-sm) var(--spacing-md);
border-top: 1px solid var(--border-color);
display: flex;
gap: var(--spacing-sm);
align-items: flex-end;
}
.chat__input textarea {
flex: 1;
resize: none;
}
`]
})
export class ChatComponent implements OnInit, OnDestroy {
sessions: Session[] = [];
activeSessionId = '';
messages: ChatMessage[] = [];
plan: PlanStatus = { sessionId: '', status: '', steps: [] };
draft = '';
private sub: Subscription | null = null;
constructor(
private wails: WailsService,
private wsService: WebSocketService
) {}
ngOnInit(): void {
this.loadSessions();
this.wsService.connect();
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
async loadSessions(): Promise<void> {
this.sessions = await this.wails.listSessions();
if (this.sessions.length > 0 && !this.activeSessionId) {
this.activeSessionId = this.sessions[0].id;
this.onSessionChange();
}
}
async onSessionChange(): Promise<void> {
if (!this.activeSessionId) return;
// Unsubscribe from previous channel
this.sub?.unsubscribe();
// Load history and plan
this.messages = await this.wails.getHistory(this.activeSessionId);
this.plan = await this.wails.getPlanStatus(this.activeSessionId);
// Subscribe to live updates
this.sub = this.wsService.subscribe(`chat:${this.activeSessionId}`).subscribe(
(msg: WSMessage) => {
if (msg.data && typeof msg.data === 'object') {
this.messages.push(msg.data as ChatMessage);
}
}
);
}
async sendMessage(event?: KeyboardEvent): Promise<void> {
if (event) {
if (event.shiftKey) return; // Allow shift+enter for newlines
event.preventDefault();
}
const text = this.draft.trim();
if (!text || !this.activeSessionId) return;
// Optimistic UI update
this.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString() });
this.draft = '';
await this.wails.sendMessage(this.activeSessionId, text);
}
async createSession(): Promise<void> {
const name = `Session ${this.sessions.length + 1}`;
const session = await this.wails.createSession(name);
this.sessions.push(session);
this.activeSessionId = session.id;
this.onSessionChange();
}
}

View file

@ -1,163 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WailsService, DashboardData } from '@shared/wails.service';
import { WebSocketService, WSMessage } from '@shared/ws.service';
import { Subscription } from 'rxjs';
interface ActivityItem {
type: string;
message: string;
timestamp: string;
}
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="dashboard">
<h2>Dashboard</h2>
<div class="dashboard__grid">
<div class="stat-card">
<div class="stat-card__value" [class.text-success]="data.connection.bridgeConnected">
{{ data.connection.bridgeConnected ? 'Online' : 'Offline' }}
</div>
<div class="stat-card__label">Bridge Status</div>
</div>
<div class="stat-card">
<div class="stat-card__value">{{ data.connection.wsClients }}</div>
<div class="stat-card__label">WS Clients</div>
</div>
<div class="stat-card">
<div class="stat-card__value">{{ data.connection.wsChannels }}</div>
<div class="stat-card__label">Active Channels</div>
</div>
<div class="stat-card">
<div class="stat-card__value">0</div>
<div class="stat-card__label">Agent Sessions</div>
</div>
</div>
<div class="dashboard__activity">
<h3>Activity Feed</h3>
<div class="activity-feed">
<div *ngFor="let item of activity" class="activity-item">
<span class="activity-item__badge badge badge--info">{{ item.type }}</span>
<span class="activity-item__msg">{{ item.message }}</span>
<span class="activity-item__time text-muted">{{ item.timestamp | date:'shortTime' }}</span>
</div>
<div *ngIf="activity.length === 0" class="text-muted" style="text-align: center; padding: var(--spacing-lg);">
No recent activity. Events will stream here in real-time.
</div>
</div>
</div>
</div>
`,
styles: [`
.dashboard {
padding: var(--spacing-md);
}
.dashboard__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--spacing-md);
margin: var(--spacing-md) 0;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
text-align: center;
}
.stat-card__value {
font-size: 28px;
font-weight: 700;
color: var(--accent-primary);
}
.stat-card__label {
font-size: 13px;
color: var(--text-muted);
margin-top: var(--spacing-xs);
}
.dashboard__activity {
margin-top: var(--spacing-lg);
}
.activity-feed {
margin-top: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
max-height: 400px;
overflow-y: auto;
}
.activity-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
font-size: 13px;
&:last-child {
border-bottom: none;
}
}
.activity-item__msg {
flex: 1;
}
.activity-item__time {
font-size: 12px;
white-space: nowrap;
}
`]
})
export class DashboardComponent implements OnInit, OnDestroy {
data: DashboardData = {
connection: { bridgeConnected: false, laravelUrl: '', wsClients: 0, wsChannels: 0 }
};
activity: ActivityItem[] = [];
private sub: Subscription | null = null;
private pollTimer: ReturnType<typeof setInterval> | null = null;
constructor(
private wails: WailsService,
private wsService: WebSocketService
) {}
ngOnInit(): void {
this.refresh();
this.pollTimer = setInterval(() => this.refresh(), 10000);
this.wsService.connect();
this.sub = this.wsService.subscribe('dashboard:activity').subscribe(
(msg: WSMessage) => {
if (msg.data && typeof msg.data === 'object') {
this.activity.unshift(msg.data as ActivityItem);
if (this.activity.length > 100) {
this.activity.pop();
}
}
}
);
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
if (this.pollTimer) clearInterval(this.pollTimer);
}
async refresh(): Promise<void> {
this.data = await this.wails.getDashboard();
}
}

View file

@ -1,177 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
type Mode = 'web' | 'stream';
@Component({
selector: 'app-jellyfin',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="jellyfin">
<header class="jellyfin__header">
<div>
<h2>Jellyfin Player</h2>
<p class="text-muted">Embedded media access for Host UK workflows.</p>
</div>
<div class="mode-switch">
<button class="btn btn--secondary" [class.is-active]="mode === 'web'" (click)="mode = 'web'">Web</button>
<button class="btn btn--secondary" [class.is-active]="mode === 'stream'" (click)="mode = 'stream'">Stream</button>
</div>
</header>
<section class="card">
<div class="form-group">
<label class="form-label">Jellyfin Server URL</label>
<input class="form-input" [(ngModel)]="serverUrl" placeholder="https://media.lthn.ai" />
</div>
<div *ngIf="mode === 'stream'" class="stream-grid">
<div class="form-group">
<label class="form-label">Item ID</label>
<input class="form-input" [(ngModel)]="itemId" placeholder="Jellyfin library item ID" />
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input class="form-input" [(ngModel)]="apiKey" placeholder="Jellyfin API key" />
</div>
<div class="form-group">
<label class="form-label">Media Source ID (optional)</label>
<input class="form-input" [(ngModel)]="mediaSourceId" placeholder="Source ID for multi-source items" />
</div>
</div>
<div class="actions">
<button class="btn btn--primary" (click)="load()">Load Player</button>
<button class="btn btn--secondary" (click)="reset()">Reset</button>
</div>
</section>
<section class="card viewer" *ngIf="loaded && mode === 'web'">
<iframe
class="jellyfin-frame"
title="Jellyfin Web"
[src]="safeWebUrl"
loading="lazy"
referrerpolicy="no-referrer"
></iframe>
</section>
<section class="card viewer" *ngIf="loaded && mode === 'stream'">
<video class="jellyfin-video" controls [src]="streamUrl"></video>
<p class="text-muted stream-hint" *ngIf="!streamUrl">Set Item ID and API key to build stream URL.</p>
</section>
</div>
`,
styles: [`
.jellyfin {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-md);
min-height: 100%;
background: var(--bg-primary);
}
.jellyfin__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
}
.mode-switch {
display: flex;
gap: var(--spacing-xs);
}
.mode-switch .btn.is-active {
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.stream-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--spacing-sm);
}
.actions {
display: flex;
gap: var(--spacing-sm);
}
.viewer {
padding: 0;
overflow: hidden;
min-height: 520px;
}
.jellyfin-frame,
.jellyfin-video {
border: 0;
width: 100%;
height: 100%;
min-height: 520px;
background: #000;
}
.stream-hint {
padding: var(--spacing-md);
margin: 0;
}
`]
})
export class JellyfinComponent {
mode: Mode = 'web';
loaded = false;
serverUrl = 'https://media.lthn.ai';
itemId = '';
apiKey = '';
mediaSourceId = '';
safeWebUrl!: SafeResourceUrl;
streamUrl = '';
constructor(private sanitizer: DomSanitizer) {
this.safeWebUrl = this.sanitizer.bypassSecurityTrustResourceUrl('https://media.lthn.ai/web/index.html');
}
load(): void {
const base = this.normalizeBase(this.serverUrl);
this.safeWebUrl = this.sanitizer.bypassSecurityTrustResourceUrl(`${base}/web/index.html`);
this.streamUrl = this.buildStreamUrl(base);
this.loaded = true;
}
reset(): void {
this.loaded = false;
this.itemId = '';
this.apiKey = '';
this.mediaSourceId = '';
this.streamUrl = '';
}
private normalizeBase(value: string): string {
const raw = value.trim() || 'https://media.lthn.ai';
const withProtocol = raw.startsWith('http://') || raw.startsWith('https://') ? raw : `https://${raw}`;
return withProtocol.replace(/\/+$/, '');
}
private buildStreamUrl(base: string): string {
if (!this.itemId.trim() || !this.apiKey.trim()) {
return '';
}
const url = new URL(`${base}/Videos/${encodeURIComponent(this.itemId.trim())}/stream`);
url.searchParams.set('api_key', this.apiKey.trim());
url.searchParams.set('static', 'true');
if (this.mediaSourceId.trim()) {
url.searchParams.set('MediaSourceId', this.mediaSourceId.trim());
}
return url.toString();
}
}

View file

@ -1,118 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChatComponent } from '../chat/chat.component';
import { BuildComponent } from '../build/build.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { JellyfinComponent } from '../jellyfin/jellyfin.component';
type Panel = 'chat' | 'build' | 'dashboard' | 'jellyfin';
@Component({
selector: 'app-main',
standalone: true,
imports: [CommonModule, ChatComponent, BuildComponent, DashboardComponent, JellyfinComponent],
template: `
<div class="ide">
<nav class="ide__sidebar">
<div class="ide__logo">Core IDE</div>
<ul class="ide__nav">
<li
*ngFor="let item of navItems"
class="ide__nav-item"
[class.active]="activePanel === item.id"
(click)="activePanel = item.id"
>
<span class="ide__nav-icon">{{ item.icon }}</span>
<span class="ide__nav-label">{{ item.label }}</span>
</li>
</ul>
<div class="ide__nav-footer text-muted">v0.1.0</div>
</nav>
<main class="ide__content">
<app-chat *ngIf="activePanel === 'chat'" />
<app-build *ngIf="activePanel === 'build'" />
<app-dashboard *ngIf="activePanel === 'dashboard'" />
<app-jellyfin *ngIf="activePanel === 'jellyfin'" />
</main>
</div>
`,
styles: [`
.ide {
display: flex;
height: 100vh;
overflow: hidden;
}
.ide__sidebar {
width: var(--sidebar-width);
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: var(--spacing-md) 0;
flex-shrink: 0;
}
.ide__logo {
padding: 0 var(--spacing-md);
font-size: 16px;
font-weight: 700;
color: var(--accent-primary);
margin-bottom: var(--spacing-lg);
}
.ide__nav {
list-style: none;
flex: 1;
}
.ide__nav-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
border-left: 3px solid transparent;
&:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
&.active {
color: var(--accent-primary);
background: rgba(57, 208, 216, 0.08);
border-left-color: var(--accent-primary);
}
}
.ide__nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.ide__nav-footer {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 12px;
}
.ide__content {
flex: 1;
overflow: auto;
}
`]
})
export class MainComponent {
activePanel: Panel = 'dashboard';
navItems: { id: Panel; label: string; icon: string }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: '\u25A6' },
{ id: 'chat', label: 'Chat', icon: '\u2709' },
{ id: 'build', label: 'Builds', icon: '\u2699' },
{ id: 'jellyfin', label: 'Jellyfin', icon: '\u25B6' },
];
}

View file

@ -1,105 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="settings">
<h2>Settings</h2>
<div class="settings__section">
<h3>Connection</h3>
<div class="form-group">
<label class="form-label">Laravel WebSocket URL</label>
<input
class="form-input"
[(ngModel)]="laravelUrl"
placeholder="ws://localhost:9876/ws"
/>
</div>
<div class="form-group">
<label class="form-label">Workspace Root</label>
<input
class="form-input"
[(ngModel)]="workspaceRoot"
placeholder="/path/to/workspace"
/>
</div>
</div>
<div class="settings__section">
<h3>Appearance</h3>
<div class="form-group">
<label class="form-label">Theme</label>
<select class="form-select" [(ngModel)]="theme">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
</div>
<div class="settings__actions">
<button class="btn btn--primary" (click)="save()">Save Settings</button>
</div>
</div>
`,
styles: [`
.settings {
padding: var(--spacing-lg);
max-width: 500px;
}
.settings__section {
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
&:first-of-type {
margin-top: var(--spacing-md);
padding-top: 0;
border-top: none;
}
}
.settings__actions {
margin-top: var(--spacing-lg);
}
`]
})
export class SettingsComponent implements OnInit {
laravelUrl = 'ws://localhost:9876/ws';
workspaceRoot = '.';
theme = 'dark';
ngOnInit(): void {
// Settings will be loaded from the Go backend
const saved = localStorage.getItem('ide-settings');
if (saved) {
try {
const parsed = JSON.parse(saved);
this.laravelUrl = parsed.laravelUrl ?? this.laravelUrl;
this.workspaceRoot = parsed.workspaceRoot ?? this.workspaceRoot;
this.theme = parsed.theme ?? this.theme;
} catch {
// Ignore parse errors
}
}
}
save(): void {
localStorage.setItem('ide-settings', JSON.stringify({
laravelUrl: this.laravelUrl,
workspaceRoot: this.workspaceRoot,
theme: this.theme,
}));
if (this.theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
}

View file

@ -1,133 +0,0 @@
import { Injectable } from '@angular/core';
// Type-safe wrapper for Wails v3 Go service bindings.
// At runtime, `window.go.main.{ServiceName}.{Method}()` returns a Promise.
interface WailsGo {
main: {
IDEService: {
GetConnectionStatus(): Promise<ConnectionStatus>;
GetDashboard(): Promise<DashboardData>;
ShowWindow(name: string): Promise<void>;
};
ChatService: {
SendMessage(sessionId: string, message: string): Promise<boolean>;
GetHistory(sessionId: string): Promise<ChatMessage[]>;
ListSessions(): Promise<Session[]>;
CreateSession(name: string): Promise<Session>;
GetPlanStatus(sessionId: string): Promise<PlanStatus>;
};
BuildService: {
GetBuilds(repo: string): Promise<Build[]>;
GetBuildLogs(buildId: string): Promise<string[]>;
};
};
}
export interface ConnectionStatus {
bridgeConnected: boolean;
laravelUrl: string;
wsClients: number;
wsChannels: number;
}
export interface DashboardData {
connection: ConnectionStatus;
}
export interface ChatMessage {
role: string;
content: string;
timestamp: string;
}
export interface Session {
id: string;
name: string;
status: string;
createdAt: string;
}
export interface PlanStatus {
sessionId: string;
status: string;
steps: PlanStep[];
}
export interface PlanStep {
name: string;
status: string;
}
export interface Build {
id: string;
repo: string;
branch: string;
status: string;
duration?: string;
startedAt: string;
}
declare global {
interface Window {
go: WailsGo;
}
}
@Injectable({ providedIn: 'root' })
export class WailsService {
private get ide() { return window.go?.main?.IDEService; }
private get chat() { return window.go?.main?.ChatService; }
private get build() { return window.go?.main?.BuildService; }
// IDE
getConnectionStatus(): Promise<ConnectionStatus> {
return this.ide?.GetConnectionStatus() ?? Promise.resolve({
bridgeConnected: false, laravelUrl: '', wsClients: 0, wsChannels: 0
});
}
getDashboard(): Promise<DashboardData> {
return this.ide?.GetDashboard() ?? Promise.resolve({
connection: { bridgeConnected: false, laravelUrl: '', wsClients: 0, wsChannels: 0 }
});
}
showWindow(name: string): Promise<void> {
return this.ide?.ShowWindow(name) ?? Promise.resolve();
}
// Chat
sendMessage(sessionId: string, message: string): Promise<boolean> {
return this.chat?.SendMessage(sessionId, message) ?? Promise.resolve(false);
}
getHistory(sessionId: string): Promise<ChatMessage[]> {
return this.chat?.GetHistory(sessionId) ?? Promise.resolve([]);
}
listSessions(): Promise<Session[]> {
return this.chat?.ListSessions() ?? Promise.resolve([]);
}
createSession(name: string): Promise<Session> {
return this.chat?.CreateSession(name) ?? Promise.resolve({
id: '', name, status: 'offline', createdAt: ''
});
}
getPlanStatus(sessionId: string): Promise<PlanStatus> {
return this.chat?.GetPlanStatus(sessionId) ?? Promise.resolve({
sessionId, status: 'offline', steps: []
});
}
// Build
getBuilds(repo: string = ''): Promise<Build[]> {
return this.build?.GetBuilds(repo) ?? Promise.resolve([]);
}
getBuildLogs(buildId: string): Promise<string[]> {
return this.build?.GetBuildLogs(buildId) ?? Promise.resolve([]);
}
}

View file

@ -1,89 +0,0 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface WSMessage {
type: string;
channel?: string;
processId?: string;
data?: unknown;
timestamp: string;
}
@Injectable({ providedIn: 'root' })
export class WebSocketService implements OnDestroy {
private ws: WebSocket | null = null;
private messages$ = new Subject<WSMessage>();
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private url = 'ws://127.0.0.1:9877/ws';
private connected = false;
connect(url?: string): void {
if (url) this.url = url;
this.doConnect();
}
private doConnect(): void {
if (this.ws) {
this.ws.close();
}
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.connected = true;
console.log('[WS] Connected');
};
this.ws.onmessage = (event: MessageEvent) => {
try {
const msg: WSMessage = JSON.parse(event.data);
this.messages$.next(msg);
} catch {
console.warn('[WS] Failed to parse message');
}
};
this.ws.onclose = () => {
this.connected = false;
console.log('[WS] Disconnected, reconnecting in 3s...');
this.reconnectTimer = setTimeout(() => this.doConnect(), 3000);
};
this.ws.onerror = () => {
this.ws?.close();
};
}
subscribe(channel: string): Observable<WSMessage> {
// Send subscribe command to hub
this.send({ type: 'subscribe', data: channel, timestamp: new Date().toISOString() });
return this.messages$.pipe(
filter(msg => msg.channel === channel)
);
}
unsubscribe(channel: string): void {
this.send({ type: 'unsubscribe', data: channel, timestamp: new Date().toISOString() });
}
send(msg: WSMessage): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(msg));
}
}
get isConnected(): boolean {
return this.connected;
}
get allMessages$(): Observable<WSMessage> {
return this.messages$.asObservable();
}
ngOnDestroy(): void {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
this.ws?.close();
this.messages$.complete();
}
}

View file

@ -1,124 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WailsService, ConnectionStatus } from '@shared/wails.service';
@Component({
selector: 'app-tray',
standalone: true,
imports: [CommonModule],
template: `
<div class="tray">
<div class="tray__header">
<h3>Core IDE</h3>
<span class="badge" [class]="status.bridgeConnected ? 'badge--success' : 'badge--danger'">
{{ status.bridgeConnected ? 'Online' : 'Offline' }}
</span>
</div>
<div class="tray__stats">
<div class="stat">
<span class="stat__value">{{ status.wsClients }}</span>
<span class="stat__label">WS Clients</span>
</div>
<div class="stat">
<span class="stat__value">{{ status.wsChannels }}</span>
<span class="stat__label">Channels</span>
</div>
</div>
<div class="tray__actions">
<button class="btn btn--primary" (click)="openMain()">Open IDE</button>
<button class="btn btn--secondary" (click)="openSettings()">Settings</button>
</div>
<div class="tray__footer text-muted">
Laravel bridge: {{ status.bridgeConnected ? 'connected' : 'disconnected' }}
</div>
</div>
`,
styles: [`
.tray {
padding: var(--spacing-md);
height: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.tray__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.tray__stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm);
}
.stat {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
text-align: center;
}
.stat__value {
display: block;
font-size: 24px;
font-weight: 600;
color: var(--accent-primary);
}
.stat__label {
font-size: 12px;
color: var(--text-muted);
}
.tray__actions {
display: flex;
gap: var(--spacing-sm);
}
.tray__actions .btn {
flex: 1;
}
.tray__footer {
margin-top: auto;
font-size: 12px;
text-align: center;
}
`]
})
export class TrayComponent implements OnInit {
status: ConnectionStatus = {
bridgeConnected: false,
laravelUrl: '',
wsClients: 0,
wsChannels: 0
};
private pollTimer: ReturnType<typeof setInterval> | null = null;
constructor(private wails: WailsService) {}
ngOnInit(): void {
this.refresh();
this.pollTimer = setInterval(() => this.refresh(), 5000);
}
async refresh(): Promise<void> {
this.status = await this.wails.getConnectionStatus();
}
openMain(): void {
this.wails.showWindow('main');
}
openSettings(): void {
this.wails.showWindow('settings');
}
}

View file

@ -1,13 +0,0 @@
<!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/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View file

@ -1,6 +0,0 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View file

@ -1,247 +0,0 @@
// Core IDE Global Styles
:root {
// Dark theme (default) IDE accent: teal/cyan
--bg-primary: #161b22;
--bg-secondary: #0d1117;
--bg-tertiary: #21262d;
--bg-sidebar: #131820;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--accent-primary: #39d0d8;
--accent-secondary: #58a6ff;
--accent-success: #3fb950;
--accent-warning: #d29922;
--accent-danger: #f85149;
// Spacing
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
// Border radius
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 12px;
// Font
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
// IDE-specific
--sidebar-width: 240px;
--chat-input-height: 80px;
}
// Reset
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
body {
font-family: var(--font-family);
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--bg-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// Typography
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.25;
margin-bottom: var(--spacing-sm);
}
h1 { font-size: 24px; }
h2 { font-size: 20px; }
h3 { font-size: 16px; }
h4 { font-size: 14px; }
a {
color: var(--accent-secondary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
code, pre {
font-family: var(--font-mono);
font-size: 13px;
}
code {
padding: 2px 6px;
background-color: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
pre {
padding: var(--spacing-md);
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
overflow-x: auto;
}
// Scrollbar styling
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
&:hover {
background: var(--text-muted);
}
}
// Buttons
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
font-size: 14px;
font-weight: 500;
line-height: 1;
border: 1px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--primary {
background-color: var(--accent-primary);
color: #0d1117;
&:hover:not(:disabled) {
opacity: 0.9;
}
}
&--secondary {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-primary);
&:hover:not(:disabled) {
background-color: var(--bg-secondary);
}
}
&--danger {
background-color: var(--accent-danger);
color: white;
}
&--ghost {
background: transparent;
color: var(--text-secondary);
&:hover:not(:disabled) {
color: var(--text-primary);
background-color: var(--bg-tertiary);
}
}
}
// Forms
.form-group {
margin-bottom: var(--spacing-md);
}
.form-label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: 500;
color: var(--text-primary);
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 14px;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
&:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(57, 208, 216, 0.15);
}
&::placeholder {
color: var(--text-muted);
}
}
// Badges
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
border-radius: 999px;
&--success {
background-color: rgba(63, 185, 80, 0.15);
color: var(--accent-success);
}
&--warning {
background-color: rgba(210, 153, 34, 0.15);
color: var(--accent-warning);
}
&--danger {
background-color: rgba(248, 81, 73, 0.15);
color: var(--accent-danger);
}
&--info {
background-color: rgba(57, 208, 216, 0.15);
color: var(--accent-primary);
}
}
// Utility classes
.text-muted { color: var(--text-muted); }
.text-success { color: var(--accent-success); }
.text-danger { color: var(--accent-danger); }
.text-warning { color: var(--accent-warning); }
.mono { font-family: var(--font-mono); }

View file

@ -1,13 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View file

@ -1,35 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
],
"paths": {
"@app/*": ["src/app/*"],
"@shared/*": ["src/app/shared/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View file

@ -1,57 +0,0 @@
module forge.lthn.ai/core/go/cmd/core-ide
go 1.25.5
require (
github.com/gorilla/websocket v1.5.3
forge.lthn.ai/core/go v0.0.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.64
)
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/coder/websocket v1.8.14 // 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/jsonschema-go v0.4.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modelcontextprotocol/go-sdk v1.2.0 // 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
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.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 forge.lthn.ai/core/go => ../..

View file

@ -1,165 +0,0 @@
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/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=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/crypto v0.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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

View file

@ -1,25 +0,0 @@
// Package icons provides embedded icon assets for the Core IDE application.
package icons
import _ "embed"
// TrayTemplate is the template icon for macOS systray (22x22 PNG, black on transparent).
// Template icons automatically adapt to light/dark mode on macOS.
//
//go:embed tray-template.png
var TrayTemplate []byte
// TrayLight is the light mode icon for Windows/Linux systray.
//
//go:embed tray-light.png
var TrayLight []byte
// TrayDark is the dark mode icon for Windows/Linux systray.
//
//go:embed tray-dark.png
var TrayDark []byte
// AppIcon is the main application icon.
//
//go:embed appicon.png
var AppIcon []byte

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

View file

@ -1,83 +0,0 @@
package main
import (
"context"
"log"
"forge.lthn.ai/core/go/pkg/mcp/ide"
"forge.lthn.ai/core/go/pkg/ws"
"github.com/wailsapp/wails/v3/pkg/application"
)
// IDEService provides core IDE bindings for the frontend.
type IDEService struct {
app *application.App
ideSub *ide.Subsystem
hub *ws.Hub
}
// NewIDEService creates a new IDEService.
func NewIDEService(ideSub *ide.Subsystem, hub *ws.Hub) *IDEService {
return &IDEService{ideSub: ideSub, hub: hub}
}
// ServiceName returns the service name for Wails.
func (s *IDEService) ServiceName() string { return "IDEService" }
// ServiceStartup is called when the Wails application starts.
func (s *IDEService) ServiceStartup(_ context.Context, _ application.ServiceOptions) error {
log.Println("IDEService started")
return nil
}
// ServiceShutdown is called when the Wails application shuts down.
func (s *IDEService) ServiceShutdown() error {
log.Println("IDEService shutdown")
return nil
}
// ConnectionStatus represents the IDE bridge connection state.
type ConnectionStatus struct {
BridgeConnected bool `json:"bridgeConnected"`
LaravelURL string `json:"laravelUrl"`
WSClients int `json:"wsClients"`
WSChannels int `json:"wsChannels"`
}
// GetConnectionStatus returns the current bridge and WebSocket status.
func (s *IDEService) GetConnectionStatus() ConnectionStatus {
connected := false
if s.ideSub.Bridge() != nil {
connected = s.ideSub.Bridge().Connected()
}
stats := s.hub.Stats()
return ConnectionStatus{
BridgeConnected: connected,
WSClients: stats.Clients,
WSChannels: stats.Channels,
}
}
// DashboardData aggregates data for the dashboard view.
type DashboardData struct {
Connection ConnectionStatus `json:"connection"`
}
// GetDashboard returns aggregated dashboard data.
func (s *IDEService) GetDashboard() DashboardData {
return DashboardData{
Connection: s.GetConnectionStatus(),
}
}
// ShowWindow shows a named window.
func (s *IDEService) ShowWindow(name string) {
if s.app == nil {
return
}
if w, ok := s.app.Window.Get(name); ok {
w.Show()
w.Focus()
}
}

View file

@ -1,173 +0,0 @@
// Package main provides the Core IDE desktop application.
// Core IDE connects to the Laravel core-agentic backend via MCP bridge,
// providing a chat interface for AI agent sessions, build monitoring,
// and a system dashboard.
package main
import (
"context"
"embed"
"io/fs"
"log"
"net/http"
"runtime"
"strings"
"forge.lthn.ai/core/cli/pkg/ws"
"forge.lthn.ai/core/go/cmd/core-ide/icons"
"forge.lthn.ai/core/go/pkg/mcp/ide"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed all:frontend/dist/core-ide/browser
var assets embed.FS
func main() {
staticAssets, err := fs.Sub(assets, "frontend/dist/core-ide/browser")
if err != nil {
log.Fatal(err)
}
// Create shared WebSocket hub for real-time streaming
hub := ws.NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
// Create IDE subsystem (bridge to Laravel core-agentic)
ideSub := ide.New(hub)
ideSub.StartBridge(ctx)
// Create Wails services
ideService := NewIDEService(ideSub, hub)
chatService := NewChatService(ideSub)
buildService := NewBuildService(ideSub)
// Create MCP bridge (SERVER: HTTP tool server + CLIENT: WebSocket relay)
mcpBridge := NewMCPBridge(hub, 9877)
app := application.New(application.Options{
Name: "Core IDE",
Description: "Host UK Platform IDE - AI Agent Sessions, Build Monitoring & Dashboard",
Services: []application.Service{
application.NewService(ideService),
application.NewService(chatService),
application.NewService(buildService),
application.NewService(mcpBridge),
},
Assets: application.AssetOptions{
Handler: spaHandler(staticAssets),
},
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
})
ideService.app = app
setupSystemTray(app, ideService)
log.Println("Starting Core IDE...")
log.Println(" - System tray active")
log.Println(" - MCP bridge (SERVER) on :9877")
log.Println(" - Claude bridge (CLIENT) → MCP core on :9876")
if err := app.Run(); err != nil {
log.Fatal(err)
}
cancel()
}
// setupSystemTray configures the system tray icon, menu, and windows.
func setupSystemTray(app *application.App, ideService *IDEService) {
systray := app.SystemTray.New()
systray.SetTooltip("Core IDE")
if runtime.GOOS == "darwin" {
systray.SetTemplateIcon(icons.TrayTemplate)
} else {
systray.SetDarkModeIcon(icons.TrayDark)
systray.SetIcon(icons.TrayLight)
}
// Tray panel window
trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "tray-panel",
Title: "Core IDE",
Width: 400,
Height: 500,
URL: "/tray",
Hidden: true,
Frameless: true,
BackgroundColour: application.NewRGB(22, 27, 34),
})
systray.AttachWindow(trayWindow).WindowOffset(5)
// Main IDE window
app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Core IDE",
Width: 1400,
Height: 900,
URL: "/main",
Hidden: true,
BackgroundColour: application.NewRGB(22, 27, 34),
})
// Settings window
app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Core IDE Settings",
Width: 600,
Height: 500,
URL: "/settings",
Hidden: true,
BackgroundColour: application.NewRGB(22, 27, 34),
})
// Tray menu
trayMenu := app.Menu.New()
statusItem := trayMenu.Add("Status: Connecting...")
statusItem.SetEnabled(false)
trayMenu.AddSeparator()
trayMenu.Add("Open IDE").OnClick(func(ctx *application.Context) {
if w, ok := app.Window.Get("main"); ok {
w.Show()
w.Focus()
}
})
trayMenu.Add("Settings...").OnClick(func(ctx *application.Context) {
if w, ok := app.Window.Get("settings"); ok {
w.Show()
w.Focus()
}
})
trayMenu.AddSeparator()
trayMenu.Add("Quit Core IDE").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(trayMenu)
}
// spaHandler wraps an fs.FS to serve static files with SPA fallback.
func spaHandler(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
if _, err := fs.Stat(fsys, path); err != nil {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}

View file

@ -1,504 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"forge.lthn.ai/core/go/pkg/ws"
"github.com/wailsapp/wails/v3/pkg/application"
)
// MCPBridge is the SERVER bridge that exposes MCP tools via HTTP.
// AI agents call these endpoints to control windows, execute JS in webviews,
// access the clipboard, show notifications, and query the app state.
type MCPBridge struct {
app *application.App
hub *ws.Hub
claudeBridge *ClaudeBridge
port int
running bool
mu sync.Mutex
}
// NewMCPBridge creates a new MCP bridge server.
func NewMCPBridge(hub *ws.Hub, port int) *MCPBridge {
cb := NewClaudeBridge("ws://localhost:9876/ws")
return &MCPBridge{
hub: hub,
claudeBridge: cb,
port: port,
}
}
// ServiceName returns the Wails service name.
func (b *MCPBridge) ServiceName() string { return "MCPBridge" }
// ServiceStartup is called by Wails when the app starts.
func (b *MCPBridge) ServiceStartup(_ context.Context, _ application.ServiceOptions) error {
b.app = application.Get()
go b.startHTTPServer()
log.Printf("MCP Bridge started on port %d", b.port)
return nil
}
// ServiceShutdown is called when the app shuts down.
func (b *MCPBridge) ServiceShutdown() error {
b.mu.Lock()
defer b.mu.Unlock()
b.running = false
return nil
}
// startHTTPServer starts the HTTP server for MCP tools and WebSocket.
func (b *MCPBridge) startHTTPServer() {
b.mu.Lock()
b.running = true
b.mu.Unlock()
// Start the Claude bridge (CLIENT → MCP core on :9876)
b.claudeBridge.Start()
mux := http.NewServeMux()
// WebSocket endpoint for Angular frontend
mux.HandleFunc("/ws", b.hub.HandleWebSocket)
// Claude bridge WebSocket relay (GUI clients ↔ MCP core)
mux.HandleFunc("/claude", b.claudeBridge.HandleWebSocket)
// MCP server endpoints
mux.HandleFunc("/mcp", b.handleMCPInfo)
mux.HandleFunc("/mcp/tools", b.handleMCPTools)
mux.HandleFunc("/mcp/call", b.handleMCPCall)
// Health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"mcp": true,
"claudeBridge": b.claudeBridge.Connected(),
})
})
addr := fmt.Sprintf("127.0.0.1:%d", b.port)
log.Printf("MCP HTTP server listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Printf("MCP HTTP server error: %v", err)
}
}
// handleMCPInfo returns MCP server information.
func (b *MCPBridge) handleMCPInfo(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json.NewEncoder(w).Encode(map[string]any{
"name": "core-ide",
"version": "0.1.0",
"capabilities": map[string]any{
"webview": true,
"windowControl": true,
"clipboard": true,
"notifications": true,
"websocket": fmt.Sprintf("ws://localhost:%d/ws", b.port),
"claude": fmt.Sprintf("ws://localhost:%d/claude", b.port),
},
})
}
// handleMCPTools returns the list of available tools.
func (b *MCPBridge) handleMCPTools(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
tools := []map[string]string{
// Window management
{"name": "window_list", "description": "List all windows with positions and sizes"},
{"name": "window_get", "description": "Get info about a specific window"},
{"name": "window_position", "description": "Move a window to specific coordinates"},
{"name": "window_size", "description": "Resize a window"},
{"name": "window_bounds", "description": "Set position and size in one call"},
{"name": "window_maximize", "description": "Maximize a window"},
{"name": "window_minimize", "description": "Minimize a window"},
{"name": "window_restore", "description": "Restore from maximized/minimized"},
{"name": "window_focus", "description": "Bring window to front"},
{"name": "window_visibility", "description": "Show or hide a window"},
{"name": "window_title", "description": "Change window title"},
{"name": "window_title_get", "description": "Get current window title"},
{"name": "window_fullscreen", "description": "Toggle fullscreen mode"},
{"name": "window_always_on_top", "description": "Pin window above others"},
{"name": "window_create", "description": "Create a new window at specific position"},
{"name": "window_close", "description": "Close a window by name"},
{"name": "window_background_colour", "description": "Set window background colour with alpha"},
// Webview interaction
{"name": "webview_eval", "description": "Execute JavaScript in a window's webview"},
{"name": "webview_navigate", "description": "Navigate window to a URL"},
{"name": "webview_list", "description": "List windows with webview info"},
// System integration
{"name": "clipboard_read", "description": "Read text from system clipboard"},
{"name": "clipboard_write", "description": "Write text to system clipboard"},
// System tray
{"name": "tray_set_tooltip", "description": "Set system tray tooltip"},
{"name": "tray_set_label", "description": "Set system tray label"},
}
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
}
// handleMCPCall handles tool calls via HTTP POST.
func (b *MCPBridge) handleMCPCall(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Tool string `json:"tool"`
Params map[string]any `json:"params"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var result map[string]any
if len(req.Tool) > 8 && req.Tool[:8] == "webview_" {
result = b.executeWebviewTool(req.Tool, req.Params)
} else {
result = b.executeWindowTool(req.Tool, req.Params)
}
json.NewEncoder(w).Encode(result)
}
// executeWindowTool handles window, clipboard, tray, and notification tools.
func (b *MCPBridge) executeWindowTool(tool string, params map[string]any) map[string]any {
if b.app == nil {
return map[string]any{"error": "app not available"}
}
switch tool {
case "window_list":
return b.windowList()
case "window_get":
name := strParam(params, "name")
return b.windowGet(name)
case "window_position":
name := strParam(params, "name")
x := intParam(params, "x")
y := intParam(params, "y")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetPosition(x, y)
return map[string]any{"success": true, "name": name, "x": x, "y": y}
case "window_size":
name := strParam(params, "name")
width := intParam(params, "width")
height := intParam(params, "height")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetSize(width, height)
return map[string]any{"success": true, "name": name, "width": width, "height": height}
case "window_bounds":
name := strParam(params, "name")
x := intParam(params, "x")
y := intParam(params, "y")
width := intParam(params, "width")
height := intParam(params, "height")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetPosition(x, y)
w.SetSize(width, height)
return map[string]any{"success": true, "name": name, "x": x, "y": y, "width": width, "height": height}
case "window_maximize":
name := strParam(params, "name")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.Maximise()
return map[string]any{"success": true, "action": "maximize"}
case "window_minimize":
name := strParam(params, "name")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.Minimise()
return map[string]any{"success": true, "action": "minimize"}
case "window_restore":
name := strParam(params, "name")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.Restore()
return map[string]any{"success": true, "action": "restore"}
case "window_focus":
name := strParam(params, "name")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.Show()
w.Focus()
return map[string]any{"success": true, "action": "focus"}
case "window_visibility":
name := strParam(params, "name")
visible, _ := params["visible"].(bool)
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
if visible {
w.Show()
} else {
w.Hide()
}
return map[string]any{"success": true, "visible": visible}
case "window_title":
name := strParam(params, "name")
title := strParam(params, "title")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetTitle(title)
return map[string]any{"success": true, "title": title}
case "window_title_get":
name := strParam(params, "name")
_, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
// Wails v3 Window interface has SetTitle but no Title getter;
// return the window name as a fallback identifier.
return map[string]any{"name": name}
case "window_fullscreen":
name := strParam(params, "name")
fullscreen, _ := params["fullscreen"].(bool)
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
if fullscreen {
w.Fullscreen()
} else {
w.UnFullscreen()
}
return map[string]any{"success": true, "fullscreen": fullscreen}
case "window_always_on_top":
name := strParam(params, "name")
onTop, _ := params["onTop"].(bool)
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetAlwaysOnTop(onTop)
return map[string]any{"success": true, "alwaysOnTop": onTop}
case "window_create":
name := strParam(params, "name")
title := strParam(params, "title")
url := strParam(params, "url")
x := intParam(params, "x")
y := intParam(params, "y")
width := intParam(params, "width")
height := intParam(params, "height")
if width == 0 {
width = 800
}
if height == 0 {
height = 600
}
opts := application.WebviewWindowOptions{
Name: name,
Title: title,
URL: url,
Width: width,
Height: height,
Hidden: false,
BackgroundColour: application.NewRGB(22, 27, 34),
}
w := b.app.Window.NewWithOptions(opts)
if x != 0 || y != 0 {
w.SetPosition(x, y)
}
return map[string]any{"success": true, "name": name}
case "window_close":
name := strParam(params, "name")
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.Close()
return map[string]any{"success": true, "action": "close"}
case "window_background_colour":
name := strParam(params, "name")
r := uint8(intParam(params, "r"))
g := uint8(intParam(params, "g"))
bv := uint8(intParam(params, "b"))
a := uint8(intParam(params, "a"))
if a == 0 {
a = 255
}
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
w.SetBackgroundColour(application.NewRGBA(r, g, bv, a))
return map[string]any{"success": true}
case "clipboard_read":
text, ok := b.app.Clipboard.Text()
if !ok {
return map[string]any{"error": "failed to read clipboard"}
}
return map[string]any{"text": text}
case "clipboard_write":
text, _ := params["text"].(string)
ok := b.app.Clipboard.SetText(text)
if !ok {
return map[string]any{"error": "failed to write clipboard"}
}
return map[string]any{"success": true}
case "tray_set_tooltip":
// System tray is managed at startup; this is informational
return map[string]any{"info": "tray tooltip can be set via system tray menu"}
case "tray_set_label":
return map[string]any{"info": "tray label can be set via system tray menu"}
default:
return map[string]any{"error": "unknown tool", "tool": tool}
}
}
// executeWebviewTool handles webview/JS tools.
func (b *MCPBridge) executeWebviewTool(tool string, params map[string]any) map[string]any {
if b.app == nil {
return map[string]any{"error": "app not available"}
}
switch tool {
case "webview_eval":
windowName := strParam(params, "window")
code := strParam(params, "code")
w, ok := b.app.Window.Get(windowName)
if !ok {
return map[string]any{"error": "window not found", "window": windowName}
}
w.ExecJS(code)
return map[string]any{"success": true, "window": windowName}
case "webview_navigate":
windowName := strParam(params, "window")
url := strParam(params, "url")
w, ok := b.app.Window.Get(windowName)
if !ok {
return map[string]any{"error": "window not found", "window": windowName}
}
w.SetURL(url)
return map[string]any{"success": true, "url": url}
case "webview_list":
return b.windowList()
default:
return map[string]any{"error": "unknown webview tool", "tool": tool}
}
}
// windowList returns info for all known windows.
func (b *MCPBridge) windowList() map[string]any {
knownNames := []string{"tray-panel", "main", "settings"}
var windows []map[string]any
for _, name := range knownNames {
w, ok := b.app.Window.Get(name)
if !ok {
continue
}
x, y := w.Position()
width, height := w.Size()
windows = append(windows, map[string]any{
"name": name,
"title": w.Name(),
"x": x,
"y": y,
"width": width,
"height": height,
})
}
return map[string]any{"windows": windows}
}
// windowGet returns info for a specific window.
func (b *MCPBridge) windowGet(name string) map[string]any {
w, ok := b.app.Window.Get(name)
if !ok {
return map[string]any{"error": "window not found", "name": name}
}
x, y := w.Position()
width, height := w.Size()
return map[string]any{
"window": map[string]any{
"name": name,
"title": w.Name(),
"x": x,
"y": y,
"width": width,
"height": height,
},
}
}
// Parameter helpers
func strParam(params map[string]any, key string) string {
if v, ok := params[key].(string); ok {
return v
}
return ""
}
func intParam(params map[string]any, key string) int {
if v, ok := params[key].(float64); ok {
return int(v)
}
return 0
}

View file

@ -1,6 +1,6 @@
package crypt package crypt
import "forge.lthn.ai/core/cli/pkg/cli" import "forge.lthn.ai/core/go/pkg/cli"
func init() { func init() {
cli.RegisterCommands(AddCryptCommands) cli.RegisterCommands(AddCryptCommands)

View file

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/crypt" "forge.lthn.ai/core/go/pkg/crypt"
) )
// Checksum command flags // Checksum command flags

View file

@ -5,8 +5,8 @@ import (
"os" "os"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/crypt" "forge.lthn.ai/core/go/pkg/crypt"
) )
// Encrypt command flags // Encrypt command flags

View file

@ -3,8 +3,8 @@ package crypt
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/crypt" "forge.lthn.ai/core/go/pkg/crypt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )

View file

@ -6,7 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
) )
// Keygen command flags // Keygen command flags

View file

@ -7,9 +7,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/log" "forge.lthn.ai/core/go/pkg/log"
"forge.lthn.ai/core/cli/pkg/mcp" "forge.lthn.ai/core/go/pkg/mcp"
) )
func init() { func init() {

View file

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/ansible" "forge.lthn.ai/core/go/pkg/ansible"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View file

@ -1,7 +1,7 @@
package deploy package deploy
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View file

@ -6,9 +6,9 @@ import (
"fmt" "fmt"
"os" "os"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/deploy/coolify" "forge.lthn.ai/core/go/pkg/deploy/coolify"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View file

@ -1,8 +1,8 @@
package dev package dev
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// addAPICommands adds the 'api' command and its subcommands to the given parent command. // addAPICommands adds the 'api' command and its subcommands to the given parent command.

View file

@ -14,12 +14,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
core "forge.lthn.ai/core/cli/pkg/framework/core" core "forge.lthn.ai/core/go/pkg/framework/core"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// Apply command flags // Apply command flags

View file

@ -3,9 +3,9 @@ package dev
import ( import (
"context" "context"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/framework" "forge.lthn.ai/core/go/pkg/framework"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
) )
// WorkBundle contains the Core instance for dev work operations. // WorkBundle contains the Core instance for dev work operations.

View file

@ -8,10 +8,10 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// CI-specific styles (aliases to shared) // CI-specific styles (aliases to shared)

View file

@ -5,10 +5,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
coreio "forge.lthn.ai/core/cli/pkg/io" coreio "forge.lthn.ai/core/go/pkg/io"
) )
// Commit command flags // Commit command flags

View file

@ -33,8 +33,8 @@
package dev package dev
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
func init() { func init() {

View file

@ -14,12 +14,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
coreio "forge.lthn.ai/core/cli/pkg/io" coreio "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/log" "forge.lthn.ai/core/go/pkg/log"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// File sync command flags // File sync command flags

View file

@ -6,9 +6,9 @@ import (
"sort" "sort"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Health command flags // Health command flags

View file

@ -4,10 +4,10 @@ import (
"errors" "errors"
"sort" "sort"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// Impact-specific styles (aliases to shared) // Impact-specific styles (aliases to shared)

View file

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Issue-specific styles (aliases to shared) // Issue-specific styles (aliases to shared)

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"os/exec" "os/exec"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Pull command flags // Pull command flags

View file

@ -5,9 +5,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Push command flags // Push command flags

View file

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// PR-specific styles (aliases to shared) // PR-specific styles (aliases to shared)

View file

@ -8,9 +8,9 @@ import (
"path/filepath" "path/filepath"
"text/template" "text/template"
"forge.lthn.ai/core/cli/pkg/cli" // Added "forge.lthn.ai/core/go/pkg/cli" // Added
"forge.lthn.ai/core/cli/pkg/i18n" // Added "forge.lthn.ai/core/go/pkg/i18n" // Added
coreio "forge.lthn.ai/core/cli/pkg/io" coreio "forge.lthn.ai/core/go/pkg/io"
// Added // Added
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"

View file

@ -6,10 +6,10 @@ import (
"os" "os"
"time" "time"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/devops" "forge.lthn.ai/core/go/pkg/devops"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
) )
// addVMCommands adds the dev environment VM commands to the dev parent command. // addVMCommands adds the dev environment VM commands to the dev parent command.

View file

@ -7,10 +7,10 @@ import (
"sort" "sort"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Work command flags // Work command flags

View file

@ -5,9 +5,9 @@ import (
"sort" "sort"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
) )
// Workflow command flags // Workflow command flags

View file

@ -4,7 +4,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
) )
func TestFindWorkflows_Good(t *testing.T) { func TestFindWorkflows_Good(t *testing.T) {

View file

@ -5,11 +5,11 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/internal/cmd/workspace" "forge.lthn.ai/core/cli/cmd/workspace"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// loadRegistryWithConfig loads the registry and applies workspace configuration. // loadRegistryWithConfig loads the registry and applies workspace configuration.

View file

@ -5,10 +5,10 @@ import (
"sort" "sort"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/agentic" "forge.lthn.ai/core/go/pkg/agentic"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/framework" "forge.lthn.ai/core/go/pkg/framework"
"forge.lthn.ai/core/cli/pkg/git" "forge.lthn.ai/core/go/pkg/git"
) )
// Tasks for dev service // Tasks for dev service

View file

@ -8,7 +8,7 @@
// to a central location for unified documentation builds. // to a central location for unified documentation builds.
package docs package docs
import "forge.lthn.ai/core/cli/pkg/cli" import "forge.lthn.ai/core/go/pkg/cli"
func init() { func init() {
cli.RegisterCommands(AddDocsCommands) cli.RegisterCommands(AddDocsCommands)

View file

@ -2,8 +2,8 @@
package docs package docs
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Style and utility aliases from shared // Style and utility aliases from shared

View file

@ -3,8 +3,8 @@ package docs
import ( import (
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// Flag variable for list command // Flag variable for list command

View file

@ -6,11 +6,11 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/internal/cmd/workspace" "forge.lthn.ai/core/cli/cmd/workspace"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// RepoDocInfo holds documentation info for a repo // RepoDocInfo holds documentation info for a repo

View file

@ -4,9 +4,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
) )
// Flag variables for sync command // Flag variables for sync command

View file

@ -4,7 +4,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// check represents a tool check configuration // check represents a tool check configuration

View file

@ -11,7 +11,7 @@
package doctor package doctor
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View file

@ -4,8 +4,8 @@ package doctor
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View file

@ -7,9 +7,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/io" "forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/cli/pkg/repos" "forge.lthn.ai/core/go/pkg/repos"
) )
// checkGitHubSSH checks if SSH keys exist for GitHub access // checkGitHubSSH checks if SSH keys exist for GitHub access

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"runtime" "runtime"
"forge.lthn.ai/core/cli/pkg/i18n" "forge.lthn.ai/core/go/pkg/i18n"
) )
// printInstallInstructions prints OS-specific installation instructions // printInstallInstructions prints OS-specific installation instructions

View file

@ -3,8 +3,8 @@ package forge
import ( import (
"fmt" "fmt"
"forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go/pkg/cli"
fg "forge.lthn.ai/core/cli/pkg/forge" fg "forge.lthn.ai/core/go/pkg/forge"
) )
// Auth command flags. // Auth command flags.

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