package app import ( "context" "flag" "fmt" "io" "sort" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" ) // Status reports effective local/remote session state. func Status(ctx context.Context, args []string, out io.Writer) error { fs := flag.NewFlagSet("status", flag.ContinueOnError) fs.SetOutput(io.Discard) var flags commonConfigFlags addCommonConfigFlags(fs, &flags) if err := parseSessionAwareFlags("status", fs, args, &flags.sessionID); err != nil { return err } if strings.TrimSpace(flags.sessionID) == "" { return fmt.Errorf("status: session_id is required") } cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions()) if err != nil { return fmt.Errorf("status: %w", err) } if err := config.Validate(cfg); err != nil { return fmt.Errorf("status: %w", err) } paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID) fmt.Fprintf(out, "Session: %s\n", cfg.Session.SessionID) fmt.Fprintf(out, "Campaign: %s\n", cfg.Session.Campaign) fmt.Fprintf(out, "Workspace: %s\n", paths.Root) fmt.Fprintf(out, "Session config: %s\n", sessionSourceSummary(cfg)) writeStatusStableInputs(out, inspectStableInputs(cfg)) writeStatusLocalAudio(out, inspectLocalAudioPresence(cfg)) if m, err := loadLocalManifest(ctx, paths.ManifestPath); err != nil { fmt.Fprintf(out, "Local manifest: error: %v\n", err) } else if m == nil { fmt.Fprintln(out, "Local manifest: missing") } else { fmt.Fprintf(out, "Local manifest: %s\n", paths.ManifestPath) writeStageStatuses(out, m) } store, storeErr := objectStoreIfConfigured(ctx, cfg) if storeErr != nil { fmt.Fprintf(out, "Remote publish: unavailable: %v\n", storeErr) } else if store != nil { current := inspectRemoteCurrentState(ctx, cfg, store) if current.Err != nil { fmt.Fprintf(out, "Remote publish: missing or unavailable: %v\n", current.Err) } else { fmt.Fprintf(out, "Remote publish: current run %s\n", current.State.RunID) fmt.Fprintf(out, "Remote manifest: %s\n", current.State.CurrentManifestKey) } } writeStatusRemoteAudio(ctx, out, cfg, store, storeErr) writeStatusPreviousArtifacts(out, inspectPreviousArtifactReadiness( ctx, cfg, store, artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)), )) lockChecks := inspectEffectiveLocks(ctx, cfg, store) locks := lockChecks.Locks lockErr := lockChecks.Err if catalog, catalogErr := buildHelperArtifactCatalog(cfg); catalogErr != nil { fmt.Fprintf(out, "Remote outputs: error: %v\n", catalogErr) } else if storeErr == nil { catalogLocks := locks if lockErr != nil { catalogLocks = &effectiveLocks{ Static: staticPublishLocks(cfg), All: staticPublishLocks(cfg), } } publishedRemoteState := map[string]string{} if store != nil { publishedRemoteState = remotePublishedOutputAvailability(ctx, cfg, store, catalog) } fmt.Fprintln(out, "Remote outputs:") writeArtifactList(out, cfg, catalog, catalogLocks, publishedRemoteState) } if lockErr != nil { fmt.Fprintf(out, "Publish locks: error: %v\n", lockErr) } else { writeLocks(out, cfg, locks) } fmt.Fprintln(out, "Next actions:") fmt.Fprintf(out, "- narratio session validate %s\n", cfg.Session.SessionID) fmt.Fprintf(out, "- narratio session restore %s --dry-run\n", cfg.Session.SessionID) return nil } func writeStatusStableInputs(out io.Writer, checks []stableInputCheck) { if len(checks) == 0 { return } for _, check := range checks { if check.Err != nil { if strings.TrimSpace(check.Path) != "" { fmt.Fprintf(out, "Stable input %s: unavailable: %v\n", check.Name, check.Err) } else { fmt.Fprintf(out, "Stable input %s: unavailable: %s\n", check.Name, check.Err.Error()) } continue } fmt.Fprintf(out, "Stable input %s: %s\n", check.Name, check.Path) } } func writeStatusLocalAudio(out io.Writer, check localAudioCheck) { if !check.Checked { return } if check.Err != nil { fmt.Fprintf(out, "Local audio: unavailable: %v\n", check.Err) return } fmt.Fprintf(out, "Local audio: %d file(s)\n", len(check.Paths)) } func writeStatusRemoteAudio(ctx context.Context, out io.Writer, cfg *config.Config, store storage.ObjectStore, storeErr error) { if cfg.Session.Inputs.AudioS3 == nil { return } if storeErr != nil { fmt.Fprintf(out, "Remote audio: unavailable: %v\n", storeErr) return } check := inspectRemoteAudioPresence(ctx, cfg, store) if check.Err != nil { fmt.Fprintf(out, "Remote audio: unavailable: %v\n", check.Err) return } fmt.Fprintf(out, "Remote audio: %d .flac object(s)\n", len(check.Keys)) } func writeStatusPreviousArtifacts(out io.Writer, readiness previousArtifactReadiness) { if len(readiness.Requirements) == 0 { fmt.Fprintln(out, "Previous-session artifacts: not required") return } if readiness.MissingID { fmt.Fprintln(out, "Previous-session artifacts: unavailable: previous_session_id is required by configured previous-session artifacts") return } if readiness.Err != nil { fmt.Fprintf(out, "Previous-session artifacts: unavailable: %v\n", readiness.Err) return } names := make([]string, 0, len(readiness.Requirements)) for _, req := range readiness.Requirements { names = append(names, fmt.Sprintf("%s(required=%t)", req.Name, req.Required)) } sort.Strings(names) fmt.Fprintf(out, "Previous-session artifacts: ready: %s\n", strings.Join(names, ", ")) }