feat(plugin): add Claude Code plugin for host-uk framework
Core plugin providing: - Skills: core CLI reference, PHP patterns, Go patterns - Commands: /core:remember for context persistence - Hooks: - PreToolUse: block dangerous commands (rm -rf, sed -i, grep -l |) - PreToolUse: enforce core CLI over raw go/php commands - PostToolUse: auto-format Go/PHP, check for debug statements - PostToolUse: warn about uncommitted work after git commit - PreCompact: save state to prevent amnesia after auto-compact - SessionStart: restore context from recent sessions (<3h) - MCP: core CLI server integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
96eaed507c
commit
a93cc3540a
23 changed files with 3294 additions and 0 deletions
134
docs/plans/2026-01-30-i18n-v2-design.md
Normal file
134
docs/plans/2026-01-30-i18n-v2-design.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# i18n Package Refactor Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Refactor pkg/i18n to be extensible without breaking changes in future. Based on Gemini review recommendations.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### Renamed/Merged
|
||||||
|
| Current | New | Reason |
|
||||||
|
|---------|-----|--------|
|
||||||
|
| `interfaces.go` | `types.go` | Contains types, not interfaces |
|
||||||
|
| `mutate.go` | `loader.go` | Loads/flattens JSON |
|
||||||
|
| `actions.go` | `hooks.go` | Missing key callbacks |
|
||||||
|
| `checks.go` | (merge into loader.go) | Loading helpers |
|
||||||
|
| `mode.go` | (merge into types.go) | Just one type |
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `handler.go` | KeyHandler interface + built-in handlers |
|
||||||
|
| `context.go` | TranslationContext + C() helper |
|
||||||
|
|
||||||
|
### Unchanged
|
||||||
|
`grammar.go`, `language.go`, `localise.go`, `debug.go`, `numbers.go`, `time.go`, `i18n.go`, `intents.go`, `compose.go`, `transform.go`
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
### KeyHandler
|
||||||
|
```go
|
||||||
|
// KeyHandler processes translation keys before standard lookup.
|
||||||
|
type KeyHandler interface {
|
||||||
|
Match(key string) bool
|
||||||
|
Handle(key string, args []any, next func() string) string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Built-in handlers:
|
||||||
|
- `LabelHandler` - `i18n.label.*` → "Status:"
|
||||||
|
- `ProgressHandler` - `i18n.progress.*` → "Building..."
|
||||||
|
- `CountHandler` - `i18n.count.*` → "5 files"
|
||||||
|
- `NumericHandler` - `i18n.numeric.*` → formatted numbers
|
||||||
|
- `DoneHandler` - `i18n.done.*` → "File deleted"
|
||||||
|
- `FailHandler` - `i18n.fail.*` → "Failed to delete file"
|
||||||
|
|
||||||
|
### Loader
|
||||||
|
```go
|
||||||
|
// Loader provides translation data to the Service.
|
||||||
|
type Loader interface {
|
||||||
|
Load(lang string) (map[string]Message, *GrammarData, error)
|
||||||
|
Languages() []string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Built-in: `FSLoader` for embedded/filesystem JSON.
|
||||||
|
|
||||||
|
### TranslationContext
|
||||||
|
```go
|
||||||
|
type TranslationContext struct {
|
||||||
|
Context string
|
||||||
|
Gender string
|
||||||
|
Formality Formality
|
||||||
|
Extra map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func C(context string) *TranslationContext
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Changes
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Service struct {
|
||||||
|
loader Loader
|
||||||
|
messages map[string]map[string]Message
|
||||||
|
grammar map[string]*GrammarData
|
||||||
|
currentLang string
|
||||||
|
fallbackLang string
|
||||||
|
formality Formality
|
||||||
|
mode Mode
|
||||||
|
debug bool
|
||||||
|
handlers []KeyHandler
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constructors
|
||||||
|
```go
|
||||||
|
func New() (*Service, error)
|
||||||
|
func NewWithLoader(loader Loader, opts ...Option) (*Service, error)
|
||||||
|
|
||||||
|
type Option func(*Service)
|
||||||
|
func WithDefaultHandlers() Option
|
||||||
|
func WithFallback(lang string) Option
|
||||||
|
func WithFormality(f Formality) Option
|
||||||
|
```
|
||||||
|
|
||||||
|
### T() Flow
|
||||||
|
1. Parse args → extract Context, Subject, data
|
||||||
|
2. Run handler chain (each can handle or call next)
|
||||||
|
3. Standard lookup with context suffix fallback
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Keep
|
||||||
|
- `T(key, args...)`, `Raw(key, args...)`
|
||||||
|
- `S(noun, value)` - Subject builder
|
||||||
|
- `SetLanguage()`, `CurrentLanguage()`, `SetMode()`, `CurrentMode()`
|
||||||
|
- `SetFormality()`, `SetDebug()`, `Direction()`, `IsRTL()`
|
||||||
|
- Grammar: `PastTense()`, `Gerund()`, `Pluralize()`, `Article()`, `Title()`, `Label()`, `Progress()`
|
||||||
|
|
||||||
|
### Add
|
||||||
|
- `C(context)` - Context builder
|
||||||
|
- `NewWithLoader()` - Custom loader support
|
||||||
|
- `AddHandler()`, `PrependHandler()` - Custom handlers
|
||||||
|
|
||||||
|
### Remove (No Aliases)
|
||||||
|
- `NewSubject()` - use `S()`
|
||||||
|
- `N()` - use `T("i18n.numeric.*")`
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
- Constructor signature changes
|
||||||
|
- Internal file reorganisation
|
||||||
|
- No backwards compatibility layer
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
1. Create new files (types.go, handler.go, loader.go, context.go, hooks.go)
|
||||||
|
2. Move types from interfaces.go → types.go
|
||||||
|
3. Implement Loader interface + FSLoader
|
||||||
|
4. Implement KeyHandler interface + built-in handlers
|
||||||
|
5. Implement TranslationContext
|
||||||
|
6. Update Service struct + constructors
|
||||||
|
7. Update T() to use handler chain
|
||||||
|
8. Update package-level functions in i18n.go
|
||||||
|
9. Delete old files
|
||||||
|
10. Update tests
|
||||||
1685
docs/plans/2026-01-31-semantic-cli-output.md
Normal file
1685
docs/plans/2026-01-31-semantic-cli-output.md
Normal file
File diff suppressed because it is too large
Load diff
193
pkg/cli/command.go
Normal file
193
pkg/cli/command.go
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Command Type Re-export
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Command is the cobra command type.
|
||||||
|
// Re-exported for convenience so packages don't need to import cobra directly.
|
||||||
|
type Command = cobra.Command
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Command Builders
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// NewCommand creates a new command with a RunE handler.
|
||||||
|
// This is the standard way to create commands that may return errors.
|
||||||
|
//
|
||||||
|
// cmd := cli.NewCommand("build", "Build the project", "", func(cmd *cli.Command, args []string) error {
|
||||||
|
// // Build logic
|
||||||
|
// return nil
|
||||||
|
// })
|
||||||
|
func NewCommand(use, short, long string, run func(cmd *Command, args []string) error) *Command {
|
||||||
|
cmd := &Command{
|
||||||
|
Use: use,
|
||||||
|
Short: short,
|
||||||
|
RunE: run,
|
||||||
|
}
|
||||||
|
if long != "" {
|
||||||
|
cmd.Long = long
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGroup creates a new command group (no RunE).
|
||||||
|
// Use this for parent commands that only contain subcommands.
|
||||||
|
//
|
||||||
|
// devCmd := cli.NewGroup("dev", "Development commands", "")
|
||||||
|
// devCmd.AddCommand(buildCmd, testCmd)
|
||||||
|
func NewGroup(use, short, long string) *Command {
|
||||||
|
cmd := &Command{
|
||||||
|
Use: use,
|
||||||
|
Short: short,
|
||||||
|
}
|
||||||
|
if long != "" {
|
||||||
|
cmd.Long = long
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRun creates a new command with a simple Run handler (no error return).
|
||||||
|
// Use when the command cannot fail.
|
||||||
|
//
|
||||||
|
// cmd := cli.NewRun("version", "Show version", "", func(cmd *cli.Command, args []string) {
|
||||||
|
// cli.Println("v1.0.0")
|
||||||
|
// })
|
||||||
|
func NewRun(use, short, long string, run func(cmd *Command, args []string)) *Command {
|
||||||
|
cmd := &Command{
|
||||||
|
Use: use,
|
||||||
|
Short: short,
|
||||||
|
Run: run,
|
||||||
|
}
|
||||||
|
if long != "" {
|
||||||
|
cmd.Long = long
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Flag Helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// StringFlag adds a string flag to a command.
|
||||||
|
// The value will be stored in the provided pointer.
|
||||||
|
//
|
||||||
|
// var output string
|
||||||
|
// cli.StringFlag(cmd, &output, "output", "o", "", "Output file path")
|
||||||
|
func StringFlag(cmd *Command, ptr *string, name, short, def, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.Flags().StringVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.Flags().StringVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolFlag adds a boolean flag to a command.
|
||||||
|
// The value will be stored in the provided pointer.
|
||||||
|
//
|
||||||
|
// var verbose bool
|
||||||
|
// cli.BoolFlag(cmd, &verbose, "verbose", "v", false, "Enable verbose output")
|
||||||
|
func BoolFlag(cmd *Command, ptr *bool, name, short string, def bool, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.Flags().BoolVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.Flags().BoolVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntFlag adds an integer flag to a command.
|
||||||
|
// The value will be stored in the provided pointer.
|
||||||
|
//
|
||||||
|
// var count int
|
||||||
|
// cli.IntFlag(cmd, &count, "count", "n", 10, "Number of items")
|
||||||
|
func IntFlag(cmd *Command, ptr *int, name, short string, def int, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.Flags().IntVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.Flags().IntVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSliceFlag adds a string slice flag to a command.
|
||||||
|
// The value will be stored in the provided pointer.
|
||||||
|
//
|
||||||
|
// var tags []string
|
||||||
|
// cli.StringSliceFlag(cmd, &tags, "tag", "t", nil, "Tags to apply")
|
||||||
|
func StringSliceFlag(cmd *Command, ptr *[]string, name, short string, def []string, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.Flags().StringSliceVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.Flags().StringSliceVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Persistent Flag Helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// PersistentStringFlag adds a persistent string flag (inherited by subcommands).
|
||||||
|
func PersistentStringFlag(cmd *Command, ptr *string, name, short, def, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.PersistentFlags().StringVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.PersistentFlags().StringVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistentBoolFlag adds a persistent boolean flag (inherited by subcommands).
|
||||||
|
func PersistentBoolFlag(cmd *Command, ptr *bool, name, short string, def bool, usage string) {
|
||||||
|
if short != "" {
|
||||||
|
cmd.PersistentFlags().BoolVarP(ptr, name, short, def, usage)
|
||||||
|
} else {
|
||||||
|
cmd.PersistentFlags().BoolVar(ptr, name, def, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Command Configuration
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// WithArgs sets the Args validation function for a command.
|
||||||
|
// Returns the command for chaining.
|
||||||
|
//
|
||||||
|
// cmd := cli.NewCommand("build", "Build", "", run).WithArgs(cobra.ExactArgs(1))
|
||||||
|
func WithArgs(cmd *Command, args cobra.PositionalArgs) *Command {
|
||||||
|
cmd.Args = args
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExample sets the Example field for a command.
|
||||||
|
// Returns the command for chaining.
|
||||||
|
func WithExample(cmd *Command, example string) *Command {
|
||||||
|
cmd.Example = example
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExactArgs returns a PositionalArgs that accepts exactly N arguments.
|
||||||
|
func ExactArgs(n int) cobra.PositionalArgs {
|
||||||
|
return cobra.ExactArgs(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimumNArgs returns a PositionalArgs that accepts minimum N arguments.
|
||||||
|
func MinimumNArgs(n int) cobra.PositionalArgs {
|
||||||
|
return cobra.MinimumNArgs(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaximumNArgs returns a PositionalArgs that accepts maximum N arguments.
|
||||||
|
func MaximumNArgs(n int) cobra.PositionalArgs {
|
||||||
|
return cobra.MaximumNArgs(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoArgs returns a PositionalArgs that accepts no arguments.
|
||||||
|
func NoArgs() cobra.PositionalArgs {
|
||||||
|
return cobra.NoArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArbitraryArgs returns a PositionalArgs that accepts any arguments.
|
||||||
|
func ArbitraryArgs() cobra.PositionalArgs {
|
||||||
|
return cobra.ArbitraryArgs
|
||||||
|
}
|
||||||
116
pkg/cli/errors.go
Normal file
116
pkg/cli/errors.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Error Creation (replace fmt.Errorf)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Err creates a new error from a format string.
|
||||||
|
// This is a direct replacement for fmt.Errorf.
|
||||||
|
func Err(format string, args ...any) error {
|
||||||
|
return fmt.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap wraps an error with a message.
|
||||||
|
// Returns nil if err is nil.
|
||||||
|
//
|
||||||
|
// return cli.Wrap(err, "load config") // "load config: <original error>"
|
||||||
|
func Wrap(err error, msg string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %w", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapVerb wraps an error using i18n grammar for "Failed to verb subject".
|
||||||
|
// Uses the i18n.ActionFailed function for proper grammar composition.
|
||||||
|
// Returns nil if err is nil.
|
||||||
|
//
|
||||||
|
// return cli.WrapVerb(err, "load", "config") // "Failed to load config: <original error>"
|
||||||
|
func WrapVerb(err error, verb, subject string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msg := i18n.ActionFailed(verb, subject)
|
||||||
|
return fmt.Errorf("%s: %w", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAction wraps an error using i18n grammar for "Failed to verb".
|
||||||
|
// Uses the i18n.ActionFailed function for proper grammar composition.
|
||||||
|
// Returns nil if err is nil.
|
||||||
|
//
|
||||||
|
// return cli.WrapAction(err, "connect") // "Failed to connect: <original error>"
|
||||||
|
func WrapAction(err error, verb string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msg := i18n.ActionFailed(verb, "")
|
||||||
|
return fmt.Errorf("%s: %w", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Error Helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Is reports whether any error in err's tree matches target.
|
||||||
|
// This is a re-export of errors.Is for convenience.
|
||||||
|
func Is(err, target error) bool {
|
||||||
|
return errors.Is(err, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As finds the first error in err's tree that matches target.
|
||||||
|
// This is a re-export of errors.As for convenience.
|
||||||
|
func As(err error, target any) bool {
|
||||||
|
return errors.As(err, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join returns an error that wraps the given errors.
|
||||||
|
// This is a re-export of errors.Join for convenience.
|
||||||
|
func Join(errs ...error) error {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Fatal Functions (print and exit)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Fatalf prints a formatted error message and exits with code 1.
|
||||||
|
func Fatalf(format string, args ...any) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalWrap prints a wrapped error message and exits with code 1.
|
||||||
|
// Does nothing if err is nil.
|
||||||
|
//
|
||||||
|
// cli.FatalWrap(err, "load config") // Prints "✗ load config: <error>" and exits
|
||||||
|
func FatalWrap(err error, msg string) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
||||||
|
fmt.Println(ErrorStyle.Render(SymbolCross + " " + fullMsg))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalWrapVerb prints a wrapped error using i18n grammar and exits with code 1.
|
||||||
|
// Does nothing if err is nil.
|
||||||
|
//
|
||||||
|
// cli.FatalWrapVerb(err, "load", "config") // Prints "✗ Failed to load config: <error>" and exits
|
||||||
|
func FatalWrapVerb(err error, verb, subject string) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := i18n.ActionFailed(verb, subject)
|
||||||
|
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
||||||
|
fmt.Println(ErrorStyle.Render(SymbolCross + " " + fullMsg))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
176
pkg/cli/output.go
Normal file
176
pkg/cli/output.go
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Style Namespace
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Styles provides namespaced access to CLI styles.
|
||||||
|
// Usage: cli.Style.Dim.Render("text"), cli.Style.Success.Render("done")
|
||||||
|
var Style = struct {
|
||||||
|
// Text styles
|
||||||
|
Dim lipgloss.Style
|
||||||
|
Muted lipgloss.Style
|
||||||
|
Bold lipgloss.Style
|
||||||
|
Value lipgloss.Style
|
||||||
|
Accent lipgloss.Style
|
||||||
|
Code lipgloss.Style
|
||||||
|
Key lipgloss.Style
|
||||||
|
Number lipgloss.Style
|
||||||
|
Link lipgloss.Style
|
||||||
|
Header lipgloss.Style
|
||||||
|
Title lipgloss.Style
|
||||||
|
Stage lipgloss.Style
|
||||||
|
PrNum lipgloss.Style
|
||||||
|
AccentL lipgloss.Style
|
||||||
|
|
||||||
|
// Status styles
|
||||||
|
Success lipgloss.Style
|
||||||
|
Error lipgloss.Style
|
||||||
|
Warning lipgloss.Style
|
||||||
|
Info lipgloss.Style
|
||||||
|
|
||||||
|
// Git styles
|
||||||
|
Dirty lipgloss.Style
|
||||||
|
Ahead lipgloss.Style
|
||||||
|
Behind lipgloss.Style
|
||||||
|
Clean lipgloss.Style
|
||||||
|
Conflict lipgloss.Style
|
||||||
|
|
||||||
|
// Repo name style
|
||||||
|
Repo lipgloss.Style
|
||||||
|
|
||||||
|
// Coverage styles
|
||||||
|
CoverageHigh lipgloss.Style
|
||||||
|
CoverageMed lipgloss.Style
|
||||||
|
CoverageLow lipgloss.Style
|
||||||
|
|
||||||
|
// Priority styles
|
||||||
|
PriorityHigh lipgloss.Style
|
||||||
|
PriorityMedium lipgloss.Style
|
||||||
|
PriorityLow lipgloss.Style
|
||||||
|
|
||||||
|
// Severity styles
|
||||||
|
SeverityCritical lipgloss.Style
|
||||||
|
SeverityHigh lipgloss.Style
|
||||||
|
SeverityMedium lipgloss.Style
|
||||||
|
SeverityLow lipgloss.Style
|
||||||
|
|
||||||
|
// Status indicator styles
|
||||||
|
StatusPending lipgloss.Style
|
||||||
|
StatusRunning lipgloss.Style
|
||||||
|
StatusSuccess lipgloss.Style
|
||||||
|
StatusError lipgloss.Style
|
||||||
|
StatusWarning lipgloss.Style
|
||||||
|
|
||||||
|
// Deploy styles
|
||||||
|
DeploySuccess lipgloss.Style
|
||||||
|
DeployPending lipgloss.Style
|
||||||
|
DeployFailed lipgloss.Style
|
||||||
|
|
||||||
|
// Box styles
|
||||||
|
Box lipgloss.Style
|
||||||
|
BoxHeader lipgloss.Style
|
||||||
|
ErrorBox lipgloss.Style
|
||||||
|
SuccessBox lipgloss.Style
|
||||||
|
}{
|
||||||
|
// Text styles
|
||||||
|
Dim: DimStyle,
|
||||||
|
Muted: MutedStyle,
|
||||||
|
Bold: BoldStyle,
|
||||||
|
Value: ValueStyle,
|
||||||
|
Accent: AccentStyle,
|
||||||
|
Code: CodeStyle,
|
||||||
|
Key: KeyStyle,
|
||||||
|
Number: NumberStyle,
|
||||||
|
Link: LinkStyle,
|
||||||
|
Header: HeaderStyle,
|
||||||
|
Title: TitleStyle,
|
||||||
|
Stage: StageStyle,
|
||||||
|
PrNum: PrNumberStyle,
|
||||||
|
AccentL: AccentLabelStyle,
|
||||||
|
|
||||||
|
// Status styles
|
||||||
|
Success: SuccessStyle,
|
||||||
|
Error: ErrorStyle,
|
||||||
|
Warning: WarningStyle,
|
||||||
|
Info: InfoStyle,
|
||||||
|
|
||||||
|
// Git styles
|
||||||
|
Dirty: GitDirtyStyle,
|
||||||
|
Ahead: GitAheadStyle,
|
||||||
|
Behind: GitBehindStyle,
|
||||||
|
Clean: GitCleanStyle,
|
||||||
|
Conflict: GitConflictStyle,
|
||||||
|
|
||||||
|
// Repo name style
|
||||||
|
Repo: RepoNameStyle,
|
||||||
|
|
||||||
|
// Coverage styles
|
||||||
|
CoverageHigh: CoverageHighStyle,
|
||||||
|
CoverageMed: CoverageMedStyle,
|
||||||
|
CoverageLow: CoverageLowStyle,
|
||||||
|
|
||||||
|
// Priority styles
|
||||||
|
PriorityHigh: PriorityHighStyle,
|
||||||
|
PriorityMedium: PriorityMediumStyle,
|
||||||
|
PriorityLow: PriorityLowStyle,
|
||||||
|
|
||||||
|
// Severity styles
|
||||||
|
SeverityCritical: SeverityCriticalStyle,
|
||||||
|
SeverityHigh: SeverityHighStyle,
|
||||||
|
SeverityMedium: SeverityMediumStyle,
|
||||||
|
SeverityLow: SeverityLowStyle,
|
||||||
|
|
||||||
|
// Status indicator styles
|
||||||
|
StatusPending: StatusPendingStyle,
|
||||||
|
StatusRunning: StatusRunningStyle,
|
||||||
|
StatusSuccess: StatusSuccessStyle,
|
||||||
|
StatusError: StatusErrorStyle,
|
||||||
|
StatusWarning: StatusWarningStyle,
|
||||||
|
|
||||||
|
// Deploy styles
|
||||||
|
DeploySuccess: DeploySuccessStyle,
|
||||||
|
DeployPending: DeployPendingStyle,
|
||||||
|
DeployFailed: DeployFailedStyle,
|
||||||
|
|
||||||
|
// Box styles
|
||||||
|
Box: BoxStyle,
|
||||||
|
BoxHeader: BoxHeaderStyle,
|
||||||
|
ErrorBox: ErrorBoxStyle,
|
||||||
|
SuccessBox: SuccessBoxStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Core Output Functions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Line translates a key via i18n.T and prints with newline.
|
||||||
|
// If no key is provided, prints an empty line.
|
||||||
|
//
|
||||||
|
// cli.Line("i18n.progress.check") // prints "Checking...\n"
|
||||||
|
// cli.Line("cmd.dev.ci.short") // prints translated text + \n
|
||||||
|
// cli.Line("greeting", map[string]any{"Name": "World"}) // with args
|
||||||
|
// cli.Line("") // prints empty line
|
||||||
|
func Line(key string, args ...any) {
|
||||||
|
if key == "" {
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(i18n.T(key, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Input Functions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Scanln reads from stdin, similar to fmt.Scanln.
|
||||||
|
func Scanln(a ...any) (int, error) {
|
||||||
|
return fmt.Scanln(a...)
|
||||||
|
}
|
||||||
88
pkg/cli/strings.go
Normal file
88
pkg/cli/strings.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// String Formatting (replace fmt.Sprintf)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Sprintf formats a string.
|
||||||
|
// This is a direct replacement for fmt.Sprintf.
|
||||||
|
func Sprintf(format string, args ...any) string {
|
||||||
|
return fmt.Sprintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint formats using the default formats for its operands.
|
||||||
|
// This is a direct replacement for fmt.Sprint.
|
||||||
|
func Sprint(args ...any) string {
|
||||||
|
return fmt.Sprint(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Styled String Functions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Styled returns text formatted with a style.
|
||||||
|
// Example: cli.Styled(cli.Style.Success, "Done!")
|
||||||
|
func Styled(style lipgloss.Style, text string) string {
|
||||||
|
return style.Render(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styledf returns formatted text with a style.
|
||||||
|
// Example: cli.Styledf(cli.Style.Success, "Processed %d items", count)
|
||||||
|
func Styledf(style lipgloss.Style, format string, args ...any) string {
|
||||||
|
return style.Render(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Pre-styled Formatting Functions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// SuccessStr returns a success-styled string with checkmark.
|
||||||
|
func SuccessStr(msg string) string {
|
||||||
|
return SuccessStyle.Render(SymbolCheck + " " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorStr returns an error-styled string with cross.
|
||||||
|
func ErrorStr(msg string) string {
|
||||||
|
return ErrorStyle.Render(SymbolCross + " " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarningStr returns a warning-styled string with warning symbol.
|
||||||
|
func WarningStr(msg string) string {
|
||||||
|
return WarningStyle.Render(SymbolWarning + " " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoStr returns an info-styled string with info symbol.
|
||||||
|
func InfoStr(msg string) string {
|
||||||
|
return InfoStyle.Render(SymbolInfo + " " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DimStr returns a dim-styled string.
|
||||||
|
func DimStr(msg string) string {
|
||||||
|
return DimStyle.Render(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoldStr returns a bold-styled string.
|
||||||
|
func BoldStr(msg string) string {
|
||||||
|
return BoldStyle.Render(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Numeric Formatting
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Itoa converts an integer to a string.
|
||||||
|
// This is a convenience function similar to strconv.Itoa.
|
||||||
|
func Itoa(n int) string {
|
||||||
|
return fmt.Sprintf("%d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Itoa64 converts an int64 to a string.
|
||||||
|
func Itoa64(n int64) string {
|
||||||
|
return fmt.Sprintf("%d", n)
|
||||||
|
}
|
||||||
36
plugin/commands/remember.md
Normal file
36
plugin/commands/remember.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
name: remember
|
||||||
|
description: Save a fact or decision to context for persistence across compacts
|
||||||
|
args: <fact to remember>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remember Context
|
||||||
|
|
||||||
|
Save the provided fact to `~/.claude/sessions/context.json`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/core:remember Use Action pattern not Service
|
||||||
|
/core:remember User prefers UK English
|
||||||
|
/core:remember RFC: minimal state in pre-compact hook
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Run this command to save the fact:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/plugins/cache/core/scripts/capture-context.sh "<fact>" "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if running from the plugin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"${CLAUDE_PLUGIN_ROOT}/scripts/capture-context.sh" "<fact>" "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
The fact will be:
|
||||||
|
- Stored in context.json (max 20 items)
|
||||||
|
- Included in pre-compact snapshots
|
||||||
|
- Auto-cleared after 3 hours of inactivity
|
||||||
102
plugin/hooks/prefer-core.sh
Executable file
102
plugin/hooks/prefer-core.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook: Block dangerous commands, enforce core CLI
|
||||||
|
#
|
||||||
|
# BLOCKS:
|
||||||
|
# - Raw go commands (use core go *)
|
||||||
|
# - Destructive grep patterns (sed -i, xargs rm, etc.)
|
||||||
|
# - Mass file operations (rm -rf, mv/cp with wildcards)
|
||||||
|
# - Any sed outside of safe patterns
|
||||||
|
#
|
||||||
|
# This prevents "efficient shortcuts" that nuke codebases
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
command=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
|
||||||
|
# === HARD BLOCKS - Never allow these ===
|
||||||
|
|
||||||
|
# Block rm -rf, rm -r (except for known safe paths like node_modules, vendor, .cache)
|
||||||
|
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r|--recursive)'; then
|
||||||
|
# Allow only specific safe directories
|
||||||
|
if ! echo "$command" | grep -qE 'rm\s+(-rf|-r)\s+(node_modules|vendor|\.cache|dist|build|__pycache__|\.pytest_cache|/tmp/)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: Recursive delete is not allowed. Delete files individually or ask the user to run this command."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block mv/cp with wildcards (mass file moves)
|
||||||
|
if echo "$command" | grep -qE '(mv|cp)\s+.*\*'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: Mass file move/copy with wildcards is not allowed. Move files individually."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block xargs with rm, mv, cp (mass operations)
|
||||||
|
if echo "$command" | grep -qE 'xargs\s+.*(rm|mv|cp)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: xargs with file operations is not allowed. Too risky for mass changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block find -exec with rm, mv, cp
|
||||||
|
if echo "$command" | grep -qE 'find\s+.*-exec\s+.*(rm|mv|cp)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: find -exec with file operations is not allowed. Too risky for mass changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block ALL sed -i (in-place editing)
|
||||||
|
if echo "$command" | grep -qE 'sed\s+(-[a-zA-Z]*i|--in-place)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: sed -i (in-place edit) is never allowed. Use the Edit tool for file changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block sed piped to file operations
|
||||||
|
if echo "$command" | grep -qE 'sed.*\|.*tee|sed.*>'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: sed with file output is not allowed. Use the Edit tool for file changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block grep with -l piped to xargs/rm/sed (the classic codebase nuke pattern)
|
||||||
|
if echo "$command" | grep -qE 'grep\s+.*-l.*\|'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: grep -l piped to other commands is the classic codebase nuke pattern. Not allowed."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block perl -i, awk with file redirection (sed alternatives)
|
||||||
|
if echo "$command" | grep -qE 'perl\s+-[a-zA-Z]*i|awk.*>'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: In-place file editing with perl/awk is not allowed. Use the Edit tool."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === REQUIRE CORE CLI ===
|
||||||
|
|
||||||
|
# Block raw go commands
|
||||||
|
case "$command" in
|
||||||
|
"go test"*|"go build"*|"go fmt"*|"go mod tidy"*|"go vet"*|"go run"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core go test`, `core build`, `core go fmt --fix`, etc. Raw go commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"go "*)
|
||||||
|
# Other go commands - warn but allow
|
||||||
|
echo '{"decision": "block", "message": "Prefer `core go *` commands. If core does not have this command, ask the user."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Block raw php commands
|
||||||
|
case "$command" in
|
||||||
|
"php artisan serve"*|"./vendor/bin/pest"*|"./vendor/bin/pint"*|"./vendor/bin/phpstan"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core php dev`, `core php test`, `core php fmt`, `core php analyse`. Raw php commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"composer test"*|"composer lint"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core php test` or `core php fmt`. Raw composer commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Block golangci-lint directly
|
||||||
|
if echo "$command" | grep -qE '^golangci-lint'; then
|
||||||
|
echo '{"decision": "block", "message": "Use `core go lint` instead of golangci-lint directly."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === APPROVED ===
|
||||||
|
echo '{"decision": "approve"}'
|
||||||
109
plugin/plugin.json
Normal file
109
plugin/plugin.json
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Host UK unified framework - Go CLI, PHP framework, multi-repo management",
|
||||||
|
"dependencies": [
|
||||||
|
"superpowers@claude-plugins-official"
|
||||||
|
],
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"path": "skills/core.md",
|
||||||
|
"description": "Use when working in host-uk repositories. Provides core CLI command reference."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core-php",
|
||||||
|
"path": "skills/php.md",
|
||||||
|
"description": "Use when creating PHP modules, services, or actions in core-* packages."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core-go",
|
||||||
|
"path": "skills/go.md",
|
||||||
|
"description": "Use when creating Go packages or extending the core CLI."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "remember",
|
||||||
|
"path": "commands/remember.md",
|
||||||
|
"description": "Save a fact or decision to context"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"script": "scripts/session-start.sh",
|
||||||
|
"description": "Check for recent session state on startup"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreCompact": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"script": "scripts/pre-compact.sh",
|
||||||
|
"description": "Save state before auto-compact to prevent amnesia"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "hooks/prefer-core.sh",
|
||||||
|
"description": "Suggest core CLI instead of raw go/php commands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"script": "scripts/block-docs.sh",
|
||||||
|
"description": "Block random .md files, keep docs consolidated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/suggest-compact.sh",
|
||||||
|
"description": "Suggest /compact at logical intervals"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"script": "scripts/suggest-compact.sh",
|
||||||
|
"description": "Suggest /compact at logical intervals"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/php-format.sh",
|
||||||
|
"description": "Auto-format PHP files after edits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/go-format.sh",
|
||||||
|
"description": "Auto-format Go files after edits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/check-debug.sh",
|
||||||
|
"description": "Warn about debug statements (dd, dump, fmt.Println)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/pr-created.sh",
|
||||||
|
"description": "Log PR URL after creation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/extract-actionables.sh",
|
||||||
|
"description": "Extract actionables from core CLI output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/post-commit-check.sh",
|
||||||
|
"description": "Warn about uncommitted work after git commit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"core": {
|
||||||
|
"command": "core",
|
||||||
|
"args": ["mcp", "serve"],
|
||||||
|
"description": "Core CLI MCP server for multi-repo operations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
plugin/scripts/block-docs.sh
Executable file
27
plugin/scripts/block-docs.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Block creation of random .md files - keeps docs consolidated
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" ]]; then
|
||||||
|
# Allow known documentation files
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*README.md|*CLAUDE.md|*AGENTS.md|*CONTRIBUTING.md|*CHANGELOG.md|*LICENSE.md)
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
# Allow docs/ directory
|
||||||
|
*/docs/*.md|*/docs/**/*.md)
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
# Block other .md files
|
||||||
|
*.md)
|
||||||
|
echo '{"decision": "block", "message": "Use README.md or docs/ for documentation. Random .md files clutter the repo."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
44
plugin/scripts/capture-context.sh
Executable file
44
plugin/scripts/capture-context.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Capture context facts from tool output or conversation
|
||||||
|
# Called by PostToolUse hooks to extract actionable items
|
||||||
|
#
|
||||||
|
# Stores in ~/.claude/sessions/context.json as:
|
||||||
|
# [{"fact": "...", "source": "core go qa", "ts": 1234567890}, ...]
|
||||||
|
|
||||||
|
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
|
||||||
|
TIMESTAMP=$(date '+%s')
|
||||||
|
THREE_HOURS=10800
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.claude/sessions"
|
||||||
|
|
||||||
|
# Initialize if missing or stale
|
||||||
|
if [[ -f "$CONTEXT_FILE" ]]; then
|
||||||
|
FIRST_TS=$(jq -r '.[0].ts // 0' "$CONTEXT_FILE" 2>/dev/null)
|
||||||
|
NOW=$(date '+%s')
|
||||||
|
AGE=$((NOW - FIRST_TS))
|
||||||
|
if [[ $AGE -gt $THREE_HOURS ]]; then
|
||||||
|
echo "[]" > "$CONTEXT_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[]" > "$CONTEXT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read input (fact and source passed as args or stdin)
|
||||||
|
FACT="${1:-}"
|
||||||
|
SOURCE="${2:-manual}"
|
||||||
|
|
||||||
|
if [[ -z "$FACT" ]]; then
|
||||||
|
# Try reading from stdin
|
||||||
|
read -r FACT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$FACT" ]]; then
|
||||||
|
# Append to context (keep last 20 items)
|
||||||
|
jq --arg fact "$FACT" --arg source "$SOURCE" --argjson ts "$TIMESTAMP" \
|
||||||
|
'. + [{"fact": $fact, "source": $source, "ts": $ts}] | .[-20:]' \
|
||||||
|
"$CONTEXT_FILE" > "${CONTEXT_FILE}.tmp" && mv "${CONTEXT_FILE}.tmp" "$CONTEXT_FILE"
|
||||||
|
|
||||||
|
echo "[Context] Saved: $FACT" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
27
plugin/scripts/check-debug.sh
Executable file
27
plugin/scripts/check-debug.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Warn about debug statements left in code after edits
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*.go)
|
||||||
|
# Check for fmt.Println, log.Println debug statements
|
||||||
|
if grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
|
||||||
|
echo "[Hook] WARNING: Debug prints found in $FILE_PATH" >&2
|
||||||
|
grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 >&2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*.php)
|
||||||
|
# Check for dd(), dump(), var_dump(), print_r()
|
||||||
|
if grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
|
||||||
|
echo "[Hook] WARNING: Debug statements found in $FILE_PATH" >&2
|
||||||
|
grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 >&2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
34
plugin/scripts/extract-actionables.sh
Executable file
34
plugin/scripts/extract-actionables.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Extract actionable items from core CLI output
|
||||||
|
# Called PostToolUse on Bash commands that run core
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
|
||||||
|
|
||||||
|
CONTEXT_SCRIPT="$(dirname "$0")/capture-context.sh"
|
||||||
|
|
||||||
|
# Extract actionables from specific core commands
|
||||||
|
case "$COMMAND" in
|
||||||
|
"core go qa"*|"core go test"*|"core go lint"*)
|
||||||
|
# Extract error/warning lines
|
||||||
|
echo "$OUTPUT" | grep -E "^(ERROR|WARN|FAIL|---)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core go"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"core php test"*|"core php analyse"*)
|
||||||
|
# Extract PHP errors
|
||||||
|
echo "$OUTPUT" | grep -E "^(FAIL|Error|×)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core php"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"core build"*)
|
||||||
|
# Extract build errors
|
||||||
|
echo "$OUTPUT" | grep -E "^(error|cannot|undefined)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core build"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Pass through
|
||||||
|
echo "$input"
|
||||||
19
plugin/scripts/go-format.sh
Executable file
19
plugin/scripts/go-format.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Auto-format Go files after edits using core go fmt
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
# Run gofmt/goimports on the file silently
|
||||||
|
if command -v core &> /dev/null; then
|
||||||
|
core go fmt --fix "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif command -v goimports &> /dev/null; then
|
||||||
|
goimports -w "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif command -v gofmt &> /dev/null; then
|
||||||
|
gofmt -w "$FILE_PATH" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
17
plugin/scripts/php-format.sh
Executable file
17
plugin/scripts/php-format.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Auto-format PHP files after edits using core php fmt
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
# Run Pint on the file silently
|
||||||
|
if command -v core &> /dev/null; then
|
||||||
|
core php fmt --fix "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif [[ -f "./vendor/bin/pint" ]]; then
|
||||||
|
./vendor/bin/pint "$FILE_PATH" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
51
plugin/scripts/post-commit-check.sh
Executable file
51
plugin/scripts/post-commit-check.sh
Executable file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Post-commit hook: Check for uncommitted work that might get lost
|
||||||
|
#
|
||||||
|
# After committing task-specific files, check if there's other work
|
||||||
|
# in the repo that should be committed or stashed
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
|
||||||
|
# Only run after git commit
|
||||||
|
if ! echo "$COMMAND" | grep -qE '^git commit'; then
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for remaining uncommitted changes
|
||||||
|
UNSTAGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
TOTAL=$((UNSTAGED + STAGED + UNTRACKED))
|
||||||
|
|
||||||
|
if [[ $TOTAL -gt 0 ]]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
echo "[PostCommit] WARNING: Uncommitted work remains" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
|
||||||
|
if [[ $UNSTAGED -gt 0 ]]; then
|
||||||
|
echo " Modified (unstaged): $UNSTAGED files" >&2
|
||||||
|
git diff --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
[[ $UNSTAGED -gt 5 ]] && echo " ... and $((UNSTAGED - 5)) more" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $STAGED -gt 0 ]]; then
|
||||||
|
echo " Staged (not committed): $STAGED files" >&2
|
||||||
|
git diff --cached --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $UNTRACKED -gt 0 ]]; then
|
||||||
|
echo " Untracked: $UNTRACKED files" >&2
|
||||||
|
git ls-files --others --exclude-standard 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
[[ $UNTRACKED -gt 5 ]] && echo " ... and $((UNTRACKED - 5)) more" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo "Consider: commit these, stash them, or confirm they're intentionally left" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
18
plugin/scripts/pr-created.sh
Executable file
18
plugin/scripts/pr-created.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Log PR URL and provide review command after PR creation
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
|
||||||
|
|
||||||
|
if [[ "$COMMAND" == *"gh pr create"* ]]; then
|
||||||
|
PR_URL=$(echo "$OUTPUT" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+' | head -1)
|
||||||
|
if [[ -n "$PR_URL" ]]; then
|
||||||
|
REPO=$(echo "$PR_URL" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\1|')
|
||||||
|
PR_NUM=$(echo "$PR_URL" | sed -E 's|.*/pull/([0-9]+)|\1|')
|
||||||
|
echo "[Hook] PR created: $PR_URL" >&2
|
||||||
|
echo "[Hook] To review: gh pr review $PR_NUM --repo $REPO" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
69
plugin/scripts/pre-compact.sh
Executable file
69
plugin/scripts/pre-compact.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Pre-compact: Save minimal state for Claude to resume after auto-compact
|
||||||
|
#
|
||||||
|
# Captures:
|
||||||
|
# - Working directory + branch
|
||||||
|
# - Git status (files touched)
|
||||||
|
# - Todo state (in_progress items)
|
||||||
|
# - Context facts (decisions, actionables)
|
||||||
|
|
||||||
|
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
|
||||||
|
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
|
||||||
|
TIMESTAMP=$(date '+%s')
|
||||||
|
CWD=$(pwd)
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.claude/sessions"
|
||||||
|
|
||||||
|
# Get todo state
|
||||||
|
TODOS=""
|
||||||
|
if [[ -f "${HOME}/.claude/todos/current.json" ]]; then
|
||||||
|
TODOS=$(cat "${HOME}/.claude/todos/current.json" 2>/dev/null | head -50)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get git status
|
||||||
|
GIT_STATUS=""
|
||||||
|
BRANCH=""
|
||||||
|
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||||
|
GIT_STATUS=$(git status --short 2>/dev/null | head -15)
|
||||||
|
BRANCH=$(git branch --show-current 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get context facts
|
||||||
|
CONTEXT=""
|
||||||
|
if [[ -f "$CONTEXT_FILE" ]]; then
|
||||||
|
CONTEXT=$(jq -r '.[] | "- [\(.source)] \(.fact)"' "$CONTEXT_FILE" 2>/dev/null | tail -10)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$STATE_FILE" << EOF
|
||||||
|
---
|
||||||
|
timestamp: ${TIMESTAMP}
|
||||||
|
cwd: ${CWD}
|
||||||
|
branch: ${BRANCH:-none}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resume After Compact
|
||||||
|
|
||||||
|
You were mid-task. Do NOT assume work is complete.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
\`${CWD}\` on \`${BRANCH:-no branch}\`
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
\`\`\`
|
||||||
|
${GIT_STATUS:-none}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Todos (in_progress = NOT done)
|
||||||
|
\`\`\`json
|
||||||
|
${TODOS:-check /todos}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Context (decisions & actionables)
|
||||||
|
${CONTEXT:-none captured}
|
||||||
|
|
||||||
|
## Next
|
||||||
|
Continue the in_progress todo.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[PreCompact] Snapshot saved" >&2
|
||||||
|
exit 0
|
||||||
34
plugin/scripts/session-start.sh
Executable file
34
plugin/scripts/session-start.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Session start: Read scratchpad if recent, otherwise start fresh
|
||||||
|
# 3 hour window - if older, you've moved on mentally
|
||||||
|
|
||||||
|
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
|
||||||
|
THREE_HOURS=10800 # seconds
|
||||||
|
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
# Get timestamp from file
|
||||||
|
FILE_TS=$(grep -E '^timestamp:' "$STATE_FILE" 2>/dev/null | cut -d' ' -f2)
|
||||||
|
NOW=$(date '+%s')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_TS" ]]; then
|
||||||
|
AGE=$((NOW - FILE_TS))
|
||||||
|
|
||||||
|
if [[ $AGE -lt $THREE_HOURS ]]; then
|
||||||
|
# Recent - read it back
|
||||||
|
echo "[SessionStart] Found recent scratchpad ($(($AGE / 60)) min ago)" >&2
|
||||||
|
echo "[SessionStart] Reading previous state..." >&2
|
||||||
|
echo "" >&2
|
||||||
|
cat "$STATE_FILE" >&2
|
||||||
|
echo "" >&2
|
||||||
|
else
|
||||||
|
# Stale - delete and start fresh
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
echo "[SessionStart] Previous session >3h old - starting fresh" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No timestamp, delete it
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
28
plugin/scripts/suggest-compact.sh
Executable file
28
plugin/scripts/suggest-compact.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Suggest /compact at logical intervals to manage context window
|
||||||
|
# Tracks tool calls per session, suggests compaction every 50 calls
|
||||||
|
|
||||||
|
SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
|
||||||
|
COUNTER_FILE="/tmp/claude-tool-count-${SESSION_ID}"
|
||||||
|
THRESHOLD="${COMPACT_THRESHOLD:-50}"
|
||||||
|
|
||||||
|
# Read or initialize counter
|
||||||
|
if [[ -f "$COUNTER_FILE" ]]; then
|
||||||
|
COUNT=$(($(cat "$COUNTER_FILE") + 1))
|
||||||
|
else
|
||||||
|
COUNT=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$COUNT" > "$COUNTER_FILE"
|
||||||
|
|
||||||
|
# Suggest compact at threshold
|
||||||
|
if [[ $COUNT -eq $THRESHOLD ]]; then
|
||||||
|
echo "[Compact] ${THRESHOLD} tool calls - consider /compact if transitioning phases" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Suggest at intervals after threshold
|
||||||
|
if [[ $COUNT -gt $THRESHOLD ]] && [[ $((COUNT % 25)) -eq 0 ]]; then
|
||||||
|
echo "[Compact] ${COUNT} tool calls - good checkpoint for /compact" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
60
plugin/skills/core.md
Normal file
60
plugin/skills/core.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
name: core
|
||||||
|
description: Use when working in host-uk repositories, running tests, building, releasing, or managing multi-repo workflows. Provides the core CLI command reference.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Core CLI
|
||||||
|
|
||||||
|
The `core` command provides a unified interface for Go/PHP development and multi-repo management.
|
||||||
|
|
||||||
|
**Rule:** Always prefer `core <command>` over raw commands.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Go tests | `core go test` |
|
||||||
|
| Go coverage | `core go cov` |
|
||||||
|
| Go format | `core go fmt --fix` |
|
||||||
|
| Go lint | `core go lint` |
|
||||||
|
| PHP dev server | `core php dev` |
|
||||||
|
| PHP tests | `core php test` |
|
||||||
|
| PHP format | `core php fmt --fix` |
|
||||||
|
| Build | `core build` |
|
||||||
|
| Preview release | `core ci` |
|
||||||
|
| Publish | `core ci --were-go-for-launch` |
|
||||||
|
| Multi-repo status | `core dev health` |
|
||||||
|
| Commit dirty repos | `core dev commit` |
|
||||||
|
| Push repos | `core dev push` |
|
||||||
|
|
||||||
|
## Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
Go project?
|
||||||
|
tests: core go test
|
||||||
|
format: core go fmt --fix
|
||||||
|
build: core build
|
||||||
|
|
||||||
|
PHP project?
|
||||||
|
dev: core php dev
|
||||||
|
tests: core php test
|
||||||
|
format: core php fmt --fix
|
||||||
|
deploy: core php deploy
|
||||||
|
|
||||||
|
Multiple repos?
|
||||||
|
status: core dev health
|
||||||
|
commit: core dev commit
|
||||||
|
push: core dev push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
| Wrong | Right |
|
||||||
|
|-------|-------|
|
||||||
|
| `go test ./...` | `core go test` |
|
||||||
|
| `go build` | `core build` |
|
||||||
|
| `php artisan serve` | `core php dev` |
|
||||||
|
| `./vendor/bin/pest` | `core php test` |
|
||||||
|
| `git status` per repo | `core dev health` |
|
||||||
|
|
||||||
|
Run `core --help` or `core <cmd> --help` for full options.
|
||||||
107
plugin/skills/go.md
Normal file
107
plugin/skills/go.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
---
|
||||||
|
name: core-go
|
||||||
|
description: Use when creating Go packages or extending the core CLI.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go Framework Patterns
|
||||||
|
|
||||||
|
Core CLI uses `pkg/` for reusable packages. Use `core go` commands.
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
core/
|
||||||
|
├── main.go # CLI entry point
|
||||||
|
├── pkg/
|
||||||
|
│ ├── cli/ # CLI framework, output, errors
|
||||||
|
│ ├── {domain}/ # Domain package
|
||||||
|
│ │ ├── cmd_{name}.go # Cobra command definitions
|
||||||
|
│ │ ├── service.go # Business logic
|
||||||
|
│ │ └── *_test.go # Tests
|
||||||
|
│ └── ...
|
||||||
|
└── internal/ # Private packages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a CLI Command
|
||||||
|
|
||||||
|
1. Create `pkg/{domain}/cmd_{name}.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/host-uk/core/pkg/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewNameCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "name",
|
||||||
|
Short: cli.T("domain.name.short"),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// Implementation
|
||||||
|
cli.Success("Done")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register in parent command.
|
||||||
|
|
||||||
|
## CLI Output Helpers
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/host-uk/core/pkg/cli"
|
||||||
|
|
||||||
|
cli.Success("Operation completed") // Green check
|
||||||
|
cli.Warning("Something to note") // Yellow warning
|
||||||
|
cli.Error("Something failed") // Red error
|
||||||
|
cli.Info("Informational message") // Blue info
|
||||||
|
cli.Fatal(err) // Print error and exit 1
|
||||||
|
|
||||||
|
// Structured output
|
||||||
|
cli.Table(headers, rows)
|
||||||
|
cli.JSON(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## i18n Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use cli.T() for translatable strings
|
||||||
|
cli.T("domain.action.success")
|
||||||
|
cli.T("domain.action.error", "details", value)
|
||||||
|
|
||||||
|
// Define in pkg/i18n/locales/en.yaml:
|
||||||
|
domain:
|
||||||
|
action:
|
||||||
|
success: "Operation completed successfully"
|
||||||
|
error: "Failed: {{.details}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Naming
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestFeature_Good(t *testing.T) { /* happy path */ }
|
||||||
|
func TestFeature_Bad(t *testing.T) { /* expected errors */ }
|
||||||
|
func TestFeature_Ugly(t *testing.T) { /* panics, edge cases */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Run tests | `core go test` |
|
||||||
|
| Coverage | `core go cov` |
|
||||||
|
| Format | `core go fmt --fix` |
|
||||||
|
| Lint | `core go lint` |
|
||||||
|
| Build | `core build` |
|
||||||
|
| Install | `core go install` |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `CGO_ENABLED=0` for all builds
|
||||||
|
- UK English in user-facing strings
|
||||||
|
- All errors via `cli.E("context", "message", err)`
|
||||||
|
- Table-driven tests preferred
|
||||||
120
plugin/skills/php.md
Normal file
120
plugin/skills/php.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
---
|
||||||
|
name: core-php
|
||||||
|
description: Use when creating PHP modules, services, or actions in core-* packages.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PHP Framework Patterns
|
||||||
|
|
||||||
|
Host UK PHP modules follow strict conventions. Use `core php` commands.
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
core-{name}/
|
||||||
|
├── src/
|
||||||
|
│ ├── Core/ # Namespace: Core\{Name}
|
||||||
|
│ │ ├── Boot.php # Module bootstrap (listens to lifecycle events)
|
||||||
|
│ │ ├── Actions/ # Single-purpose business logic
|
||||||
|
│ │ └── Models/ # Eloquent models
|
||||||
|
│ └── Mod/ # Namespace: Core\Mod\{Name} (optional extensions)
|
||||||
|
├── resources/views/ # Blade templates
|
||||||
|
├── routes/ # Route definitions
|
||||||
|
├── database/migrations/ # Migrations
|
||||||
|
├── tests/ # Pest tests
|
||||||
|
└── composer.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boot Class Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name};
|
||||||
|
|
||||||
|
use Core\Php\Events\WebRoutesRegistering;
|
||||||
|
use Core\Php\Events\AdminPanelBooting;
|
||||||
|
|
||||||
|
class Boot
|
||||||
|
{
|
||||||
|
public static array $listens = [
|
||||||
|
WebRoutesRegistering::class => 'onWebRoutes',
|
||||||
|
AdminPanelBooting::class => ['onAdmin', 10], // With priority
|
||||||
|
];
|
||||||
|
|
||||||
|
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||||
|
{
|
||||||
|
$event->router->middleware('web')->group(__DIR__ . '/../routes/web.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAdmin(AdminPanelBooting $event): void
|
||||||
|
{
|
||||||
|
$event->panel->resources([...]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name}\Actions;
|
||||||
|
|
||||||
|
use Core\Php\Action;
|
||||||
|
|
||||||
|
class CreateThing
|
||||||
|
{
|
||||||
|
use Action;
|
||||||
|
|
||||||
|
public function handle(User $user, array $data): Thing
|
||||||
|
{
|
||||||
|
return Thing::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
...$data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: CreateThing::run($user, $validated);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenant Models
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name}\Models;
|
||||||
|
|
||||||
|
use Core\Tenant\Concerns\BelongsToWorkspace;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Thing extends Model
|
||||||
|
{
|
||||||
|
use BelongsToWorkspace; // Auto-scopes queries, sets workspace_id
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'workspace_id'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Run tests | `core php test` |
|
||||||
|
| Format | `core php fmt --fix` |
|
||||||
|
| Analyse | `core php analyse` |
|
||||||
|
| Dev server | `core php dev` |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Always `declare(strict_types=1);`
|
||||||
|
- UK English: colour, organisation, centre
|
||||||
|
- Type hints on all parameters and returns
|
||||||
|
- Pest for tests, not PHPUnit
|
||||||
|
- Flux Pro for UI, not vanilla Alpine
|
||||||
Loading…
Add table
Reference in a new issue