package ckprompt import ( "context" "errors" "fmt" "io" "strconv" commandkit "gitea.maximumdirect.net/eric/CommandKit" "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" configkit "gitea.maximumdirect.net/eric/ConfigKit" promptkit "gitea.maximumdirect.net/eric/promptkit" ) // PromptRequest selects one named prompt for inspection. type PromptRequest struct { // Selection carries the configuration and overlay values supplied to the command. Selection ckconfig.Selection // ID is the selected named prompt. ID string // Version carries the explicitly supplied prompt version, if present. Version commandkit.Supplied[string] } // ProfileRequest selects one named profile for inspection. type ProfileRequest struct { // Selection carries the configuration and overlay values supplied to the command. Selection ckconfig.Selection // ID is the selected named profile. ID string } // RequirementsRequest selects a prompt context for native requirements // inspection. type RequirementsRequest struct { // Selection carries the configuration and overlay values supplied to the command. Selection ckconfig.Selection // Inspection forwards PromptKit's native prompt, version, profile, variable, // and session discovery context. Optional version, profile, and session // strings retain PromptKit's empty-value semantics. Inspection promptkit.RequirementInspectionRequest } // Suite describes application-owned input, shared state, and optional catalog, // named inspection, and requirements inspection capabilities. Its private // issues retain adapter construction failures. // Factory results and callback effects remain caller-owned: Command does not // deep-copy arbitrary objects or repair aliases returned by factories. // A reached operation prepares one input, applies its captured selectors, and // checks the input before creating state and calling the selected provider. // Factories are not invoked during construction, installation, help, or version, // and need not run after failure. type Suite[I, S any] struct { // NewInput is called once when definition, requirements, or catalog input // preparation is reached, before applying selectors and calling CheckInput. // If nil, input is the zero value of I, including nil for pointer types. NewInput func() I // NewState is called once when an inspection or catalog callback is reached, // after input checks pass. If nil, state is the zero value of S, including // nil for pointer types. NewState func() S // Selectors declares the original typed flags accepted by definition, // requirements, and catalog commands. Selectors []commandkit.Flag[I] // CheckInput validates selected definition, requirements, or catalog input // before state creation. CheckInput func(ckconfig.Selection, I) error // InspectPrompt inspects the selected prompt after input and state preparation. InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error) // InspectProfile inspects the selected profile after input and state preparation. InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error) // InspectRequirements inspects native PromptKit requirements after input and // state preparation. It is optional; a nil callback omits the requirements // command without disabling other capabilities. InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error) // ListPrompts lists prompt identities after input and state preparation. // It is optional; a nil callback does not install a prompt catalog command. ListPrompts func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) // ListProfiles lists profile IDs after input and state preparation. // It is optional; a nil callback does not install a profile catalog command. ListProfiles func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]string], error) issues []error } type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error) // ConfigScope adapts PromptKit's captured configuration inspection to the // standard configuration commands, including native comparison when the scope // declares Diff. The loader is not called during declaration construction or // installation; it is called only when a config operation selects the // promptkit scope, with that operation's input and shared state. PromptKit // diagnostics and supported error identity are returned through ckconfig's // acquisition and comparison contracts. func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) ckconfig.Scope[I, S] { scope := ckconfig.Scope[I, S]{ Name: "promptkit", Summary: "PromptKit engine configuration.", Operations: []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain, ckconfig.Diff}, } if load == nil { return ckconfig.WithScopeError(scope, errors.New("PromptKit configuration loader is nil")) } scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) { _, inspection, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err } return commandkit.Acquired[ckconfig.View]{ Value: ckconfig.View{ Effective: inspection, Sources: inspection, Explanation: inspection, }, Diagnostics: diagnostics, }, nil } scope.Compare = func(ctx context.Context, request ckconfig.ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) { if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("PromptKit scope %q comparison before left load: %w", scope.Name, err) } _, leftInspection, leftDiagnostics, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load) diagnostics := append([]commandkit.Diagnostic(nil), leftDiagnostics...) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err) } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err) } _, rightInspection, rightDiagnostics, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load) diagnostics = append(diagnostics, rightDiagnostics...) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err) } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err) } differences, err := leftInspection.Diff(rightInspection) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err) } return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: diagnostics}, nil } return scope } func promptKitContextError(ctx context.Context) error { if ctx == nil { return nil } return ctx.Err() } // FromEngine supplies prompt and profile catalog callbacks, named inspection // callbacks, and a requirements inspection callback backed by a PromptKit engine // loader. The requirements callback forwards RequirementsRequest.Inspection to // Engine.InspectRequirements. FromEngine does not call the loader during // construction. Each selected operation passes its context, supplied // selection, initialized input, and shared invocation state to the loader before // calling PromptKit's public listing or inspection method. Applications can // narrow, wrap, or replace these callbacks before Command captures them. A nil // loader is retained as an adapter issue. func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] { if load == nil { return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}} } return Suite[I, S]{ ListPrompts: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: diagnostics}, err } prompts, err := engine.ListPrompts(ctx) return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: diagnostics}, err }, ListProfiles: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err } profiles, err := engine.ListProfiles(ctx) return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: diagnostics}, err }, InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err } inspection, err := engine.InspectPrompt(ctx, request.ID, request.Version.Value) return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err }, InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err } inspection, err := engine.InspectProfile(ctx, request.ID) return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err }, InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: diagnostics}, err } inspection, err := engine.InspectRequirements(ctx, request.Inspection) return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: diagnostics}, err }, } } func loadPromptKitEngine[I, S any]( ctx context.Context, selection ckconfig.Selection, input I, state S, side string, load engineLoader[I, S], ) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) { engine, err := load(ctx, selection, input, state) if err != nil { var loadErr *configkit.LoadError if errors.As(err, &loadErr) { return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()), err } return nil, promptkit.ConfigurationInspection{}, nil, err } if engine == nil { return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine") } inspection, err := engine.InspectConfiguration() if err != nil { return nil, promptkit.ConfigurationInspection{}, nil, err } diagnostics := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics()) if ctx != nil { if err := ctx.Err(); err != nil { return nil, promptkit.ConfigurationInspection{}, diagnostics, err } } return engine, inspection, diagnostics, nil } func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { if len(records) == 0 { return nil } severity := commandkit.SeverityWarning for _, record := range records { if record.Severity == configkit.SeverityError { severity = commandkit.SeverityError break } } // Snapshot provider-owned slices before deferring their report to invocation. sources = append([]configkit.SourceInfo(nil), sources...) records = append([]configkit.Diagnostic(nil), records...) for index := range records { if records[index].Primary != nil { primary := *records[index].Primary records[index].Primary = &primary } records[index].Related = append([]configkit.Location(nil), records[index].Related...) } return []commandkit.Diagnostic{{ Severity: severity, Write: func(writer io.Writer) error { heading := "Configuration scope " + strconv.Quote(scope) if side != "" { heading += " (" + side + ")" } if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil { return err } if len(sources) > 0 { if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil { return err } for _, source := range sources { if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil { return err } } } return configkit.WriteDiagnostics(writer, records) }, }} }