Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ builds:
- -tags=netgo
ldflags:
- -s -w
- -X github.com/specsnl/specs-cli/pkg/cmd.Version={{ .Version }}
- -X github.com/specsnl/specs-cli/internal/cmd.Version={{ .Version }}
env:
- CGO_ENABLED=0
goos: [linux, darwin]
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
&& CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build \
-trimpath \
-tags netgo \
-ldflags "-s -w -X ${GO_MODULE}/pkg/cmd.Version=${SPECS_VERSION}" -o ./specs
-ldflags "-s -w -X ${GO_MODULE}/internal/cmd.Version=${SPECS_VERSION}" -o ./specs

# Latest version: https://hub.docker.com/_/debian/tags
FROM debian:13.4-slim
Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/architecture/library-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ weight: 4

## Interactive Prompts: huh

**Decision:** `charm.land/huh/v2` — full replacement for the old `pkg/prompt`.
**Decision:** `charm.land/huh/v2` — full replacement for the old `internal/prompt`.

**Rationale:**
- Usable in standalone mode (`form.Run()` blocks like a normal function call).
Expand All @@ -42,7 +42,7 @@ weight: 4
**Rationale:**
- CSS-like chainable API for colour, bold/italic/underline, padding, margins, borders, and alignment.
- Handles colour downsampling automatically (24-bit → 8-bit → 4-bit based on terminal capability).
- `pkg/util/output` provides the logger and table renderer on top of lipgloss.
- `internal/util/output` provides the logger and table renderer on top of lipgloss.

**Libraries replaced:**
- `github.com/fatih/color`
Expand All @@ -54,7 +54,7 @@ weight: 4

`charm.land/bubbles/v2` is pulled in transitively by huh but is not used directly.

The `specs template list` table renderer (`pkg/util/output/table.go`) is implemented
The `specs template list` table renderer (`internal/util/output/table.go`) is implemented
using lipgloss only — auto-sized columns, styled headers, and a border rendered with
lipgloss styles. No bubbles table component is used.

Expand Down
58 changes: 29 additions & 29 deletions docs/content/docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ weight: 1
specs-cli/
├── main.go # main() — XDG init, cmd.Execute()
├── go.mod
└── pkg/
└── internal/
├── specs/ # global config & constants
│ ├── configuration.go # XDG paths, file name constants
│ └── errors.go # sentinel errors
Expand Down Expand Up @@ -244,9 +244,9 @@ flowchart TD

---

## Error Handling (`pkg/specs/errors.go`)
## Error Handling (`internal/specs/errors.go`)

Sentinel errors are declared in `pkg/specs/errors.go` and should always be wrapped with `%w`
Sentinel errors are declared in `internal/specs/errors.go` and should always be wrapped with `%w`
so that callers can use `errors.Is` to distinguish them:

| Sentinel | Kind string | Raised when |
Expand All @@ -267,7 +267,7 @@ or `""` when no known sentinel is wrapped.

---

## Output System (`pkg/util/output`)
## Output System (`internal/util/output`)

All user-facing output goes through the `output.Writer` interface:

Expand Down Expand Up @@ -318,16 +318,16 @@ logger. `NewApp()` calls `slog.SetDefault` to install a text handler at `Info` l

| Package | Function | Level | Attributes |
|---------|----------|-------|------------|
| `pkg/template` | `Get` | Debug | `template`, `keys`, `computed` |
| `pkg/template` | `Execute` | Debug | `path`, `dest`, `action` (render/verbatim/skip) |
| `pkg/template` | `Execute` | Info | `template`, `dest`, `rendered`, `verbatim`, `skipped` (summary) |
| `pkg/template` | `ApplyComputed` | Debug | `key`, `source`="computed" |
| `pkg/hooks` | `Hooks.Run` | Debug | `trigger`, `commands`, `command` |
| `pkg/cmd` | `executeTemplate` | Debug | `key`, `source` (values_file/arg_flag/default/prompt) — one log per key, final source only |
| `pkg/registry` | `Upgrade` | Debug | `template`, `repo`, `branch`, `target_ref`, `latest_version` |
| `pkg/util/git` | `Clone` | Debug | `repo`, `dest`, `branch` (start and complete) |
| `pkg/util/git` | `Describe` | Debug | `dest`, `commit`, `version` (or `error` on failure) |
| `pkg/util/git` | `CheckRemoteContext` | Debug | `repo`, `branch`, `dest`, `up_to_date`, `latest_version`, `error_kind` |
| `internal/template` | `Get` | Debug | `template`, `keys`, `computed` |
| `internal/template` | `Execute` | Debug | `path`, `dest`, `action` (render/verbatim/skip) |
| `internal/template` | `Execute` | Info | `template`, `dest`, `rendered`, `verbatim`, `skipped` (summary) |
| `internal/template` | `ApplyComputed` | Debug | `key`, `source`="computed" |
| `internal/hooks` | `Hooks.Run` | Debug | `trigger`, `commands`, `command` |
| `internal/cmd` | `executeTemplate` | Debug | `key`, `source` (values_file/arg_flag/default/prompt) — one log per key, final source only |
| `internal/registry` | `Upgrade` | Debug | `template`, `repo`, `branch`, `target_ref`, `latest_version` |
| `internal/util/git` | `Clone` | Debug | `repo`, `dest`, `branch` (start and complete) |
| `internal/util/git` | `Describe` | Debug | `dest`, `commit`, `version` (or `error` on failure) |
| `internal/util/git` | `CheckRemoteContext` | Debug | `repo`, `branch`, `dest`, `up_to_date`, `latest_version`, `error_kind` |

