tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
cmds for dids
tree.fail
4 months ago
f66a7546
907728d3
+1008
-10
4 changed files
expand all
collapse all
unified
split
cmd
plcbundle
commands
did.go
stream.go
main.go
ui
progress.go
+981
cmd/plcbundle/commands/did.go
···
1
1
+
// repo/cmd/plcbundle/commands/did.go
2
2
+
package commands
3
3
+
4
4
+
import (
5
5
+
"bufio"
6
6
+
"context"
7
7
+
"fmt"
8
8
+
"os"
9
9
+
"strings"
10
10
+
"time"
11
11
+
12
12
+
"github.com/goccy/go-json"
13
13
+
"github.com/spf13/cobra"
14
14
+
"tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui"
15
15
+
"tangled.org/atscan.net/plcbundle/internal/plcclient"
16
16
+
)
17
17
+
18
18
+
func NewDIDCommand() *cobra.Command {
19
19
+
cmd := &cobra.Command{
20
20
+
Use: "did",
21
21
+
Aliases: []string{"d"},
22
22
+
Short: "DID operations and queries",
23
23
+
Long: `DID operations and queries
24
24
+
25
25
+
Query and analyze DIDs in the bundle repository. All commands
26
26
+
require a DID index to be built for optimal performance.`,
27
27
+
28
28
+
Example: ` # Lookup all operations for a DID
29
29
+
plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe
30
30
+
31
31
+
# Resolve to current DID document
32
32
+
plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe
33
33
+
34
34
+
# Show complete audit log
35
35
+
plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe
36
36
+
37
37
+
# Show DID statistics
38
38
+
plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe
39
39
+
40
40
+
# Batch process from file
41
41
+
plcbundle did batch dids.txt`,
42
42
+
}
43
43
+
44
44
+
// Add subcommands
45
45
+
cmd.AddCommand(newDIDLookupCommand())
46
46
+
cmd.AddCommand(newDIDResolveCommand())
47
47
+
cmd.AddCommand(newDIDHistoryCommand())
48
48
+
cmd.AddCommand(newDIDBatchCommand())
49
49
+
cmd.AddCommand(newDIDStatsCommand())
50
50
+
51
51
+
return cmd
52
52
+
}
53
53
+
54
54
+
// ============================================================================
55
55
+
// DID LOOKUP - Find all operations for a DID
56
56
+
// ============================================================================
57
57
+
58
58
+
func newDIDLookupCommand() *cobra.Command {
59
59
+
var (
60
60
+
verbose bool
61
61
+
showJSON bool
62
62
+
)
63
63
+
64
64
+
cmd := &cobra.Command{
65
65
+
Use: "lookup <did>",
66
66
+
Aliases: []string{"find", "get"},
67
67
+
Short: "Find all operations for a DID",
68
68
+
Long: `Find all operations for a DID
69
69
+
70
70
+
Retrieves all operations (both bundled and mempool) for a specific DID,
71
71
+
showing bundle locations, timestamps, and nullification status.
72
72
+
73
73
+
Requires DID index to be built. If not available, will fall back to
74
74
+
full scan (slow).`,
75
75
+
76
76
+
Example: ` # Lookup DID operations
77
77
+
plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe
78
78
+
79
79
+
# Verbose output with timing
80
80
+
plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe -v
81
81
+
82
82
+
# JSON output
83
83
+
plcbundle did lookup did:plc:524tuhdhh3m7li5gycdn6boe --json
84
84
+
85
85
+
# Using alias
86
86
+
plcbundle did find did:plc:524tuhdhh3m7li5gycdn6boe`,
87
87
+
88
88
+
Args: cobra.ExactArgs(1),
89
89
+
90
90
+
RunE: func(cmd *cobra.Command, args []string) error {
91
91
+
did := args[0]
92
92
+
93
93
+
mgr, _, err := getManagerFromCommand(cmd, "")
94
94
+
if err != nil {
95
95
+
return err
96
96
+
}
97
97
+
defer mgr.Close()
98
98
+
99
99
+
stats := mgr.GetDIDIndexStats()
100
100
+
if !stats["exists"].(bool) {
101
101
+
fmt.Fprintf(os.Stderr, "⚠️ DID index not found. Run: plcbundle index build\n")
102
102
+
fmt.Fprintf(os.Stderr, " Falling back to full scan (slow)...\n\n")
103
103
+
}
104
104
+
105
105
+
totalStart := time.Now()
106
106
+
ctx := context.Background()
107
107
+
108
108
+
// Lookup operations
109
109
+
lookupStart := time.Now()
110
110
+
opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, verbose)
111
111
+
if err != nil {
112
112
+
return err
113
113
+
}
114
114
+
lookupElapsed := time.Since(lookupStart)
115
115
+
116
116
+
// Check mempool
117
117
+
mempoolStart := time.Now()
118
118
+
mempoolOps, err := mgr.GetDIDOperationsFromMempool(did)
119
119
+
if err != nil {
120
120
+
return fmt.Errorf("error checking mempool: %w", err)
121
121
+
}
122
122
+
mempoolElapsed := time.Since(mempoolStart)
123
123
+
124
124
+
totalElapsed := time.Since(totalStart)
125
125
+
126
126
+
if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
127
127
+
if showJSON {
128
128
+
fmt.Println("{\"found\": false, \"operations\": []}")
129
129
+
} else {
130
130
+
fmt.Printf("DID not found (searched in %s)\n", totalElapsed)
131
131
+
}
132
132
+
return nil
133
133
+
}
134
134
+
135
135
+
if showJSON {
136
136
+
return outputLookupJSON(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed)
137
137
+
}
138
138
+
139
139
+
return displayLookupResults(did, opsWithLoc, mempoolOps, totalElapsed, lookupElapsed, mempoolElapsed, verbose, stats)
140
140
+
},
141
141
+
}
142
142
+
143
143
+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose debug output")
144
144
+
cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON")
145
145
+
146
146
+
return cmd
147
147
+
}
148
148
+
149
149
+
// ============================================================================
150
150
+
// DID RESOLVE - Resolve to current document
151
151
+
// ============================================================================
152
152
+
153
153
+
func newDIDResolveCommand() *cobra.Command {
154
154
+
var (
155
155
+
verbose bool
156
156
+
showTiming bool
157
157
+
raw bool
158
158
+
)
159
159
+
160
160
+
cmd := &cobra.Command{
161
161
+
Use: "resolve <did>",
162
162
+
Aliases: []string{"doc", "document"},
163
163
+
Short: "Resolve DID to current document",
164
164
+
Long: `Resolve DID to current W3C DID document
165
165
+
166
166
+
Resolves a DID to its current state by applying all non-nullified
167
167
+
operations in chronological order. Returns standard W3C DID document.
168
168
+
169
169
+
Optimized for speed: checks mempool first, then uses DID index for
170
170
+
O(1) lookup of latest operation.`,
171
171
+
172
172
+
Example: ` # Resolve DID
173
173
+
plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe
174
174
+
175
175
+
# Show timing breakdown
176
176
+
plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe --timing
177
177
+
178
178
+
# Get raw PLC state (not W3C format)
179
179
+
plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe --raw
180
180
+
181
181
+
# Pipe to jq
182
182
+
plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe | jq .service`,
183
183
+
184
184
+
Args: cobra.ExactArgs(1),
185
185
+
186
186
+
RunE: func(cmd *cobra.Command, args []string) error {
187
187
+
did := args[0]
188
188
+
189
189
+
mgr, _, err := getManagerFromCommand(cmd, "")
190
190
+
if err != nil {
191
191
+
return err
192
192
+
}
193
193
+
defer mgr.Close()
194
194
+
195
195
+
ctx := context.Background()
196
196
+
197
197
+
if showTiming {
198
198
+
fmt.Fprintf(os.Stderr, "Resolving: %s\n", did)
199
199
+
}
200
200
+
201
201
+
if verbose {
202
202
+
mgr.GetDIDIndex().SetVerbose(true)
203
203
+
}
204
204
+
205
205
+
result, err := mgr.ResolveDID(ctx, did)
206
206
+
if err != nil {
207
207
+
return err
208
208
+
}
209
209
+
210
210
+
// Display timing if requested
211
211
+
if showTiming {
212
212
+
if result.Source == "mempool" {
213
213
+
fmt.Fprintf(os.Stderr, "Mempool check: %s (✓ found)\n", result.MempoolTime)
214
214
+
fmt.Fprintf(os.Stderr, "Total: %s\n\n", result.TotalTime)
215
215
+
} else {
216
216
+
fmt.Fprintf(os.Stderr, "Mempool: %s | Index: %s | Load: %s | Total: %s\n",
217
217
+
result.MempoolTime, result.IndexTime, result.LoadOpTime, result.TotalTime)
218
218
+
fmt.Fprintf(os.Stderr, "Source: bundle %06d, position %d\n\n",
219
219
+
result.BundleNumber, result.Position)
220
220
+
}
221
221
+
}
222
222
+
223
223
+
// Output document
224
224
+
data, _ := json.MarshalIndent(result.Document, "", " ")
225
225
+
fmt.Println(string(data))
226
226
+
227
227
+
return nil
228
228
+
},
229
229
+
}
230
230
+
231
231
+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose debug output")
232
232
+
cmd.Flags().BoolVar(&showTiming, "timing", false, "Show timing breakdown")
233
233
+
cmd.Flags().BoolVar(&raw, "raw", false, "Output raw PLC state (not W3C document)")
234
234
+
235
235
+
return cmd
236
236
+
}
237
237
+
238
238
+
// ============================================================================
239
239
+
// DID HISTORY - Show complete audit log
240
240
+
// ============================================================================
241
241
+
242
242
+
func newDIDHistoryCommand() *cobra.Command {
243
243
+
var (
244
244
+
verbose bool
245
245
+
showJSON bool
246
246
+
compact bool
247
247
+
includeNullified bool
248
248
+
)
249
249
+
250
250
+
cmd := &cobra.Command{
251
251
+
Use: "history <did>",
252
252
+
Aliases: []string{"log", "audit"},
253
253
+
Short: "Show complete DID audit log",
254
254
+
Long: `Show complete DID audit log
255
255
+
256
256
+
Displays all operations for a DID in chronological order, showing
257
257
+
the complete history including nullified operations.
258
258
+
259
259
+
This provides a full audit trail of all changes to the DID.`,
260
260
+
261
261
+
Example: ` # Show full history
262
262
+
plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe
263
263
+
264
264
+
# Include nullified operations
265
265
+
plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --include-nullified
266
266
+
267
267
+
# Compact one-line format
268
268
+
plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --compact
269
269
+
270
270
+
# JSON output
271
271
+
plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe --json`,
272
272
+
273
273
+
Args: cobra.ExactArgs(1),
274
274
+
275
275
+
RunE: func(cmd *cobra.Command, args []string) error {
276
276
+
did := args[0]
277
277
+
278
278
+
mgr, _, err := getManagerFromCommand(cmd, "")
279
279
+
if err != nil {
280
280
+
return err
281
281
+
}
282
282
+
defer mgr.Close()
283
283
+
284
284
+
ctx := context.Background()
285
285
+
286
286
+
// Get all operations with locations
287
287
+
opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, verbose)
288
288
+
if err != nil {
289
289
+
return err
290
290
+
}
291
291
+
292
292
+
// Get mempool operations
293
293
+
mempoolOps, err := mgr.GetDIDOperationsFromMempool(did)
294
294
+
if err != nil {
295
295
+
return err
296
296
+
}
297
297
+
298
298
+
if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
299
299
+
fmt.Fprintf(os.Stderr, "DID not found: %s\n", did)
300
300
+
return nil
301
301
+
}
302
302
+
303
303
+
if showJSON {
304
304
+
return outputHistoryJSON(did, opsWithLoc, mempoolOps)
305
305
+
}
306
306
+
307
307
+
return displayHistory(did, opsWithLoc, mempoolOps, compact, includeNullified)
308
308
+
},
309
309
+
}
310
310
+
311
311
+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
312
312
+
cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON")
313
313
+
cmd.Flags().BoolVar(&compact, "compact", false, "Compact one-line format")
314
314
+
cmd.Flags().BoolVar(&includeNullified, "include-nullified", false, "Show nullified operations")
315
315
+
316
316
+
return cmd
317
317
+
}
318
318
+
319
319
+
// ============================================================================
320
320
+
// DID BATCH - Process multiple DIDs from file or stdin
321
321
+
// ============================================================================
322
322
+
323
323
+
func newDIDBatchCommand() *cobra.Command {
324
324
+
var (
325
325
+
action string
326
326
+
workers int
327
327
+
outputFile string
328
328
+
fromStdin bool
329
329
+
)
330
330
+
331
331
+
cmd := &cobra.Command{
332
332
+
Use: "batch [file]",
333
333
+
Short: "Process multiple DIDs from file or stdin",
334
334
+
Long: `Process multiple DIDs from file or stdin
335
335
+
336
336
+
Read DIDs from a file (one per line) or stdin and perform batch operations.
337
337
+
Supports parallel processing for better performance.
338
338
+
339
339
+
Actions:
340
340
+
lookup - Lookup all DIDs and show summary
341
341
+
resolve - Resolve all DIDs to documents
342
342
+
export - Export all operations to JSONL
343
343
+
344
344
+
Input formats:
345
345
+
- File path: reads DIDs from file
346
346
+
- "-" or --stdin: reads DIDs from stdin
347
347
+
- Omit file + use --stdin: reads from stdin`,
348
348
+
349
349
+
Example: ` # Batch lookup from file
350
350
+
plcbundle did batch dids.txt --action lookup
351
351
+
352
352
+
# Read from stdin
353
353
+
cat dids.txt | plcbundle did batch --stdin --action lookup
354
354
+
cat dids.txt | plcbundle did batch - --action resolve
355
355
+
356
356
+
# Export operations for DIDs from stdin
357
357
+
echo "did:plc:524tuhdhh3m7li5gycdn6boe" | plcbundle did batch - --action export
358
358
+
359
359
+
# Pipe results
360
360
+
plcbundle did batch dids.txt --action resolve -o resolved.jsonl
361
361
+
362
362
+
# Parallel processing
363
363
+
cat dids.txt | plcbundle did batch --stdin --action lookup --workers 8
364
364
+
365
365
+
# Chain commands
366
366
+
grep "did:plc:" some_file.txt | plcbundle did batch - --action export > ops.jsonl`,
367
367
+
368
368
+
Args: cobra.MaximumNArgs(1),
369
369
+
370
370
+
RunE: func(cmd *cobra.Command, args []string) error {
371
371
+
var filename string
372
372
+
373
373
+
// Determine input source
374
374
+
if len(args) > 0 {
375
375
+
filename = args[0]
376
376
+
if filename == "-" {
377
377
+
fromStdin = true
378
378
+
}
379
379
+
} else if !fromStdin {
380
380
+
return fmt.Errorf("either provide filename or use --stdin flag\n" +
381
381
+
"Examples:\n" +
382
382
+
" plcbundle did batch dids.txt\n" +
383
383
+
" plcbundle did batch --stdin\n" +
384
384
+
" cat dids.txt | plcbundle did batch -")
385
385
+
}
386
386
+
387
387
+
mgr, _, err := getManagerFromCommand(cmd, "")
388
388
+
if err != nil {
389
389
+
return err
390
390
+
}
391
391
+
defer mgr.Close()
392
392
+
393
393
+
return processBatchDIDs(mgr, filename, batchOptions{
394
394
+
action: action,
395
395
+
workers: workers,
396
396
+
outputFile: outputFile,
397
397
+
fromStdin: fromStdin,
398
398
+
})
399
399
+
},
400
400
+
}
401
401
+
402
402
+
cmd.Flags().StringVar(&action, "action", "lookup", "Action: lookup, resolve, export")
403
403
+
cmd.Flags().IntVar(&workers, "workers", 4, "Number of parallel workers")
404
404
+
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file (default: stdout)")
405
405
+
cmd.Flags().BoolVar(&fromStdin, "stdin", false, "Read DIDs from stdin")
406
406
+
407
407
+
return cmd
408
408
+
}
409
409
+
410
410
+
// ============================================================================
411
411
+
// DID STATS - Show DID activity statistics
412
412
+
// ============================================================================
413
413
+
414
414
+
func newDIDStatsCommand() *cobra.Command {
415
415
+
var (
416
416
+
showGlobal bool
417
417
+
showJSON bool
418
418
+
)
419
419
+
420
420
+
cmd := &cobra.Command{
421
421
+
Use: "stats [did]",
422
422
+
Short: "Show DID activity statistics",
423
423
+
Long: `Show DID activity statistics
424
424
+
425
425
+
Display statistics for a specific DID or global DID index stats.
426
426
+
427
427
+
With DID: shows operation count, first/last activity, bundle distribution
428
428
+
Without DID: shows global index statistics`,
429
429
+
430
430
+
Example: ` # Stats for specific DID
431
431
+
plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe
432
432
+
433
433
+
# Global index stats
434
434
+
plcbundle did stats --global
435
435
+
plcbundle did stats
436
436
+
437
437
+
# JSON output
438
438
+
plcbundle did stats did:plc:524tuhdhh3m7li5gycdn6boe --json`,
439
439
+
440
440
+
Args: cobra.MaximumNArgs(1),
441
441
+
442
442
+
RunE: func(cmd *cobra.Command, args []string) error {
443
443
+
mgr, dir, err := getManagerFromCommand(cmd, "")
444
444
+
if err != nil {
445
445
+
return err
446
446
+
}
447
447
+
defer mgr.Close()
448
448
+
449
449
+
// Global stats
450
450
+
if len(args) == 0 || showGlobal {
451
451
+
return showGlobalDIDStats(mgr, dir, showJSON)
452
452
+
}
453
453
+
454
454
+
// Specific DID stats
455
455
+
did := args[0]
456
456
+
return showDIDStats(mgr, did, showJSON)
457
457
+
},
458
458
+
}
459
459
+
460
460
+
cmd.Flags().BoolVar(&showGlobal, "global", false, "Show global index stats")
461
461
+
cmd.Flags().BoolVar(&showJSON, "json", false, "Output as JSON")
462
462
+
463
463
+
return cmd
464
464
+
}
465
465
+
466
466
+
// ============================================================================
467
467
+
// Helper Functions
468
468
+
// ============================================================================
469
469
+
470
470
+
type batchOptions struct {
471
471
+
action string
472
472
+
workers int
473
473
+
outputFile string
474
474
+
fromStdin bool
475
475
+
}
476
476
+
477
477
+
func processBatchDIDs(mgr BundleManager, filename string, opts batchOptions) error {
478
478
+
// Determine input source
479
479
+
var input *os.File
480
480
+
var err error
481
481
+
482
482
+
if opts.fromStdin {
483
483
+
input = os.Stdin
484
484
+
fmt.Fprintf(os.Stderr, "Reading DIDs from stdin...\n")
485
485
+
} else {
486
486
+
input, err = os.Open(filename)
487
487
+
if err != nil {
488
488
+
return fmt.Errorf("failed to open file: %w", err)
489
489
+
}
490
490
+
defer input.Close()
491
491
+
fmt.Fprintf(os.Stderr, "Reading DIDs from: %s\n", filename)
492
492
+
}
493
493
+
494
494
+
// Read DIDs
495
495
+
var dids []string
496
496
+
scanner := bufio.NewScanner(input)
497
497
+
498
498
+
// Increase buffer size for large input
499
499
+
buf := make([]byte, 64*1024)
500
500
+
scanner.Buffer(buf, 1024*1024)
501
501
+
502
502
+
lineNum := 0
503
503
+
for scanner.Scan() {
504
504
+
lineNum++
505
505
+
line := strings.TrimSpace(scanner.Text())
506
506
+
507
507
+
// Skip empty lines and comments
508
508
+
if line == "" || strings.HasPrefix(line, "#") {
509
509
+
continue
510
510
+
}
511
511
+
512
512
+
// Basic validation
513
513
+
if !strings.HasPrefix(line, "did:plc:") {
514
514
+
fmt.Fprintf(os.Stderr, "⚠️ Line %d: skipping invalid DID: %s\n", lineNum, line)
515
515
+
continue
516
516
+
}
517
517
+
518
518
+
dids = append(dids, line)
519
519
+
}
520
520
+
521
521
+
if err := scanner.Err(); err != nil {
522
522
+
return fmt.Errorf("error reading input: %w", err)
523
523
+
}
524
524
+
525
525
+
if len(dids) == 0 {
526
526
+
return fmt.Errorf("no valid DIDs found in input")
527
527
+
}
528
528
+
529
529
+
fmt.Fprintf(os.Stderr, "Processing %d DIDs with action '%s' (%d workers)\n\n",
530
530
+
len(dids), opts.action, opts.workers)
531
531
+
532
532
+
// Setup output
533
533
+
var output *os.File
534
534
+
if opts.outputFile != "" {
535
535
+
output, err = os.Create(opts.outputFile)
536
536
+
if err != nil {
537
537
+
return fmt.Errorf("failed to create output file: %w", err)
538
538
+
}
539
539
+
defer output.Close()
540
540
+
fmt.Fprintf(os.Stderr, "Output: %s\n\n", opts.outputFile)
541
541
+
} else {
542
542
+
output = os.Stdout
543
543
+
}
544
544
+
545
545
+
// Process based on action
546
546
+
switch opts.action {
547
547
+
case "lookup":
548
548
+
return batchLookup(mgr, dids, output, opts.workers)
549
549
+
case "resolve":
550
550
+
return batchResolve(mgr, dids, output, opts.workers)
551
551
+
case "export":
552
552
+
return batchExport(mgr, dids, output, opts.workers)
553
553
+
default:
554
554
+
return fmt.Errorf("unknown action: %s (valid: lookup, resolve, export)", opts.action)
555
555
+
}
556
556
+
}
557
557
+
558
558
+
func showGlobalDIDStats(mgr BundleManager, dir string, showJSON bool) error {
559
559
+
stats := mgr.GetDIDIndexStats()
560
560
+
561
561
+
if !stats["exists"].(bool) {
562
562
+
fmt.Printf("DID index does not exist\n")
563
563
+
fmt.Printf("Run: plcbundle index build\n")
564
564
+
return nil
565
565
+
}
566
566
+
567
567
+
if showJSON {
568
568
+
data, _ := json.MarshalIndent(stats, "", " ")
569
569
+
fmt.Println(string(data))
570
570
+
return nil
571
571
+
}
572
572
+
573
573
+
indexedDIDs := stats["indexed_dids"].(int64)
574
574
+
mempoolDIDs := stats["mempool_dids"].(int64)
575
575
+
totalDIDs := stats["total_dids"].(int64)
576
576
+
577
577
+
fmt.Printf("\nDID Index Statistics\n")
578
578
+
fmt.Printf("════════════════════\n\n")
579
579
+
fmt.Printf(" Location: %s/.plcbundle/\n", dir)
580
580
+
581
581
+
if mempoolDIDs > 0 {
582
582
+
fmt.Printf(" Indexed DIDs: %s (in bundles)\n", formatNumber(int(indexedDIDs)))
583
583
+
fmt.Printf(" Mempool DIDs: %s (not yet bundled)\n", formatNumber(int(mempoolDIDs)))
584
584
+
fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs)))
585
585
+
} else {
586
586
+
fmt.Printf(" Total DIDs: %s\n", formatNumber(int(totalDIDs)))
587
587
+
}
588
588
+
589
589
+
fmt.Printf(" Shard count: %d\n", stats["shard_count"])
590
590
+
fmt.Printf(" Last bundle: %06d\n", stats["last_bundle"])
591
591
+
fmt.Printf(" Updated: %s\n\n", stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05"))
592
592
+
593
593
+
fmt.Printf(" Cached shards: %d / %d\n", stats["cached_shards"], stats["cache_limit"])
594
594
+
595
595
+
if cachedList, ok := stats["cache_order"].([]int); ok && len(cachedList) > 0 {
596
596
+
fmt.Printf(" Hot shards: ")
597
597
+
for i, shard := range cachedList {
598
598
+
if i > 0 {
599
599
+
fmt.Printf(", ")
600
600
+
}
601
601
+
if i >= 10 {
602
602
+
fmt.Printf("... (+%d more)", len(cachedList)-10)
603
603
+
break
604
604
+
}
605
605
+
fmt.Printf("%02x", shard)
606
606
+
}
607
607
+
fmt.Printf("\n")
608
608
+
}
609
609
+
610
610
+
fmt.Printf("\n")
611
611
+
return nil
612
612
+
}
613
613
+
614
614
+
func showDIDStats(mgr BundleManager, did string, showJSON bool) error {
615
615
+
ctx := context.Background()
616
616
+
617
617
+
// Get operations
618
618
+
opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false)
619
619
+
if err != nil {
620
620
+
return err
621
621
+
}
622
622
+
623
623
+
mempoolOps, err := mgr.GetDIDOperationsFromMempool(did)
624
624
+
if err != nil {
625
625
+
return err
626
626
+
}
627
627
+
628
628
+
if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
629
629
+
fmt.Fprintf(os.Stderr, "DID not found: %s\n", did)
630
630
+
return nil
631
631
+
}
632
632
+
633
633
+
// Calculate stats
634
634
+
totalOps := len(opsWithLoc) + len(mempoolOps)
635
635
+
nullifiedCount := 0
636
636
+
for _, owl := range opsWithLoc {
637
637
+
if owl.Operation.IsNullified() {
638
638
+
nullifiedCount++
639
639
+
}
640
640
+
}
641
641
+
642
642
+
bundleSpan := 0
643
643
+
if len(opsWithLoc) > 0 {
644
644
+
bundles := make(map[int]bool)
645
645
+
for _, owl := range opsWithLoc {
646
646
+
bundles[owl.Bundle] = true
647
647
+
}
648
648
+
bundleSpan = len(bundles)
649
649
+
}
650
650
+
651
651
+
if showJSON {
652
652
+
output := map[string]interface{}{
653
653
+
"did": did,
654
654
+
"total_operations": totalOps,
655
655
+
"bundled": len(opsWithLoc),
656
656
+
"mempool": len(mempoolOps),
657
657
+
"nullified": nullifiedCount,
658
658
+
"active": totalOps - nullifiedCount,
659
659
+
"bundle_span": bundleSpan,
660
660
+
}
661
661
+
data, _ := json.MarshalIndent(output, "", " ")
662
662
+
fmt.Println(string(data))
663
663
+
return nil
664
664
+
}
665
665
+
666
666
+
fmt.Printf("\nDID Statistics\n")
667
667
+
fmt.Printf("══════════════\n\n")
668
668
+
fmt.Printf(" DID: %s\n\n", did)
669
669
+
fmt.Printf(" Total operations: %d\n", totalOps)
670
670
+
fmt.Printf(" Active: %d\n", totalOps-nullifiedCount)
671
671
+
if nullifiedCount > 0 {
672
672
+
fmt.Printf(" Nullified: %d\n", nullifiedCount)
673
673
+
}
674
674
+
if len(opsWithLoc) > 0 {
675
675
+
fmt.Printf(" Bundled: %d\n", len(opsWithLoc))
676
676
+
fmt.Printf(" Bundle span: %d bundles\n", bundleSpan)
677
677
+
}
678
678
+
if len(mempoolOps) > 0 {
679
679
+
fmt.Printf(" Mempool: %d\n", len(mempoolOps))
680
680
+
}
681
681
+
fmt.Printf("\n")
682
682
+
683
683
+
return nil
684
684
+
}
685
685
+
686
686
+
func displayHistory(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, compact bool, includeNullified bool) error {
687
687
+
if compact {
688
688
+
return displayHistoryCompact(did, opsWithLoc, mempoolOps, includeNullified)
689
689
+
}
690
690
+
return displayHistoryDetailed(did, opsWithLoc, mempoolOps, includeNullified)
691
691
+
}
692
692
+
693
693
+
func displayHistoryCompact(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, includeNullified bool) error {
694
694
+
fmt.Printf("DID History: %s\n\n", did)
695
695
+
696
696
+
for _, owl := range opsWithLoc {
697
697
+
if !includeNullified && owl.Operation.IsNullified() {
698
698
+
continue
699
699
+
}
700
700
+
701
701
+
status := "✓"
702
702
+
if owl.Operation.IsNullified() {
703
703
+
status = "✗"
704
704
+
}
705
705
+
706
706
+
fmt.Printf("%s [%06d:%04d] %s %s\n",
707
707
+
status,
708
708
+
owl.Bundle,
709
709
+
owl.Position,
710
710
+
owl.Operation.CreatedAt.Format("2006-01-02 15:04:05"),
711
711
+
owl.Operation.CID)
712
712
+
}
713
713
+
714
714
+
for _, op := range mempoolOps {
715
715
+
fmt.Printf("✓ [mempool ] %s %s\n",
716
716
+
op.CreatedAt.Format("2006-01-02 15:04:05"),
717
717
+
op.CID)
718
718
+
}
719
719
+
720
720
+
return nil
721
721
+
}
722
722
+
723
723
+
func displayHistoryDetailed(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation, includeNullified bool) error {
724
724
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n")
725
725
+
fmt.Printf(" DID Audit Log\n")
726
726
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n\n")
727
727
+
fmt.Printf("DID: %s\n\n", did)
728
728
+
729
729
+
for i, owl := range opsWithLoc {
730
730
+
if !includeNullified && owl.Operation.IsNullified() {
731
731
+
continue
732
732
+
}
733
733
+
734
734
+
op := owl.Operation
735
735
+
status := "✓ Active"
736
736
+
if op.IsNullified() {
737
737
+
status = "✗ Nullified"
738
738
+
}
739
739
+
740
740
+
fmt.Printf("Operation %d [Bundle %06d, Position %04d]\n", i+1, owl.Bundle, owl.Position)
741
741
+
fmt.Printf(" CID: %s\n", op.CID)
742
742
+
fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST"))
743
743
+
fmt.Printf(" Status: %s\n", status)
744
744
+
745
745
+
if opData, err := op.GetOperationData(); err == nil && opData != nil {
746
746
+
showOperationDetails(&op)
747
747
+
}
748
748
+
749
749
+
fmt.Printf("\n")
750
750
+
}
751
751
+
752
752
+
if len(mempoolOps) > 0 {
753
753
+
fmt.Printf("Mempool Operations (%d)\n", len(mempoolOps))
754
754
+
fmt.Printf("══════════════════════════════════════════════════════════════\n\n")
755
755
+
756
756
+
for i, op := range mempoolOps {
757
757
+
fmt.Printf("Operation %d [Mempool]\n", i+1)
758
758
+
fmt.Printf(" CID: %s\n", op.CID)
759
759
+
fmt.Printf(" Created: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000 MST"))
760
760
+
fmt.Printf(" Status: ✓ Active\n")
761
761
+
fmt.Printf("\n")
762
762
+
}
763
763
+
}
764
764
+
765
765
+
return nil
766
766
+
}
767
767
+
768
768
+
func outputHistoryJSON(did string, opsWithLoc []PLCOperationWithLocation, mempoolOps []plcclient.PLCOperation) error {
769
769
+
output := map[string]interface{}{
770
770
+
"did": did,
771
771
+
"bundled": make([]map[string]interface{}, 0),
772
772
+
"mempool": make([]map[string]interface{}, 0),
773
773
+
}
774
774
+
775
775
+
for _, owl := range opsWithLoc {
776
776
+
output["bundled"] = append(output["bundled"].([]map[string]interface{}), map[string]interface{}{
777
777
+
"bundle": owl.Bundle,
778
778
+
"position": owl.Position,
779
779
+
"cid": owl.Operation.CID,
780
780
+
"nullified": owl.Operation.IsNullified(),
781
781
+
"created_at": owl.Operation.CreatedAt.Format(time.RFC3339Nano),
782
782
+
})
783
783
+
}
784
784
+
785
785
+
for _, op := range mempoolOps {
786
786
+
output["mempool"] = append(output["mempool"].([]map[string]interface{}), map[string]interface{}{
787
787
+
"cid": op.CID,
788
788
+
"nullified": op.IsNullified(),
789
789
+
"created_at": op.CreatedAt.Format(time.RFC3339Nano),
790
790
+
})
791
791
+
}
792
792
+
793
793
+
data, _ := json.MarshalIndent(output, "", " ")
794
794
+
fmt.Println(string(data))
795
795
+
796
796
+
return nil
797
797
+
}
798
798
+
799
799
+
func batchLookup(mgr BundleManager, dids []string, output *os.File, workers int) error {
800
800
+
progress := ui.NewProgressBar(len(dids))
801
801
+
ctx := context.Background()
802
802
+
803
803
+
// CSV header
804
804
+
fmt.Fprintf(output, "did,status,operation_count,bundled,mempool,nullified\n")
805
805
+
806
806
+
found := 0
807
807
+
notFound := 0
808
808
+
errorCount := 0
809
809
+
810
810
+
for i, did := range dids {
811
811
+
opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false)
812
812
+
if err != nil {
813
813
+
errorCount++
814
814
+
fmt.Fprintf(output, "%s,error,0,0,0,0\n", did)
815
815
+
progress.Set(i + 1)
816
816
+
continue
817
817
+
}
818
818
+
819
819
+
mempoolOps, _ := mgr.GetDIDOperationsFromMempool(did)
820
820
+
821
821
+
if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
822
822
+
notFound++
823
823
+
fmt.Fprintf(output, "%s,not_found,0,0,0,0\n", did)
824
824
+
} else {
825
825
+
found++
826
826
+
827
827
+
// Count nullified
828
828
+
nullified := 0
829
829
+
for _, owl := range opsWithLoc {
830
830
+
if owl.Operation.IsNullified() {
831
831
+
nullified++
832
832
+
}
833
833
+
}
834
834
+
835
835
+
fmt.Fprintf(output, "%s,found,%d,%d,%d,%d\n",
836
836
+
did,
837
837
+
len(opsWithLoc)+len(mempoolOps),
838
838
+
len(opsWithLoc),
839
839
+
len(mempoolOps),
840
840
+
nullified)
841
841
+
}
842
842
+
843
843
+
progress.Set(i + 1)
844
844
+
}
845
845
+
846
846
+
progress.Finish()
847
847
+
848
848
+
fmt.Fprintf(os.Stderr, "\n✓ Batch lookup complete\n")
849
849
+
fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids))
850
850
+
fmt.Fprintf(os.Stderr, " Found: %d\n", found)
851
851
+
fmt.Fprintf(os.Stderr, " Not found: %d\n", notFound)
852
852
+
if errorCount > 0 {
853
853
+
fmt.Fprintf(os.Stderr, " Errors: %d\n", errorCount)
854
854
+
}
855
855
+
856
856
+
return nil
857
857
+
}
858
858
+
859
859
+
func batchResolve(mgr BundleManager, dids []string, output *os.File, workers int) error {
860
860
+
progress := ui.NewProgressBar(len(dids))
861
861
+
ctx := context.Background()
862
862
+
863
863
+
resolved := 0
864
864
+
failed := 0
865
865
+
866
866
+
// Use buffered writer
867
867
+
writer := bufio.NewWriterSize(output, 512*1024)
868
868
+
defer writer.Flush()
869
869
+
870
870
+
for i, did := range dids {
871
871
+
result, err := mgr.ResolveDID(ctx, did)
872
872
+
if err != nil {
873
873
+
failed++
874
874
+
if i < 10 {
875
875
+
fmt.Fprintf(os.Stderr, "Failed to resolve %s: %v\n", did, err)
876
876
+
}
877
877
+
} else {
878
878
+
resolved++
879
879
+
data, _ := json.Marshal(result.Document)
880
880
+
writer.Write(data)
881
881
+
writer.WriteByte('\n')
882
882
+
883
883
+
if i%100 == 0 {
884
884
+
writer.Flush()
885
885
+
}
886
886
+
}
887
887
+
888
888
+
progress.Set(i + 1)
889
889
+
}
890
890
+
891
891
+
writer.Flush()
892
892
+
progress.Finish()
893
893
+
894
894
+
fmt.Fprintf(os.Stderr, "\n✓ Batch resolve complete\n")
895
895
+
fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids))
896
896
+
fmt.Fprintf(os.Stderr, " Resolved: %d\n", resolved)
897
897
+
if failed > 0 {
898
898
+
fmt.Fprintf(os.Stderr, " Failed: %d\n", failed)
899
899
+
}
900
900
+
901
901
+
return nil
902
902
+
}
903
903
+
904
904
+
func batchExport(mgr BundleManager, dids []string, output *os.File, workers int) error {
905
905
+
progress := ui.NewProgressBar(len(dids))
906
906
+
ctx := context.Background()
907
907
+
908
908
+
totalOps := 0
909
909
+
processedDIDs := 0
910
910
+
errorCount := 0
911
911
+
912
912
+
// Use buffered writer for better performance
913
913
+
writer := bufio.NewWriterSize(output, 512*1024)
914
914
+
defer writer.Flush()
915
915
+
916
916
+
for i, did := range dids {
917
917
+
opsWithLoc, err := mgr.GetDIDOperationsWithLocations(ctx, did, false)
918
918
+
if err != nil {
919
919
+
errorCount++
920
920
+
if i < 10 { // Only log first few errors
921
921
+
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", did, err)
922
922
+
}
923
923
+
progress.Set(i + 1)
924
924
+
continue
925
925
+
}
926
926
+
927
927
+
// Get mempool operations too
928
928
+
mempoolOps, _ := mgr.GetDIDOperationsFromMempool(did)
929
929
+
930
930
+
if len(opsWithLoc) == 0 && len(mempoolOps) == 0 {
931
931
+
progress.Set(i + 1)
932
932
+
continue
933
933
+
}
934
934
+
935
935
+
processedDIDs++
936
936
+
937
937
+
// Export bundled operations
938
938
+
for _, owl := range opsWithLoc {
939
939
+
if len(owl.Operation.RawJSON) > 0 {
940
940
+
writer.Write(owl.Operation.RawJSON)
941
941
+
} else {
942
942
+
data, _ := json.Marshal(owl.Operation)
943
943
+
writer.Write(data)
944
944
+
}
945
945
+
writer.WriteByte('\n')
946
946
+
totalOps++
947
947
+
}
948
948
+
949
949
+
// Export mempool operations
950
950
+
for _, op := range mempoolOps {
951
951
+
if len(op.RawJSON) > 0 {
952
952
+
writer.Write(op.RawJSON)
953
953
+
} else {
954
954
+
data, _ := json.Marshal(op)
955
955
+
writer.Write(data)
956
956
+
}
957
957
+
writer.WriteByte('\n')
958
958
+
totalOps++
959
959
+
}
960
960
+
961
961
+
// Flush periodically
962
962
+
if i%100 == 0 {
963
963
+
writer.Flush()
964
964
+
}
965
965
+
966
966
+
progress.Set(i + 1)
967
967
+
}
968
968
+
969
969
+
writer.Flush()
970
970
+
progress.Finish()
971
971
+
972
972
+
fmt.Fprintf(os.Stderr, "\n✓ Batch export complete\n")
973
973
+
fmt.Fprintf(os.Stderr, " DIDs input: %d\n", len(dids))
974
974
+
fmt.Fprintf(os.Stderr, " DIDs processed: %d\n", processedDIDs)
975
975
+
fmt.Fprintf(os.Stderr, " Operations: %s\n", formatNumber(totalOps))
976
976
+
if errorCount > 0 {
977
977
+
fmt.Fprintf(os.Stderr, " Errors: %d\n", errorCount)
978
978
+
}
979
979
+
980
980
+
return nil
981
981
+
}
+3
-2
cmd/plcbundle/commands/stream.go
···
21
21
)
22
22
23
23
cmd := &cobra.Command{
24
24
-
Use: "stream [flags]",
25
25
-
Short: "Stream operations to stdout (JSONL)",
24
24
+
Use: "stream [flags]",
25
25
+
Aliases: []string{"backfill"},
26
26
+
Short: "Stream operations to stdout (JSONL)",
26
27
Long: `Stream operations to stdout in JSONL format
27
28
28
29
Outputs PLC operations as newline-delimited JSON to stdout.
+2
-2
cmd/plcbundle/main.go
···
60
60
cmd.AddCommand(commands.NewVerifyCommand())
61
61
cmd.AddCommand(commands.NewDiffCommand())
62
62
/*cmd.AddCommand(commands.NewStatsCommand())
63
63
-
cmd.AddCommand(commands.NewInspectCommand())
63
63
+
cmd.AddCommand(commands.NewInspectCommand())*/
64
64
65
65
// Namespaced commands
66
66
cmd.AddCommand(commands.NewDIDCommand())
67
67
-
cmd.AddCommand(commands.NewIndexCommand())
67
67
+
/*cmd.AddCommand(commands.NewIndexCommand())
68
68
cmd.AddCommand(commands.NewMempoolCommand())
69
69
cmd.AddCommand(commands.NewDetectorCommand())
70
70
+22
-6
cmd/plcbundle/ui/progress.go
···
102
102
103
103
remaining := pb.total - pb.current
104
104
var eta time.Duration
105
105
-
if speed > 0 {
105
105
+
if speed > 0 && remaining > 0 {
106
106
eta = time.Duration(float64(remaining)/speed) * time.Second
107
107
}
108
108
109
109
+
// ✨ FIX: Check if complete
110
110
+
isComplete := pb.current >= pb.total
111
111
+
109
112
if pb.showBytes && pb.currentBytes > 0 {
110
113
mbProcessed := float64(pb.currentBytes) / (1000 * 1000)
111
114
mbPerSec := mbProcessed / elapsed.Seconds()
112
115
113
113
-
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | ETA: %s ",
114
114
-
bar, percent, pb.current, pb.total, speed, mbPerSec, formatETA(eta))
116
116
+
if isComplete {
117
117
+
// ✨ Don't show ETA when done
118
118
+
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | Done ",
119
119
+
bar, percent, pb.current, pb.total, speed, mbPerSec)
120
120
+
} else {
121
121
+
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | %.1f MB/s | ETA: %s ",
122
122
+
bar, percent, pb.current, pb.total, speed, mbPerSec, formatETA(eta))
123
123
+
}
115
124
} else {
116
116
-
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | ETA: %s ",
117
117
-
bar, percent, pb.current, pb.total, speed, formatETA(eta))
125
125
+
if isComplete {
126
126
+
// ✨ Don't show ETA when done
127
127
+
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | Done ",
128
128
+
bar, percent, pb.current, pb.total, speed)
129
129
+
} else {
130
130
+
fmt.Fprintf(os.Stderr, "\r [%s] %6.2f%% | %d/%d | %.1f/s | ETA: %s ",
131
131
+
bar, percent, pb.current, pb.total, speed, formatETA(eta))
132
132
+
}
118
133
}
119
134
}
120
135
121
136
func formatETA(d time.Duration) string {
137
137
+
// ✨ This should never be called with 0 now, but keep as fallback
122
138
if d == 0 {
123
123
-
return "calculating..."
139
139
+
return "0s"
124
140
}
125
141
if d < time.Minute {
126
142
return fmt.Sprintf("%ds", int(d.Seconds()))