hostlaunch

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 7 Imported by: 0

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

View Source
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

func BuildCmd(ctx context.Context, name string, spec RunnerSpec) *exec.Cmd

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

func IsKnownRunner(runner string) bool

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

func LayerHostEnv(base []string, projectConfig, secrets, envVars map[string]string) []string

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):

  1. base — the parent process env (typically os.Environ()). Wins last.
  2. 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.
  3. 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.
  4. 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

func LoadSecretsFile(path string) (map[string]string, error)

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

func PIDPath(name string) (string, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL