feat(ai,security,ide): add agentic MVP, security jobs, and Core IDE desktop app
Wire up AI infrastructure with unified pkg/ai package (metrics JSONL, RAG integration), move RAG under `core ai rag`, add `core ai metrics` command, and enrich task context with Qdrant documentation. Add `--target` flag to all security commands for external repo scanning, `core security jobs` for distributing findings as GitHub Issues, and consistent error logging across scan/deps/alerts/secrets commands. Add Core IDE Wails v3 desktop app with Angular 20 frontend, MCP bridge (loopback-only HTTP server), WebSocket hub, and Claude Code bridge. Production-ready with Lethean CIC branding, macOS code signing support, and security hardening (origin validation, body size limits, URL scheme checks, memory leak prevention, XSS mitigation). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
@ -8,9 +8,12 @@
|
|||
// - task:commit: Create commits with task references
|
||||
// - task:pr: Create pull requests linked to tasks
|
||||
// - claude: Claude Code CLI integration (planned)
|
||||
// - rag: RAG tools (ingest, query, collections)
|
||||
// - metrics: View AI/security event metrics
|
||||
package ai
|
||||
|
||||
import (
|
||||
ragcmd "github.com/host-uk/core/internal/cmd/rag"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
|
@ -57,6 +60,12 @@ func initCommands() {
|
|||
|
||||
// Add agentic task commands
|
||||
AddAgenticCommands(aiCmd)
|
||||
|
||||
// Add RAG subcommands (core ai rag ...)
|
||||
ragcmd.AddRAGSubcommands(aiCmd)
|
||||
|
||||
// Add metrics subcommand (core ai metrics)
|
||||
addMetricsCommand(aiCmd)
|
||||
}
|
||||
|
||||
// AddAICommands registers the 'ai' command and all subcommands.
|
||||
|
|
|
|||
131
internal/cmd/ai/cmd_metrics.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// cmd_metrics.go implements the metrics viewing command.
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
metricsSince string
|
||||
metricsJSON bool
|
||||
)
|
||||
|
||||
var metricsCmd = &cli.Command{
|
||||
Use: "metrics",
|
||||
Short: i18n.T("cmd.ai.metrics.short"),
|
||||
Long: i18n.T("cmd.ai.metrics.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
return runMetrics()
|
||||
},
|
||||
}
|
||||
|
||||
func initMetricsFlags() {
|
||||
metricsCmd.Flags().StringVar(&metricsSince, "since", "7d", i18n.T("cmd.ai.metrics.flag.since"))
|
||||
metricsCmd.Flags().BoolVar(&metricsJSON, "json", false, i18n.T("common.flag.json"))
|
||||
}
|
||||
|
||||
func addMetricsCommand(parent *cli.Command) {
|
||||
initMetricsFlags()
|
||||
parent.AddCommand(metricsCmd)
|
||||
}
|
||||
|
||||
func runMetrics() error {
|
||||
since, err := parseDuration(metricsSince)
|
||||
if err != nil {
|
||||
return cli.Err("invalid --since value %q: %v", metricsSince, err)
|
||||
}
|
||||
|
||||
sinceTime := time.Now().Add(-since)
|
||||
events, err := ai.ReadEvents(sinceTime)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "read", "metrics")
|
||||
}
|
||||
|
||||
if metricsJSON {
|
||||
summary := ai.Summary(events)
|
||||
output, err := json.MarshalIndent(summary, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
summary := ai.Summary(events)
|
||||
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", dimStyle.Render("Period:"), metricsSince)
|
||||
total, _ := summary["total"].(int)
|
||||
cli.Print("%s %d\n", dimStyle.Render("Total events:"), total)
|
||||
cli.Blank()
|
||||
|
||||
// By type
|
||||
if byType, ok := summary["by_type"].([]map[string]any); ok && len(byType) > 0 {
|
||||
cli.Print("%s\n", dimStyle.Render("By type:"))
|
||||
for _, entry := range byType {
|
||||
cli.Print(" %-30s %v\n", entry["key"], entry["count"])
|
||||
}
|
||||
cli.Blank()
|
||||
}
|
||||
|
||||
// By repo
|
||||
if byRepo, ok := summary["by_repo"].([]map[string]any); ok && len(byRepo) > 0 {
|
||||
cli.Print("%s\n", dimStyle.Render("By repo:"))
|
||||
for _, entry := range byRepo {
|
||||
cli.Print(" %-30s %v\n", entry["key"], entry["count"])
|
||||
}
|
||||
cli.Blank()
|
||||
}
|
||||
|
||||
// By agent
|
||||
if byAgent, ok := summary["by_agent"].([]map[string]any); ok && len(byAgent) > 0 {
|
||||
cli.Print("%s\n", dimStyle.Render("By contributor:"))
|
||||
for _, entry := range byAgent {
|
||||
cli.Print(" %-30s %v\n", entry["key"], entry["count"])
|
||||
}
|
||||
cli.Blank()
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
cli.Text(i18n.T("cmd.ai.metrics.none_found"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseDuration parses a human-friendly duration like "7d", "24h", "30d".
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
if len(s) < 2 {
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
|
||||
unit := s[len(s)-1]
|
||||
value := s[:len(s)-1]
|
||||
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(value, "%d", &n); err != nil {
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("duration must be positive: %s", s)
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case 'd':
|
||||
return time.Duration(n) * 24 * time.Hour, nil
|
||||
case 'h':
|
||||
return time.Duration(n) * time.Hour, nil
|
||||
case 'm':
|
||||
return time.Duration(n) * time.Minute, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown unit %c in duration: %s", unit, s)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/agentic"
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
|
@ -165,6 +166,13 @@ var taskCmd = &cli.Command{
|
|||
return cli.WrapVerb(err, "claim", "task")
|
||||
}
|
||||
|
||||
// Record task claim event
|
||||
_ = ai.Record(ai.Event{
|
||||
Type: "task.claimed",
|
||||
AgentID: cfg.AgentID,
|
||||
Data: map[string]any{"task_id": task.ID, "title": task.Title},
|
||||
})
|
||||
|
||||
cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.claim", "task"))
|
||||
cli.Print(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/agentic"
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
|
@ -92,6 +93,13 @@ var taskCompleteCmd = &cli.Command{
|
|||
return cli.WrapVerb(err, "complete", "task")
|
||||
}
|
||||
|
||||
// Record task completion event
|
||||
_ = ai.Record(ai.Event{
|
||||
Type: "task.completed",
|
||||
AgentID: cfg.AgentID,
|
||||
Data: map[string]any{"task_id": taskID, "success": !taskCompleteFailed},
|
||||
})
|
||||
|
||||
if taskCompleteFailed {
|
||||
cli.Print("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID}))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
// Package rag provides RAG (Retrieval Augmented Generation) commands.
|
||||
//
|
||||
// Commands:
|
||||
// - core rag ingest: Ingest markdown files into Qdrant
|
||||
// - core rag query: Query the vector database
|
||||
// - core rag collections: List and manage collections
|
||||
// - core ai rag ingest: Ingest markdown files into Qdrant
|
||||
// - core ai rag query: Query the vector database
|
||||
// - core ai rag collections: List and manage collections
|
||||
package rag
|
||||
|
||||
import (
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.RegisterCommands(AddRAGCommands)
|
||||
}
|
||||
|
||||
// AddRAGCommands registers the 'rag' command and all subcommands.
|
||||
func AddRAGCommands(root *cobra.Command) {
|
||||
// AddRAGSubcommands registers the 'rag' command as a subcommand of parent.
|
||||
// Called from the ai command package to mount under "core ai rag".
|
||||
func AddRAGSubcommands(parent *cobra.Command) {
|
||||
initFlags()
|
||||
ragCmd.AddCommand(ingestCmd)
|
||||
ragCmd.AddCommand(queryCmd)
|
||||
ragCmd.AddCommand(collectionsCmd)
|
||||
root.AddCommand(ragCmd)
|
||||
parent.AddCommand(ragCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ func addAlertsCommand(parent *cli.Command) {
|
|||
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
|
||||
cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
|
||||
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json"))
|
||||
cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target"))
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
|
@ -43,6 +44,11 @@ func runAlerts() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// External target mode: bypass registry entirely
|
||||
if securityTarget != "" {
|
||||
return runAlertsForTarget(securityTarget)
|
||||
}
|
||||
|
||||
reg, err := loadRegistry(securityRegistryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -173,6 +179,124 @@ func runAlerts() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// runAlertsForTarget runs unified alert checks against an external repo target.
|
||||
func runAlertsForTarget(target string) error {
|
||||
repo, fullName := buildTargetRepo(target)
|
||||
if repo == nil {
|
||||
return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)")
|
||||
}
|
||||
|
||||
var allAlerts []AlertOutput
|
||||
summary := &AlertSummary{}
|
||||
|
||||
// Fetch Dependabot alerts
|
||||
depAlerts, err := fetchDependabotAlerts(fullName)
|
||||
if err == nil {
|
||||
for _, alert := range depAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
severity := alert.Advisory.Severity
|
||||
if !filterBySeverity(severity, securitySeverity) {
|
||||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
ID: alert.Advisory.CVEID,
|
||||
Package: alert.Dependency.Package.Name,
|
||||
Version: alert.SecurityVulnerability.VulnerableVersionRange,
|
||||
Type: "dependabot",
|
||||
Message: alert.Advisory.Summary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch code scanning alerts
|
||||
codeAlerts, err := fetchCodeScanningAlerts(fullName)
|
||||
if err == nil {
|
||||
for _, alert := range codeAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
severity := alert.Rule.Severity
|
||||
if !filterBySeverity(severity, securitySeverity) {
|
||||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
|
||||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
ID: alert.Rule.ID,
|
||||
Location: location,
|
||||
Type: alert.Tool.Name,
|
||||
Message: alert.Rule.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch secret scanning alerts
|
||||
secretAlerts, err := fetchSecretScanningAlerts(fullName)
|
||||
if err == nil {
|
||||
for _, alert := range secretAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
if !filterBySeverity("high", securitySeverity) {
|
||||
continue
|
||||
}
|
||||
summary.Add("high")
|
||||
allAlerts = append(allAlerts, AlertOutput{
|
||||
Repo: repo.Name,
|
||||
Severity: "high",
|
||||
ID: fmt.Sprintf("secret-%d", alert.Number),
|
||||
Type: "secret-scanning",
|
||||
Message: alert.SecretType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Alerts ("+fullName+"):"), summary.String())
|
||||
cli.Blank()
|
||||
|
||||
if len(allAlerts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, alert := range allAlerts {
|
||||
sevStyle := severityStyle(alert.Severity)
|
||||
location := alert.Package
|
||||
if location == "" {
|
||||
location = alert.Location
|
||||
}
|
||||
if alert.Version != "" {
|
||||
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
|
||||
}
|
||||
cli.Print("%-20s %s %-16s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
alert.ID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Type),
|
||||
)
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
|
||||
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
|
||||
output, err := runGHAPI(endpoint)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ func addDepsCommand(parent *cli.Command) {
|
|||
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
|
||||
cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
|
||||
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json"))
|
||||
cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target"))
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
|
@ -44,6 +45,11 @@ func runDeps() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// External target mode: bypass registry entirely
|
||||
if securityTarget != "" {
|
||||
return runDepsForTarget(securityTarget)
|
||||
}
|
||||
|
||||
reg, err := loadRegistry(securityRegistryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -62,6 +68,7 @@ func runDeps() error {
|
|||
|
||||
alerts, err := fetchDependabotAlerts(repoFullName)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -132,3 +139,72 @@ func runDeps() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runDepsForTarget runs dependency checks against an external repo target.
|
||||
func runDepsForTarget(target string) error {
|
||||
repo, fullName := buildTargetRepo(target)
|
||||
if repo == nil {
|
||||
return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)")
|
||||
}
|
||||
|
||||
var allAlerts []DepAlert
|
||||
summary := &AlertSummary{}
|
||||
|
||||
alerts, err := fetchDependabotAlerts(fullName)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "fetch dependabot alerts for "+fullName)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
severity := alert.Advisory.Severity
|
||||
if !filterBySeverity(severity, securitySeverity) {
|
||||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
allAlerts = append(allAlerts, DepAlert{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
CVE: alert.Advisory.CVEID,
|
||||
Package: alert.Dependency.Package.Name,
|
||||
Ecosystem: alert.Dependency.Package.Ecosystem,
|
||||
Vulnerable: alert.SecurityVulnerability.VulnerableVersionRange,
|
||||
PatchedVersion: alert.SecurityVulnerability.FirstPatchedVersion.Identifier,
|
||||
Manifest: alert.Dependency.ManifestPath,
|
||||
Summary: alert.Advisory.Summary,
|
||||
})
|
||||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Dependabot ("+fullName+"):"), summary.String())
|
||||
cli.Blank()
|
||||
|
||||
for _, alert := range allAlerts {
|
||||
sevStyle := severityStyle(alert.Severity)
|
||||
upgrade := alert.Vulnerable
|
||||
if alert.PatchedVersion != "" {
|
||||
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
|
||||
}
|
||||
cli.Print("%-16s %s %-16s %-30s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
alert.CVE,
|
||||
alert.Package,
|
||||
upgrade,
|
||||
)
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
229
internal/cmd/security/cmd_jobs.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
jobsTargets []string
|
||||
jobsIssueRepo string
|
||||
jobsDryRun bool
|
||||
jobsCopies int
|
||||
)
|
||||
|
||||
func addJobsCommand(parent *cli.Command) {
|
||||
cmd := &cli.Command{
|
||||
Use: "jobs",
|
||||
Short: i18n.T("cmd.security.jobs.short"),
|
||||
Long: i18n.T("cmd.security.jobs.long"),
|
||||
RunE: func(c *cli.Command, args []string) error {
|
||||
return runJobs()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringSliceVar(&jobsTargets, "targets", nil, i18n.T("cmd.security.jobs.flag.targets"))
|
||||
cmd.Flags().StringVar(&jobsIssueRepo, "issue-repo", "host-uk/core", i18n.T("cmd.security.jobs.flag.issue_repo"))
|
||||
cmd.Flags().BoolVar(&jobsDryRun, "dry-run", false, i18n.T("cmd.security.jobs.flag.dry_run"))
|
||||
cmd.Flags().IntVar(&jobsCopies, "copies", 1, i18n.T("cmd.security.jobs.flag.copies"))
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func runJobs() error {
|
||||
if err := checkGH(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(jobsTargets) == 0 {
|
||||
return cli.Err("at least one --targets value required (e.g. --targets wailsapp/wails)")
|
||||
}
|
||||
|
||||
if jobsCopies < 1 {
|
||||
return cli.Err("--copies must be at least 1")
|
||||
}
|
||||
|
||||
var failedCount int
|
||||
for _, target := range jobsTargets {
|
||||
if err := createJobForTarget(target); err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.ErrorStyle.Render(">>"), target, err)
|
||||
failedCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if failedCount == len(jobsTargets) {
|
||||
return cli.Err("all targets failed to process")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createJobForTarget(target string) error {
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid target format: use owner/repo")
|
||||
}
|
||||
|
||||
// Gather findings
|
||||
summary := &AlertSummary{}
|
||||
var findings []string
|
||||
var fetchErrors int
|
||||
|
||||
// Code scanning
|
||||
codeAlerts, err := fetchCodeScanningAlerts(target)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: failed to fetch code scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err)
|
||||
fetchErrors++
|
||||
}
|
||||
if err == nil {
|
||||
for _, alert := range codeAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
severity := alert.Rule.Severity
|
||||
if severity == "" {
|
||||
severity = "medium"
|
||||
}
|
||||
summary.Add(severity)
|
||||
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s:%d)",
|
||||
strings.ToUpper(severity), alert.Tool.Name, alert.Rule.Description,
|
||||
alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine))
|
||||
}
|
||||
}
|
||||
|
||||
// Dependabot
|
||||
depAlerts, err := fetchDependabotAlerts(target)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: failed to fetch dependabot alerts: %v\n", cli.WarningStyle.Render(">>"), target, err)
|
||||
fetchErrors++
|
||||
}
|
||||
if err == nil {
|
||||
for _, alert := range depAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
summary.Add(alert.Advisory.Severity)
|
||||
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s)",
|
||||
strings.ToUpper(alert.Advisory.Severity), alert.Dependency.Package.Name,
|
||||
alert.Advisory.Summary, alert.Advisory.CVEID))
|
||||
}
|
||||
}
|
||||
|
||||
// Secret scanning
|
||||
secretAlerts, err := fetchSecretScanningAlerts(target)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: failed to fetch secret scanning alerts: %v\n", cli.WarningStyle.Render(">>"), target, err)
|
||||
fetchErrors++
|
||||
}
|
||||
if err == nil {
|
||||
for _, alert := range secretAlerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
summary.Add("high")
|
||||
findings = append(findings, fmt.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
|
||||
}
|
||||
}
|
||||
|
||||
if fetchErrors == 3 {
|
||||
return fmt.Errorf("failed to fetch any alerts for %s", target)
|
||||
}
|
||||
|
||||
if summary.Total == 0 {
|
||||
cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), target, "No open findings")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build issue body
|
||||
title := fmt.Sprintf("Security scan: %s", target)
|
||||
body := buildJobIssueBody(target, summary, findings)
|
||||
|
||||
for i := range jobsCopies {
|
||||
issueTitle := title
|
||||
if jobsCopies > 1 {
|
||||
issueTitle = fmt.Sprintf("%s (#%d)", title, i+1)
|
||||
}
|
||||
|
||||
if jobsDryRun {
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("[dry-run] Would create issue:"), issueTitle)
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render(" Repo:"), jobsIssueRepo)
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render(" Labels:"), "type:security-scan,repo:"+target)
|
||||
cli.Print("%s %d findings\n", cli.DimStyle.Render(" Findings:"), summary.Total)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create issue via gh CLI
|
||||
cmd := exec.Command("gh", "issue", "create",
|
||||
"--repo", jobsIssueRepo,
|
||||
"--title", issueTitle,
|
||||
"--body", body,
|
||||
"--label", "type:security-scan,repo:"+target,
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output)))
|
||||
}
|
||||
|
||||
issueURL := strings.TrimSpace(string(output))
|
||||
cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), issueTitle, issueURL)
|
||||
|
||||
// Record metrics
|
||||
_ = ai.Record(ai.Event{
|
||||
Type: "security.job_created",
|
||||
Timestamp: time.Now(),
|
||||
Repo: target,
|
||||
Data: map[string]any{
|
||||
"issue_repo": jobsIssueRepo,
|
||||
"issue_url": issueURL,
|
||||
"total": summary.Total,
|
||||
"critical": summary.Critical,
|
||||
"high": summary.High,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "## Security Scan: %s\n\n", target)
|
||||
fmt.Fprintf(&sb, "**Summary:** %s\n\n", summary.String())
|
||||
|
||||
sb.WriteString("### Findings\n\n")
|
||||
if len(findings) > 50 {
|
||||
// Truncate long lists
|
||||
for _, f := range findings[:50] {
|
||||
sb.WriteString(f + "\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n... and %d more\n", len(findings)-50)
|
||||
} else {
|
||||
for _, f := range findings {
|
||||
sb.WriteString(f + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n### Checklist\n\n")
|
||||
sb.WriteString("- [ ] Review findings above\n")
|
||||
sb.WriteString("- [ ] Triage by severity (critical/high first)\n")
|
||||
sb.WriteString("- [ ] Create PRs for fixes\n")
|
||||
sb.WriteString("- [ ] Verify fixes resolve alerts\n")
|
||||
|
||||
sb.WriteString("\n### Instructions\n\n")
|
||||
sb.WriteString("1. Claim this issue by assigning yourself\n")
|
||||
fmt.Fprintf(&sb, "2. Run `core security alerts --target %s` for the latest findings\n", target)
|
||||
sb.WriteString("3. Work through the checklist above\n")
|
||||
sb.WriteString("4. Close this issue when all findings are addressed\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@ package security
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
)
|
||||
|
|
@ -27,6 +29,7 @@ func addScanCommand(parent *cli.Command) {
|
|||
cmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.security.flag.severity"))
|
||||
cmd.Flags().StringVar(&scanTool, "tool", "", i18n.T("cmd.security.scan.flag.tool"))
|
||||
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json"))
|
||||
cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target"))
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
|
@ -48,6 +51,11 @@ func runScan() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// External target mode: bypass registry entirely
|
||||
if securityTarget != "" {
|
||||
return runScanForTarget(securityTarget)
|
||||
}
|
||||
|
||||
reg, err := loadRegistry(securityRegistryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -66,6 +74,7 @@ func runScan() error {
|
|||
|
||||
alerts, err := fetchCodeScanningAlerts(repoFullName)
|
||||
if err != nil {
|
||||
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +113,19 @@ func runScan() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
_ = ai.Record(ai.Event{
|
||||
Type: "security.scan",
|
||||
Timestamp: time.Now(),
|
||||
Data: map[string]any{
|
||||
"total": summary.Total,
|
||||
"critical": summary.Critical,
|
||||
"high": summary.High,
|
||||
"medium": summary.Medium,
|
||||
"low": summary.Low,
|
||||
},
|
||||
})
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
|
|
@ -140,3 +162,93 @@ func runScan() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runScanForTarget runs a code scanning check against an external repo target.
|
||||
func runScanForTarget(target string) error {
|
||||
repo, fullName := buildTargetRepo(target)
|
||||
if repo == nil {
|
||||
return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)")
|
||||
}
|
||||
|
||||
var allAlerts []ScanAlert
|
||||
summary := &AlertSummary{}
|
||||
|
||||
alerts, err := fetchCodeScanningAlerts(fullName)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "fetch code-scanning alerts for "+fullName)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
if scanTool != "" && alert.Tool.Name != scanTool {
|
||||
continue
|
||||
}
|
||||
severity := alert.Rule.Severity
|
||||
if severity == "" {
|
||||
severity = "medium"
|
||||
}
|
||||
if !filterBySeverity(severity, securitySeverity) {
|
||||
continue
|
||||
}
|
||||
summary.Add(severity)
|
||||
allAlerts = append(allAlerts, ScanAlert{
|
||||
Repo: repo.Name,
|
||||
Severity: severity,
|
||||
RuleID: alert.Rule.ID,
|
||||
Tool: alert.Tool.Name,
|
||||
Path: alert.MostRecentInstance.Location.Path,
|
||||
Line: alert.MostRecentInstance.Location.StartLine,
|
||||
Description: alert.Rule.Description,
|
||||
Message: alert.MostRecentInstance.Message.Text,
|
||||
})
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
_ = ai.Record(ai.Event{
|
||||
Type: "security.scan",
|
||||
Timestamp: time.Now(),
|
||||
Repo: fullName,
|
||||
Data: map[string]any{
|
||||
"target": fullName,
|
||||
"total": summary.Total,
|
||||
"critical": summary.Critical,
|
||||
"high": summary.High,
|
||||
"medium": summary.Medium,
|
||||
"low": summary.Low,
|
||||
},
|
||||
})
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Code Scanning ("+fullName+"):"), summary.String())
|
||||
cli.Blank()
|
||||
|
||||
if len(allAlerts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, alert := range allAlerts {
|
||||
sevStyle := severityStyle(alert.Severity)
|
||||
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
|
||||
cli.Print("%-16s %s %-20s %-40s %s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
|
||||
alert.RuleID,
|
||||
location,
|
||||
cli.DimStyle.Render(alert.Tool),
|
||||
)
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func addSecretsCommand(parent *cli.Command) {
|
|||
cmd.Flags().StringVar(&securityRegistryPath, "registry", "", i18n.T("common.flag.registry"))
|
||||
cmd.Flags().StringVar(&securityRepo, "repo", "", i18n.T("cmd.security.flag.repo"))
|
||||
cmd.Flags().BoolVar(&securityJSON, "json", false, i18n.T("common.flag.json"))
|
||||
cmd.Flags().StringVar(&securityTarget, "target", "", i18n.T("cmd.security.flag.target"))
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
|
@ -40,6 +41,11 @@ func runSecrets() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// External target mode: bypass registry entirely
|
||||
if securityTarget != "" {
|
||||
return runSecretsForTarget(securityTarget)
|
||||
}
|
||||
|
||||
reg, err := loadRegistry(securityRegistryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -119,3 +125,67 @@ func runSecrets() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSecretsForTarget runs secret scanning checks against an external repo target.
|
||||
func runSecretsForTarget(target string) error {
|
||||
repo, fullName := buildTargetRepo(target)
|
||||
if repo == nil {
|
||||
return cli.Err("invalid target format: use owner/repo (e.g. wailsapp/wails)")
|
||||
}
|
||||
|
||||
var allAlerts []SecretAlert
|
||||
openCount := 0
|
||||
|
||||
alerts, err := fetchSecretScanningAlerts(fullName)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "fetch secret-scanning alerts for "+fullName)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
if alert.State != "open" {
|
||||
continue
|
||||
}
|
||||
openCount++
|
||||
allAlerts = append(allAlerts, SecretAlert{
|
||||
Repo: repo.Name,
|
||||
Number: alert.Number,
|
||||
SecretType: alert.SecretType,
|
||||
State: alert.State,
|
||||
Resolution: alert.Resolution,
|
||||
PushProtection: alert.PushProtection,
|
||||
})
|
||||
}
|
||||
|
||||
if securityJSON {
|
||||
output, err := json.MarshalIndent(allAlerts, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
if openCount > 0 {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
|
||||
} else {
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.SuccessStyle.Render("No exposed secrets"))
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
for _, alert := range allAlerts {
|
||||
bypassed := ""
|
||||
if alert.PushProtection {
|
||||
bypassed = cli.WarningStyle.Render(" (push protection bypassed)")
|
||||
}
|
||||
cli.Print("%-16s %-6d %-30s%s\n",
|
||||
cli.ValueStyle.Render(alert.Repo),
|
||||
alert.Number,
|
||||
cli.ErrorStyle.Render(alert.SecretType),
|
||||
bypassed,
|
||||
)
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ var (
|
|||
securityRepo string
|
||||
securitySeverity string
|
||||
securityJSON bool
|
||||
securityTarget string // External repo target (e.g. "wailsapp/wails")
|
||||
)
|
||||
|
||||
// AddSecurityCommands adds the 'security' command to the root.
|
||||
|
|
@ -31,6 +32,7 @@ func AddSecurityCommands(root *cli.Command) {
|
|||
addDepsCommand(secCmd)
|
||||
addScanCommand(secCmd)
|
||||
addSecretsCommand(secCmd)
|
||||
addJobsCommand(secCmd)
|
||||
|
||||
root.AddCommand(secCmd)
|
||||
}
|
||||
|
|
@ -192,6 +194,16 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
|
|||
return reg.List()
|
||||
}
|
||||
|
||||
// buildTargetRepo creates a synthetic Repo entry for an external target (e.g. "wailsapp/wails").
|
||||
func buildTargetRepo(target string) (*repos.Repo, string) {
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return nil, ""
|
||||
}
|
||||
return &repos.Repo{Name: parts[1]}, target
|
||||
}
|
||||
|
||||
|
||||
// AlertSummary holds aggregated alert counts.
|
||||
type AlertSummary struct {
|
||||
Critical int
|
||||
|
|
|
|||
7
internal/core-ide/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.task
|
||||
.idea
|
||||
bin
|
||||
frontend/dist
|
||||
frontend/node_modules
|
||||
build/linux/appimage/build
|
||||
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||
71
internal/core-ide/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Wails3 Angular Template
|
||||
|
||||
- Angular 20
|
||||
- Wails3
|
||||
|
||||

|
||||
|
||||
Includes all Angular CLI guidelines, Web Awesome, and Font Awesome.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Navigate to your project directory in the terminal.
|
||||
|
||||
make a new project using Wails3:
|
||||
|
||||
```
|
||||
wails3 init -n MyWailsApp -t https://github.com/Snider/wails-angular-template@v0.0.1
|
||||
cd MyWailsApp
|
||||
```
|
||||
|
||||
2. To run your application in development mode, use the following command:
|
||||
|
||||
```
|
||||
wails3 dev
|
||||
```
|
||||
|
||||
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||
|
||||
3. To build your application for production, use:
|
||||
|
||||
```
|
||||
wails3 build
|
||||
```
|
||||
|
||||
This will create a production-ready executable in the `build` directory.
|
||||
|
||||
## Exploring Wails3 Features
|
||||
|
||||
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||
|
||||
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
|
||||
|
||||
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||
|
||||
```
|
||||
go run .
|
||||
```
|
||||
|
||||
Note: Some examples may be under development during the alpha phase.
|
||||
|
||||
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references.
|
||||
|
||||
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
|
||||
|
||||
## Project Structure
|
||||
|
||||
Take a moment to familiarize yourself with your project structure:
|
||||
|
||||
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
|
||||
- `main.go`: The entry point of your Go backend
|
||||
- `app.go`: Define your application structure and methods here
|
||||
- `wails.json`: Configuration file for your Wails project
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Modify the frontend in the `frontend/` directory to create your desired UI.
|
||||
2. Add backend functionality in `main.go`.
|
||||
3. Use `wails3 dev` to see your changes in real-time.
|
||||
4. When ready, build your application with `wails3 build`.
|
||||
|
||||
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
|
||||
34
internal/core-ide/Taskfile.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ./build/Taskfile.yml
|
||||
windows: ./build/windows/Taskfile.yml
|
||||
darwin: ./build/darwin/Taskfile.yml
|
||||
linux: ./build/linux/Taskfile.yml
|
||||
|
||||
vars:
|
||||
APP_NAME: "core-ide"
|
||||
BIN_DIR: "bin"
|
||||
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application
|
||||
cmds:
|
||||
- task: "{{OS}}:build"
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
cmds:
|
||||
- task: "{{OS}}:package"
|
||||
|
||||
run:
|
||||
summary: Runs the application
|
||||
cmds:
|
||||
- task: "{{OS}}:run"
|
||||
|
||||
dev:
|
||||
summary: Runs the application in development mode
|
||||
cmds:
|
||||
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||
|
||||
91
internal/core-ide/build/Taskfile.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
go:mod:tidy:
|
||||
summary: Runs `go mod tidy`
|
||||
internal: true
|
||||
cmds:
|
||||
- go mod tidy
|
||||
|
||||
install:frontend:deps:
|
||||
summary: Install frontend dependencies
|
||||
dir: frontend
|
||||
sources:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
generates:
|
||||
- node_modules/*
|
||||
preconditions:
|
||||
- sh: npm version
|
||||
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
|
||||
cmds:
|
||||
- npm install
|
||||
|
||||
build:frontend:
|
||||
label: build:frontend (PRODUCTION={{.PRODUCTION}})
|
||||
summary: Build the frontend project
|
||||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
- task: install:frontend:deps
|
||||
- task: generate:bindings
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
cmds:
|
||||
- npm run {{.BUILD_COMMAND}} -q
|
||||
env:
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
vars:
|
||||
BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}'
|
||||
|
||||
|
||||
generate:bindings:
|
||||
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
|
||||
summary: Generates bindings for the frontend
|
||||
deps:
|
||||
- task: go:mod:tidy
|
||||
sources:
|
||||
- "**/*.[jt]s"
|
||||
- exclude: frontend/**/*
|
||||
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- frontend/bindings/**/*
|
||||
cmds:
|
||||
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=false -ts -i
|
||||
|
||||
generate:icons:
|
||||
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||
dir: build
|
||||
sources:
|
||||
- "appicon.png"
|
||||
generates:
|
||||
- "darwin/icons.icns"
|
||||
- "windows/icon.ico"
|
||||
cmds:
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
|
||||
|
||||
dev:frontend:
|
||||
summary: Runs the frontend in development mode
|
||||
dir: frontend
|
||||
deps:
|
||||
- task: install:frontend:deps
|
||||
cmds:
|
||||
- npm run dev -- --port {{.VITE_PORT}}
|
||||
vars:
|
||||
VITE_PORT: '{{.VITE_PORT | default "5173"}}'
|
||||
|
||||
update:build-assets:
|
||||
summary: Updates the build assets
|
||||
dir: build
|
||||
preconditions:
|
||||
- sh: '[ -n "{{.APP_NAME}}" ]'
|
||||
msg: "APP_NAME variable is required"
|
||||
cmds:
|
||||
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
||||
BIN
internal/core-ide/build/appicon.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
62
internal/core-ide/build/config.yml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# This file contains the configuration for this project.
|
||||
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||
# Note that this will overwrite any changes you have made to the assets.
|
||||
version: '3'
|
||||
|
||||
# This information is used to generate the build assets.
|
||||
info:
|
||||
companyName: "Lethean Community Interest Company" # The name of the company
|
||||
productName: "Core IDE" # The name of the application
|
||||
productIdentifier: "com.lethean.core-ide" # The unique product identifier
|
||||
description: "Core IDE - Development Environment" # The application description
|
||||
copyright: "(c) 2026, Lethean Community Interest Company. EUPL-1.2" # Copyright text
|
||||
comments: "Host UK Core IDE" # Comments
|
||||
version: "0.0.1" # The application version
|
||||
|
||||
# Dev mode configuration
|
||||
dev_mode:
|
||||
root_path: .
|
||||
log_level: warn
|
||||
debounce: 1000
|
||||
ignore:
|
||||
dir:
|
||||
- .git
|
||||
- node_modules
|
||||
- frontend
|
||||
- bin
|
||||
file:
|
||||
- .DS_Store
|
||||
- .gitignore
|
||||
- .gitkeep
|
||||
watched_extension:
|
||||
- "*.go"
|
||||
git_ignore: true
|
||||
executes:
|
||||
- cmd: wails3 task common:install:frontend:deps
|
||||
type: once
|
||||
- cmd: wails3 task common:dev:frontend
|
||||
type: background
|
||||
- cmd: go mod tidy
|
||||
type: blocking
|
||||
- cmd: wails3 task build
|
||||
type: blocking
|
||||
- cmd: wails3 task run
|
||||
type: primary
|
||||
|
||||
# File Associations
|
||||
fileAssociations:
|
||||
# - ext: wails
|
||||
# name: Wails
|
||||
# description: Wails Application File
|
||||
# iconName: wailsFileIcon
|
||||
# role: Editor
|
||||
# - ext: jpg
|
||||
# name: JPEG
|
||||
# description: Image File
|
||||
# iconName: jpegFileIcon
|
||||
# role: Editor
|
||||
# mimeType: image/jpeg # (optional)
|
||||
|
||||
# Other data
|
||||
other:
|
||||
- name: My Other Data
|
||||
32
internal/core-ide/build/darwin/Info.dev.plist
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Core IDE (Dev)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>core-ide</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.lethean.core-ide.dev</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Core IDE Development Build</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2026 Lethean Community Interest Company. EUPL-1.2</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
27
internal/core-ide/build/darwin/Info.plist
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Core IDE</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>core-ide</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.lethean.core-ide</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Core IDE - Development Environment</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2026 Lethean Community Interest Company. EUPL-1.2</string>
|
||||
</dict>
|
||||
</plist>
|
||||
85
internal/core-ide/build/darwin/Taskfile.yml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Creates a production build of the application
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||
env:
|
||||
GOOS: darwin
|
||||
CGO_ENABLED: 1
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
build:universal:
|
||||
summary: Builds darwin universal binary (arm64 + amd64)
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
ARCH: amd64
|
||||
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
|
||||
PRODUCTION: '{{.PRODUCTION | default "true"}}'
|
||||
- task: build
|
||||
vars:
|
||||
ARCH: arm64
|
||||
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
PRODUCTION: '{{.PRODUCTION | default "true"}}'
|
||||
cmds:
|
||||
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application into a `.app` bundle
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: create:app:bundle
|
||||
|
||||
package:universal:
|
||||
summary: Packages darwin universal binary (arm64 + amd64)
|
||||
deps:
|
||||
- task: build:universal
|
||||
cmds:
|
||||
- task: create:app:bundle
|
||||
|
||||
|
||||
create:app:bundle:
|
||||
summary: Creates an `.app` bundle
|
||||
cmds:
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
|
||||
|
||||
run:
|
||||
deps:
|
||||
- task: build
|
||||
cmds:
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS
|
||||
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
|
||||
BIN
internal/core-ide/build/darwin/icons.icns
Normal file
119
internal/core-ide/build/linux/Taskfile.yml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application for Linux
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 1
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application for Linux
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: create:appimage
|
||||
- task: create:deb
|
||||
- task: create:rpm
|
||||
- task: create:aur
|
||||
|
||||
create:appimage:
|
||||
summary: Creates an AppImage
|
||||
dir: build/linux/appimage
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
- task: generate:dotdesktop
|
||||
cmds:
|
||||
- cp {{.APP_BINARY}} {{.APP_NAME}}
|
||||
- cp ../../appicon.png {{.APP_NAME}}.png
|
||||
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
|
||||
vars:
|
||||
APP_NAME: '{{.APP_NAME}}'
|
||||
APP_BINARY: '../../../bin/{{.APP_NAME}}'
|
||||
ICON: '{{.APP_NAME}}.png'
|
||||
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
|
||||
OUTPUT_DIR: '../../../bin'
|
||||
|
||||
create:deb:
|
||||
summary: Creates a deb package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:deb
|
||||
|
||||
create:rpm:
|
||||
summary: Creates a rpm package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:rpm
|
||||
|
||||
create:aur:
|
||||
summary: Creates a arch linux packager package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:aur
|
||||
|
||||
generate:deb:
|
||||
summary: Creates a deb package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:rpm:
|
||||
summary: Creates a rpm package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:aur:
|
||||
summary: Creates a arch linux packager package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:dotdesktop:
|
||||
summary: Generates a `.desktop` file
|
||||
dir: build
|
||||
cmds:
|
||||
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
|
||||
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
|
||||
vars:
|
||||
APP_NAME: '{{.APP_NAME}}'
|
||||
EXEC: '{{.APP_NAME}}'
|
||||
ICON: '{{.APP_NAME}}'
|
||||
CATEGORIES: 'Development;'
|
||||
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
40
internal/core-ide/build/linux/appimage/build.sh
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2018-Present Lea Anthony
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Fail script on any error
|
||||
set -euxo pipefail
|
||||
|
||||
# Define variables
|
||||
APP_DIR="${APP_NAME}.AppDir"
|
||||
|
||||
# Create AppDir structure
|
||||
mkdir -p "${APP_DIR}/usr/bin"
|
||||
cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
|
||||
cp "${ICON_PATH}" "${APP_DIR}/"
|
||||
cp "${DESKTOP_FILE}" "${APP_DIR}/"
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64)
|
||||
DEPLOY_ARCH="x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
DEPLOY_ARCH="aarch64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: ${ARCH}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Download linuxdeploy and make it executable
|
||||
wget -q -4 -N "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${DEPLOY_ARCH}.AppImage"
|
||||
chmod +x "linuxdeploy-${DEPLOY_ARCH}.AppImage"
|
||||
|
||||
# Run linuxdeploy to bundle the application
|
||||
"./linuxdeploy-${DEPLOY_ARCH}.AppImage" --appdir "${APP_DIR}" --output appimage
|
||||
|
||||
# Rename the generated AppImage (glob must be unquoted)
|
||||
mv ${APP_NAME}*.AppImage "${APP_NAME}.AppImage"
|
||||
|
||||
13
internal/core-ide/build/linux/desktop
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=My Product
|
||||
Comment=My Product Description
|
||||
# The Exec line includes %u to pass the URL to the application
|
||||
Exec=/usr/local/bin/wails-angular-template %u
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=wails-angular-template
|
||||
Categories=Utility;
|
||||
StartupWMClass=wails-angular-template
|
||||
|
||||
|
||||
67
internal/core-ide/build/linux/nfpm/nfpm.yaml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Feel free to remove those if you don't want/need to use them.
|
||||
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||
#
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
|
||||
name: "core-ide"
|
||||
arch: ${GOARCH}
|
||||
platform: "linux"
|
||||
version: "0.1.0"
|
||||
section: "default"
|
||||
priority: "extra"
|
||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||
description: "Core IDE - Development Environment"
|
||||
vendor: "Lethean Community Interest Company"
|
||||
homepage: "https://host.uk.com"
|
||||
license: "EUPL-1.2"
|
||||
release: "1"
|
||||
|
||||
contents:
|
||||
- src: "./bin/core-ide"
|
||||
dst: "/usr/local/bin/core-ide"
|
||||
- src: "./build/appicon.png"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/core-ide.png"
|
||||
- src: "./build/linux/core-ide.desktop"
|
||||
dst: "/usr/share/applications/core-ide.desktop"
|
||||
|
||||
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
|
||||
depends:
|
||||
- libgtk-3-0
|
||||
- libwebkit2gtk-4.1-0
|
||||
|
||||
# Distribution-specific overrides for different package formats and WebKit versions
|
||||
overrides:
|
||||
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.1)
|
||||
rpm:
|
||||
depends:
|
||||
- gtk3
|
||||
- webkit2gtk4.1
|
||||
|
||||
# Arch Linux packages (WebKit 4.1)
|
||||
archlinux:
|
||||
depends:
|
||||
- gtk3
|
||||
- webkit2gtk-4.1
|
||||
|
||||
# scripts section to ensure desktop database is updated after install
|
||||
scripts:
|
||||
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
|
||||
# You can also add preremove, postremove if needed
|
||||
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
|
||||
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
|
||||
|
||||
# replaces:
|
||||
# - foobar
|
||||
# provides:
|
||||
# - bar
|
||||
# depends:
|
||||
# - gtk3
|
||||
# - libwebkit2gtk
|
||||
# recommends:
|
||||
# - whatever
|
||||
# suggests:
|
||||
# - something-else
|
||||
# conflicts:
|
||||
# - not-foo
|
||||
# - not-bar
|
||||
# changelog: "changelog.yaml"
|
||||
21
internal/core-ide/build/linux/nfpm/scripts/postinstall.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Update desktop database for .desktop file changes
|
||||
# This makes the application appear in application menus and registers its capabilities.
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
echo "Updating desktop database..."
|
||||
update-desktop-database -q /usr/share/applications
|
||||
else
|
||||
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
|
||||
fi
|
||||
|
||||
# Update MIME database for custom URL schemes (x-scheme-handler)
|
||||
# This ensures the system knows how to handle your custom protocols.
|
||||
if command -v update-mime-database >/dev/null 2>&1; then
|
||||
echo "Updating MIME database..."
|
||||
update-mime-database -n /usr/share/mime
|
||||
else
|
||||
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
1
internal/core-ide/build/linux/nfpm/scripts/postremove.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/bin/bash
|
||||
1
internal/core-ide/build/linux/nfpm/scripts/preinstall.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/bin/bash
|
||||
1
internal/core-ide/build/linux/nfpm/scripts/preremove.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/bin/bash
|
||||
98
internal/core-ide/build/windows/Taskfile.yml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application for Windows
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- task: generate:syso
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||
- cmd: powershell Remove-item *.syso
|
||||
platforms: [windows]
|
||||
- cmd: rm -f *.syso
|
||||
platforms: [linux, darwin]
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
GOOS: windows
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
cmds:
|
||||
- |-
|
||||
if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then
|
||||
task create:msix:package
|
||||
else
|
||||
task create:nsis:installer
|
||||
fi
|
||||
vars:
|
||||
FORMAT: '{{.FORMAT | default "nsis"}}'
|
||||
|
||||
generate:syso:
|
||||
summary: Generates Windows `.syso` file
|
||||
dir: build
|
||||
cmds:
|
||||
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
|
||||
create:nsis:installer:
|
||||
summary: Creates an NSIS installer
|
||||
dir: build/windows/nsis
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
|
||||
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
|
||||
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||
|
||||
create:msix:package:
|
||||
summary: Creates an MSIX package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- |-
|
||||
wails3 tool msix \
|
||||
--config "{{.ROOT_DIR}}/wails.json" \
|
||||
--name "{{.APP_NAME}}" \
|
||||
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
|
||||
--arch "{{.ARCH}}" \
|
||||
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
|
||||
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
|
||||
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
|
||||
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
CERT_PATH: '{{.CERT_PATH | default ""}}'
|
||||
PUBLISHER: '{{.PUBLISHER | default ""}}'
|
||||
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||
|
||||
install:msix:tools:
|
||||
summary: Installs tools required for MSIX packaging
|
||||
cmds:
|
||||
- wails3 tool msix-install-tools
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
|
||||
BIN
internal/core-ide/build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
15
internal/core-ide/build/windows/info.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"fixed": {
|
||||
"file_version": "0.1.0"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "0.1.0",
|
||||
"CompanyName": "Lethean Community Interest Company",
|
||||
"FileDescription": "Core IDE — Desktop development environment",
|
||||
"LegalCopyright": "© 2026 Lethean Community Interest Company. EUPL-1.2",
|
||||
"ProductName": "Core IDE",
|
||||
"Comments": "Built with Wails v3 and Angular"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
internal/core-ide/build/windows/msix/app_manifest.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
|
||||
|
||||
<Identity
|
||||
Name="com.lethean.core-ide"
|
||||
Publisher="CN=Lethean Community Interest Company"
|
||||
Version="0.1.0.0"
|
||||
ProcessorArchitecture="x64" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Core IDE</DisplayName>
|
||||
<PublisherDisplayName>Lethean Community Interest Company</PublisherDisplayName>
|
||||
<Description>Core IDE - Development Environment</Description>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="en-us" />
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="CoreIDE" Executable="core-ide.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="Core IDE"
|
||||
Description="Core IDE - Development Environment"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.fullTrustProcess" Executable="core-ide.exe" />
|
||||
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
|
||||
</Capabilities>
|
||||
</Package>
|
||||
54
internal/core-ide/build/windows/msix/template.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MsixPackagingToolTemplate
|
||||
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
|
||||
<Settings
|
||||
AllowTelemetry="false"
|
||||
ApplyACLsToPackageFiles="true"
|
||||
GenerateCommandLineFile="true"
|
||||
AllowPromptForPassword="false">
|
||||
</Settings>
|
||||
<Installer
|
||||
Path="wails-angular-template"
|
||||
Arguments=""
|
||||
InstallLocation="C:\Program Files\My Company\My Product">
|
||||
</Installer>
|
||||
<PackageInformation
|
||||
PackageName="My Product"
|
||||
PackageDisplayName="My Product"
|
||||
PublisherName="CN=My Company"
|
||||
PublisherDisplayName="My Company"
|
||||
Version="0.1.0.0"
|
||||
PackageDescription="My Product Description">
|
||||
<Capabilities>
|
||||
<Capability Name="runFullTrust" />
|
||||
|
||||
</Capabilities>
|
||||
<Applications>
|
||||
<Application
|
||||
Id="com.wails.wails-angular-template"
|
||||
Description="My Product Description"
|
||||
DisplayName="My Product"
|
||||
ExecutableName="wails-angular-template"
|
||||
EntryPoint="Windows.FullTrustApplication">
|
||||
|
||||
</Application>
|
||||
</Applications>
|
||||
<Resources>
|
||||
<Resource Language="en-us" />
|
||||
</Resources>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
<Properties>
|
||||
<Framework>false</Framework>
|
||||
<DisplayName>My Product</DisplayName>
|
||||
<PublisherDisplayName>My Company</PublisherDisplayName>
|
||||
<Description>My Product Description</Description>
|
||||
<Logo>Assets\AppIcon.png</Logo>
|
||||
</Properties>
|
||||
</PackageInformation>
|
||||
<SaveLocation PackagePath="wails-angular-template.msix" />
|
||||
<PackageIntegrity>
|
||||
<CertificatePath></CertificatePath>
|
||||
</PackageIntegrity>
|
||||
</MsixPackagingToolTemplate>
|
||||
114
internal/core-ide/build/windows/nsis/project.nsi
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "my-project" # Default "wails-angular-template"
|
||||
## !define INFO_COMPANYNAME "My Company" # Default "My Company"
|
||||
## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
||||
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$APPDATA\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
236
internal/core-ide/build/windows/nsis/wails_tools.nsh
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "core-ide"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "Lethean Community Interest Company"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "Core IDE"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "0.1.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "© 2026 Lethean Community Interest Company. EUPL-1.2"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
|
||||
!macroend
|
||||
22
internal/core-ide/build/windows/wails.exe.manifest
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.wails-angular-template" version="0.1.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
183
internal/core-ide/claude_bridge.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true // Allow requests with no Origin header (same-origin)
|
||||
}
|
||||
host := r.Host
|
||||
return origin == "http://"+host || origin == "https://"+host ||
|
||||
strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1")
|
||||
},
|
||||
}
|
||||
|
||||
// ClaudeBridge forwards messages between GUI clients and the MCP core WebSocket.
|
||||
type ClaudeBridge struct {
|
||||
mcpConn *websocket.Conn
|
||||
mcpURL string
|
||||
clients map[*websocket.Conn]bool
|
||||
clientsMu sync.RWMutex
|
||||
broadcast chan []byte
|
||||
reconnectMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClaudeBridge creates a new bridge to the MCP core WebSocket.
|
||||
func NewClaudeBridge(mcpURL string) *ClaudeBridge {
|
||||
return &ClaudeBridge{
|
||||
mcpURL: mcpURL,
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
broadcast: make(chan []byte, 256),
|
||||
}
|
||||
}
|
||||
|
||||
// Start connects to the MCP WebSocket and starts the bridge.
|
||||
func (cb *ClaudeBridge) Start() {
|
||||
go cb.connectToMCP()
|
||||
go cb.broadcastLoop()
|
||||
}
|
||||
|
||||
// connectToMCP establishes connection to the MCP core WebSocket.
|
||||
func (cb *ClaudeBridge) connectToMCP() {
|
||||
for {
|
||||
cb.reconnectMu.Lock()
|
||||
if cb.mcpConn != nil {
|
||||
cb.mcpConn.Close()
|
||||
}
|
||||
|
||||
log.Printf("Claude bridge connecting to MCP at %s", cb.mcpURL)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(cb.mcpURL, nil)
|
||||
if err != nil {
|
||||
log.Printf("Claude bridge failed to connect to MCP: %v", err)
|
||||
cb.reconnectMu.Unlock()
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
cb.mcpConn = conn
|
||||
cb.reconnectMu.Unlock()
|
||||
log.Printf("Claude bridge connected to MCP")
|
||||
|
||||
// Read messages from MCP and broadcast to clients
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Claude bridge MCP read error: %v", err)
|
||||
break
|
||||
}
|
||||
select {
|
||||
case cb.broadcast <- message:
|
||||
default:
|
||||
log.Printf("Claude bridge: broadcast channel full, dropping message")
|
||||
}
|
||||
}
|
||||
|
||||
// Connection lost, retry
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastLoop sends messages from MCP to all connected clients.
|
||||
func (cb *ClaudeBridge) broadcastLoop() {
|
||||
for message := range cb.broadcast {
|
||||
var failedClients []*websocket.Conn
|
||||
cb.clientsMu.RLock()
|
||||
for client := range cb.clients {
|
||||
err := client.WriteMessage(websocket.TextMessage, message)
|
||||
if err != nil {
|
||||
log.Printf("Claude bridge client write error: %v", err)
|
||||
failedClients = append(failedClients, client)
|
||||
}
|
||||
}
|
||||
cb.clientsMu.RUnlock()
|
||||
|
||||
if len(failedClients) > 0 {
|
||||
cb.clientsMu.Lock()
|
||||
for _, client := range failedClients {
|
||||
delete(cb.clients, client)
|
||||
client.Close()
|
||||
}
|
||||
cb.clientsMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWebSocket handles WebSocket connections from GUI clients.
|
||||
func (cb *ClaudeBridge) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Claude bridge upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send connected message before registering to avoid concurrent writes
|
||||
connMsg, _ := json.Marshal(map[string]any{
|
||||
"type": "system",
|
||||
"data": "Connected to Claude bridge",
|
||||
"timestamp": time.Now(),
|
||||
})
|
||||
if err := conn.WriteMessage(websocket.TextMessage, connMsg); err != nil {
|
||||
log.Printf("Claude bridge initial write error: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
cb.clientsMu.Lock()
|
||||
cb.clients[conn] = true
|
||||
cb.clientsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
cb.clientsMu.Lock()
|
||||
delete(cb.clients, conn)
|
||||
cb.clientsMu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Read messages from client and forward to MCP
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse the message to check type
|
||||
var msg map[string]any
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Forward claude_message to MCP
|
||||
if msgType, ok := msg["type"].(string); ok && msgType == "claude_message" {
|
||||
cb.sendToMCP(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendToMCP sends a message to the MCP WebSocket.
|
||||
func (cb *ClaudeBridge) sendToMCP(message []byte) {
|
||||
cb.reconnectMu.Lock()
|
||||
defer cb.reconnectMu.Unlock()
|
||||
|
||||
if cb.mcpConn == nil {
|
||||
log.Printf("Claude bridge: MCP not connected")
|
||||
return
|
||||
}
|
||||
|
||||
err := cb.mcpConn.WriteMessage(websocket.TextMessage, message)
|
||||
if err != nil {
|
||||
log.Printf("Claude bridge MCP write error: %v", err)
|
||||
}
|
||||
}
|
||||
17
internal/core-ide/frontend/.editorconfig
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
43
internal/core-ide/frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
59
internal/core-ide/frontend/README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# WailsAngularTemplate
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.6.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
98
internal/core-ide/frontend/angular.json
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"wails-angular-template": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "wails-angular-template:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "wails-angular-template:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
internal/core-ide/frontend/bindings/changeme/greetservice.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function Greet(name: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1411160069, name);
|
||||
}
|
||||
7
internal/core-ide/frontend/bindings/changeme/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as GreetService from "./greetservice.js";
|
||||
export {
|
||||
GreetService
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function Greet(name: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1411160069, name);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as GreetService from "./greetservice.js";
|
||||
export {
|
||||
GreetService
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
//@ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
Object.freeze($Create.Events);
|
||||
2
internal/core-ide/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
9759
internal/core-ide/frontend/package-lock.json
generated
Normal file
57
internal/core-ide/frontend/package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "wails-angular-template",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"dev": "ng serve --configuration development",
|
||||
"build": "ng build",
|
||||
"build:dev": "ng build --configuration development",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:wails-angular-template": "node dist/wails-angular-template/server/server.mjs"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.3.0",
|
||||
"@angular/compiler": "^20.3.0",
|
||||
"@angular/core": "^20.3.0",
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/platform-server": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"@angular/ssr": "^20.3.6",
|
||||
"@wailsio/runtime": "3.0.0-alpha.72",
|
||||
"express": "^5.1.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.3.6",
|
||||
"@angular/cli": "^20.3.6",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^20.17.19",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
}
|
||||
93
internal/core-ide/frontend/public/Inter Font License.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
internal/core-ide/frontend/public/Inter-Medium.ttf
Normal file
BIN
internal/core-ide/frontend/public/angular.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
1
internal/core-ide/frontend/public/javascript.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
157
internal/core-ide/frontend/public/style.css
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
:root {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("./Inter-Medium.ttf") format("truetype");
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 3em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #e80000aa);
|
||||
}
|
||||
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #f7df1eaa);
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
color: black;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
BIN
internal/core-ide/frontend/public/wails.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
12
internal/core-ide/frontend/src/app/app.config.server.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
||||
import { provideServerRendering, withRoutes } from '@angular/ssr';
|
||||
import { appConfig } from './app.config';
|
||||
import { serverRoutes } from './app.routes.server';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(withRoutes(serverRoutes))
|
||||
]
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
13
internal/core-ide/frontend/src/app/app.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes), provideClientHydration(withEventReplay())
|
||||
]
|
||||
};
|
||||
23
internal/core-ide/frontend/src/app/app.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<div class="container">
|
||||
<div>
|
||||
<a (click)='openLink("https://v3alpha.wails.io")'>
|
||||
<img src="/wails.png" class="logo" alt="Wails logo"/>
|
||||
</a>
|
||||
<a (click)='openLink("https://angular.io")'>
|
||||
<img src="/angular.png" class="logo vanilla" alt="Angular logo"/>
|
||||
</a>
|
||||
</div>
|
||||
<h1>Wails + Angular v20</h1>
|
||||
<div class="card">
|
||||
<div class="result">{{ result() }}</div>
|
||||
<div class="input-box">
|
||||
<input class="input" type="text" autocomplete="off" [value]="name()" (input)="name.set($event.target.value)"/>
|
||||
<button class="btn" (click)="doGreet()">Greet</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div><p>Click on the Wails logo to learn more</p></div>
|
||||
<div><p>{{ time() }}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet />
|
||||
8
internal/core-ide/frontend/src/app/app.routes.server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Client
|
||||
}
|
||||
];
|
||||
17
internal/core-ide/frontend/src/app/app.routes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { TrayComponent } from './pages/tray/tray.component';
|
||||
import { IdeComponent } from './pages/ide/ide.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
// System tray panel - standalone compact UI
|
||||
{ path: 'tray', component: TrayComponent },
|
||||
|
||||
// Full IDE interface
|
||||
{ path: 'ide', component: IdeComponent },
|
||||
|
||||
// Default to tray for the root (tray panel is the default view)
|
||||
{ path: '', redirectTo: 'tray', pathMatch: 'full' },
|
||||
|
||||
// Catch-all
|
||||
{ path: '**', redirectTo: 'tray' },
|
||||
];
|
||||
0
internal/core-ide/frontend/src/app/app.scss
Normal file
23
internal/core-ide/frontend/src/app/app.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, wails-angular-template');
|
||||
});
|
||||
});
|
||||
17
internal/core-ide/frontend/src/app/app.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class App {}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: SafeHtml;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-items">
|
||||
@for (item of navItems; track item.id) {
|
||||
<button
|
||||
class="nav-item"
|
||||
[class.active]="currentRoute === item.id"
|
||||
(click)="routeChange.emit(item.id)"
|
||||
[title]="item.label">
|
||||
<div class="nav-icon" [innerHTML]="item.icon"></div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="nav-item" (click)="routeChange.emit('settings')" title="Settings">
|
||||
<div class="nav-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
`,
|
||||
styles: [`
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 56px;
|
||||
background: #16161e;
|
||||
border-right: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #565f89;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #a9b1d6;
|
||||
background: rgba(122, 162, 247, 0.1);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #7aa2f7;
|
||||
background: rgba(122, 162, 247, 0.15);
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
background: #7aa2f7;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.nav-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
border-top: 1px solid #24283b;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class SidebarComponent {
|
||||
@Input() currentRoute = 'dashboard';
|
||||
@Output() routeChange = new EventEmitter<string>();
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
this.navItems = this.createNavItems();
|
||||
}
|
||||
|
||||
navItems: NavItem[];
|
||||
|
||||
private createNavItems(): NavItem[] {
|
||||
return [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
|
||||
</svg>`)
|
||||
},
|
||||
{
|
||||
id: 'explorer',
|
||||
label: 'Explorer',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>`)
|
||||
},
|
||||
{
|
||||
id: 'search',
|
||||
label: 'Search',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>`)
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
label: 'Source Control',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
|
||||
</svg>`)
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
label: 'Debug',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
|
||||
</svg>`)
|
||||
},
|
||||
{
|
||||
id: 'terminal',
|
||||
label: 'Terminal',
|
||||
icon: this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>`)
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
506
internal/core-ide/frontend/src/app/pages/ide/ide.component.ts
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
import { Component, signal, OnInit, OnDestroy, PLATFORM_ID, Inject } from '@angular/core';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { SidebarComponent } from '../../components/sidebar/sidebar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ide',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SidebarComponent],
|
||||
template: `
|
||||
<div class="ide-layout">
|
||||
<app-sidebar [currentRoute]="currentRoute()" (routeChange)="onRouteChange($event)"></app-sidebar>
|
||||
|
||||
<div class="ide-main">
|
||||
<!-- Top Bar -->
|
||||
<div class="top-bar">
|
||||
<div class="breadcrumb">
|
||||
<span class="breadcrumb-item">Core IDE</span>
|
||||
<span class="breadcrumb-sep">/</span>
|
||||
<span class="breadcrumb-item active">{{ currentRoute() }}</span>
|
||||
</div>
|
||||
<div class="top-bar-actions">
|
||||
<span class="time">{{ currentTime() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="ide-content">
|
||||
@switch (currentRoute()) {
|
||||
@case ('dashboard') {
|
||||
<div class="dashboard-view">
|
||||
<h1>Welcome to Core IDE</h1>
|
||||
<p class="subtitle">Your development environment is ready.</p>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon projects">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ projectCount() }}</span>
|
||||
<span class="stat-label">Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon tasks">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ taskCount() }}</span>
|
||||
<span class="stat-label">Tasks</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon git">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ gitChanges() }}</span>
|
||||
<span class="stat-label">Changes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon status">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value status-ok">OK</span>
|
||||
<span class="stat-label">Status</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<h2>Quick Actions</h2>
|
||||
<div class="actions-grid">
|
||||
<button class="action-card" (click)="emitAction('new-project')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span>New Project</span>
|
||||
</button>
|
||||
<button class="action-card" (click)="emitAction('open-project')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<span>Open Project</span>
|
||||
</button>
|
||||
<button class="action-card" (click)="emitAction('run-dev')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Run Dev Server</span>
|
||||
</button>
|
||||
<button class="action-card" (click)="onRouteChange('terminal')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Open Terminal</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('explorer') {
|
||||
<div class="panel-view">
|
||||
<h2>File Explorer</h2>
|
||||
<p>Browse and manage your project files.</p>
|
||||
</div>
|
||||
}
|
||||
@case ('search') {
|
||||
<div class="panel-view">
|
||||
<h2>Search</h2>
|
||||
<p>Search across all files in your workspace.</p>
|
||||
</div>
|
||||
}
|
||||
@case ('git') {
|
||||
<div class="panel-view">
|
||||
<h2>Source Control</h2>
|
||||
<p>Manage your Git repositories and commits.</p>
|
||||
</div>
|
||||
}
|
||||
@case ('debug') {
|
||||
<div class="panel-view">
|
||||
<h2>Debug</h2>
|
||||
<p>Debug your applications.</p>
|
||||
</div>
|
||||
}
|
||||
@case ('terminal') {
|
||||
<div class="panel-view terminal">
|
||||
<h2>Terminal</h2>
|
||||
<div class="terminal-output">
|
||||
<pre>$ core dev health
|
||||
18 repos | clean | synced
|
||||
|
||||
$ _</pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('settings') {
|
||||
<div class="panel-view">
|
||||
<h2>Settings</h2>
|
||||
<p>Configure your IDE preferences.</p>
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<div class="panel-view">
|
||||
<h2>{{ currentRoute() }}</h2>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<span class="status-item branch">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/>
|
||||
</svg>
|
||||
main
|
||||
</span>
|
||||
<span class="status-item">UTF-8</span>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span class="status-item">Core IDE v0.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ide-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #1a1b26;
|
||||
color: #a9b1d6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.ide-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 1rem;
|
||||
background: #16161e;
|
||||
border-bottom: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #c0caf5;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.breadcrumb-sep {
|
||||
color: #414868;
|
||||
}
|
||||
|
||||
.top-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
color: #565f89;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.ide-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-view h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #565f89;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: #16161e;
|
||||
border: 1px solid #24283b;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.stat-icon.projects {
|
||||
background: rgba(122, 162, 247, 0.15);
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
.stat-icon.tasks {
|
||||
background: rgba(158, 206, 106, 0.15);
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.stat-icon.git {
|
||||
background: rgba(247, 118, 142, 0.15);
|
||||
color: #f7768e;
|
||||
}
|
||||
|
||||
.stat-icon.status {
|
||||
background: rgba(158, 206, 106, 0.15);
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
.stat-value.status-ok {
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8125rem;
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
.quick-actions h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background: #16161e;
|
||||
border: 1px solid #24283b;
|
||||
border-radius: 8px;
|
||||
color: #a9b1d6;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
background: #1f2335;
|
||||
border-color: #7aa2f7;
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
.action-card svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
.action-card span {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.panel-view {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-view h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.panel-view p {
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
.panel-view.terminal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
background: #16161e;
|
||||
border: 1px solid #24283b;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 0.875rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.terminal-output pre {
|
||||
margin: 0;
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 24px;
|
||||
padding: 0 0.75rem;
|
||||
background: #7aa2f7;
|
||||
color: #1a1b26;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-left, .status-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.status-item svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.stats-grid, .actions-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats-grid, .actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class IdeComponent implements OnInit, OnDestroy {
|
||||
private isBrowser: boolean;
|
||||
private timeEventCleanup?: () => void;
|
||||
currentRoute = signal('dashboard');
|
||||
currentTime = signal('');
|
||||
projectCount = signal(18);
|
||||
taskCount = signal(5);
|
||||
gitChanges = signal(12);
|
||||
|
||||
constructor(@Inject(PLATFORM_ID) platformId: Object) {
|
||||
this.isBrowser = isPlatformBrowser(platformId);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
import('@wailsio/runtime').then(({ Events }) => {
|
||||
this.timeEventCleanup = Events.On('time', (time: { data: string }) => {
|
||||
this.currentTime.set(time.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.timeEventCleanup?.();
|
||||
}
|
||||
|
||||
onRouteChange(route: string) {
|
||||
this.currentRoute.set(route);
|
||||
}
|
||||
|
||||
emitAction(action: string) {
|
||||
if (!this.isBrowser) return;
|
||||
import('@wailsio/runtime').then(({ Events }) => {
|
||||
Events.Emit('action', action);
|
||||
});
|
||||
}
|
||||
}
|
||||
444
internal/core-ide/frontend/src/app/pages/tray/tray.component.ts
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
import { Component, signal, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Events } from '@wailsio/runtime';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tray',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div class="tray-container">
|
||||
<!-- Header -->
|
||||
<div class="tray-header">
|
||||
<div class="tray-logo">
|
||||
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
|
||||
</svg>
|
||||
<span>Core IDE</span>
|
||||
</div>
|
||||
<div class="tray-controls">
|
||||
<button class="control-btn" (click)="openIDE()" title="Open IDE">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="tray-stats">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Status</span>
|
||||
<span class="stat-value" [class.active]="isActive()">
|
||||
{{ isActive() ? 'Running' : 'Idle' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Projects</span>
|
||||
<span class="stat-value">{{ projectCount() }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Active Tasks</span>
|
||||
<span class="stat-value">{{ taskCount() }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Time</span>
|
||||
<span class="stat-value mono">{{ currentTime() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="actions-section">
|
||||
<div class="section-header">Quick Actions</div>
|
||||
<div class="actions-list">
|
||||
<button class="action-btn" (click)="emitAction('new-project')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span>New Project</span>
|
||||
</button>
|
||||
<button class="action-btn" (click)="emitAction('open-project')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<span>Open Project</span>
|
||||
</button>
|
||||
<button class="action-btn" (click)="emitAction('run-dev')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Run Dev Server</span>
|
||||
</button>
|
||||
<button class="action-btn danger" (click)="emitAction('stop-all')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
|
||||
</svg>
|
||||
<span>Stop All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Projects -->
|
||||
<div class="projects-section">
|
||||
<div class="section-header">Recent Projects</div>
|
||||
<div class="projects-list">
|
||||
@for (project of recentProjects(); track project.name) {
|
||||
<button class="project-item" (click)="openProject(project.path)">
|
||||
<div class="project-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="project-info">
|
||||
<span class="project-name">{{ project.name }}</span>
|
||||
<span class="project-path">{{ project.path }}</span>
|
||||
</div>
|
||||
</button>
|
||||
} @empty {
|
||||
<div class="no-projects">No recent projects</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="tray-footer">
|
||||
<div class="connection-status" [class.connected]="isActive()">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ isActive() ? 'Services Running' : 'Ready' }}</span>
|
||||
</div>
|
||||
<button class="footer-btn" (click)="openIDE()">Open IDE</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tray-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1a1b26;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #a9b1d6;
|
||||
}
|
||||
|
||||
.tray-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #16161e;
|
||||
border-bottom: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.tray-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
.tray-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: 1px solid #24283b;
|
||||
border-radius: 6px;
|
||||
color: #7aa2f7;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #24283b;
|
||||
border-color: #7aa2f7;
|
||||
}
|
||||
|
||||
.control-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tray-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 1rem;
|
||||
background: #16161e;
|
||||
border-bottom: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8125rem;
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
.stat-value.active {
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.stat-value.mono {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.actions-section, .projects-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: #565f89;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: #1a1b26;
|
||||
border-bottom: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: #16161e;
|
||||
border: 1px solid #24283b;
|
||||
border-radius: 6px;
|
||||
color: #a9b1d6;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #24283b;
|
||||
border-color: #414868;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #f7768e;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
border-color: #f7768e;
|
||||
background: rgba(247, 118, 142, 0.1);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.projects-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.projects-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #a9b1d6;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.project-item:hover {
|
||||
background: #24283b;
|
||||
}
|
||||
|
||||
.project-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #24283b;
|
||||
border-radius: 6px;
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
.project-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #c0caf5;
|
||||
}
|
||||
|
||||
.project-path {
|
||||
font-size: 0.6875rem;
|
||||
color: #565f89;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-projects {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
color: #565f89;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.tray-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 1rem;
|
||||
background: #16161e;
|
||||
border-top: 1px solid #24283b;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #565f89;
|
||||
}
|
||||
|
||||
.connection-status.connected .status-dot {
|
||||
background: #9ece6a;
|
||||
box-shadow: 0 0 4px #9ece6a;
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: #7aa2f7;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #1a1b26;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.footer-btn:hover {
|
||||
background: #89b4fa;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class TrayComponent implements OnInit, OnDestroy {
|
||||
currentTime = signal('');
|
||||
isActive = signal(false);
|
||||
projectCount = signal(3);
|
||||
taskCount = signal(0);
|
||||
recentProjects = signal([
|
||||
{ name: 'core', path: '~/Code/host-uk/core' },
|
||||
{ name: 'core-gui', path: '~/Code/host-uk/core-gui' },
|
||||
{ name: 'core-php', path: '~/Code/host-uk/core-php' },
|
||||
]);
|
||||
private timeEventCleanup?: () => void;
|
||||
|
||||
ngOnInit() {
|
||||
this.timeEventCleanup = Events.On('time', (time: { data: string }) => {
|
||||
this.currentTime.set(time.data);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.timeEventCleanup?.();
|
||||
}
|
||||
|
||||
openIDE() {
|
||||
Events.Emit('action', 'open-ide');
|
||||
}
|
||||
|
||||
emitAction(action: string) {
|
||||
Events.Emit('action', action);
|
||||
}
|
||||
|
||||
openProject(path: string) {
|
||||
Events.Emit('open-project', path);
|
||||
}
|
||||
}
|
||||
13
internal/core-ide/frontend/src/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Core IDE</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/assets/wails.png"/>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
8
internal/core-ide/frontend/src/main.server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
|
||||
import { App } from './app/app';
|
||||
import { config } from './app/app.config.server';
|
||||
|
||||
const bootstrap = (context: BootstrapContext) =>
|
||||
bootstrapApplication(App, config, context);
|
||||
|
||||
export default bootstrap;
|
||||
6
internal/core-ide/frontend/src/main.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
68
internal/core-ide/frontend/src/server.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
AngularNodeAppEngine,
|
||||
createNodeRequestHandler,
|
||||
isMainModule,
|
||||
writeResponseToNodeResponse,
|
||||
} from '@angular/ssr/node';
|
||||
import express from 'express';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const browserDistFolder = join(import.meta.dirname, '../browser');
|
||||
|
||||
const app = express();
|
||||
const angularApp = new AngularNodeAppEngine();
|
||||
|
||||
/**
|
||||
* Example Express Rest API endpoints can be defined here.
|
||||
* Uncomment and define endpoints as necessary.
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* app.get('/api/{*splat}', (req, res) => {
|
||||
* // Handle API request
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serve static files from /browser
|
||||
*/
|
||||
app.use(
|
||||
express.static(browserDistFolder, {
|
||||
maxAge: '1y',
|
||||
index: false,
|
||||
redirect: false,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle all other requests by rendering the Angular application.
|
||||
*/
|
||||
app.use((req, res, next) => {
|
||||
angularApp
|
||||
.handle(req)
|
||||
.then((response) =>
|
||||
response ? writeResponseToNodeResponse(response, res) : next(),
|
||||
)
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the server if this module is the main entry point, or it is ran via PM2.
|
||||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
|
||||
*/
|
||||
if (isMainModule(import.meta.url) || process.env['pm_id']) {
|
||||
const port = process.env['PORT'] || 4000;
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||
});
|
||||
server.on('error', (error: Error) => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
|
||||
*/
|
||||
export const reqHandler = createNodeRequestHandler(app);
|
||||
63
internal/core-ide/frontend/src/styles.scss
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* Global styles for Core IDE */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #1a1b26;
|
||||
color: #a9b1d6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
app-root {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #16161e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #414868;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #565f89;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid #7aa2f7;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button reset */
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
17
internal/core-ide/frontend/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
41
internal/core-ide/frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@bindings/*": [
|
||||
"bindings/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
internal/core-ide/frontend/tsconfig.spec.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
54
internal/core-ide/go.mod
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
module github.com/host-uk/core/internal/core-ide
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/wailsapp/wails/v3 v3.0.0-alpha.64
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/host-uk/core-gui v0.0.0
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lmittmann/tint v1.1.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/host-uk/core-gui => ../../../core-gui
|
||||
151
internal/core-ide/go.sum
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64 h1:xAhLFVfdbg7XdZQ5mMQmBv2BglWu8hMqe50Z+3UJvBs=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64/go.mod h1:zvgNL/mlFcX8aRGu6KOz9AHrMmTBD+4hJRQIONqF/Yw=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
7
internal/core-ide/greetservice.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
type GreetService struct{}
|
||||
|
||||
func (g *GreetService) Greet(name string) string {
|
||||
return "Hello " + name + "!"
|
||||
}
|
||||
BIN
internal/core-ide/icons/apptray.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
19
internal/core-ide/icons/icons.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package icons
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// AppTray is the main application tray icon.
|
||||
//
|
||||
//go:embed apptray.png
|
||||
var AppTray []byte
|
||||
|
||||
// SystrayMacTemplate is the template icon for macOS systray (22x22 PNG, black on transparent).
|
||||
// Template icons automatically adapt to light/dark mode on macOS.
|
||||
//
|
||||
//go:embed systray-mac-template.png
|
||||
var SystrayMacTemplate []byte
|
||||
|
||||
// SystrayDefault is the default icon for Windows/Linux systray.
|
||||
//
|
||||
//go:embed systray-default.png
|
||||
var SystrayDefault []byte
|
||||
BIN
internal/core-ide/icons/systray-default.png
Normal file
|
After Width: | Height: | Size: 228 B |
BIN
internal/core-ide/icons/systray-mac-template.png
Normal file
|
After Width: | Height: | Size: 167 B |
80
internal/core-ide/main.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/host-uk/core/internal/core-ide/icons"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist/wails-angular-template/browser
|
||||
var assets embed.FS
|
||||
|
||||
// Default MCP port for the embedded server
|
||||
const mcpPort = 9877
|
||||
|
||||
func main() {
|
||||
// Strip the embed path prefix so files are served from root
|
||||
staticAssets, err := fs.Sub(assets, "frontend/dist/wails-angular-template/browser")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create the MCP bridge for Claude Code integration
|
||||
mcpBridge := NewMCPBridge(mcpPort)
|
||||
|
||||
app := application.New(application.Options{
|
||||
Name: "Core IDE",
|
||||
Description: "Host UK Core IDE - Development Environment",
|
||||
Services: []application.Service{
|
||||
application.NewService(&GreetService{}),
|
||||
application.NewService(mcpBridge),
|
||||
},
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.AssetFileServerFS(staticAssets),
|
||||
},
|
||||
Mac: application.MacOptions{
|
||||
ActivationPolicy: application.ActivationPolicyAccessory,
|
||||
},
|
||||
})
|
||||
|
||||
// System tray
|
||||
systray := app.SystemTray.New()
|
||||
systray.SetTooltip("Core IDE")
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
systray.SetTemplateIcon(icons.AppTray)
|
||||
} else {
|
||||
systray.SetDarkModeIcon(icons.AppTray)
|
||||
systray.SetIcon(icons.AppTray)
|
||||
}
|
||||
|
||||
// Tray panel window
|
||||
trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Name: "tray-panel",
|
||||
Title: "Core IDE",
|
||||
Width: 380,
|
||||
Height: 480,
|
||||
URL: "/tray",
|
||||
Hidden: true,
|
||||
Frameless: true,
|
||||
BackgroundColour: application.NewRGB(26, 27, 38),
|
||||
})
|
||||
systray.AttachWindow(trayWindow).WindowOffset(5)
|
||||
|
||||
// Tray menu
|
||||
trayMenu := app.Menu.New()
|
||||
trayMenu.Add("Quit").OnClick(func(ctx *application.Context) {
|
||||
app.Quit()
|
||||
})
|
||||
systray.SetMenu(trayMenu)
|
||||
|
||||
log.Println("Starting Core IDE...")
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
520
internal/core-ide/mcp_bridge.go
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core-gui/pkg/webview"
|
||||
"github.com/host-uk/core-gui/pkg/ws"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// MCPBridge wires together WebView and WebSocket services
|
||||
// and starts the MCP HTTP server after Wails initializes.
|
||||
type MCPBridge struct {
|
||||
webview *webview.Service
|
||||
wsHub *ws.Hub
|
||||
claudeBridge *ClaudeBridge
|
||||
app *application.App
|
||||
port int
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewMCPBridge creates a new MCP bridge with all services wired up.
|
||||
func NewMCPBridge(port int) *MCPBridge {
|
||||
wv := webview.New()
|
||||
hub := ws.NewHub()
|
||||
|
||||
// Create Claude bridge to forward messages to MCP core on port 9876
|
||||
claudeBridge := NewClaudeBridge("ws://localhost:9876/ws")
|
||||
|
||||
return &MCPBridge{
|
||||
webview: wv,
|
||||
wsHub: hub,
|
||||
claudeBridge: claudeBridge,
|
||||
port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStartup is called by Wails when the app starts.
|
||||
// This wires up the app reference and starts the HTTP server.
|
||||
func (b *MCPBridge) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Get the Wails app reference
|
||||
b.app = application.Get()
|
||||
if b.app == nil {
|
||||
return fmt.Errorf("failed to get Wails app reference")
|
||||
}
|
||||
|
||||
// Wire up the WebView service with the app
|
||||
b.webview.SetApp(b.app)
|
||||
|
||||
// Set up console listener
|
||||
b.webview.SetupConsoleListener()
|
||||
|
||||
// Inject console capture into all windows after a short delay
|
||||
// (windows may not be created yet)
|
||||
go b.injectConsoleCapture()
|
||||
|
||||
// Start the HTTP server for MCP
|
||||
go b.startHTTPServer()
|
||||
|
||||
log.Printf("MCP Bridge started on port %d", b.port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// injectConsoleCapture injects the console capture script into windows.
|
||||
func (b *MCPBridge) injectConsoleCapture() {
|
||||
// Wait for windows to be created (poll with timeout)
|
||||
var windows []webview.WindowInfo
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
windows = b.webview.ListWindows()
|
||||
if len(windows) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(windows) == 0 {
|
||||
log.Printf("MCP Bridge: no windows found after waiting")
|
||||
return
|
||||
}
|
||||
for _, w := range windows {
|
||||
if err := b.webview.InjectConsoleCapture(w.Name); err != nil {
|
||||
log.Printf("Failed to inject console capture in %s: %v", w.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startHTTPServer starts the HTTP server for MCP and WebSocket.
|
||||
func (b *MCPBridge) startHTTPServer() {
|
||||
b.mu.Lock()
|
||||
b.running = true
|
||||
b.mu.Unlock()
|
||||
|
||||
// Start the WebSocket hub
|
||||
hubCtx := context.Background()
|
||||
go b.wsHub.Run(hubCtx)
|
||||
|
||||
// Claude bridge disabled - port 9876 is not an MCP WebSocket server
|
||||
// b.claudeBridge.Start()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// WebSocket endpoint for GUI clients
|
||||
mux.HandleFunc("/ws", b.wsHub.HandleWebSocket)
|
||||
|
||||
// MCP info endpoint
|
||||
mux.HandleFunc("/mcp", b.handleMCPInfo)
|
||||
|
||||
// MCP tools endpoint
|
||||
mux.HandleFunc("/mcp/tools", b.handleMCPTools)
|
||||
mux.HandleFunc("/mcp/call", b.handleMCPCall)
|
||||
|
||||
// Health check
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"mcp": true,
|
||||
"webview": b.webview != nil,
|
||||
})
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", b.port)
|
||||
log.Printf("MCP HTTP server listening on %s", addr)
|
||||
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Printf("MCP HTTP server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleMCPInfo returns MCP server information.
|
||||
func (b *MCPBridge) handleMCPInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
|
||||
|
||||
info := map[string]any{
|
||||
"name": "core-ide",
|
||||
"version": "0.1.0",
|
||||
"capabilities": map[string]any{
|
||||
"webview": true,
|
||||
"websocket": fmt.Sprintf("ws://localhost:%d/ws", b.port),
|
||||
},
|
||||
}
|
||||
json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
// handleMCPTools returns the list of available tools.
|
||||
func (b *MCPBridge) handleMCPTools(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
|
||||
|
||||
tools := []map[string]string{
|
||||
// WebView interaction (JS runtime, console, DOM)
|
||||
{"name": "webview_list", "description": "List windows"},
|
||||
{"name": "webview_eval", "description": "Execute JavaScript"},
|
||||
{"name": "webview_console", "description": "Get console messages"},
|
||||
{"name": "webview_console_clear", "description": "Clear console buffer"},
|
||||
{"name": "webview_click", "description": "Click element"},
|
||||
{"name": "webview_type", "description": "Type into element"},
|
||||
{"name": "webview_query", "description": "Query DOM elements"},
|
||||
{"name": "webview_navigate", "description": "Navigate to URL"},
|
||||
{"name": "webview_source", "description": "Get page source"},
|
||||
{"name": "webview_url", "description": "Get current page URL"},
|
||||
{"name": "webview_title", "description": "Get current page title"},
|
||||
{"name": "webview_screenshot", "description": "Capture page as base64 PNG"},
|
||||
{"name": "webview_screenshot_element", "description": "Capture specific element as PNG"},
|
||||
{"name": "webview_scroll", "description": "Scroll to element or position"},
|
||||
{"name": "webview_hover", "description": "Hover over element"},
|
||||
{"name": "webview_select", "description": "Select option in dropdown"},
|
||||
{"name": "webview_check", "description": "Check/uncheck checkbox or radio"},
|
||||
{"name": "webview_element_info", "description": "Get detailed info about element"},
|
||||
{"name": "webview_computed_style", "description": "Get computed styles for element"},
|
||||
{"name": "webview_highlight", "description": "Visually highlight element"},
|
||||
{"name": "webview_dom_tree", "description": "Get DOM tree structure"},
|
||||
{"name": "webview_errors", "description": "Get captured error messages"},
|
||||
{"name": "webview_performance", "description": "Get performance metrics"},
|
||||
{"name": "webview_resources", "description": "List loaded resources"},
|
||||
{"name": "webview_network", "description": "Get network requests log"},
|
||||
{"name": "webview_network_clear", "description": "Clear network request log"},
|
||||
{"name": "webview_network_inject", "description": "Inject network interceptor for detailed logging"},
|
||||
{"name": "webview_pdf", "description": "Export page as PDF (base64 data URI)"},
|
||||
{"name": "webview_print", "description": "Open print dialog for window"},
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
|
||||
}
|
||||
|
||||
// handleMCPCall handles tool calls via HTTP POST.
|
||||
func (b *MCPBridge) handleMCPCall(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Tool string `json:"tool"`
|
||||
Params map[string]any `json:"params"`
|
||||
}
|
||||
|
||||
// Limit request body to 1MB
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := b.executeWebviewTool(req.Tool, req.Params)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// executeWebviewTool handles webview/JS tool execution.
|
||||
func (b *MCPBridge) executeWebviewTool(tool string, params map[string]any) map[string]any {
|
||||
if b.webview == nil {
|
||||
return map[string]any{"error": "webview service not available"}
|
||||
}
|
||||
|
||||
switch tool {
|
||||
case "webview_list":
|
||||
windows := b.webview.ListWindows()
|
||||
return map[string]any{"windows": windows}
|
||||
|
||||
case "webview_eval":
|
||||
windowName := getStringParam(params, "window")
|
||||
code := getStringParam(params, "code")
|
||||
result, err := b.webview.ExecJS(windowName, code)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"result": result}
|
||||
|
||||
case "webview_console":
|
||||
level := getStringParam(params, "level")
|
||||
limit := getIntParam(params, "limit")
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
messages := b.webview.GetConsoleMessages(level, limit)
|
||||
return map[string]any{"messages": messages}
|
||||
|
||||
case "webview_console_clear":
|
||||
b.webview.ClearConsole()
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_click":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
err := b.webview.Click(windowName, selector)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_type":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
text := getStringParam(params, "text")
|
||||
err := b.webview.Type(windowName, selector, text)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_query":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
result, err := b.webview.QuerySelector(windowName, selector)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"elements": result}
|
||||
|
||||
case "webview_navigate":
|
||||
windowName := getStringParam(params, "window")
|
||||
rawURL := getStringParam(params, "url")
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return map[string]any{"error": "only http/https URLs are allowed"}
|
||||
}
|
||||
err = b.webview.Navigate(windowName, rawURL)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_source":
|
||||
windowName := getStringParam(params, "window")
|
||||
result, err := b.webview.GetPageSource(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"source": result}
|
||||
|
||||
case "webview_url":
|
||||
windowName := getStringParam(params, "window")
|
||||
result, err := b.webview.GetURL(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"url": result}
|
||||
|
||||
case "webview_title":
|
||||
windowName := getStringParam(params, "window")
|
||||
result, err := b.webview.GetTitle(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"title": result}
|
||||
|
||||
case "webview_screenshot":
|
||||
windowName := getStringParam(params, "window")
|
||||
data, err := b.webview.Screenshot(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"data": data}
|
||||
|
||||
case "webview_screenshot_element":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
data, err := b.webview.ScreenshotElement(windowName, selector)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"data": data}
|
||||
|
||||
case "webview_scroll":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
x := getIntParam(params, "x")
|
||||
y := getIntParam(params, "y")
|
||||
err := b.webview.Scroll(windowName, selector, x, y)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_hover":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
err := b.webview.Hover(windowName, selector)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_select":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
value := getStringParam(params, "value")
|
||||
err := b.webview.Select(windowName, selector, value)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_check":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
checked, _ := params["checked"].(bool)
|
||||
err := b.webview.Check(windowName, selector, checked)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_element_info":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
result, err := b.webview.GetElementInfo(windowName, selector)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"element": result}
|
||||
|
||||
case "webview_computed_style":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
var properties []string
|
||||
if props, ok := params["properties"].([]any); ok {
|
||||
for _, p := range props {
|
||||
if s, ok := p.(string); ok {
|
||||
properties = append(properties, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
result, err := b.webview.GetComputedStyle(windowName, selector, properties)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"styles": result}
|
||||
|
||||
case "webview_highlight":
|
||||
windowName := getStringParam(params, "window")
|
||||
selector := getStringParam(params, "selector")
|
||||
duration := getIntParam(params, "duration")
|
||||
err := b.webview.Highlight(windowName, selector, duration)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_dom_tree":
|
||||
windowName := getStringParam(params, "window")
|
||||
maxDepth := getIntParam(params, "maxDepth")
|
||||
result, err := b.webview.GetDOMTree(windowName, maxDepth)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"tree": result}
|
||||
|
||||
case "webview_errors":
|
||||
limit := getIntParam(params, "limit")
|
||||
if limit == 0 {
|
||||
limit = 50
|
||||
}
|
||||
errors := b.webview.GetErrors(limit)
|
||||
return map[string]any{"errors": errors}
|
||||
|
||||
case "webview_performance":
|
||||
windowName := getStringParam(params, "window")
|
||||
result, err := b.webview.GetPerformance(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"performance": result}
|
||||
|
||||
case "webview_resources":
|
||||
windowName := getStringParam(params, "window")
|
||||
result, err := b.webview.GetResources(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"resources": result}
|
||||
|
||||
case "webview_network":
|
||||
windowName := getStringParam(params, "window")
|
||||
limit := getIntParam(params, "limit")
|
||||
result, err := b.webview.GetNetworkRequests(windowName, limit)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"requests": result}
|
||||
|
||||
case "webview_network_clear":
|
||||
windowName := getStringParam(params, "window")
|
||||
err := b.webview.ClearNetworkRequests(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_network_inject":
|
||||
windowName := getStringParam(params, "window")
|
||||
err := b.webview.InjectNetworkInterceptor(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "webview_pdf":
|
||||
windowName := getStringParam(params, "window")
|
||||
options := make(map[string]any)
|
||||
if filename := getStringParam(params, "filename"); filename != "" {
|
||||
options["filename"] = filename
|
||||
}
|
||||
if margin, ok := params["margin"].(float64); ok {
|
||||
options["margin"] = margin
|
||||
}
|
||||
data, err := b.webview.ExportToPDF(windowName, options)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"data": data}
|
||||
|
||||
case "webview_print":
|
||||
windowName := getStringParam(params, "window")
|
||||
err := b.webview.PrintToPDF(windowName)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
default:
|
||||
return map[string]any{"error": "unknown tool", "tool": tool}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for parameter extraction
|
||||
func getStringParam(params map[string]any, key string) string {
|
||||
if v, ok := params[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getIntParam(params map[string]any, key string) int {
|
||||
if v, ok := params[key].(float64); ok {
|
||||
return int(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
BIN
internal/core-ide/wails3-angular-template.jpg
Normal file
|
After Width: | Height: | Size: 238 KiB |
25
internal/variants/core_ide.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//go:build ide
|
||||
|
||||
// core_ide.go imports packages for the Core IDE desktop application.
|
||||
//
|
||||
// Build with: go build -tags ide
|
||||
//
|
||||
// This is the Wails v3 GUI variant featuring:
|
||||
// - System tray with quick actions
|
||||
// - Tray panel for status/notifications
|
||||
// - Angular frontend
|
||||
// - All CLI commands available via IPC
|
||||
|
||||
package variants
|
||||
|
||||
import (
|
||||
// Core IDE GUI
|
||||
_ "github.com/host-uk/core/internal/core-ide"
|
||||
|
||||
// CLI commands available via IPC
|
||||
_ "github.com/host-uk/core/internal/cmd/ai"
|
||||
_ "github.com/host-uk/core/internal/cmd/dev"
|
||||
_ "github.com/host-uk/core/internal/cmd/deploy"
|
||||
_ "github.com/host-uk/core/internal/cmd/php"
|
||||
_ "github.com/host-uk/core/internal/cmd/rag"
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
// This is the default build variant with all development tools:
|
||||
// - dev: Multi-repo git workflows (commit, push, pull, sync)
|
||||
// - ai: AI agent task management
|
||||
// - ai: AI agent task management + RAG + metrics
|
||||
// - go: Go module and build tools
|
||||
// - php: Laravel/Composer development tools
|
||||
// - build: Cross-platform compilation
|
||||
|
|
@ -20,7 +20,6 @@
|
|||
// - test: Test runner with coverage
|
||||
// - qa: Quality assurance workflows
|
||||
// - monitor: Security monitoring aggregation
|
||||
// - rag: RAG (Retrieval Augmented Generation) tools
|
||||
|
||||
package variants
|
||||
|
||||
|
|
@ -39,7 +38,6 @@ import (
|
|||
_ "github.com/host-uk/core/internal/cmd/php"
|
||||
_ "github.com/host-uk/core/internal/cmd/pkgcmd"
|
||||
_ "github.com/host-uk/core/internal/cmd/qa"
|
||||
_ "github.com/host-uk/core/internal/cmd/rag"
|
||||
_ "github.com/host-uk/core/internal/cmd/sdk"
|
||||
_ "github.com/host-uk/core/internal/cmd/security"
|
||||
_ "github.com/host-uk/core/internal/cmd/setup"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/ai"
|
||||
"github.com/host-uk/core/pkg/log"
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +35,8 @@ type TaskContext struct {
|
|||
RecentCommits string `json:"recent_commits"`
|
||||
// RelatedCode contains code snippets related to the task.
|
||||
RelatedCode []FileContent `json:"related_code"`
|
||||
// RAGContext contains relevant documentation from the vector database.
|
||||
RAGContext string `json:"rag_context,omitempty"`
|
||||
}
|
||||
|
||||
// BuildTaskContext gathers context for AI collaboration on a task.
|
||||
|
|
@ -79,6 +82,13 @@ func BuildTaskContext(task *Task, dir string) (*TaskContext, error) {
|
|||
}
|
||||
ctx.RelatedCode = relatedCode
|
||||
|
||||
// Query RAG for relevant documentation (graceful degradation)
|
||||
ragCtx := ai.QueryRAGForTask(ai.TaskInfo{
|
||||
Title: task.Title,
|
||||
Description: task.Description,
|
||||
})
|
||||
ctx.RAGContext = ragCtx
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
|
|
@ -331,5 +341,12 @@ func (tc *TaskContext) FormatContext() string {
|
|||
}
|
||||
}
|
||||
|
||||
// Relevant documentation from RAG
|
||||
if tc.RAGContext != "" {
|
||||
sb.WriteString("## Relevant Documentation\n")
|
||||
sb.WriteString(tc.RAGContext)
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
|||
11
pkg/ai/ai.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Package ai provides the unified AI package for the core CLI.
|
||||
//
|
||||
// It composes functionality from pkg/rag (vector search) and pkg/agentic
|
||||
// (task management) into a single public API surface. New AI features
|
||||
// should be added here; existing packages remain importable but pkg/ai
|
||||
// is the canonical entry point.
|
||||
//
|
||||
// Sub-packages composed:
|
||||
// - pkg/rag: Qdrant vector database + Ollama embeddings
|
||||
// - pkg/agentic: Task queue client and context building
|
||||
package ai
|
||||
167
pkg/ai/metrics.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event represents a recorded AI/security metric event.
|
||||
type Event struct {
|
||||
Type string `json:"type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Repo string `json:"repo,omitempty"`
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// metricsDir returns the base directory for metrics storage.
|
||||
func metricsDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".core", "ai", "metrics"), nil
|
||||
}
|
||||
|
||||
// metricsFilePath returns the JSONL file path for the given date.
|
||||
func metricsFilePath(dir string, t time.Time) string {
|
||||
return filepath.Join(dir, t.Format("2006-01-02")+".jsonl")
|
||||
}
|
||||
|
||||
// Record appends an event to the daily JSONL file at
|
||||
// ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
|
||||
func Record(event Event) error {
|
||||
if event.Timestamp.IsZero() {
|
||||
event.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
dir, err := metricsDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("create metrics directory: %w", err)
|
||||
}
|
||||
|
||||
path := metricsFilePath(dir, event.Timestamp)
|
||||
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open metrics file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal event: %w", err)
|
||||
}
|
||||
|
||||
if _, err := f.Write(append(data, '\n')); err != nil {
|
||||
return fmt.Errorf("write event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadEvents reads events from JSONL files within the given time range.
|
||||
func ReadEvents(since time.Time) ([]Event, error) {
|
||||
dir, err := metricsDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []Event
|
||||
now := time.Now()
|
||||
|
||||
// Iterate each day from since to now.
|
||||
for d := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, time.Local); !d.After(now); d = d.AddDate(0, 0, 1) {
|
||||
path := metricsFilePath(dir, d)
|
||||
|
||||
dayEvents, err := readMetricsFile(path, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, dayEvents...)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// readMetricsFile reads events from a single JSONL file, returning only those at or after since.
|
||||
func readMetricsFile(path string, since time.Time) ([]Event, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("open metrics file %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var events []Event
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
var ev Event
|
||||
if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
|
||||
continue // skip malformed lines
|
||||
}
|
||||
if !ev.Timestamp.Before(since) {
|
||||
events = append(events, ev)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("read metrics file %s: %w", path, err)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// Summary aggregates events into counts by type, repo, and agent.
|
||||
func Summary(events []Event) map[string]any {
|
||||
byType := make(map[string]int)
|
||||
byRepo := make(map[string]int)
|
||||
byAgent := make(map[string]int)
|
||||
|
||||
for _, ev := range events {
|
||||
byType[ev.Type]++
|
||||
if ev.Repo != "" {
|
||||
byRepo[ev.Repo]++
|
||||
}
|
||||
if ev.AgentID != "" {
|
||||
byAgent[ev.AgentID]++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"total": len(events),
|
||||
"by_type": sortedMap(byType),
|
||||
"by_repo": sortedMap(byRepo),
|
||||
"by_agent": sortedMap(byAgent),
|
||||
}
|
||||
}
|
||||
|
||||
// sortedMap returns a slice of key-count pairs sorted by count descending.
|
||||
func sortedMap(m map[string]int) []map[string]any {
|
||||
type entry struct {
|
||||
key string
|
||||
count int
|
||||
}
|
||||
entries := make([]entry, 0, len(m))
|
||||
for k, v := range m {
|
||||
entries = append(entries, entry{k, v})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].count > entries[j].count
|
||||
})
|
||||
result := make([]map[string]any, len(entries))
|
||||
for i, e := range entries {
|
||||
result[i] = map[string]any{"key": e.key, "count": e.count}
|
||||
}
|
||||
return result
|
||||
}
|
||||
58
pkg/ai/rag.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/rag"
|
||||
)
|
||||
|
||||
// TaskInfo carries the minimal task data needed for RAG queries,
|
||||
// avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
|
||||
type TaskInfo struct {
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
// QueryRAGForTask queries Qdrant for documentation relevant to a task.
|
||||
// It builds a query from the task title and description, queries with
|
||||
// sensible defaults, and returns formatted context. Returns "" on any
|
||||
// error (e.g. Qdrant/Ollama not running) for graceful degradation.
|
||||
func QueryRAGForTask(task TaskInfo) string {
|
||||
query := task.Title + " " + task.Description
|
||||
|
||||
// Truncate to 500 runes to keep the embedding focused.
|
||||
runes := []rune(query)
|
||||
if len(runes) > 500 {
|
||||
query = string(runes[:500])
|
||||
}
|
||||
|
||||
qdrantCfg := rag.DefaultQdrantConfig()
|
||||
qdrantClient, err := rag.NewQdrantClient(qdrantCfg)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer qdrantClient.Close()
|
||||
|
||||
ollamaCfg := rag.DefaultOllamaConfig()
|
||||
ollamaClient, err := rag.NewOllamaClient(ollamaCfg)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
queryCfg := rag.QueryConfig{
|
||||
Collection: "hostuk-docs",
|
||||
Limit: 3,
|
||||
Threshold: 0.5,
|
||||
}
|
||||
|
||||
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryCfg)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return rag.FormatResultsContext(results)
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
"cmd": {
|
||||
"ai": {
|
||||
"short": "AI agent task management",
|
||||
"long": "AI agent task management for core-agentic integration. Provides commands to list, claim, update, and complete tasks from the agentic task queue.",
|
||||
"long": "AI agent task management for core-agentic integration. Provides commands to list, claim, update, and complete tasks from the agentic task queue. Includes RAG tools and metrics.",
|
||||
"claude.short": "Claude Code integration",
|
||||
"claude.long": "Claude Code integration for AI-assisted development workflows.",
|
||||
"claude.config.short": "Configure Claude Code settings",
|
||||
|
|
@ -153,7 +153,11 @@
|
|||
"task_complete.short": "Mark a task as completed",
|
||||
"task_pr.short": "Create a pull request for a task",
|
||||
"task_pr.branch_error": "cannot create PR from {{.Branch}} branch; create a feature branch first",
|
||||
"task_update.short": "Update task status or progress"
|
||||
"task_update.short": "Update task status or progress",
|
||||
"metrics.short": "View AI and security event metrics",
|
||||
"metrics.long": "View collected metrics from AI tasks, security scans, and job creation events. Reads JSONL event logs from ~/.core/ai/metrics/.",
|
||||
"metrics.flag.since": "Time period to show (e.g. 7d, 24h, 30d)",
|
||||
"metrics.none_found": "No events recorded in this period."
|
||||
},
|
||||
"build": {
|
||||
"short": "Build projects with auto-detection and cross-compilation",
|
||||
|
|
@ -512,7 +516,14 @@
|
|||
"secrets.short": "List exposed secrets",
|
||||
"secrets.long": "List secrets detected by GitHub secret scanning.",
|
||||
"flag.repo": "Specific repo to check",
|
||||
"flag.severity": "Filter by severity (critical,high,medium,low)"
|
||||
"flag.severity": "Filter by severity (critical,high,medium,low)",
|
||||
"flag.target": "External repo to scan (e.g. wailsapp/wails)",
|
||||
"jobs.short": "Create GitHub issues from scan results",
|
||||
"jobs.long": "Create GitHub issues from security scan results so contributors can claim and work on them. Supports targeting external repositories.",
|
||||
"jobs.flag.targets": "Target repos to scan (owner/repo format)",
|
||||
"jobs.flag.issue_repo": "Repository to create issues in",
|
||||
"jobs.flag.dry_run": "Show what would be created without creating issues",
|
||||
"jobs.flag.copies": "Number of duplicate issues for parallel work"
|
||||
},
|
||||
"qa": {
|
||||
"short": "Quality assurance workflows",
|
||||
|
|
|
|||