// Package cli exposes read-only protocol endpoints, not a shell wrapper. package cli import ( "encoding/json" "fmt" "io" "time" "server-deploy/internal/appbundle" "server-deploy/internal/aptrepo" "server-deploy/internal/composepolicy" "server-deploy/internal/inspect" "server-deploy/internal/installplan" "server-deploy/internal/planner" "server-deploy/internal/preflight" ) func Run(args []string, in io.Reader, out, diagnostics io.Writer, now func() time.Time) int { fail := func(message string) int { fmt.Fprintln(diagnostics, message); return 1 } if len(args) != 1 { return fail("usage: deployctl version | inspect | preflight | plan-environment | verify-repository | verify-artifacts | plan | verify-plan | verify-package | check-package") } var response any switch args[0] { case "verify-artifacts": var request struct { Directory string `json:"directory"` ArtifactDirectory string `json:"artifactDirectory"` Suite string `json:"suite"` Architecture string `json:"architecture"` Versions map[string]string `json:"versions"` } if decodeStrict(in, &request) != nil { return fail("invalid artifact verification request") } verified, err := aptrepo.VerifyArtifacts(request.Directory, request.ArtifactDirectory, request.Suite, request.Architecture, request.Versions, now()) if err != nil { return fail("artifact verification failed") } response = verified case "verify-repository": var request struct { Directory string `json:"directory"` Suite string `json:"suite"` Architecture string `json:"architecture"` Versions map[string]string `json:"versions"` } if decodeStrict(in, &request) != nil { return fail("invalid repository verification request") } verified, err := aptrepo.Verify(request.Directory, request.Suite, request.Architecture, request.Versions, now()) if err != nil { return fail("repository verification failed") } response = verified case "plan-environment": var request struct { Lock installplan.Lock `json:"lock"` } if decodeStrict(in, &request) != nil { return fail("invalid environment lock request") } report := preflight.Collect() draft, err := installplan.Build(report, request.Lock) if err != nil { return fail("environment lock rejected") } response = struct { ProtocolVersion int `json:"protocolVersion"` Mode string `json:"mode"` ObservedAt time.Time `json:"observedAt"` Report preflight.Report `json:"report"` Draft installplan.Draft `json:"draft"` }{1, "local-environment-draft", now().UTC().Truncate(time.Second), report, draft} case "preflight": report := preflight.Collect() response = struct { ProtocolVersion int `json:"protocolVersion"` Mode string `json:"mode"` ObservedAt time.Time `json:"observedAt"` Report preflight.Report `json:"report"` Proposal preflight.Proposal `json:"proposal"` }{1, "local-environment-proposal", now().UTC().Truncate(time.Second), report, preflight.Plan(report)} case "verify-package", "check-package": var request struct { Directory string `json:"directory"` ExpectedDigest string `json:"expectedDigest"` } if err := decodeStrict(in, &request); err != nil { return fail("invalid package verification request") } verified, err := appbundle.Verify(request.Directory, request.ExpectedDigest) if err != nil { return fail("package verification failed: invalid manifest, inventory or digest") } if args[0] == "check-package" { if composepolicy.Check(verified.Manifest, verified.Files[verified.Manifest.Entrypoint]) != nil { return fail("package rejected by restricted Compose policy") } response = struct { ProtocolVersion int `json:"protocolVersion"` PolicyPassed bool `json:"policyPassed"` Profile string `json:"profile"` Digest string `json:"digest"` Executable bool `json:"executable"` PublisherAuthenticated bool `json:"publisherAuthenticated"` }{1, true, composepolicy.Profile, verified.Digest, false, false} break } response = struct { ProtocolVersion int `json:"protocolVersion"` Verified bool `json:"verified"` Executable bool `json:"executable"` PublisherAuthenticated bool `json:"publisherAuthenticated"` Digest string `json:"digest"` FileCount int `json:"fileCount"` Manifest appbundle.Manifest `json:"manifest"` }{1, true, false, false, verified.Digest, len(verified.Files), verified.Manifest} case "inspect": response = inspect.Collect() case "version": response = struct { Version string `json:"version"` ProtocolVersion int `json:"protocolVersion"` WritesEnabled bool `json:"writesEnabled"` }{"0.1.0-dev", planner.ProtocolVersion, false} case "plan": var intent planner.Intent if err := decodeStrict(in, &intent); err != nil { return fail("invalid request: expected strict protocol JSON (maximum 1 MiB)") } plan, err := planner.Build(intent, now()) if err != nil { return fail("invalid deployment intent") } response = struct { ProtocolVersion int `json:"protocolVersion"` Mode string `json:"mode"` Executable bool `json:"executable"` Plan planner.Plan `json:"plan"` }{planner.ProtocolVersion, "offline-preview", false, plan} case "verify-plan": var request struct { Plan planner.Plan `json:"plan"` Current planner.Intent `json:"current"` } if err := decodeStrict(in, &request); err != nil { return fail("invalid verification request") } if err := request.Plan.Verify(request.Current, now()); err != nil { return fail("plan rejected: expired, changed or invalid") } response = struct { ProtocolVersion int `json:"protocolVersion"` Mode string `json:"mode"` Valid bool `json:"valid"` Executable bool `json:"executable"` }{planner.ProtocolVersion, "offline-preview", true, false} default: return fail("unsupported command; deployment writes are not enabled") } if err := json.NewEncoder(out).Encode(response); err != nil { return fail("cannot write response") } return 0 }