Documentation
¶
Overview ¶
Package hostlaunch composes exec.Cmds for host-mode services and frontends, plus the small env-file helpers both call sites need.
Two CLI surfaces target the same dispatch matrix:
- `forge run <svc>` — single-service host runner (foreground or background; backed by a per-service PID file).
- `forge up` host phase — N-service loop that hangs the cmds off a process registry for cascade teardown.
Both pick a runner (go-run / air / binary / delve), default the env file to `.env.<env>`, and layer the env-file values onto the child process. Before this package existed, the dispatch was duplicated across `internal/cli/run.go` (buildRunHostCmd / runHostService) and `internal/cli/up.go` (buildHostServiceCmd / upHostServices) — same runner table, two implementations.
The package intentionally does NOT own the process lifecycle:
- foreground stream-prefix + signal handling lives in the single- service `forge run` path because it has different semantics (one process, persistent PID file, `stop` subcommand);
- the N-process registry that `forge up` uses for cascade teardown stays in `internal/cli/up.go` for the same reason.
What's shared here is the pure command-construction matrix plus the minimal env-file parser. Anything that tracks PIDs / streams output / handles signals stays at the call site.
Index ¶
- Constants
- func BuildCmd(ctx context.Context, name string, spec RunnerSpec) *exec.Cmd
- func IsKnownRunner(runner string) bool
- func LayerHostEnv(base []string, projectConfig, secrets, envVars map[string]string) []string
- func LoadSecretsFile(path string) (map[string]string, error)
- func PIDPath(name string) (string, error)
- type RunnerSpec
Constants ¶
const ( // DefaultDelvePort is the dlv --listen=:<port> default when KCL // doesn't pin one explicitly. Matches the historical // `forge run --debug` shape. DefaultDelvePort = 2345 // DefaultAirConfig is the `air -c <path>` default when KCL doesn't // pin one explicitly. Mirrors the `air` tool's own default — // `.air.toml` at the project root. DefaultAirConfig = ".air.toml" // DefaultGoRunTarget is the `go run <target> server <name>` package // used when a RunnerSpec doesn't carry an explicit GoRunCmd (the // service's KCL GoBuild.cmd). It is NOT "./cmd" — the scaffold lays // the project binary down at cmd/<project>, and a project that has // not yet wired build resolution still needs a sane default. Callers // that know the service's build cmd should set GoRunCmd so the // host-run target matches the build target exactly. DefaultGoRunTarget = "./cmd" )
Defaults pinned by the runner dispatch. Exported so tests and CLI help text can reference them without re-deriving the magic numbers.
Variables ¶
This section is empty.
Functions ¶
func BuildCmd ¶
BuildCmd composes the *exec.Cmd for a host-mode service.
Runner dispatch:
- air: `air -c <spec.AirConfig|.air.toml>`
- binary: `./bin/<name>`
- delve: `dlv exec --headless --listen=:<port> ... ./bin/<name>`
- default ("" / "go-run" / unknown): `go run <spec.GoRunCmd|./cmd> server <name>` — the go-run target is the service's KCL GoBuild.cmd, NOT a hardcoded ./cmd.
Unknown runners fall through to go-run rather than erroring — this preserves the `forge run` behaviour and prevents a typo in KCL from hard-failing the host phase. Callers that want strict matching (the up orchestrator does) should check `IsKnownRunner(spec.Runner)` first and report their own error.
func IsKnownRunner ¶
IsKnownRunner reports whether the runner name is one of the explicitly-supported dispatch keys. Callers that want to refuse unknown runners (rather than silently fall through to go-run) check this before BuildCmd.
func LayerHostEnv ¶
LayerHostEnv composes the env for a host-mode subprocess.
final = base ⊕ projectConfig ⊕ secrets ⊕ envVars
Order (each later layer overrides earlier on key conflict among the three map layers — base os.Environ() always wins so a developer's shell override beats them all):
- base — the parent process env (typically os.Environ()). Wins last.
- projectConfig — forge.yaml `environments[<env>].config` projected to env-var strings. Same non-secret values cluster-mode services see via the ConfigMap projection, layered here so host-mode services don't drift from cluster-mode behavior. Lowest precedence among the extra layers: secrets and envVars both override on conflict because dev-local overrides (secrets) and KCL pins (envVars) are more specific.
- secrets — KEY=VALUE pairs from the gitignored secrets_file (`.env.<env>`). Wins over projectConfig so a developer can override forge.yaml values locally without editing committed config.
- envVars — KCL-declared per-env config. Wins over secrets so reproducible per-env config can't drift across machines.
Returns a fresh []string safe to assign to cmd.Env. A nil projectConfig is treated as an empty layer so the pre-extension callers stay terse.
func LoadSecretsFile ¶
LoadSecretsFile reads a gitignored secrets dotenv into a map. Returns (nil, nil) when path is empty so the caller can unconditionally call this. Missing-file is logged via the returned warn-only error wrapping os.ErrNotExist; permission / parse errors propagate.
Distinct from the legacy "env_file" load: the secrets-file contract is "if present, layer first; KCL env_vars override on conflict" — see LayerHostEnv for the composition.
func PIDPath ¶
PIDPath returns the canonical per-service PID file path:
$HOME/.cache/forge/run/<service>.pid
Canonical convention. Used by `forge run <svc>` (foreground cleanup + background detach + stop subcommand). `forge up` uses its own per-env state file under $HOME/.cache/forge/up/<env>.pids because it tracks N processes (services + frontends + port-forwards) and the per-env grouping is the unit of teardown there; the two conventions coexist deliberately.
Types ¶
type RunnerSpec ¶
type RunnerSpec struct {
Runner string // "" | "go-run" | "air" | "binary" | "delve"
AirConfig string // path relative to project root; default DefaultAirConfig
DelvePort int // dlv --listen=:<port>; default DefaultDelvePort
// GoRunCmd is the go-run target package — the service's KCL
// GoBuild.cmd (e.g. "./cmd/myapp"). The go-run dispatch runs
// `go run <GoRunCmd> server <name>` so it points at the project's
// real cmd/<bin> package rather than a hardcoded "./cmd". Empty
// falls back to DefaultGoRunTarget so a caller that hasn't wired the
// build resolution yet still launches.
GoRunCmd string
// Command, when non-empty, is run verbatim (Command[0] + args) instead
// of any runner convention — the escape hatch for host services whose
// entrypoint doesn't fit `go run ./cmd server <name>`. The canonical
// case is a sibling-repo binary: pair it with WorkingDir so the
// command's own relative paths resolve against the sibling root, e.g.
// Command=["go","run","./cmd/reliant","server","api"] +
// WorkingDir="../reliant". Relative paths in Command resolve against
// the effective cmd.Dir (WorkingDir), matching shell semantics.
Command []string
// WorkingDir is the subprocess cwd override. Empty = inherit parent.
// Relative paths resolve against ProjectDir; absolute paths are
// used verbatim.
WorkingDir string
// ProjectDir is the forge project root used to resolve a relative
// WorkingDir. Ignored when WorkingDir is empty or absolute. Empty
// ProjectDir + relative WorkingDir falls through to the parent's
// cwd interpretation (exec.Cmd default).
ProjectDir string
}
RunnerSpec is the dispatch input. The Runner/AirConfig/DelvePort fields mirror the KCL HostDeploy block; env composition (env_vars from KCL + an optional gitignored secrets dotenv) is layered by the caller via LoadSecretsFile and LayerHostEnv so this package stays vendor-neutral about how config gets sourced.
An empty Runner falls through to the legacy go-run shape so projects that haven't migrated to the deploy module yet keep working.
WorkingDir + ProjectDir control the subprocess's working directory. When WorkingDir is empty, the subprocess inherits the parent's cwd (the project root, where forge was invoked). When WorkingDir is set:
- absolute paths are used as-is;
- relative paths resolve against ProjectDir (which the CLI sets to the forge project root).
The cross-repo Air case is the load-bearing example: a forge project declares `WorkingDir: "../sibling-repo"` so an Air config that lives in the sibling repo and references build paths relative to ITS own repo root resolves correctly even though forge itself runs from the caller's project root.