-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.go
More file actions
258 lines (218 loc) · 7.59 KB
/
sync.go
File metadata and controls
258 lines (218 loc) · 7.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
package starmap
import (
"context"
"time"
"github.com/agentstation/starmap/internal/sources/modelsdev"
"github.com/agentstation/starmap/pkg/authority"
"github.com/agentstation/starmap/pkg/catalogs"
"github.com/agentstation/starmap/pkg/differ"
"github.com/agentstation/starmap/pkg/errors"
"github.com/agentstation/starmap/pkg/logging"
"github.com/agentstation/starmap/pkg/reconciler"
"github.com/agentstation/starmap/pkg/save"
"github.com/agentstation/starmap/pkg/sources"
"github.com/agentstation/starmap/pkg/sync"
)
// Sync synchronizes the catalog with provider APIs using staged source execution.
func (c *client) Sync(ctx context.Context, opts ...sync.Option) (*sync.Result, error) {
// Step 0: Check and set context if nil
if ctx == nil {
ctx = context.Background()
}
// Step 1: Parse options with defaults
options := sync.Defaults().Apply(opts...)
// Step 2: Setup context with timeout
var cancel context.CancelFunc
if options.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, options.Timeout)
} else {
cancel = func() {} // No-op cancel if no timeout
}
defer cancel()
// Step 3: Load merged local catalog (embedded + file if exists)
local, err := catalogs.NewLocal(options.OutputPath)
if err != nil {
return nil, errors.WrapResource("load", "catalog", "local", err)
}
// Step 4: Validate options upfront with local catalog
if err = options.Validate(local.Providers()); err != nil {
return nil, err
}
// Step 5: filter sources by options
srcs := c.filterSources(options, local)
// Step 6: Resolve dependencies and filter sources
srcs, err = resolveDependencies(ctx, srcs, options)
if err != nil {
return nil, err
}
// Step 7: Cleanup sources
// Use background context with timeout for cleanup to ensure it runs even if sync context is cancelled
defer func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cleanupCancel()
if cleanupErr := cleanup(cleanupCtx, srcs); cleanupErr != nil {
logging.Warn().Err(cleanupErr).Msg("Source cleanup errors occurred")
}
}()
// Step 8: Fetch catalogs from all sources
if err = fetch(ctx, srcs, options.SourceOptions()); err != nil {
return nil, err
}
// Step 9: Get existing catalog for baseline comparison
existing, err := c.Catalog()
if err != nil {
// If we can't get existing catalog, use empty one
existing = catalogs.NewEmpty()
logging.Debug().Msg("No existing catalog found, using empty baseline")
}
// Step 10: Reconcile catalogs from all sources with baseline
result, err := update(ctx, existing, srcs)
if err != nil {
return nil, err
}
// Step 11: Log change summary if changes detected
if result.Changeset != nil && result.Changeset.HasChanges() {
logging.Info().
Int("added", len(result.Changeset.Models.Added)).
Int("updated", len(result.Changeset.Models.Updated)).
Int("removed", len(result.Changeset.Models.Removed)).
Msg("Changes detected")
} else {
logging.Info().Msg("No changes detected")
}
// Step 12: Create sync result directly from reconciler's changeset
syncResult := sync.ChangesetToResult(
result.Changeset,
options.DryRun,
options.OutputPath,
result.ProviderAPICounts,
result.ModelProviderMap,
)
// Step 13: Apply changes if not dry run
shouldSave := result.Changeset != nil && result.Changeset.HasChanges()
// Force save if reformat or fresh flag is set (even without changes)
if options.Reformat || options.Fresh {
shouldSave = true
if result.Changeset == nil || !result.Changeset.HasChanges() {
logging.Info().
Bool("reformat", options.Reformat).
Bool("force", options.Fresh).
Msg("Forcing save due to reformat/force flag")
}
}
if !options.DryRun && shouldSave {
// Create empty changeset if nil but we're forcing save
changeset := result.Changeset
if changeset == nil {
changeset = &differ.Changeset{}
}
if err := c.save(result.Catalog, options, changeset); err != nil {
return nil, err
}
} else if options.DryRun {
logging.Info().Bool("dry_run", true).Msg("Dry run completed - no changes applied")
}
return syncResult, nil
}
// update reconciles the catalog with an optional baseline for comparison.
func update(ctx context.Context, baseline catalogs.Catalog, srcs []sources.Source) (*reconciler.Result, error) {
// create reconciler options
opts := []reconciler.Option{
reconciler.WithStrategy(reconciler.NewAuthorityStrategy(authority.New())),
}
// Add baseline if provided
if baseline != nil {
opts = append(opts, reconciler.WithBaseline(baseline))
}
// create a new reconciler
reconcile, err := reconciler.New(opts...)
if err != nil {
return nil, errors.WrapResource("create", "reconciler", "", err)
}
// reconcile the sources catalogs into a single result
result, err := reconcile.Sources(ctx, sources.ProvidersID, srcs)
if err != nil {
return nil, &errors.SyncError{
Provider: "all",
Err: err,
}
}
return result, nil
}
// ============================================================================
// Helper Methods for Sync
// ============================================================================
// save applies the catalog changes if not in dry-run mode.
func (c *client) save(result catalogs.Catalog, options *sync.Options, changeset *differ.Changeset) error {
// Update internal catalog first
c.mu.Lock()
oldCatalog := c.catalog
c.catalog = result
c.mu.Unlock()
// Save to output path if specified
if options.OutputPath != "" {
// Debug: check what providers have models
providers := result.Providers().List()
for _, p := range providers {
modelCount := 0
if p.Models != nil {
modelCount = len(p.Models)
}
logging.Info().
Str("provider", string(p.ID)).
Int("models", modelCount).
Msg("Provider model count before save")
}
if saveable, ok := result.(catalogs.Persistence); ok {
if err := saveable.Save(save.WithPath(options.OutputPath)); err != nil {
return errors.WrapIO("write", options.OutputPath, err)
}
// Copy models.dev logos after successful save
// Convert provider values to pointers for logo copying
providerPtrs := make([]*catalogs.Provider, len(providers))
for i := range providers {
providerPtrs[i] = &providers[i]
}
// Copy provider logos if we have providers and an output path
if len(providerPtrs) > 0 {
logging.Debug().
Int("provider_count", len(providerPtrs)).
Str("output_path", options.OutputPath).
Msg("Copying provider logos from models.dev")
if logoErr := modelsdev.CopyProviderLogos(options.OutputPath, providerPtrs); logoErr != nil {
logging.Warn().
Err(logoErr).
Msg("Could not copy provider logos")
// Non-fatal error - continue without logos
}
}
// Copy author logos from provider logos
authors := result.Authors().List()
if len(authors) > 0 {
logging.Debug().
Int("author_count", len(authors)).
Str("output_path", options.OutputPath).
Msg("Copying author logos from models.dev provider logos")
if logoErr := modelsdev.CopyAuthorLogos(options.OutputPath, authors, result.Providers()); logoErr != nil {
logging.Warn().
Err(logoErr).
Msg("Could not copy author logos")
// Non-fatal error - continue without logos
}
}
}
} else {
// Save to default location
if saveable, ok := result.(catalogs.Persistence); ok {
if err := saveable.Save(save.WithPath(options.OutputPath)); err != nil {
return errors.WrapIO("write", "catalog", err)
}
}
}
logging.Info().
Int("changes_applied", changeset.Summary.TotalChanges).
Msg("Sync completed successfully")
// Trigger hooks for catalog changes
c.hooks.triggerUpdate(oldCatalog, result)
return nil
}