1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package cli
6
7import (
8 "context"
9 "errors"
10 "fmt"
11
12 "git.secluded.site/np/internal/config"
13 "git.secluded.site/np/internal/db"
14 "git.secluded.site/np/internal/event"
15 "git.secluded.site/np/internal/goal"
16 "git.secluded.site/np/internal/i18n"
17 "git.secluded.site/np/internal/session"
18 "git.secluded.site/np/internal/task"
19 "git.secluded.site/np/internal/timeutil"
20)
21
22// Environment aggregates long-lived services needed by CLI commands.
23type Environment struct {
24 DB *db.Database
25 Clock timeutil.Clock
26 Config config.Config
27 Localizer *i18n.Localizer
28 SessionStore *session.Store
29 GoalStore *goal.Store
30 TaskStore *task.Store
31 EventStore *event.Store
32}
33
34// OpenEnvironment initialises storage and domain stores for CLI commands.
35func OpenEnvironment(opts db.Options, clock timeutil.Clock, cfg config.Config, localizer *i18n.Localizer) (*Environment, error) {
36 database, err := db.Open(opts)
37 if err != nil {
38 return nil, fmt.Errorf("cli: open database: %w", err)
39 }
40
41 if clock == nil {
42 clock = timeutil.UTCClock{}
43 }
44
45 env := &Environment{
46 DB: database,
47 Clock: clock,
48 Config: cfg,
49 Localizer: localizer,
50 SessionStore: session.NewStore(database, clock),
51 GoalStore: goal.NewStore(database, clock),
52 TaskStore: task.NewStore(database, clock),
53 EventStore: event.NewStore(database, clock),
54 }
55 return env, nil
56}
57
58// Close releases resources allocated by the environment.
59func (e *Environment) Close() error {
60 if e == nil || e.DB == nil {
61 return nil
62 }
63 return e.DB.Close()
64}
65
66// ActiveSession resolves the active session for path (or parent paths).
67func (e *Environment) ActiveSession(ctx context.Context, path string) (session.Document, bool, error) {
68 if e == nil || e.SessionStore == nil {
69 return session.Document{}, false, errors.New("cli: environment not initialised")
70 }
71 return e.SessionStore.ActiveByPath(ctx, path)
72}
73
74// LoadGoal retrieves the goal for sid. When absent, ok is false.
75func (e *Environment) LoadGoal(ctx context.Context, sid string) (goal.Document, bool, error) {
76 if e == nil || e.GoalStore == nil {
77 return goal.Document{}, false, errors.New("cli: environment not initialised")
78 }
79 doc, err := e.GoalStore.Get(ctx, sid)
80 if err != nil {
81 if errors.Is(err, goal.ErrNotFound) {
82 return goal.Document{}, false, nil
83 }
84 return goal.Document{}, false, err
85 }
86 return doc, true, nil
87}
88
89// LoadTasks returns tasks for sid sorted by creation order.
90func (e *Environment) LoadTasks(ctx context.Context, sid string) ([]task.Task, error) {
91 if e == nil || e.TaskStore == nil {
92 return nil, errors.New("cli: environment not initialised")
93 }
94 return e.TaskStore.List(ctx, sid)
95}
96
97// LoadTasksByStatus returns tasks filtered by status. When status is empty,
98// all tasks are returned.
99func (e *Environment) LoadTasksByStatus(ctx context.Context, sid string, status task.Status) ([]task.Task, error) {
100 if e == nil || e.TaskStore == nil {
101 return nil, errors.New("cli: environment not initialised")
102 }
103 if status == "" {
104 return e.TaskStore.List(ctx, sid)
105 }
106 return e.TaskStore.ListByStatus(ctx, sid, status)
107}
108
109type contextKey string
110
111const environmentContextKey contextKey = "git.secluded.site/np/internal/cli/environment"
112
113// WithEnvironment stores env inside ctx for retrieval by subcommands.
114func WithEnvironment(ctx context.Context, env *Environment) context.Context {
115 return context.WithValue(ctx, environmentContextKey, env)
116}
117
118// FromContext extracts an Environment that was previously embedded with WithEnvironment.
119func FromContext(ctx context.Context) (*Environment, bool) {
120 if ctx == nil {
121 return nil, false
122 }
123 env, ok := ctx.Value(environmentContextKey).(*Environment)
124 if !ok || env == nil {
125 return nil, false
126 }
127 return env, true
128}