run.go RawHistoryFolder

1// Package app wires vev's runtime together via dependency injection. It is
2// the single place where usecases (client, daemon) are connected to concrete
3// adapters (ipc transport/listener, terminal); usecases themselves never
4// import adapters.
5package app
6
7import (
8	"context"
9	"errors"
10	"fmt"
11	"io"
12	"log/slog"
13	_ "net/http/pprof"
14	"os"
15	"path/filepath"
16	"time"
17
18	"github.com/bnema/vev/internal/adapters/clock"
19	"github.com/bnema/vev/internal/adapters/config"
20	"github.com/bnema/vev/internal/domain"
21	"github.com/bnema/vev/internal/logging"
22	"github.com/bnema/vev/internal/platform"
23	"github.com/bnema/vev/internal/protocol"
24	"github.com/bnema/vev/internal/usecase/client"
25)
26
27// Build metadata. Defaults describe a plain `go build`; releases overwrite
28// them via -ldflags "-X github.com/bnema/vev/internal/app.version=..." (and
29// .commit / .date) — see .goreleaser.yaml.
30var (
31	version = "0.1.0-dev"
32	commit  = "none"
33	date    = "unknown"
34)
35
36// versionLine renders the --version output.
37func versionLine() string {
38	return fmt.Sprintf("vev %s (commit %s, built %s)", version, commit, date)
39}
40
41// Run is the entry point invoked by main. It parses args into a command and
42// dispatches it.
43func Run(args []string) error {
44	if err := platform.ActivateDevelopmentEnvironment(); err != nil {
45		return err
46	}
47	cmd, err := parseArgs(args)
48	if err != nil {
49		return err
50	}
51	return dispatch(context.Background(), cmd)
52}
53
54// dispatch runs the parsed command.
55func dispatch(ctx context.Context, cmd command) error {
56	switch cmd.kind {
57	case kindHelp:
58		fmt.Println(usageText)
59		return nil
60	case kindVersion:
61		fmt.Println(versionLine())
62		return nil
63	case kindWebRenew:
64		return renewWebToken(ctx)
65	case kindWebDaemon:
66		return launchWebDaemon(ctx, cmd.web)
67	case kindWebServe:
68		return runWebDaemon(ctx, cmd.web)
69	case kindDaemon:
70		return runDaemon()
71	case kindDaemonLauncher:
72		return runDaemonLauncher()
73	case kindBrokerMuxStdio:
74		return runBrokerMuxStdioCommand(ctx, cmd.brokerMux)
75	case kindBrokerMuxQUICBootstrap:
76		return runBrokerMuxQUICBootstrapCommand(ctx, cmd.brokerMux)
77	case kindBrokerMuxQUICProxy:
78		return runBrokerMuxQUICProxyCommand(ctx, cmd.brokerMux)
79	case kindProductionBrokerServe:
80		return runBrokerServeCommand(ctx, cmd.brokerServe)
81	case kindProductionBrokerLauncher:
82		return runProductionBrokerLauncherCommand(ctx)
83	case kindBrokerReady:
84		return runBrokerReady(ctx, cmd.brokerReady, os.Stdout)
85	case kindDaemonStop:
86		return requestDaemonStop(ctx)
87	case kindUIRemoteCleanup:
88		return runUIRemoteCleanup(ctx)
89	case kindUIDriver:
90		return runUIDriver(ctx, cmd.uiDriver)
91	case kindList:
92		return runList(ctx, cmd)
93	case kindHost:
94		return runHostCommand(ctx, cmd, defaultRemoteHostDeps())
95	case kindKill:
96		if cmd.killAll {
97			return runKillAll(ctx, os.Stdout)
98		}
99		return runKill(ctx, cmd.name, cmd.killSessions)
100	case kindCmd:
101		return runCmd(ctx, cmd.cmd)
102	case kindEnv:
103		return runEnv(ctx, cmd.env)
104	case kindAttach:
105		return runAttachWithOptions(ctx, cmd.intent, cmd.name, cmd.remoteTarget, cmd.ui)
106	default:
107		return usagef("unhandled command")
108	}
109}
110
111func configureLogging(component logging.Component, rotateAtRuntime bool) (*slog.Logger, io.Closer, error) {
112	return logging.Setup(logging.Config{
113		Dir:             platform.StateDir(),
114		Component:       component,
115		Level:           logging.EnvLevel(),
116		MaxBytes:        logging.DefaultMaxBytes,
117		RotateAtRuntime: rotateAtRuntime,
118	})
119}
120
121func logConfigWarnings(log *slog.Logger, warnings []domain.Warning) {
122	for _, warning := range warnings {
123		if warning.Line > 0 {
124			log.Warn("config warning", "line", warning.Line, "msg", warning.Msg)
125			continue
126		}
127		log.Warn("config warning", "msg", warning.Msg)
128	}
129}
130
131func loadConfigOrDefaults(log *slog.Logger, path string) domain.Config {
132	cfg, warnings, err := config.Load(path)
133	if err != nil {
134		if log != nil {
135			log.Warn("loading config failed; using defaults", "path", path, "err", err)
136		}
137		cfg = domain.Defaults()
138	}
139	if log != nil {
140		logConfigWarnings(log, warnings)
141	}
142	return cfg
143}
144
145const (
146	uiRemoteCleanupCommand = "_ui-cleanup"
147	// daemonStopCommand is the hidden, environment-scoped graceful stop of
148	// this environment's daemon, for harnesses and scripts. Users run
149	// `kill --all` instead.
150	daemonStopCommand = "_daemon-stop"
151)
152
153const daemonStopTimeout = 2 * time.Second
154
155var errDaemonNotRunning = errors.New("no daemon running")
156
157func runUIRemoteCleanup(ctx context.Context) error {
158	root := os.Getenv("VEV_ENV_ROOT")
159	if !filepath.IsAbs(root) || filepath.Clean(root) == string(filepath.Separator) {
160		return errors.New("remote UI cleanup requires a private absolute launch root")
161	}
162	if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir == "" || runtimeDir != filepath.Join(root, "runtime") {
163		return errors.New("remote UI cleanup requires the launch runtime directory")
164	}
165	if err := requestDaemonStop(ctx); err != nil && !errors.Is(err, errDaemonNotRunning) {
166		return err
167	}
168	return nil
169}
170
171// connectDaemonStopBroker is the broker connection seam for requestDaemonStop.
172var connectDaemonStopBroker = connectExistingBroker
173
174func requestDaemonStop(ctx context.Context) error {
175	ctx, cancel := context.WithTimeout(ctx, daemonStopTimeout)
176	defer cancel()
177	service, err := connectDaemonStopBroker(ctx)
178	if err != nil {
179		if errors.Is(err, errBrokerAbsent) {
180			return stopDaemonWithoutBroker(ctx)
181		}
182		return unreachableBrokerError(err)
183	}
184	defer func() { _ = service.Close() }()
185	operations, err := client.NewBrokerOperations(service, clock.New())
186	if err != nil {
187		return err
188	}
189	result, err := operations.StopDaemon(ctx, localBrokerOperationRoute(service.Snapshot()))
190	if err != nil {
191		return err
192	}
193	return brokerKillResultError(result)
194}
195
196func unreachableBrokerError(err error) error {
197	return &exitCoded{code: 3, err: fmt.Errorf("%w: %v", errDaemonUnreachable, err)}
198}
199
200// stopDaemonWithoutBroker asks an existing local daemon to stop directly. A
201// daemon routinely outlives the idle broker, so "no broker" never means "no
202// daemon".
203func stopDaemonWithoutBroker(ctx context.Context) error {
204	reply, err := exchangeLocalDaemon(ctx, protocol.Kill{RequestID: 1, Scope: protocol.KillDaemon})
205	if errors.Is(err, errNoLocalDaemon) {
206		return errDaemonNotRunning
207	}
208	if err != nil {
209		return fmt.Errorf("stop local daemon: %w", err)
210	}
211	result, ok := reply.(protocol.KillResult)
212	if !ok {
213		return fmt.Errorf("unexpected daemon stop reply %T", reply)
214	}
215	return brokerKillResultError(result)
216}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close