cli/internal/cmd/dev/cmd_sync.go
Snider 03c9188d79
feat: infrastructure packages and lint cleanup (#281)
* ci: consolidate duplicate workflows and merge CodeQL configs

Remove 17 duplicate workflow files that were split copies of the
combined originals. Each family (CI, CodeQL, Coverage, PR Build,
Alpha Release) had the same job duplicated across separate
push/pull_request/schedule/manual trigger files.

Merge codeql.yml and codescan.yml into a single codeql.yml with
a language matrix covering go, javascript-typescript, python,
and actions — matching the previous default setup coverage.

Remaining workflows (one per family):
- ci.yml (push + PR + manual)
- codeql.yml (push + PR + schedule, all languages)
- coverage.yml (push + PR + manual)
- alpha-release.yml (push + manual)
- pr-build.yml (PR + manual)
- release.yml (tag push)
- agent-verify.yml, auto-label.yml, auto-project.yml

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

* feat: add collect, config, crypt, plugin packages and fix all lint issues

Add four new infrastructure packages with CLI commands:
- pkg/config: layered configuration (defaults → file → env → flags)
- pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums)
- pkg/plugin: plugin system with GitHub-based install/update/remove
- pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate)

Fix all golangci-lint issues across the entire codebase (~100 errcheck,
staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that
`core go qa` passes with 0 issues.

Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:34:43 +00:00

174 lines
4.5 KiB
Go

package dev
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"text/template"
"github.com/host-uk/core/pkg/cli" // Added
"github.com/host-uk/core/pkg/i18n" // Added
coreio "github.com/host-uk/core/pkg/io"
// Added
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// addSyncCommand adds the 'sync' command to the given parent command.
func addSyncCommand(parent *cli.Command) {
syncCmd := &cli.Command{
Use: "sync",
Short: i18n.T("cmd.dev.sync.short"),
Long: i18n.T("cmd.dev.sync.long"),
RunE: func(cmd *cli.Command, args []string) error {
if err := runSync(); err != nil {
return cli.Wrap(err, i18n.Label("error"))
}
cli.Text(i18n.T("i18n.done.sync", "public APIs"))
return nil
},
}
parent.AddCommand(syncCmd)
}
type symbolInfo struct {
Name string
Kind string // "var", "func", "type", "const"
}
func runSync() error {
pkgDir := "pkg"
internalDirs, err := coreio.Local.List(pkgDir)
if err != nil {
return cli.Wrap(err, "failed to read pkg directory")
}
for _, dir := range internalDirs {
if !dir.IsDir() || dir.Name() == "core" {
continue
}
serviceName := dir.Name()
internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go")
publicDir := serviceName
publicFile := filepath.Join(publicDir, serviceName+".go")
if !coreio.Local.IsFile(internalFile) {
continue
}
symbols, err := getExportedSymbols(internalFile)
if err != nil {
return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName))
}
if err := generatePublicAPIFile(publicDir, publicFile, serviceName, symbols); err != nil {
return cli.Wrap(err, cli.Sprintf("error generating public API file for service '%s'", serviceName))
}
}
return nil
}
func getExportedSymbols(path string) ([]symbolInfo, error) {
// ParseFile expects a filename/path and reads it using os.Open by default if content is nil.
// Since we want to use our Medium abstraction, we should read the file content first.
content, err := coreio.Local.Read(path)
if err != nil {
return nil, err
}
fset := token.NewFileSet()
// ParseFile can take content as string (src argument).
node, err := parser.ParseFile(fset, path, content, parser.ParseComments)
if err != nil {
return nil, err
}
var symbols []symbolInfo
for name, obj := range node.Scope.Objects {
if ast.IsExported(name) {
kind := "unknown"
switch obj.Kind {
case ast.Con:
kind = "const"
case ast.Var:
kind = "var"
case ast.Fun:
kind = "func"
case ast.Typ:
kind = "type"
}
if kind != "unknown" {
symbols = append(symbols, symbolInfo{Name: name, Kind: kind})
}
}
}
return symbols, nil
}
const publicAPITemplate = `// package {{.ServiceName}} provides the public API for the {{.ServiceName}} service.
package {{.ServiceName}}
import (
// Import the internal implementation with an alias.
impl "github.com/host-uk/core/{{.ServiceName}}"
// Import the core contracts to re-export the interface.
"github.com/host-uk/core/core"
)
{{range .Symbols}}
{{- if eq .Kind "type"}}
// {{.Name}} is the public type for the {{.Name}} service. It is a type alias
// to the underlying implementation, making it transparent to the user.
type {{.Name}} = impl.{{.Name}}
{{else if eq .Kind "const"}}
// {{.Name}} is a public constant that points to the real constant in the implementation package.
const {{.Name}} = impl.{{.Name}}
{{else if eq .Kind "var"}}
// {{.Name}} is a public variable that points to the real variable in the implementation package.
var {{.Name}} = impl.{{.Name}}
{{else if eq .Kind "func"}}
// {{.Name}} is a public function that points to the real function in the implementation package.
var {{.Name}} = impl.{{.Name}}
{{end}}
{{end}}
// {{.InterfaceName}} is the public interface for the {{.ServiceName}} service.
type {{.InterfaceName}} = core.{{.InterfaceName}}
`
func generatePublicAPIFile(dir, path, serviceName string, symbols []symbolInfo) error {
if err := coreio.Local.EnsureDir(dir); err != nil {
return err
}
tmpl, err := template.New("publicAPI").Parse(publicAPITemplate)
if err != nil {
return err
}
tcaser := cases.Title(language.English)
interfaceName := tcaser.String(serviceName)
data := struct {
ServiceName string
Symbols []symbolInfo
InterfaceName string
}{
ServiceName: serviceName,
Symbols: symbols,
InterfaceName: interfaceName,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return err
}
return coreio.Local.Write(path, buf.String())
}