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 } // 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 } // CatalogRequest selects the configuration and native namespace filter for a // prompt, profile, or backend catalog operation. A zero Filter preserves // PromptKit's native default selection. type CatalogRequest struct { // Selection carries the configuration and overlay supplied to the command. Selection ckconfig.Selection // Filter is forwarded unchanged to each filterable native catalog. Its zero // value preserves PromptKit's default namespace selection. Filter promptkit.NamespaceFilter } // NamespaceRequest selects the configuration and native resource kinds for a // namespace catalog operation. An empty Kinds slice preserves PromptKit's // native default selection. type NamespaceRequest struct { // Selection carries the configuration and overlay supplied to the command. Selection ckconfig.Selection // Kinds is forwarded unchanged to the native namespace catalog. An empty // slice preserves PromptKit's default kind selection. Kinds []promptkit.ResourceKind } // 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, requirements discovery, concrete request validation, and // smoke testing 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 inspection, requirements, validation, smoke, // 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, validation, smoke, 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 inspection, // requirements, validation, smoke, and catalog commands. Selectors []commandkit.Flag[I] // CheckInput validates selected inspection, requirements, validation, smoke, // 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 // `promptkit requirements show` without disabling validation or other // capabilities. InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error) // ValidateRequest validates a concrete PromptKit request after input and // state preparation. It is optional; a nil callback omits // `promptkit requirements validate` without disabling discovery or other // capabilities. The acquisition carries diagnostics and an empty value on // success; PromptKit errors are returned unchanged where supported. ValidateRequest func(context.Context, ValidationRequest, I, S) (commandkit.Acquired[struct{}], error) // SmokeTest runs one concrete native smoke-test operation after input and // state preparation. It is optional; nil omits only the smoke-test command. // The acquisition retains a native report even when the callback also // returns an error. For `test all`, CommandKit calls the captured callback // sequentially for each selected kind after preparing input and state once. SmokeTest func(context.Context, SmokeRequest, I, S) (commandkit.Acquired[*promptkit.SmokeTestReport], error) // ListPrompts lists prompt identities after input and state preparation. // It is optional independently of the other catalogs; nil omits only this // catalog command. ListPrompts func(context.Context, CatalogRequest, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) // ListProfiles lists profile IDs after input and state preparation. It is // optional independently of the other catalogs; nil omits only this catalog // command. ListProfiles func(context.Context, CatalogRequest, I, S) (commandkit.Acquired[[]string], error) // ListBackends lists backend IDs after input and state preparation. It is // optional independently of the other catalogs; nil omits only this catalog // command. ListBackends func(context.Context, CatalogRequest, I, S) (commandkit.Acquired[[]string], error) // ListNamespaces lists native namespace names after input and state // preparation. It is optional independently of the other catalogs; nil omits // only this catalog command. ListNamespaces func(context.Context, NamespaceRequest, I, S) (commandkit.Acquired[[]string], error) // ListCollections lists collection information after input and state // preparation. It is optional independently of the other catalogs; nil // omits only this catalog command. ListCollections func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) issues []error } // WithRequestValidation attaches PromptKit-backed concrete request validation // to suite while preserving its other capabilities and retained issues. Those // issues remain attached if capabilities are narrowed before Command captures // the suite. The // builder, when present, runs before the engine loader and receives the seed, // input, state, and invocation context. Its returned request is passed to // PromptKit unchanged; the loader receives the seed's original configuration // selection. A nil builder forwards the seed request unchanged. A builder // failure prevents engine loading and preserves its supported error identity. // Native validation errors are returned with supported identity, and // configuration diagnostics from engine loading remain in the acquisition. A // nil loader is retained as a suite wiring issue and does not install a // validation callback. Callback references and objects remain caller-owned. func WithRequestValidation[I, S any]( suite Suite[I, S], load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error), build func(context.Context, ValidationRequest, I, S) (promptkit.RunRequest, error), ) Suite[I, S] { if load == nil { issues := append([]error(nil), suite.issues...) suite.issues = append(issues, errors.New("PromptKit engine loader is nil")) return suite } suite.ValidateRequest = func(ctx context.Context, request ValidationRequest, input I, state S) (commandkit.Acquired[struct{}], error) { if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation before building: %w", err) } concreteRequest := request.Request if build != nil { built, err := build(ctx, request, input, state) if err != nil { return commandkit.Acquired[struct{}]{}, err } concreteRequest = built } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation after building: %w", err) } engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation before native validation: %w", err) } if err := engine.ValidateRequest(ctx, concreteRequest); err != nil { return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation after native validation: %w", err) } return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, nil } return suite } 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 eight PromptKit-backed callbacks: prompt, profile, // backend, namespace, and collection catalogs, plus named prompt and profile // inspection and requirements discovery. // The requirements callback forwards RequirementsRequest.Inspection to // Engine.InspectRequirements. Concrete request validation is an independent // capability and can be composed explicitly with WithRequestValidation. Smoke // testing is also an explicit capability installed with WithSmokeTesting. // 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. Each callback returns loader diagnostics with the native // operation's value and error. 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, request CatalogRequest, input I, state S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: diagnostics}, err } prompts, err := engine.ListPrompts(ctx, request.Filter) return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: diagnostics}, err }, ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err } profiles, err := engine.ListProfiles(ctx, request.Filter) return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: diagnostics}, err }, ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err } backends, err := engine.ListBackends(ctx, request.Filter) return commandkit.Acquired[[]string]{Value: backends, Diagnostics: diagnostics}, err }, ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err } namespaces, err := engine.ListNamespaces(ctx, request.Kinds...) return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: diagnostics}, err }, ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) { engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: diagnostics}, err } collections, err := engine.ListCollections(ctx) return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, 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) 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) }, }} }