Files
deepseek-harness/packages/sdk/helper/src/questions/headless-prompt-port.ts
imccyu dcd886798f feat(sdk): headless PromptPort + create/config headless plan
Add HeadlessPromptPort (fail-loud non-interactive PromptPort), thread
prefilled feature values through FeatureConfigurator, and let CreateWizard
and ConfigWorkflow accept a headless feature plan that skips the interactive
tree/suggests prompts. Per-file 100% coverage on all changed files.
2026-07-18 16:05:14 +08:00

98 lines
3.7 KiB
TypeScript

/**
* Non-interactive prompt port for headless create/config and skill-driven runs.
*
* @module @deepseek-ai/dsh-helper/questions/headless-prompt-port
*/
import type {
ConfirmPromptRequest,
MultiSelectPromptRequest,
NestedMultiSelectRequest,
NestedMultiSelectValue,
PromptOutcome,
PromptPort,
SecretPromptRequest,
SelectPromptRequest,
TextPromptRequest,
} from './prompt-port.ts'
/**
* Raised when a headless run reaches a decision that was neither prefilled nor
* carries a usable default. The message names the unanswered prompt so an agent
* or CI caller can see exactly which input the spec must supply.
*/
export class HeadlessPromptError extends Error {
/** The unanswered prompt's user-facing message. */
readonly prompt: string
/** Build an error naming the unanswered prompt. */
constructor(prompt: string) {
super(`headless run needs an answer for: ${prompt}`)
this.name = 'HeadlessPromptError'
this.prompt = prompt
}
}
/** Resolve an answered outcome. */
function answered<T>(value: T): Promise<PromptOutcome<T>> {
return Promise.resolve({ status: 'answered', value })
}
/** Reject with a named unanswered-prompt error. */
function unanswered<T>(message: string): Promise<PromptOutcome<T>> {
return Promise.reject(new HeadlessPromptError(message))
}
/**
* A {@link PromptPort} that never blocks on a terminal.
*
* Answers are expected to arrive as prefilled values through the `Question` /
* `FeatureConfigurator` layers, so in a fully specified run this port is never
* reached. When it *is* reached, it takes the prompt's own declared default
* (`defaultValue` / `initialValue`) if one exists; otherwise it fails loud with
* {@link HeadlessPromptError}. Nested feature selection has no scalar default,
* so it always fails loud — headless callers must supply the feature set through
* the spec rather than the tree picker.
*/
export class HeadlessPromptPort implements PromptPort {
/** Answer visible text from its default, or fail loud. */
text(request: TextPromptRequest): Promise<PromptOutcome<string>> {
const fallback = request.initialValue ?? request.defaultValue
if (fallback === undefined) return unanswered(request.message)
const diagnostic = request.validate?.(fallback)
if (diagnostic) return unanswered(`${request.message} (${diagnostic})`)
return answered(fallback)
}
/** A secret has no safe default: always fail loud. */
secret(request: SecretPromptRequest): Promise<PromptOutcome<string>> {
return unanswered(request.message)
}
/** Answer a single choice from its initial value, or fail loud. */
select<T>(request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> {
if (request.initialValue === undefined) return unanswered(request.message)
return answered(request.initialValue)
}
/** Answer a multi-choice from its initial values, or fail loud when required. */
multiselect<T>(request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> {
const initial = request.initialValues ?? []
if (request.required && initial.length === 0) return unanswered(request.message)
return answered(initial)
}
/** Answer a confirmation from its initial value, or fail loud. */
confirm(request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> {
if (request.initialValue === undefined) return unanswered(request.message)
return answered(request.initialValue)
}
/** Nested feature selection has no scalar default: always fail loud. */
nestedMultiselect<TValue, TChoice>(
request: NestedMultiSelectRequest<TValue, TChoice>,
): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> {
return unanswered(request.message)
}
}