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. Its acquisition carries independent loader diagnostics and // terminal context alongside any native inspection result. InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error) // InspectProfile inspects the selected profile after input and state // preparation. Its acquisition carries independent loader diagnostics and // terminal context alongside any native inspection result. InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error) // InspectRequirements inspects native PromptKit requirements after input and // state preparation. Its acquisition carries independent loader diagnostics // and terminal context alongside any native inspection result. 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 independent diagnostics, terminal // context, 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, independent diagnostics, and // terminal context 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 supply terminal explanations and are returned with // supported identity, and // configuration diagnostics and terminal context from engine loading remain // separate 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, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation before native validation: %w", err) } if err := engine.ValidateRequest(ctx, concreteRequest); err != nil { messages = messages.withOperationError(err) return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation after native validation: %w", err) } return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, nil } return suite } type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error) type promptKitAcquisition struct { diagnostics []commandkit.Diagnostic explanation commandkit.Explanation } // withOperationError deliberately presents a native PromptKit operation error. // Loader and request-builder errors remain application-owned. Native structured // context takes precedence when an acquisition already supplies it. func (messages promptKitAcquisition) withOperationError(err error) promptKitAcquisition { if err == nil || messages.explanation.Message != "" { return messages } for _, detail := range messages.explanation.Details { if detail != nil { return messages } } messages.explanation.Message = err.Error() return messages } // 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 // independent diagnostics, terminal context, 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, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[ckconfig.View]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } return commandkit.Acquired[ckconfig.View]{ Value: ckconfig.View{ Effective: inspection, Sources: inspection, Explanation: inspection, }, Diagnostics: messages.diagnostics, Explanation: messages.explanation, }, 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, messages, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err) } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err) } _, rightInspection, rightMessages, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load) messages = appendPromptKitAcquisition(messages, rightMessages) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err) } if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err) } differences, err := leftInspection.Diff(rightInspection) messages = messages.withOperationError(err) if err != nil { return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err) } return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 independent loader diagnostics and // terminal context alongside the native operation's value and error. Native // operation errors supply terminal explanations when structured context is absent. 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, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } prompts, err := engine.ListPrompts(ctx, request.Filter) messages = messages.withOperationError(err) return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } profiles, err := engine.ListProfiles(ctx, request.Filter) messages = messages.withOperationError(err) return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } backends, err := engine.ListBackends(ctx, request.Filter) messages = messages.withOperationError(err) return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } namespaces, err := engine.ListNamespaces(ctx, request.Kinds...) messages = messages.withOperationError(err) return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) { engine, _, messages, err := loadPromptKitEngine(ctx, selection, input, state, "", load) if err != nil { return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } collections, err := engine.ListCollections(ctx) messages = messages.withOperationError(err) return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } inspection, err := engine.InspectPrompt(ctx, request.ID) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } inspection, err := engine.InspectProfile(ctx, request.ID) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err }, InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) { engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } inspection, err := engine.InspectRequirements(ctx, request.Inspection) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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, promptKitAcquisition, error) { engine, err := load(ctx, selection, input, state) if err != nil { var loadErr *configkit.LoadError if errors.As(err, &loadErr) { messages := promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()) return nil, promptkit.ConfigurationInspection{}, messages, err } return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, err } if engine == nil { return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, errors.New("PromptKit configuration loader returned a nil engine") } inspection, err := engine.InspectConfiguration() messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics()) messages = messages.withOperationError(err) if err != nil { return nil, inspection, messages, err } if ctx != nil { if err := ctx.Err(); err != nil { return nil, promptkit.ConfigurationInspection{}, messages, err } } return engine, inspection, messages, nil } func appendPromptKitAcquisition(left, right promptKitAcquisition) promptKitAcquisition { left.diagnostics = append(left.diagnostics, right.diagnostics...) left.explanation.Details = append(left.explanation.Details, right.explanation.Details...) if left.explanation.Message == "" { left.explanation.Message = right.explanation.Message } return left } func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) promptKitAcquisition { if len(records) == 0 { return promptKitAcquisition{} } // 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...) } var warnings, failures []configkit.Diagnostic for _, record := range records { if record.Severity == configkit.SeverityError { failures = append(failures, record) } else { warnings = append(warnings, record) } } messages := promptKitAcquisition{} if len(warnings) > 0 { messages.diagnostics = []commandkit.Diagnostic{{ Severity: commandkit.SeverityWarning, Write: func(writer io.Writer) error { return writePromptKitDiagnostics(writer, scope, side, sources, warnings) }, }} } if len(failures) > 0 { messages.explanation = commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error { return writePromptKitDiagnostics(writer, scope, side, sources, failures) }}} } return messages } func writePromptKitDiagnostics(writer io.Writer, scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) 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) }