@@ -26,19 +26,20 @@ import (
2626)
2727
2828type app struct {
29- Doctor doctorCmd `cmd:"" help:"Check local configuration."`
30- Metadata metadataCmd `cmd:"" help:"Print machine-readable metadata."`
31- Describe describeCmd `cmd:"" help:"Print machine-readable command schemas."`
32- Status statusCmd `cmd:"" help:"Print local archive status."`
33- Sync syncCmd `cmd:"" help:"Sync conversations from fixtures or provider APIs."`
34- Search searchCmd `cmd:"" help:"Search the local archive."`
35- Show showCmd `cmd:"" help:"Show one local conversation."`
36- Archive archiveCmd `cmd:"" help:"Write compressed age-encrypted archive output."`
37- Publish publishCmd `cmd:"" help:"Publish local SQLite state as an encrypted snapshot."`
38- Import importCmd `cmd:"" help:"Import an encrypted snapshot into local SQLite."`
39- Store storeCmd `cmd:"" help:"Inspect generic tenant-store contracts."`
40- Guard guardCmd `cmd:"" help:"Check commit guardrails."`
41- Version versionCmd `cmd:"" help:"Print version information."`
29+ Doctor doctorCmd `cmd:"" help:"Check local configuration."`
30+ Metadata metadataCmd `cmd:"" help:"Print machine-readable metadata."`
31+ Describe describeCmd `cmd:"" help:"Print machine-readable command schemas."`
32+ Status statusCmd `cmd:"" help:"Print local archive status."`
33+ Sync syncCmd `cmd:"" help:"Sync conversations from fixtures or provider APIs."`
34+ Search searchCmd `cmd:"" help:"Search the local archive."`
35+ Show showCmd `cmd:"" help:"Show one local conversation."`
36+ Archive archiveCmd `cmd:"" help:"Write compressed age-encrypted archive output."`
37+ Publish publishCmd `cmd:"" help:"Publish local SQLite state as an encrypted snapshot."`
38+ Import importCmd `cmd:"" help:"Import an encrypted snapshot into local SQLite."`
39+ Subscribe subscribeCmd `cmd:"" help:"Import snapshots from a local encrypted tenant store."`
40+ Store storeCmd `cmd:"" help:"Inspect generic tenant-store contracts."`
41+ Guard guardCmd `cmd:"" help:"Check commit guardrails."`
42+ Version versionCmd `cmd:"" help:"Print version information."`
4243}
4344
4445type commandContext struct {
@@ -633,6 +634,117 @@ func (cmd importCmd) Run(ctx commandContext) error {
633634 return writeMaybeJSON (ctx .stdout , cmd .JSON , importResult {Input : cmd .In , Records : len (records ), Sync : result })
634635}
635636
637+ type subscribeCmd struct {
638+ Store string `arg:"" help:"Local tenant store root containing manifest.json."`
639+ Identity string `help:"Age identity. Defaults to FINCRAWL_AGE_IDENTITY."`
640+ DryRun bool `name:"dry-run" help:"Verify and list planned imports without writing local state."`
641+ JSON bool `help:"Print JSON output." default:"true"`
642+ }
643+
644+ type subscribeResult struct {
645+ Store string `json:"store"`
646+ DryRun bool `json:"dry_run,omitempty"`
647+ Snapshots []subscribeSnapshot `json:"snapshots"`
648+ ImportedSnapshots int `json:"imported_snapshots"`
649+ Records int `json:"records"`
650+ Sync * store.SyncResult `json:"sync,omitempty"`
651+ }
652+
653+ type subscribeSnapshot struct {
654+ Path string `json:"path"`
655+ Records int `json:"records,omitempty"`
656+ Imported bool `json:"imported,omitempty"`
657+ }
658+
659+ type pendingSnapshotImport struct {
660+ snapshot tenantstore.SnapshotFile
661+ records []archive.Record
662+ fixture store.Fixture
663+ }
664+
665+ func (cmd subscribeCmd ) Run (ctx commandContext ) error {
666+ rt , err := config .LoadRuntime ()
667+ if err != nil {
668+ return err
669+ }
670+ report , snapshots , err := tenantstore .VerifiedSnapshots (ctx , cmd .Store )
671+ if err != nil {
672+ return err
673+ }
674+ if ! report .OK {
675+ if writeErr := writeMaybeJSON (ctx .stdout , cmd .JSON , report ); writeErr != nil {
676+ return writeErr
677+ }
678+ return fmt .Errorf ("tenant store verification failed with %d finding(s)" , len (report .Findings ))
679+ }
680+ result := subscribeResult {Store : report .Root , DryRun : cmd .DryRun , Snapshots : make ([]subscribeSnapshot , 0 , len (snapshots ))}
681+ for _ , snapshot := range snapshots {
682+ if ! strings .HasSuffix (snapshot .Path , ".jsonl.zst.age" ) {
683+ return fmt .Errorf ("subscribe currently imports only .jsonl.zst.age snapshots: %s" , snapshot .Path )
684+ }
685+ result .Snapshots = append (result .Snapshots , subscribeSnapshot {Path : snapshot .Path , Records : snapshot .Records })
686+ result .Records += snapshot .Records
687+ }
688+ if cmd .DryRun {
689+ return writeMaybeJSON (ctx .stdout , cmd .JSON , result )
690+ }
691+ identity := strings .TrimSpace (cmd .Identity )
692+ if identity == "" {
693+ identity = config .AgeIdentity ()
694+ }
695+ pending := make ([]pendingSnapshotImport , 0 , len (snapshots ))
696+ totalRecords := 0
697+ for _ , snapshot := range snapshots {
698+ records , err := archive .ReadEncryptedJSONL (snapshot .FullPath , identity )
699+ if err != nil {
700+ return fmt .Errorf ("read tenant store snapshot %s: %w" , snapshot .Path , err )
701+ }
702+ fixture , err := archive .RecordsFixture (records )
703+ if err != nil {
704+ return fmt .Errorf ("decode tenant store snapshot %s: %w" , snapshot .Path , err )
705+ }
706+ pending = append (pending , pendingSnapshotImport {snapshot : snapshot , records : records , fixture : fixture })
707+ totalRecords += len (records )
708+ }
709+ if err := config .EnsureDirs (rt ); err != nil {
710+ return err
711+ }
712+ lck , err := lock .Acquire (rt .Config .DBPath )
713+ if err != nil {
714+ return err
715+ }
716+ defer lck .Release ()
717+ aggregate := store.SyncResult {}
718+ for index , item := range pending {
719+ syncResult , err := store .SyncFixture (ctx , rt .Config .DBPath , item .fixture )
720+ if err != nil {
721+ return fmt .Errorf ("import tenant store snapshot %s: %w" , item .snapshot .Path , err )
722+ }
723+ mergeSyncResult (& aggregate , syncResult )
724+ result .Snapshots [index ].Records = len (item .records )
725+ result .Snapshots [index ].Imported = true
726+ result .Snapshots [index ].Path = item .snapshot .Path
727+ result .ImportedSnapshots ++
728+ }
729+ result .Records = totalRecords
730+ result .Sync = & aggregate
731+ return writeMaybeJSON (ctx .stdout , cmd .JSON , result )
732+ }
733+
734+ func mergeSyncResult (dst * store.SyncResult , src store.SyncResult ) {
735+ if dst .WorkspaceID == "" {
736+ dst .WorkspaceID = src .WorkspaceID
737+ }
738+ dst .Conversations += src .Conversations
739+ dst .ConversationParts += src .ConversationParts
740+ dst .Admins += src .Admins
741+ dst .Teams += src .Teams
742+ dst .Tags += src .Tags
743+ dst .Contacts += src .Contacts
744+ dst .RawBlobs += src .RawBlobs
745+ dst .Warnings = append (dst .Warnings , src .Warnings ... )
746+ }
747+
636748type storeCmd struct {
637749 Verify storeVerifyCmd `cmd:"" help:"Verify an encrypted tenant-store manifest."`
638750}
0 commit comments