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