[DEPRECATED] Go implementation of plcbundle
at main 293 lines 9.1 kB view raw
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}