### Consistent attribute keys

Expand Down Expand Up @@ -365,7 +365,7 @@ by `NewApp()` and re-set in `PersistentPreRunE` when `--debug --output=json` swa
## Hooks Execution

```go
// pkg/hooks/hooks.go
// internal/hooks/hooks.go

type Hooks struct {
PreUse []string // each entry: single command or multiline bash script
Expand All @@ -392,19 +392,19 @@ func (h *Hooks) Run(trigger, cwd string, ctx map[string]any, funcMap template.Fu

| Package | Status | Change |
|---------|--------|--------|
| `pkg/specs` | **new** | XDG paths, file name constants, sentinel errors, `KindOf()` (replaces `pkg/boilr`) |
| `pkg/registry` | **new** | on-disk template store: `Entry`, `Load()`, `Upgrade()` |
| `pkg/cmd` | updated | new `use.go`, `template_update.go`, `template_upgrade.go`, iterative conditional prompting; no longer reads project files or `__metadata.json` directly |
| `pkg/template` | updated | configurable delimiters (default `{{ }}`), `context.go`, `verbatim.go`, conditional skip, AST analysis, status; exports `LoadProjectFile()`, `LoadMetadata()`, `SaveMetadata()` |
| `pkg/hooks` | **new** | hook loading and execution |
| `pkg/util/output` | updated | lipgloss-based logger + table renderer; `WriteErr` with JSON `error_kind` for known sentinels (replaces tlog + tabular) |
| `pkg/util/values` | **new** | `--values` file (JSON/YAML) and `--arg` flag parsing |
| `pkg/host` | updated | source format parsing (github:, HTTPS, SSH, local path) |
| `internal/specs` | **new** | XDG paths, file name constants, sentinel errors, `KindOf()` (replaces `pkg/boilr`) |
| `internal/registry` | **new** | on-disk template store: `Entry`, `Load()`, `Upgrade()` |
| `internal/cmd` | updated | new `use.go`, `template_update.go`, `template_upgrade.go`, iterative conditional prompting; no longer reads project files or `__metadata.json` directly |
| `internal/template` | updated | configurable delimiters (default `{{ }}`), `context.go`, `verbatim.go`, conditional skip, AST analysis, status; exports `LoadProjectFile()`, `LoadMetadata()`, `SaveMetadata()` |
| `internal/hooks` | **new** | hook loading and execution |
| `internal/util/output` | updated | lipgloss-based logger + table renderer; `WriteErr` with JSON `error_kind` for known sentinels (replaces tlog + tabular) |
| `internal/util/values` | **new** | `--values` file (JSON/YAML) and `--arg` flag parsing |
| `internal/host` | updated | source format parsing (github:, HTTPS, SSH, local path) |
| `pkg/prompt` | **removed** | replaced by `huh` |
| `pkg/util/tlog` | **removed** | replaced by `pkg/util/output` |
| `pkg/util/tabular` | **removed** | replaced by `pkg/util/output` |
| `pkg/util/tlog` | **removed** | replaced by `internal/util/output` |
| `pkg/util/tabular` | **removed** | replaced by `internal/util/output` |
| `pkg/util/exec` | **removed** | no longer needed (hooks use `os/exec` directly) |
| `pkg/util/exit` | unchanged | |
| `pkg/util/git` | updated | SSH auth, `CheckRemoteContext()` (context-aware), `Describe()` for status tracking; `RemoteCheckResult.Err()` returns typed sentinel errors |
| `pkg/util/osutil` | updated | `CopyDir()` recursive copy |
| `pkg/util/validate` | updated | `Name()` validator (alphanumeric + hyphens + underscores) |
| `internal/util/exit` | unchanged | |
| `internal/util/git` | updated | SSH auth, `CheckRemoteContext()` (context-aware), `Describe()` for status tracking; `RemoteCheckResult.Err()` returns typed sentinel errors |
| `internal/util/osutil` | updated | `CopyDir()` recursive copy |
| `internal/util/validate` | updated | `Name()` validator (alphanumeric + hyphens + underscores) |
4 changes: 2 additions & 2 deletions docs/content/docs/architecture/template-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ executable in the registry and in every scaffolded output.

All of Go's standard `text/template` built-ins are available, plus:

### Custom Functions (`pkg/template/specsregistry.go`)
### Custom Functions (`internal/template/specsregistry.go`)

| Function | Signature | Description |
|----------|-----------|-------------|
Expand Down Expand Up @@ -241,7 +241,7 @@ the filesystem beyond their own template directory.
## Iterative Conditional Prompting

Before prompting, specs analyses the template file tree's AST to determine which variables
are guarded behind conditions (see `pkg/template/analysis.go`). Prompting is iterative:
are guarded behind conditions (see `internal/template/analysis.go`). Prompting is iterative:

1. **Pass 1** — unconditional variables (always needed, regardless of any condition)
2. **Pass 2+** — each round finds conditional variables whose guard variables are all resolved,
Expand Down
4 changes: 2 additions & 2 deletions docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ GoReleaser runs only in the release workflow — not needed for local developmen
## Version Injection

```
-X github.com/specsnl/specs-cli/pkg/cmd.Version=<version>
-X github.com/specsnl/specs-cli/internal/cmd.Version=<version>
```

GoReleaser sets `Version` to the Git tag (e.g. `1.2.3`) automatically through `-ldflags`.
Expand Down Expand Up @@ -56,7 +56,7 @@ builds:
- -tags=netgo
ldflags:
- -s -w
- -X github.com/specsnl/specs-cli/pkg/cmd.Version={{ .Version }}
- -X github.com/specsnl/specs-cli/internal/cmd.Version={{ .Version }}
env:
- CGO_ENABLED=0
goos: [linux, darwin]
Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/app.go → internal/cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"os"
"time"

pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
"github.com/specsnl/specs-cli/pkg/util/output"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
"github.com/specsnl/specs-cli/internal/util/output"
)

// HandlerFactory creates a slog.Handler wired to the given LevelVar.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/metadata_test.go → internal/cmd/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"
"time"

pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
)

func TestWriteMetadata_PreservesSuppliedCreated(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

func newResetRegistryCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"path/filepath"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

func TestResetRegistry_WipesAndRecreates(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/root.go → internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (

"github.com/spf13/cobra"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/pkg/util/output"
"github.com/specsnl/specs-cli/internal/specs"
"github.com/specsnl/specs-cli/internal/util/output"
)

// Execute creates the root command and runs it with a background context.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/root_test.go → internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strings"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

// executeCmd creates a fresh App and root command, executes it with the given
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/pkg/util/validate"
"github.com/specsnl/specs-cli/internal/specs"
"github.com/specsnl/specs-cli/internal/util/validate"
)

func newTemplateDeleteCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"os"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

func TestDelete_Success(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/host"
"github.com/specsnl/specs-cli/pkg/specs"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
"github.com/specsnl/specs-cli/pkg/util/validate"
"github.com/specsnl/specs-cli/internal/host"
"github.com/specsnl/specs-cli/internal/specs"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
"github.com/specsnl/specs-cli/internal/util/validate"
)

func newTemplateDownloadCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

func TestDownload_LocalSourceRejected(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/template_list.go → internal/cmd/template_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
"github.com/specsnl/specs-cli/internal/specs"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
"golang.org/x/sync/errgroup"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (

gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
)

// executeCmdWithCheckFn runs the command with a custom checkRemoteFn injected into the App.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/pkg/util/validate"
"github.com/specsnl/specs-cli/internal/specs"
"github.com/specsnl/specs-cli/internal/util/validate"
)

func newTemplateRenameCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/specs"
)

func TestRename_Success(t *testing.T) {
Expand Down
10 changes: 5 additions & 5 deletions pkg/cmd/template_save.go → internal/cmd/template_save.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
"github.com/specsnl/specs-cli/pkg/util/osutil"
"github.com/specsnl/specs-cli/pkg/util/validate"
"github.com/specsnl/specs-cli/internal/specs"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
"github.com/specsnl/specs-cli/internal/util/osutil"
"github.com/specsnl/specs-cli/internal/util/validate"
)

func newTemplateSaveCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"strings"
"testing"

"github.com/specsnl/specs-cli/pkg/specs"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
"github.com/specsnl/specs-cli/internal/specs"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
)

// makeFakeTemplate creates a minimal template directory structure in dir.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/specs"
pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkggit "github.com/specsnl/specs-cli/pkg/util/git"
"github.com/specsnl/specs-cli/internal/specs"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
pkggit "github.com/specsnl/specs-cli/internal/util/git"
)

func newTemplateUpdateCmd(app *App) *cobra.Command {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
"time"

pkgtemplate "github.com/specsnl/specs-cli/pkg/template"
pkgtemplate "github.com/specsnl/specs-cli/internal/template"
)

func TestUpdate_NoArgs_EmptyRegistry(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/specsnl/specs-cli/pkg/registry"
"github.com/specsnl/specs-cli/pkg/specs"
"github.com/specsnl/specs-cli/internal/registry"
"github.com/specsnl/specs-cli/internal/specs"
)

func newTemplateUpgradeCmd(app *App) *cobra.Command {
Expand Down
Loading