[DEPRECATED] Go implementation of plcbundle
1package commands
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "github.com/spf13/cobra"
12 "tangled.org/atscan.net/plcbundle-go/bundle"
13 "tangled.org/atscan.net/plcbundle-go/internal/bundleindex"
14 "tangled.org/atscan.net/plcbundle-go/internal/didindex"
15 "tangled.org/atscan.net/plcbundle-go/internal/plcclient"
16 internalsync "tangled.org/atscan.net/plcbundle-go/internal/sync"
17 "tangled.org/atscan.net/plcbundle-go/internal/types"
18)
19
20// BundleManager interface (for testing/mocking)
21type BundleManager interface {
22 Close()
23 GetIndex() *bundleindex.Index
24 LoadBundle(ctx context.Context, bundleNumber int) (*bundle.Bundle, error)
25 VerifyBundle(ctx context.Context, bundleNumber int) (*bundle.VerificationResult, error)
26 VerifyChain(ctx context.Context) (*bundle.ChainVerificationResult, error)
27 GetInfo() map[string]interface{}
28 GetMempoolStats() map[string]interface{}
29 GetMempoolOperations() ([]plcclient.PLCOperation, error)
30 ValidateMempool() error
31 RefreshMempool() error
32 ClearMempool() error
33 FetchNextBundle(ctx context.Context, verbose bool, quiet bool) (*bundle.Bundle, types.BundleProductionStats, error)
34 SaveBundle(ctx context.Context, bundle *bundle.Bundle, verbose bool, quiet bool, stats types.BundleProductionStats, skipDIDIndex bool) (time.Duration, error)
35 SaveIndex() error
36 GetDIDIndexStats() map[string]interface{}
37 GetDIDIndex() *didindex.Manager
38 BuildDIDIndex(ctx context.Context, progress func(int, int)) error
39 GetDIDOperations(ctx context.Context, did string, verbose bool) ([]plcclient.PLCOperation, []PLCOperationWithLocation, error)
40 GetDIDOperationsFromMempool(did string) ([]plcclient.PLCOperation, error)
41 GetLatestDIDOperation(ctx context.Context, did string) (*plcclient.PLCOperation, error)
42 LoadOperation(ctx context.Context, bundleNum, position int) (*plcclient.PLCOperation, error)
43 LoadOperations(ctx context.Context, bundleNumber int, positions []int) (map[int]*plcclient.PLCOperation, error)
44 CloneFromRemote(ctx context.Context, opts internalsync.CloneOptions) (*internalsync.CloneResult, error)
45 ResolveDID(ctx context.Context, did string) (*bundle.ResolveDIDResult, error)
46 RunSyncOnce(ctx context.Context, config *internalsync.SyncLoopConfig) (int, error)
47 RunSyncLoop(ctx context.Context, config *internalsync.SyncLoopConfig) error
48 GetBundleIndex() didindex.BundleIndexProvider
49 ScanDirectoryParallel(workers int, progressCallback func(current, total int, bytesProcessed int64)) (*bundle.DirectoryScanResult, error)
50 LoadBundleForDIDIndex(ctx context.Context, bundleNumber int) (*didindex.BundleData, error)
51 ResolveHandleOrDID(ctx context.Context, input string) (string, time.Duration, error)
52 SetQuiet(quiet bool)
53}
54
55// PLCOperationWithLocation wraps operation with location info
56type PLCOperationWithLocation = bundle.PLCOperationWithLocation
57
58// ============================================================================
59// MANAGER OPTIONS STRUCT
60// ============================================================================
61
62// ManagerOptions configures manager creation
63type ManagerOptions struct {
64 Cmd *cobra.Command // Optional: for reading --dir flag
65 Dir string // Optional: explicit directory (overrides Cmd flag and cwd)
66 PLCURL string // Optional: PLC directory URL
67 HandleResolverURL string // Optional: Handle resolver URL (XRPC)
68 AutoInit bool // Optional: allow creating new empty repository (default: false)
69}
70
71// ============================================================================
72// SINGLE UNIFIED getManager METHOD
73// ============================================================================
74
75// getManager creates or opens a bundle manager
76// Pass nil for default options (read-only, current directory, no PLC client)
77//
78// Examples:
79//
80// getManager(nil) // All defaults
81// getManager(&ManagerOptions{Cmd: cmd}) // Use --dir flag
82// getManager(&ManagerOptions{PLCURL: url}) // Add PLC client
83// getManager(&ManagerOptions{AutoInit: true}) // Allow creating repo
84// getManager(&ManagerOptions{Dir: "/path", AutoInit: true}) // Explicit dir + create
85func getManager(opts *ManagerOptions) (*bundle.Manager, string, error) {
86
87 // Silence usage for operational errors
88 if opts.Cmd != nil {
89 opts.Cmd.SilenceUsage = true
90 }
91
92 // Use defaults if nil
93 if opts == nil {
94 opts = &ManagerOptions{}
95 }
96
97 // Determine directory (priority: explicit Dir > Cmd flag > current working dir)
98 var dir string
99
100 if opts.Dir != "" {
101 // Explicit directory provided
102 dir = opts.Dir
103 } else if opts.Cmd != nil {
104 // Try to get from command --dir flag
105 dir, _ = opts.Cmd.Root().PersistentFlags().GetString("dir")
106 }
107
108 // Fallback to current working directory
109 if dir == "" {
110 var err error
111 dir, err = os.Getwd()
112 if err != nil {
113 return nil, "", err
114 }
115 }
116
117 // Convert to absolute path
118 absDir, err := filepath.Abs(dir)
119 if err != nil {
120 return nil, "", fmt.Errorf("invalid directory path: %w", err)
121 }
122
123 // Create config
124 config := bundle.DefaultConfig(absDir)
125 config.AutoInit = opts.AutoInit
126
127 // Check BOTH global AND local verbose flags
128 if opts.Cmd != nil {
129 globalVerbose, _ := opts.Cmd.Root().PersistentFlags().GetBool("verbose")
130 localVerbose, _ := opts.Cmd.Flags().GetBool("verbose")
131 globalQuiet, _ := opts.Cmd.Root().PersistentFlags().GetBool("quiet")
132 localQuiet, _ := opts.Cmd.Flags().GetBool("quiet")
133
134 // Use OR logic: verbose if EITHER flag is set
135 config.Verbose = globalVerbose || localVerbose
136 config.Quiet = globalQuiet || localQuiet
137 }
138
139 // Create PLC client if URL provided
140 var client *plcclient.Client
141 if opts.PLCURL != "" {
142 // Build user agent with version
143 userAgent := fmt.Sprintf("plcbundle/%s",
144 GetVersion())
145
146 client = plcclient.NewClient(
147 opts.PLCURL,
148 plcclient.WithUserAgent(userAgent),
149 )
150 }
151
152 // Set handle resolver URL from flag or option
153 handleResolverURL := opts.HandleResolverURL
154 if handleResolverURL == "" && opts.Cmd != nil {
155 handleResolverURL, _ = opts.Cmd.Root().PersistentFlags().GetString("handle-resolver")
156 }
157 // Only override default if explicitly provided
158 if handleResolverURL != "" {
159 config.HandleResolverURL = handleResolverURL
160 }
161
162 // Create manager
163 mgr, err := bundle.NewManager(config, client)
164 if err != nil {
165 return nil, "", err
166 }
167
168 return mgr, absDir, nil
169}
170
171// parseBundleRange parses bundle range string
172func parseBundleRange(rangeStr string) (start, end int, err error) {
173 if !strings.Contains(rangeStr, "-") {
174 var num int
175 _, err = fmt.Sscanf(rangeStr, "%d", &num)
176 if err != nil {
177 return 0, 0, fmt.Errorf("invalid bundle number: %w", err)
178 }
179 return num, num, nil
180 }
181
182 parts := strings.Split(rangeStr, "-")
183 if len(parts) != 2 {
184 return 0, 0, fmt.Errorf("invalid range format (expected: N or start-end)")
185 }
186
187 _, err = fmt.Sscanf(parts[0], "%d", &start)
188 if err != nil {
189 return 0, 0, fmt.Errorf("invalid start: %w", err)
190 }
191
192 _, err = fmt.Sscanf(parts[1], "%d", &end)
193 if err != nil {
194 return 0, 0, fmt.Errorf("invalid end: %w", err)
195 }
196
197 if start > end {
198 return 0, 0, fmt.Errorf("start must be <= end")
199 }
200
201 return start, end, nil
202}
203
204// Formatting helpers
205
206func formatBytes(bytes int64) string {
207 if bytes < 0 {
208 return fmt.Sprintf("-%s", formatBytes(-bytes))
209 }
210
211 const unit = 1000
212 if bytes < unit {
213 return fmt.Sprintf("%d B", bytes)
214 }
215 div, exp := int64(unit), 0
216 for n := bytes / unit; n >= unit; n /= unit {
217 div *= unit
218 exp++
219 }
220 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
221}
222
223func formatDuration(d time.Duration) string {
224 if d < time.Minute {
225 return fmt.Sprintf("%.0f seconds", d.Seconds())
226 }
227 if d < time.Hour {
228 return fmt.Sprintf("%.1f minutes", d.Minutes())
229 }
230 if d < 24*time.Hour {
231 return fmt.Sprintf("%.1f hours", d.Hours())
232 }
233 days := d.Hours() / 24
234 if days < 30 {
235 return fmt.Sprintf("%.1f days", days)
236 }
237 if days < 365 {
238 return fmt.Sprintf("%.1f months", days/30)
239 }
240 return fmt.Sprintf("%.1f years", days/365)
241}
242
243func formatNumber(n int) string {
244 s := fmt.Sprintf("%d", n)
245 var result []byte
246 for i, c := range s {
247 if i > 0 && (len(s)-i)%3 == 0 {
248 result = append(result, ',')
249 }
250 result = append(result, byte(c))
251 }
252 return string(result)
253}
254
255// commandLogger adapts to types.Logger
256type commandLogger struct {
257 quiet bool
258}
259
260func (l *commandLogger) Printf(format string, v ...interface{}) {
261 if !l.quiet {
262 fmt.Fprintf(os.Stderr, format+"\n", v...)
263 }
264}
265
266func (l *commandLogger) Println(v ...interface{}) {
267 if !l.quiet {
268 fmt.Fprintln(os.Stderr, v...)
269 }
270}
271
272// formatCount formats count with color coding
273func formatCount(count int) string {
274 if count == 0 {
275 return "\033[32m0 ✓\033[0m"
276 }
277 return fmt.Sprintf("\033[33m%d ⚠️\033[0m", count)
278}
279
280// formatCountCritical formats count with critical color coding
281func formatCountCritical(count int) string {
282 if count == 0 {
283 return "\033[32m0 ✓\033[0m"
284 }
285 return fmt.Sprintf("\033[31m%d ✗\033[0m", count)
286}
287
288func min(a, b int) int {
289 if a < b {
290 return a
291 }
292 return b
293}