refactor(cli): restructure cmd packages into subdirectories
- Move CLI commands into subdirectories matching command hierarchy:
dev/, go/, php/, build/, ci/, sdk/, pkg/, vm/, docs/, setup/, doctor/, test/, ai/
- Create shared/ package for common styles and utilities
- Add new `core ai` root command with claude subcommand
- Update package declarations and imports across all files
- Create commands.go entry points for each package
- Remove GUI-related files (moved to core-gui repo)
This makes the filesystem structure match the CLI command structure,
improving context capture and code organization.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:02:43 +00:00
|
|
|
package dev
|
2025-10-28 12:06:05 +00:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-02-02 04:20:18 +00:00
|
|
|
"context"
|
2025-10-28 12:06:05 +00:00
|
|
|
"go/ast"
|
|
|
|
|
"go/parser"
|
|
|
|
|
"go/token"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"text/template"
|
|
|
|
|
|
2026-02-02 04:20:18 +00:00
|
|
|
"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
|
2025-10-28 12:06:05 +00:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-02 04:20:18 +00:00
|
|
|
// syncInternalToPublic handles the synchronization of internal packages to public-facing directories.
|
|
|
|
|
// This function is a placeholder for future implementation.
|
|
|
|
|
func syncInternalToPublic(ctx context.Context, publicDir string) error {
|
|
|
|
|
// 1. Clean public/internal
|
|
|
|
|
// 2. Copy relevant files from internal/ to public/internal/
|
|
|
|
|
// Usually just shared logic, not private stuff.
|
|
|
|
|
|
|
|
|
|
// For now, let's assume we copy specific safe packages
|
|
|
|
|
// Logic to be refined.
|
|
|
|
|
|
|
|
|
|
// Example migration of os calls:
|
|
|
|
|
// internalDirs, err := os.ReadDir(pkgDir) -> coreio.Local.List(pkgDir)
|
|
|
|
|
// os.Stat -> coreio.Local.IsFile (returns bool) or List for existence check
|
|
|
|
|
// os.MkdirAll -> coreio.Local.EnsureDir
|
|
|
|
|
// os.WriteFile -> coreio.Local.Write
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 00:22:47 +00:00
|
|
|
// addSyncCommand adds the 'sync' command to the given parent command.
|
2026-01-31 11:39:19 +00:00
|
|
|
func addSyncCommand(parent *cli.Command) {
|
|
|
|
|
syncCmd := &cli.Command{
|
2026-01-30 00:47:54 +00:00
|
|
|
Use: "sync",
|
feat(i18n): add translation keys to all CLI commands
Replace hardcoded strings with i18n.T() calls across all cmd/* packages:
- ai, build, ci, dev, docs, doctor, go, php, pkg, sdk, setup, test, vm
Adds 500+ translation keys to en.json for command descriptions,
flag descriptions, labels, messages, and error strings.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 02:37:57 +00:00
|
|
|
Short: i18n.T("cmd.dev.sync.short"),
|
|
|
|
|
Long: i18n.T("cmd.dev.sync.long"),
|
2026-01-31 11:39:19 +00:00
|
|
|
RunE: func(cmd *cli.Command, args []string) error {
|
2026-01-30 00:47:54 +00:00
|
|
|
if err := runSync(); err != nil {
|
2026-01-31 11:39:19 +00:00
|
|
|
return cli.Wrap(err, i18n.Label("error"))
|
2026-01-30 00:47:54 +00:00
|
|
|
}
|
2026-01-31 11:39:19 +00:00
|
|
|
cli.Text(i18n.T("i18n.done.sync", "public APIs"))
|
2026-01-30 00:47:54 +00:00
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parent.AddCommand(syncCmd)
|
2025-10-28 12:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type symbolInfo struct {
|
|
|
|
|
Name string
|
|
|
|
|
Kind string // "var", "func", "type", "const"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runSync() error {
|
|
|
|
|
pkgDir := "pkg"
|
2026-02-02 04:20:18 +00:00
|
|
|
internalDirs, err := coreio.Local.List(pkgDir)
|
2025-10-28 12:06:05 +00:00
|
|
|
if err != nil {
|
2026-01-31 11:39:19 +00:00
|
|
|
return cli.Wrap(err, "failed to read pkg directory")
|
2025-10-28 12:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
2026-02-02 04:20:18 +00:00
|
|
|
if !coreio.Local.IsFile(internalFile) {
|
2025-10-28 12:06:05 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
symbols, err := getExportedSymbols(internalFile)
|
|
|
|
|
if err != nil {
|
2026-01-31 11:39:19 +00:00
|
|
|
return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName))
|
2025-10-28 12:06:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := generatePublicAPIFile(publicDir, publicFile, serviceName, symbols); err != nil {
|
2026-01-31 11:39:19 +00:00
|
|
|
return cli.Wrap(err, cli.Sprintf("error generating public API file for service '%s'", serviceName))
|
2025-10-28 12:06:05 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getExportedSymbols(path string) ([]symbolInfo, error) {
|
2026-02-02 04:20:18 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 12:06:05 +00:00
|
|
|
fset := token.NewFileSet()
|
2026-02-02 04:20:18 +00:00
|
|
|
// ParseFile can take content as string (src argument).
|
|
|
|
|
node, err := parser.ParseFile(fset, path, content, parser.ParseComments)
|
2025-10-28 12:06:05 +00:00
|
|
|
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.
|
2026-01-28 15:29:42 +00:00
|
|
|
impl "github.com/host-uk/core/{{.ServiceName}}"
|
2025-10-28 12:06:05 +00:00
|
|
|
|
|
|
|
|
// Import the core contracts to re-export the interface.
|
2026-01-28 15:29:42 +00:00
|
|
|
"github.com/host-uk/core/core"
|
2025-10-28 12:06:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
{{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 {
|
2026-02-02 04:20:18 +00:00
|
|
|
if err := coreio.Local.EnsureDir(dir); err != nil {
|
2025-10-28 12:06:05 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 04:20:18 +00:00
|
|
|
return coreio.Local.Write(path, buf.String())
|
2025-10-28 12:06:05 +00:00
|
|
|
}
|