Restructure Packages (revised)
Revision of the original proposal after api/util and api/validation were dissolved in ca68f52, c4138d8, f942c04 and 658676f. Those commits moved shared logic into resource packages (internal/application, internal/bucket, api/gitinfo), not into a flat internal/api. This revision keeps that direction and drops the parts of the original that would work against it.
Goal
Make the dependency graph match the layering we already talk about:
main.go
└── verb packages (get, create, ...) one per CLI verb, never import each other
└── resource packages (internal/application, internal/bucket, ...)
└── api, api/config, api/log, api/gitinfo
└── internal/cli (Error, exit codes, command hints)
Presentation packages (internal/format, internal/logbox) sit beside the verbs and are imported downward only, never by api.
1. Verb packages must not import each other (highest value)
Today create imports apply, auth and logs; update imports create; exec imports get. Anything two verbs share is by definition not verb-specific and belongs in a resource package, following what ca68f52 did for applications.
Concrete moves:
create.{ApplicationKongVars, BucketKongVars, KubernetesClusterOptions, SSHKeysFlags, LabelSelector, LocalReference, Target, StorageKeysWithDeprecatedFile} (used by update) → shared flag/option types go to internal/application, internal/bucket, and a new internal/kubernetescluster.
get.{ConnectionSecretMap, WriteBase64} (used by exec) → internal/secret or the owning resource package.
logs.{ApplicationQuery, BuildQuery, BuildsOfAppQuery} (used by create) → api/log, which already owns log queries.
apply.File (used by create) → api (it is a generic "apply this manifest" client operation).
auth.{Enabled, ApplyToSecret, Valid, ClusterCmd} (used by create) → the token/secret pieces to api/config or a new internal/auth; ClusterCmd needs a look because create/vcluster.go currently constructs another verb's command struct directly.
Add a guard so this does not regress: a small test in main_test.go (or a depguard rule in .golangci.yml) that lists the verb packages and fails if any of them imports another one.
2. Do not merge cli, format and logbox; fix their responsibilities instead
The original proposal merged them into internal/render. That would be a step backwards:
internal/cli is not a presentation package. It holds Error with exit codes and suggestions, and it is imported by api and api/config. Merging it into a package that pulls in bubbletea, lipgloss, loki logcli, yacspin and kong would make the API layer depend on all of that.
internal/format already mixes four concerns: object printing, progress/spinner output, kong plumbing, and command-hint strings. A merge would add error types and a TUI on top. That is exactly the grab-bag the Go guidance warns about, just with a nicer name.
internal/logbox is used by create only and carries the heaviest third-party deps. Keeping it separate is the point.
Instead:
internal/cli becomes the small "CLI contract" package: Error, exit codes, and the command-hint builder currently at format.Command(), since its only purpose is feeding Error.WithSuggestions. After that, api and api/config import cli only, and cli no longer imports format (its single use, format.Failuref, moves or gets inlined).
ManagedByAnnotation, Name and IsManagedBy move out of cli into api. They describe Kubernetes object annotations, not CLI behaviour, and api.DefaultAnnotations is their main consumer.
InterpolateFlagPlaceholders and MissingChildren are kong plumbing used by main.go only. Move them to the main package, or to internal/kongutil if a second user appears.
internal/format keeps printing, writer/reader, progress and interactivity detection. internal/logbox stays as is.
3. Rename internal/test to internal/testutil
Keep. It is a fixture and mock package, and testutil is the common name for that. Only _test.go files import it, so this is a pure gopls rename. Longer term, resource-specific fixtures (apps.go, bucket.go, mysql.go, ...) could sit next to their resource packages as internal/application/applicationtest and so on, but that is optional and should wait until step 1 settles where those packages are.
4. Move verb packages under internal/cmd/ (last, mechanical)
No repository outside ninech/nctl imports these packages as far as GitHub code search can tell, so internal/ adds intent, not protection. The move touches every command file and every open PR. Do it as one mechanical commit after steps 1 to 3, with gopls rename or gofmt -r, and coordinate with #372 and #225 which touch create and internal/application heavily. Package names stay get, create, ... so main.go wiring does not change beyond import paths.
Target layout:
nctl/
├── main.go
├── api/ client, list, annotations, DefaultAnnotations
│ ├── config/
│ ├── gitinfo/
│ └── log/ incl. Application/Build queries
├── internal/
│ ├── cli/ Error, exit codes, command hints
│ ├── format/ printing, writer/reader, progress
│ ├── logbox/ build log TUI (create only)
│ ├── application/ shared app logic and flag types
│ ├── bucket/
│ ├── kubernetescluster/
│ ├── secret/
│ ├── apifield/, apiresource/, completion/, ipcheck/
│ ├── testutil/
│ └── cmd/
│ ├── get/ create/ apply/ update/ delete/ edit/
│ └── auth/ logs/ exec/ copy/
└── completions/
Order and PR split
- Break verb-to-verb imports, one PR per edge (
update→create is the largest). Add the import guard in the first of these.
- Split
cli/format responsibilities as described in 2.
- Rename
internal/test to internal/testutil.
- Move verbs to
internal/cmd/ in a single commit.
Each step compiles and passes tests on its own. Step 4 can be dropped entirely without losing the benefits of 1 to 3.
Restructure Packages (revised)
Revision of the original proposal after
api/utilandapi/validationwere dissolved in ca68f52, c4138d8, f942c04 and 658676f. Those commits moved shared logic into resource packages (internal/application,internal/bucket,api/gitinfo), not into a flatinternal/api. This revision keeps that direction and drops the parts of the original that would work against it.Goal
Make the dependency graph match the layering we already talk about:
Presentation packages (
internal/format,internal/logbox) sit beside the verbs and are imported downward only, never byapi.1. Verb packages must not import each other (highest value)
Today
createimportsapply,authandlogs;updateimportscreate;execimportsget. Anything two verbs share is by definition not verb-specific and belongs in a resource package, following what ca68f52 did for applications.Concrete moves:
create.{ApplicationKongVars, BucketKongVars, KubernetesClusterOptions, SSHKeysFlags, LabelSelector, LocalReference, Target, StorageKeysWithDeprecatedFile}(used byupdate) → shared flag/option types go tointernal/application,internal/bucket, and a newinternal/kubernetescluster.get.{ConnectionSecretMap, WriteBase64}(used byexec) →internal/secretor the owning resource package.logs.{ApplicationQuery, BuildQuery, BuildsOfAppQuery}(used bycreate) →api/log, which already owns log queries.apply.File(used bycreate) →api(it is a generic "apply this manifest" client operation).auth.{Enabled, ApplyToSecret, Valid, ClusterCmd}(used bycreate) → the token/secret pieces toapi/configor a newinternal/auth;ClusterCmdneeds a look becausecreate/vcluster.gocurrently constructs another verb's command struct directly.Add a guard so this does not regress: a small test in
main_test.go(or adepguardrule in.golangci.yml) that lists the verb packages and fails if any of them imports another one.2. Do not merge
cli,formatandlogbox; fix their responsibilities insteadThe original proposal merged them into
internal/render. That would be a step backwards:internal/cliis not a presentation package. It holdsErrorwith exit codes and suggestions, and it is imported byapiandapi/config. Merging it into a package that pulls in bubbletea, lipgloss, loki logcli, yacspin and kong would make the API layer depend on all of that.internal/formatalready mixes four concerns: object printing, progress/spinner output, kong plumbing, and command-hint strings. A merge would add error types and a TUI on top. That is exactly the grab-bag the Go guidance warns about, just with a nicer name.internal/logboxis used bycreateonly and carries the heaviest third-party deps. Keeping it separate is the point.Instead:
internal/clibecomes the small "CLI contract" package:Error, exit codes, and the command-hint builder currently atformat.Command(), since its only purpose is feedingError.WithSuggestions. After that,apiandapi/configimportclionly, andclino longer importsformat(its single use,format.Failuref, moves or gets inlined).ManagedByAnnotation,NameandIsManagedBymove out ofcliintoapi. They describe Kubernetes object annotations, not CLI behaviour, andapi.DefaultAnnotationsis their main consumer.InterpolateFlagPlaceholdersandMissingChildrenare kong plumbing used bymain.goonly. Move them to the main package, or tointernal/kongutilif a second user appears.internal/formatkeeps printing, writer/reader, progress and interactivity detection.internal/logboxstays as is.3. Rename
internal/testtointernal/testutilKeep. It is a fixture and mock package, and
testutilis the common name for that. Only_test.gofiles import it, so this is a puregopls rename. Longer term, resource-specific fixtures (apps.go,bucket.go,mysql.go, ...) could sit next to their resource packages asinternal/application/applicationtestand so on, but that is optional and should wait until step 1 settles where those packages are.4. Move verb packages under
internal/cmd/(last, mechanical)No repository outside
ninech/nctlimports these packages as far as GitHub code search can tell, sointernal/adds intent, not protection. The move touches every command file and every open PR. Do it as one mechanical commit after steps 1 to 3, withgopls renameorgofmt -r, and coordinate with #372 and #225 which touchcreateandinternal/applicationheavily. Package names stayget,create, ... somain.gowiring does not change beyond import paths.Target layout:
Order and PR split
update→createis the largest). Add the import guard in the first of these.cli/formatresponsibilities as described in 2.internal/testtointernal/testutil.internal/cmd/in a single commit.Each step compiles and passes tests on its own. Step 4 can be dropped entirely without losing the benefits of 1 to 3.