원클릭으로
migration-cli
Migrate a CLI / library project to forge — --kind cli, second binaries, when contract.go isn't worth it.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Migrate a CLI / library project to forge — --kind cli, second binaries, when contract.go isn't worth it.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Visual-design discipline for forge frontends — brief the work before building (never invent an aesthetic without user input), declare the design system before coding, lean on the component library, use restrained color/type systems, never hand-draw complex SVGs, and verify visually before declaring done.
Write Next.js frontends — generated hooks, component library, Tailwind v4, visual verification, and Connect RPC clients.
Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers.
Write Connect RPC handlers — proto-driven codegen, the thin-translation handler pattern (validate, extract auth, convert proto↔internal, call service, wrap errors via `svcerr.Wrap`), middleware, and testing. Business logic lives in `internal/handlers/<svc>/contract.go`, never in handlers.
Forge project conventions and architecture — project structure, generated vs hand-written code, the generate pipeline, proto annotations, contracts, wiring, and naming.
Use-case orchestrators that compose two or more adapters/services. Deps are interfaces only — designed for unit tests with all-mock collaborators.
| name | migration-cli |
| description | Migrate a CLI / library project to forge — --kind cli, second binaries, when contract.go isn't worth it. |
Use this skill when the existing project is a Cobra CLI binary, a code generator, an ops tool, or a pure Go library. For network-facing apps see migration-service. For prerequisites and the overall flow see migration.
A CLI/binary is not a wiring-free special case. It uses the SAME paved path as a server: proto-driven typed internal/config, the cmdkit lib (DB open, logger, flag/env binding, report envelope), and observe-injected logging. Do NOT hand-roll os.Getenv, ad-hoc slog.Loggers, hardcoded timeouts, or hand-rolled shutdown — those are the symptoms of a missing paved path, not a CLI convention. contract.go is optional for a trivial CLI; the config/logger/shutdown paved path is not.
forge new <name>-next --kind cli --mod github.com/<owner>/<name>-next
--kind cli skips the auto-emitted Connect-RPC service, service protos, deploy/, and KCL manifests. It does NOT skip config or wiring — config and the composition are app code regardless of kind. You get:
cmd/<name>-next/main.go # Cobra root command
internal/ # DEFAULT HOME for your packages
internal/config/ # typed config — generated from proto config blocks
internal/app/ # composition (providers.go owned + compose.go generated)
forge.yaml # Project config (strictly top-level)
go.work + go.mod # Workspace + module
For a pure library (no cmd/ at all), use --kind library. Same skeleton minus the binary entrypoint.
cmd/<name>-next/main.go is the Cobra root. The display binary name is whatever cmd/<dir>/ is named — at final cutover, rename cmd/<name>-next/ → cmd/<name>/ to drop the -next suffix from the installed binary.
The Cobra Use: field inside internal/cli/root.go (or wherever your CLI source lives) is independent of the binary name. Forge's own pattern is Use: "forge" even though the migration ships as forge-next. Adjust at cutover or leave — most users only see the binary name.
os.GetenvA CLI's settings live in a <Component>Config proto message annotated with (forge.v1.config), projected into typed internal/config and bound via the cmdkit flag/env path. Scalars (string/int/bool/Duration, including timeouts) are config — they go in the config block, consumed as one typed Cfg config.<Component>Config field. Do not scatter raw os.Getenv("FOO") with magic strings or duplicate the same timeout constant across commands. forge.yaml stays strictly top-level (identity, features); per-env config lives in deploy/kcl/<env>/ for deployed binaries.
A binary still has a small typed composition for whatever it constructs: the owned Infra provider set + OpenInfra in internal/app/providers.go, plus the generated NewComponents(infra *Infra) (*Components, error) in internal/app/compose.go that builds the dependency closure in type-topological order, handing each component its Deps as interface-typed fields resolved by type off infra.<Field>, never by string name. For a one-package CLI this is a few lines; the point is that the logger, DB handle, and config flow through one owned place (Infra) — not a fresh ad-hoc logger per command. There is NO string-keyed registry and NO name-matched wiring.
Use forge add binary <name>. See the binaries skill for the full lifecycle; the short version:
forge add binary <second-binary>
This scaffolds cmd/<name>.go (a real, owned Cobra subcommand on the shared root) plus internal/<name>/{contract.go,<name>.go,<name>_test.go} and appends a binaries: entry to forge.yaml. The subcommand is a Go symbol you can jump to with its own flags/help — not a string projected through a registry.
For an admin / inspector CLI that hosts its own child subcommands (e.g. <binary> dump-state, <binary> reset-foo), forge add binary is still the right starting point: the scaffolded <name>Cmd Cobra command happily hosts both its own RunE body and child commands registered via <name>Cmd.AddCommand(...). Add child subcommands as sibling files under cmd/<name>_<subcmd>.go. The workaround-cmd-not-in-binaries lint can fire on those sibling files in current forge — a known false-positive on subcommand-glue files; treat it as advisory.
For packages like naming, validate, format helpers — pure functions on no state — forge package new is overkill. The contract+mock generation has no test seam to mock when the package has no I/O, no time, no randomness, no external state.
Three honest options:
Set in forge.yaml:
contracts:
strict: true
allow_exported_funcs: true
exclude:
- internal/<utility-pkg>
Honest about the lack of test seam. Best for string-case conversions, pure formatters.
Service structExpose functions as methods on a Service struct, define a Service interface in contract.go. Now mockable, but verbose at every call site (naming.New().ToPascalCase(s) instead of naming.ToPascalCase(s)).
Keep free functions for ergonomics, ALSO expose a Service interface that delegates to them. Best of both, slightly redundant. Pick this when consumers want to mock but you don't want to break call sites.
Decision rule: pure utilities with no I/O / time / randomness / external state → (A). Anything that touches one of those → (B) or (C). For the full pattern (interface design, generated *_gen.go, test usage), see contracts.
CLI commands map to internal/cli/ (or wherever your Cobra commands live). The --kind cli scaffold doesn't pre-emit a command tree — it gives you a Cobra root in cmd/<binary>/main.go and you build out from there. Recommended: one Cobra command per file under internal/cli/<command>.go, mirroring the source layout. Set the contracts floor before porting:
contracts:
strict: true
allow_exported_vars: false
allow_exported_funcs: false
Cobra commands themselves don't need contract.go (they are wired into the root cmd, not consumed by other packages); the rule applies to the internal/<utility>/ and internal/<service>/ packages they call into.
forge generate is mostly a no-opFor --kind cli, forge generate does NOT regenerate Connect-RPC stubs or frontend hooks (none exist). It DOES re-project internal/config from your proto config blocks and produce mock_gen.go for any internal/<name>/contract.go it finds. (Observability lives in the forge/pkg/observe interceptor/helper layer, not per-package *_gen.go wrappers.)
Run forge generate after every config-block or contract-bearing package port. Skipping it means stale mocks/config; tests against your interface compile against the previous shape.
pkg/pkg/ is ONLY for code with real external importers you will support as public API. No external importer ⇒ keep it in internal/, not pkg/. Carve out a pkg/ sub-module (its own pkg/go.mod) only when you have runtime libraries downstream consumers should import without pulling in your CLI / build-tool dep graph (Cobra, codegen libs, Delve, etc.). Symptoms:
go.sum of importers.Setup:
# Drop a fresh go.mod at pkg/
cd pkg && go mod init github.com/<owner>/<name>-next/pkg
# Copy only the deps the runtime libraries actually use (don't inherit
# the parent's entire require block).
If the parent already has go.work, add pkg to the use (...) block. Otherwise put replace github.com/<owner>/<name>-next/pkg => ./pkg in the parent go.mod. Don't do both — workspace use supersedes replace and they're equivalent for build resolution.
Every directory physically under pkg/ joins the sub-module the moment pkg/go.mod exists. If a sub-package needs to stay in the main module, move it out of pkg/ BEFORE introducing the sub-module — there's no clean way to exclude a subdir.
forge generate # config + mocks for any contract.go
forge lint # contract + general lints
go build ./... # main module + sub-modules in the workspace
go test ./...
go install ./cmd/<name>-next
$(go env GOPATH)/bin/<name>-next --help # smoke
--kind cli (or --kind library) at scaffold time. Don't disable server-shaped emission post-hoc.internal/config + cmdkit paved path as servers. No raw os.Getenv, ad-hoc loggers, or hardcoded timeouts.internal/. pkg/ is only for code with real external importers.contract.go is optional for a trivial CLI; the config/logger/shutdown paved path is not.forge add binary <name> for second binaries (real owned Cobra subcommands; see binaries). The scaffolded command also hosts child subcommands via <name>Cmd.AddCommand(...).forge package new.pkg/ sub-module: workspace use OR replace, not both.migration-service.contract.go for non-trivial packages — see contracts.migration.