From ecb50796b7b1bc55e1e9470f0738c98082f0bdee Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Mar 2026 12:24:45 +0000 Subject: [PATCH] refactor: migrate core import to dappco.re/go/core Replace forge.lthn.ai/core/go/pkg/core with dappco.re/go/core v0.4.7. Adapt to new API: core.New() returns *Core directly, services registered via c.Service(), Result replaces (any, bool, error) IPC pattern. Simplify git/agentic integration by calling package-level functions directly instead of routing through IPC service handlers. Co-Authored-By: Virgil --- cmd/dev/cmd_bundles.go | 82 +++++------- cmd/dev/cmd_commit.go | 4 +- cmd/dev/cmd_push.go | 2 +- cmd/dev/cmd_work.go | 100 ++++---------- cmd/dev/service.go | 292 +++-------------------------------------- go.mod | 3 +- go.sum | 2 + 7 files changed, 81 insertions(+), 404 deletions(-) diff --git a/cmd/dev/cmd_bundles.go b/cmd/dev/cmd_bundles.go index e35f4e3..cb9d8c2 100644 --- a/cmd/dev/cmd_bundles.go +++ b/cmd/dev/cmd_bundles.go @@ -2,10 +2,9 @@ package dev import ( "context" + "fmt" - agentic "forge.lthn.ai/core/agent/pkg/lifecycle" - "forge.lthn.ai/core/go/pkg/core" - "forge.lthn.ai/core/go-scm/git" + "dappco.re/go/core" ) // WorkBundle contains the Core instance for dev work operations. @@ -16,71 +15,52 @@ type WorkBundle struct { // WorkBundleOptions configures the work bundle. type WorkBundleOptions struct { RegistryPath string - AllowEdit bool // Allow agentic to use Write/Edit tools } // NewWorkBundle creates a bundle for dev work operations. -// Includes: dev (orchestration), git, agentic services. +// Includes: dev (orchestration) service. func NewWorkBundle(opts WorkBundleOptions) (*WorkBundle, error) { - c, err := core.New( - core.WithService(NewService(ServiceOptions{ + c := core.New() + + svc := &Service{ + ServiceRuntime: core.NewServiceRuntime(c, ServiceOptions{ RegistryPath: opts.RegistryPath, - })), - core.WithService(git.NewService(git.ServiceOptions{})), - core.WithService(agentic.NewService(agentic.ServiceOptions{ - AllowEdit: opts.AllowEdit, - })), - core.WithServiceLock(), - ) - if err != nil { - return nil, err + }), } + c.Service("dev", core.Service{ + OnStart: func() core.Result { + c.RegisterTask(svc.handleTask) + return core.Result{OK: true} + }, + }) + + c.LockEnable() + c.LockApply() + return &WorkBundle{Core: c}, nil } // Start initialises the bundle services. func (b *WorkBundle) Start(ctx context.Context) error { - return b.Core.ServiceStartup(ctx, nil) + return resultError(b.Core.ServiceStartup(ctx, nil)) } // Stop shuts down the bundle services. func (b *WorkBundle) Stop(ctx context.Context) error { - return b.Core.ServiceShutdown(ctx) + return resultError(b.Core.ServiceShutdown(ctx)) } -// StatusBundle contains the Core instance for status-only operations. -type StatusBundle struct { - Core *core.Core -} - -// StatusBundleOptions configures the status bundle. -type StatusBundleOptions struct { - RegistryPath string -} - -// NewStatusBundle creates a bundle for status-only operations. -// Includes: dev (orchestration), git services. No agentic - commits not available. -func NewStatusBundle(opts StatusBundleOptions) (*StatusBundle, error) { - c, err := core.New( - core.WithService(NewService(ServiceOptions(opts))), - core.WithService(git.NewService(git.ServiceOptions{})), - // No agentic service - TaskCommit will be unhandled - core.WithServiceLock(), - ) - if err != nil { - return nil, err +// resultError extracts an error from a failed core.Result, returning nil on success. +func resultError(r core.Result) error { + if !r.OK { + if err, ok := r.Value.(error); ok { + return err + } + if r.Value != nil { + return fmt.Errorf("service operation failed: %v", r.Value) + } + return fmt.Errorf("service operation failed") } - - return &StatusBundle{Core: c}, nil -} - -// Start initialises the bundle services. -func (b *StatusBundle) Start(ctx context.Context) error { - return b.Core.ServiceStartup(ctx, nil) -} - -// Stop shuts down the bundle services. -func (b *StatusBundle) Stop(ctx context.Context) error { - return b.Core.ServiceShutdown(ctx) + return nil } diff --git a/cmd/dev/cmd_commit.go b/cmd/dev/cmd_commit.go index a16ad17..b0c92bd 100644 --- a/cmd/dev/cmd_commit.go +++ b/cmd/dev/cmd_commit.go @@ -117,7 +117,7 @@ func runCommit(registryPath string, all bool) error { for _, s := range dirtyRepos { cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.committing")), s.Name) - if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil { + if err := doCommit(ctx, s.Path, false); err != nil { cli.Print(" %s %s\n", errorStyle.Render("x"), err) failed++ } else { @@ -192,7 +192,7 @@ func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error { cli.Blank() // Commit - if err := claudeCommit(ctx, repoPath, repoName, ""); err != nil { + if err := doCommit(ctx, repoPath, false); err != nil { cli.Print(" %s %s\n", errorStyle.Render("x"), err) return err } diff --git a/cmd/dev/cmd_push.go b/cmd/dev/cmd_push.go index 95556ce..86d5285 100644 --- a/cmd/dev/cmd_push.go +++ b/cmd/dev/cmd_push.go @@ -206,7 +206,7 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error { // Use edit-enabled commit if only untracked files (may need .gitignore fix) var err error if s.Modified == 0 && s.Staged == 0 && s.Untracked > 0 { - err = claudeEditCommit(ctx, repoPath, repoName, "") + err = doCommit(ctx, repoPath, true) } else { err = runCommitSingleRepo(ctx, repoPath, false) } diff --git a/cmd/dev/cmd_work.go b/cmd/dev/cmd_work.go index 6eea8f7..1208b0b 100644 --- a/cmd/dev/cmd_work.go +++ b/cmd/dev/cmd_work.go @@ -3,15 +3,12 @@ package dev import ( "cmp" "context" - "os" - "os/exec" "slices" "strings" "forge.lthn.ai/core/cli/pkg/cli" - agentic "forge.lthn.ai/core/agent/pkg/lifecycle" - "forge.lthn.ai/core/go-scm/git" "forge.lthn.ai/core/go-i18n" + "forge.lthn.ai/core/go-scm/git" ) // Work command flags @@ -57,42 +54,30 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { defer func() { _ = bundle.Stop(ctx) }() // Load registry and get paths - paths, names, err := func() ([]string, map[string]string, error) { - reg, _, err := loadRegistryWithConfig(registryPath) - if err != nil { - return nil, nil, err - } - var paths []string - names := make(map[string]string) - for _, repo := range reg.List() { - if repo.IsGitRepo() { - paths = append(paths, repo.Path) - names[repo.Path] = repo.Name - } - } - return paths, names, nil - }() + reg, _, err := loadRegistryWithConfig(registryPath) if err != nil { return err } + var paths []string + names := make(map[string]string) + for _, repo := range reg.List() { + if repo.IsGitRepo() { + paths = append(paths, repo.Path) + names[repo.Path] = repo.Name + } + } + if len(paths) == 0 { cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } - // QUERY git status - result, handled, err := bundle.Core.QUERY(git.QueryStatus{ + // Query git status directly + statuses := git.Status(ctx, git.StatusOptions{ Paths: paths, Names: names, }) - if !handled { - return cli.Err("git service not available") - } - if err != nil { - return err - } - statuses := result.([]git.RepoStatus) // Sort by repo name for consistent output slices.SortFunc(statuses, func(a, b git.RepoStatus) int { @@ -125,15 +110,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { cli.Blank() for _, s := range dirtyRepos { - // PERFORM commit via agentic service - _, handled, err := bundle.Core.PERFORM(agentic.TaskCommit{ - Path: s.Path, - Name: s.Name, - }) - if !handled { - cli.Print(" %s %s: %s\n", warningStyle.Render("!"), s.Name, "agentic service not available") - continue - } + err := doCommit(ctx, s.Path, false) if err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) } else { @@ -141,12 +118,11 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { } } - // Re-QUERY status after commits - result, _, _ = bundle.Core.QUERY(git.QueryStatus{ + // Re-query status after commits + statuses = git.Status(ctx, git.StatusOptions{ Paths: paths, Names: names, }) - statuses = result.([]git.RepoStatus) // Rebuild ahead repos list aheadRepos = nil @@ -187,18 +163,11 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { cli.Blank() - // PERFORM push for each repo + // Push each repo directly var divergedRepos []git.RepoStatus for _, s := range aheadRepos { - _, handled, err := bundle.Core.PERFORM(git.TaskPush{ - Path: s.Path, - Name: s.Name, - }) - if !handled { - cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, "git service not available") - continue - } + err := git.Push(ctx, s.Path) if err != nil { if git.IsNonFastForward(err) { cli.Print(" %s %s: %s\n", warningStyle.Render("!"), s.Name, i18n.T("cmd.dev.push.diverged")) @@ -220,8 +189,8 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { for _, s := range divergedRepos { cli.Print(" %s %s...\n", dimStyle.Render("↓"), s.Name) - // PERFORM pull - _, _, err := bundle.Core.PERFORM(git.TaskPull{Path: s.Path, Name: s.Name}) + // Pull directly + err := git.Pull(ctx, s.Path) if err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) continue @@ -229,8 +198,8 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { cli.Print(" %s %s...\n", dimStyle.Render("↑"), s.Name) - // PERFORM push - _, _, err = bundle.Core.PERFORM(git.TaskPush{Path: s.Path, Name: s.Name}) + // Push directly + err = git.Push(ctx, s.Path) if err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) continue @@ -318,28 +287,3 @@ func printStatusTable(statuses []git.RepoStatus) { } } -// claudeCommit shells out to claude for committing (legacy helper for other commands) -func claudeCommit(ctx context.Context, repoPath, repoName, registryPath string) error { - prompt := agentic.Prompt("commit") - - cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Glob,Grep") - cmd.Dir = repoPath - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -// claudeEditCommit shells out to claude with edit permissions (legacy helper) -func claudeEditCommit(ctx context.Context, repoPath, repoName, registryPath string) error { - prompt := agentic.Prompt("commit") - - cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Write,Edit,Glob,Grep") - cmd.Dir = repoPath - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} diff --git a/cmd/dev/service.go b/cmd/dev/service.go index 64e6cd4..272a6e1 100644 --- a/cmd/dev/service.go +++ b/cmd/dev/service.go @@ -1,32 +1,14 @@ package dev import ( - "cmp" "context" - "slices" - "strings" + "os" + "os/exec" - "forge.lthn.ai/core/cli/pkg/cli" + "dappco.re/go/core" agentic "forge.lthn.ai/core/agent/pkg/lifecycle" - "forge.lthn.ai/core/go-scm/git" - "forge.lthn.ai/core/go/pkg/core" ) -// Tasks for dev service - -// TaskWork runs the full dev workflow: status, commit, push. -type TaskWork struct { - RegistryPath string - StatusOnly bool - AutoCommit bool - AutoPush bool -} - -// TaskStatus displays git status for all repos. -type TaskStatus struct { - RegistryPath string -} - // ServiceOptions for configuring the dev service. type ServiceOptions struct { RegistryPath string @@ -37,256 +19,24 @@ type Service struct { *core.ServiceRuntime[ServiceOptions] } -// NewService creates a dev service factory. -func NewService(opts ServiceOptions) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ - ServiceRuntime: core.NewServiceRuntime(c, opts), - }, nil - } +func (s *Service) handleTask(_ *core.Core, _ core.Task) core.Result { + return core.Result{} } -// OnStartup registers task handlers. -func (s *Service) OnStartup(ctx context.Context) error { - s.Core().RegisterTask(s.handleTask) - return nil -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch m := t.(type) { - case TaskWork: - err := s.runWork(m) - return nil, true, err - - case TaskStatus: - err := s.runStatus(m) - return nil, true, err - } - return nil, false, nil -} - -func (s *Service) runWork(task TaskWork) error { - // Load registry - paths, names, err := s.loadRegistry(task.RegistryPath) - if err != nil { - return err - } - - if len(paths) == 0 { - cli.Println("No git repositories found") - return nil - } - - // QUERY git status - result, handled, err := s.Core().QUERY(git.QueryStatus{ - Paths: paths, - Names: names, - }) - if !handled { - return cli.Err("git service not available") - } - if err != nil { - return err - } - statuses := result.([]git.RepoStatus) - - // Sort by name - slices.SortFunc(statuses, func(a, b git.RepoStatus) int { - return cmp.Compare(a.Name, b.Name) - }) - - // Display status table - s.printStatusTable(statuses) - - // Collect dirty and ahead repos - var dirtyRepos []git.RepoStatus - var aheadRepos []git.RepoStatus - - for _, st := range statuses { - if st.Error != nil { - continue - } - if st.IsDirty() { - dirtyRepos = append(dirtyRepos, st) - } - if st.HasUnpushed() { - aheadRepos = append(aheadRepos, st) - } - } - - // Auto-commit dirty repos if requested - if task.AutoCommit && len(dirtyRepos) > 0 { - cli.Blank() - cli.Println("Committing changes...") - cli.Blank() - - for _, repo := range dirtyRepos { - _, handled, err := s.Core().PERFORM(agentic.TaskCommit{ - Path: repo.Path, - Name: repo.Name, - }) - if !handled { - // Agentic service not available - skip silently - cli.Print(" - %s: agentic service not available\n", repo.Name) - continue - } - if err != nil { - cli.Print(" x %s: %s\n", repo.Name, err) - } else { - cli.Print(" v %s\n", repo.Name) - } - } - - // Re-query status after commits - result, _, _ = s.Core().QUERY(git.QueryStatus{ - Paths: paths, - Names: names, - }) - statuses = result.([]git.RepoStatus) - - // Rebuild ahead repos list - aheadRepos = nil - for _, st := range statuses { - if st.Error == nil && st.HasUnpushed() { - aheadRepos = append(aheadRepos, st) - } - } - } - - // If status only, we're done - if task.StatusOnly { - if len(dirtyRepos) > 0 && !task.AutoCommit { - cli.Blank() - cli.Println("Use --commit flag to auto-commit dirty repos") - } - return nil - } - - // Push repos with unpushed commits - if len(aheadRepos) == 0 { - cli.Blank() - cli.Println("All repositories are up to date") - return nil - } - - cli.Blank() - cli.Print("%d repos with unpushed commits:\n", len(aheadRepos)) - for _, st := range aheadRepos { - cli.Print(" %s: %d commits\n", st.Name, st.Ahead) - } - - if !task.AutoPush { - cli.Blank() - cli.Print("Push all? [y/N] ") - var answer string - _, _ = cli.Scanln(&answer) - if strings.ToLower(answer) != "y" { - cli.Println("Aborted") - return nil - } - } - - cli.Blank() - - // Push each repo - for _, st := range aheadRepos { - _, handled, err := s.Core().PERFORM(git.TaskPush{ - Path: st.Path, - Name: st.Name, - }) - if !handled { - cli.Print(" x %s: git service not available\n", st.Name) - continue - } - if err != nil { - if git.IsNonFastForward(err) { - cli.Print(" ! %s: branch has diverged\n", st.Name) - } else { - cli.Print(" x %s: %s\n", st.Name, err) - } - } else { - cli.Print(" v %s\n", st.Name) - } - } - - return nil -} - -func (s *Service) runStatus(task TaskStatus) error { - paths, names, err := s.loadRegistry(task.RegistryPath) - if err != nil { - return err - } - - if len(paths) == 0 { - cli.Println("No git repositories found") - return nil - } - - result, handled, err := s.Core().QUERY(git.QueryStatus{ - Paths: paths, - Names: names, - }) - if !handled { - return cli.Err("git service not available") - } - if err != nil { - return err - } - - statuses := result.([]git.RepoStatus) - slices.SortFunc(statuses, func(a, b git.RepoStatus) int { - return cmp.Compare(a.Name, b.Name) - }) - - s.printStatusTable(statuses) - return nil -} - -func (s *Service) loadRegistry(registryPath string) ([]string, map[string]string, error) { - reg, _, err := loadRegistryWithConfig(registryPath) - if err != nil { - return nil, nil, err - } - - var paths []string - names := make(map[string]string) - - for _, repo := range reg.List() { - if repo.IsGitRepo() { - paths = append(paths, repo.Path) - names[repo.Path] = repo.Name - } - } - - return paths, names, nil -} - -func (s *Service) printStatusTable(statuses []git.RepoStatus) { - // Calculate column widths - nameWidth := 4 // "Repo" - for _, st := range statuses { - if len(st.Name) > nameWidth { - nameWidth = len(st.Name) - } - } - - // Print header - cli.Print("%-*s %8s %9s %6s %5s\n", - nameWidth, "Repo", "Modified", "Untracked", "Staged", "Ahead") - - // Print separator - cli.Text(strings.Repeat("-", nameWidth+2+10+11+8+7)) - - // Print rows - for _, st := range statuses { - if st.Error != nil { - cli.Print("%-*s error: %s\n", nameWidth, st.Name, st.Error) - continue - } - - cli.Print("%-*s %8d %9d %6d %5d\n", - nameWidth, st.Name, - st.Modified, st.Untracked, st.Staged, st.Ahead) - } +// doCommit shells out to claude for AI-assisted commit. +func doCommit(ctx context.Context, repoPath string, allowEdit bool) error { + prompt := agentic.Prompt("commit") + + tools := "Bash,Read,Glob,Grep" + if allowEdit { + tools = "Bash,Read,Write,Edit,Glob,Grep" + } + + cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", tools) + cmd.Dir = repoPath + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() } diff --git a/go.mod b/go.mod index f0c514c..e6beb04 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.26.0 require ( code.gitea.io/sdk/gitea v0.23.2 + dappco.re/go/core v0.4.7 forge.lthn.ai/core/agent v0.3.3 forge.lthn.ai/core/cli v0.3.7 - forge.lthn.ai/core/go v0.3.3 forge.lthn.ai/core/go-container v0.1.7 forge.lthn.ai/core/go-i18n v0.1.7 forge.lthn.ai/core/go-io v0.1.7 @@ -22,6 +22,7 @@ require ( require ( codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect forge.lthn.ai/core/config v0.1.8 // indirect + forge.lthn.ai/core/go v0.3.3 // indirect forge.lthn.ai/core/go-inference v0.1.6 // indirect forge.lthn.ai/core/go-store v0.1.9 // indirect github.com/42wim/httpsig v1.2.3 // indirect diff --git a/go.sum b/go.sum index 6bfdedf..1f1d7ba 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs= +dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA= +dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= forge.lthn.ai/core/agent v0.3.3 h1:lGpoD5OgvdJ5z+qofw8fBWkDB186QM7I2jjXEbtzSdA= forge.lthn.ai/core/agent v0.3.3/go.mod h1:UnrGApmKd/GzHEFcgy/tYuSfeJwxRx8UsxPhTjU5Ntw= forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=