Skip to content

Commit 336afe0

Browse files
feat: prompt for missing required values and dashboard placeholders before planning (#72)
* feat: prompt for missing required values and dashboard placeholders before planning Plan, apply and test now prompt the user for values that are still needed instead of failing validation: - Required keys that are missing (any depth). oneOf/anyOf branches are evaluated individually; if no branch is satisfied and the user hasn't started one, they choose which option to configure. Discriminator consts are filled automatically. allOf and additionalProperties are supported. - Values equal to the dashboard placeholder '<Replace me here!>' (any depth). Null values are treated as an explicit "no value" and are not prompted. Answers apply to the in-memory project only and are never written back to the config. Invalid answers fail plugin validation as before. New pieces: - entities/missing-parameter.ts: MissingParameter / ResourceMissingParameters types - utils/missing-parameters.ts: pure schema walker, value coercion, path helpers - common/resolve-missing-parameters.ts: MissingParametersResolver step (runs after plugin initialization, before validation) - Reporter.promptMissingParameters implemented for Default (ink form), Plain/Debug (readline, masked for sensitive params), Json (throws), Stub Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QnBk2Qs7TDiTz4V6x1wKK * feat: replace the missing values form with a single list prompt The ink-form based prompt was busy and needed left/right to move between resources. Missing values are now shown in one flat list: - Each row is the fully qualified name (resource id in bold, then the parameter path) followed by an inline text input. - Up/down move between rows, typing edits the focused row, Enter moves to the next row and submits from the last one, Esc cancels. - Only the focused row shows its description. Sensitive values are masked. - Empty values, numbers and booleans are checked inline; submitting jumps to the first invalid row. Plugin validation still runs afterwards. - Long lists scroll to keep the focused row visible. No fullscreen mode, so earlier output stays on screen. The component (ui/components/parameter-list) is generic so import/destroy can move to it later. Also awaits displayMessage in the DefaultComponent test; the unawaited call overwrote the shared render state of the next test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QnBk2Qs7TDiTz4V6x1wKK * feat: updated UI to be more intuitive. Show surrounding context for each required value --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77f6001 commit 336afe0

20 files changed

Lines changed: 1700 additions & 9 deletions
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
MissingParameter,
3+
MissingParameterPrompt,
4+
MissingParametersResult,
5+
ResourceMissingParameters,
6+
} from '../entities/missing-parameter.js';
7+
import { ResourceConfig } from '../entities/resource-config.js';
8+
import { Reporter } from '../ui/reporters/reporter.js';
9+
import { coerceParameterValue, formatParameterPath, setValueAtPath } from '../utils/missing-parameters.js';
10+
import { InitializationResult } from './initialize-plugins.js';
11+
12+
/**
13+
* Prompts the user for values that are still needed before planning: required keys that are missing and values set to
14+
* the dashboard placeholder ('<Replace me here!>'). Answers are applied to the in-memory project only. They are never
15+
* written back to the config file.
16+
*
17+
* Must run after plugins are initialized (resource schemas are needed) and before validation.
18+
*/
19+
export const MissingParametersResolver = {
20+
async run(initializationResult: InitializationResult, reporter: Reporter): Promise<void> {
21+
const { project, pluginManager, resourceDefinitions } = initializationResult;
22+
23+
// Unknown types are reported by validation
24+
const types = [...new Set(project.resourceConfigs.map((r) => r.type))].filter((t) => resourceDefinitions.has(t));
25+
const resourceInfoMap = new Map(
26+
(await pluginManager.getMultipleResourceInfo(types)).map((info) => [info.type, info])
27+
);
28+
29+
const requests: ResourceMissingParameters[] = [];
30+
for (const resource of project.resourceConfigs) {
31+
const resourceInfo = resourceInfoMap.get(resource.type);
32+
if (!resourceInfo) {
33+
continue;
34+
}
35+
36+
const missing = await MissingParametersResolver.resolveChoices(
37+
resourceInfo.findMissingParameters(resource),
38+
resource,
39+
reporter,
40+
);
41+
42+
// Values known from the schema (ex: discriminators of the chosen option) are filled without asking
43+
for (const parameter of missing.filter((p) => p.knownValue !== undefined)) {
44+
MissingParametersResolver.setParameter(resource, parameter, parameter.knownValue);
45+
}
46+
47+
const sensitiveParameters = resourceDefinitions.get(resource.type)?.sensitiveParameters ?? [];
48+
const prompts: MissingParameterPrompt[] = missing
49+
.filter((p) => p.knownValue === undefined)
50+
.map((p) => ({
51+
...p,
52+
label: formatParameterPath(p.path),
53+
sensitive: sensitiveParameters.includes('*') || sensitiveParameters.includes(String(p.path[0])),
54+
}));
55+
56+
if (prompts.length > 0) {
57+
requests.push({ resource, parameters: prompts, sensitiveParameters });
58+
}
59+
}
60+
61+
if (requests.length === 0) {
62+
return;
63+
}
64+
65+
const answers = await reporter.promptMissingParameters(requests);
66+
for (const [idx, { resource, parameters }] of requests.entries()) {
67+
for (const [parameterIdx, parameter] of parameters.entries()) {
68+
const value = coerceParameterValue(answers[idx]?.[parameterIdx], parameter.type, parameter.itemsType);
69+
MissingParametersResolver.setParameter(resource, parameter, value);
70+
}
71+
}
72+
},
73+
74+
/**
75+
* Flattens the result into a list of parameters. Where the schema allows multiple options (oneOf / anyOf) the user is
76+
* asked to pick one first.
77+
*/
78+
async resolveChoices(
79+
result: MissingParametersResult,
80+
resource: ResourceConfig,
81+
reporter: Reporter,
82+
): Promise<MissingParameter[]> {
83+
const parameters = [...result.parameters];
84+
85+
for (const choice of result.choices) {
86+
const location = choice.path.length > 0 ? ` at "${formatParameterPath(choice.path)}"` : '';
87+
const selected = await reporter.promptOptions(
88+
`Resource "${resource.id}" requires one of the following options${location}. Which one would you like to configure?`,
89+
choice.options.map((o) => o.label),
90+
);
91+
92+
const option = choice.options[selected];
93+
if (!option) {
94+
throw new Error(`Invalid option selected for resource "${resource.id}"`);
95+
}
96+
97+
parameters.push(...await MissingParametersResolver.resolveChoices(option.result, resource, reporter));
98+
}
99+
100+
// The same value can be reported by more than one branch of a schema
101+
const seen = new Set<string>();
102+
return parameters.filter((p) => {
103+
const key = JSON.stringify(p.path);
104+
if (seen.has(key)) return false;
105+
seen.add(key);
106+
return true;
107+
});
108+
},
109+
110+
setParameter(resource: ResourceConfig, parameter: MissingParameter, value: unknown): void {
111+
const [key, ...rest] = parameter.path;
112+
resource.setParameter(String(key), setValueAtPath(resource.parameters[key], rest, value));
113+
},
114+
};

‎src/entities/missing-parameter.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ResourceConfig } from './resource-config.js';
2+
3+
/**
4+
* The placeholder value the Codify dashboard writes into configs for values the user still needs to fill in.
5+
*/
6+
export const PLACEHOLDER_VALUE = '<Replace me here!>';
7+
8+
/** Location of a value inside a resource's parameters. Strings are object keys, numbers are array indices. */
9+
export type ParameterPath = Array<number | string>;
10+
11+
/**
12+
* A single value that must be supplied by the user before the resource can be planned.
13+
*/
14+
export interface MissingParameter {
15+
/** Path from the root of the resource's parameters. Ex: ['hosts', 0, 'HostName'] */
16+
path: ParameterPath;
17+
/** 'missing' if the key is absent but required by the schema. 'placeholder' if the value is {@link PLACEHOLDER_VALUE} */
18+
reason: 'missing' | 'placeholder';
19+
/** JSON schema type of the value (if known). Used to convert the user's input. */
20+
type?: string;
21+
/** JSON schema type of the array items (if type is 'array'). */
22+
itemsType?: string;
23+
description?: string;
24+
/** Set when the value is known without asking the user (a schema const, ex: a discriminator). It is filled automatically. */
25+
knownValue?: unknown;
26+
}
27+
28+
/**
29+
* Raised when a schema contains a oneOf / anyOf where multiple branches are equally valid. Ex: git-repository
30+
* requires either 'directory' or 'parentDirectory'. The user must pick one of the options before values can be prompted.
31+
*/
32+
export interface MissingParameterChoice {
33+
/** Path to the object the choice applies to */
34+
path: ParameterPath;
35+
options: Array<{ label: string; result: MissingParametersResult }>;
36+
}
37+
38+
export interface MissingParametersResult {
39+
parameters: MissingParameter[];
40+
choices: MissingParameterChoice[];
41+
}
42+
43+
/**
44+
* A missing parameter that is ready to be shown to the user.
45+
*/
46+
export interface MissingParameterPrompt extends MissingParameter {
47+
/** Human readable path. Ex: hosts[0].HostName */
48+
label: string;
49+
/** Input should be masked */
50+
sensitive: boolean;
51+
}
52+
53+
/**
54+
* All of the values that need to be prompted for a single resource. Passed to {@link Reporter.promptMissingParameters}.
55+
*/
56+
export interface ResourceMissingParameters {
57+
resource: ResourceConfig;
58+
parameters: MissingParameterPrompt[];
59+
/** Sensitive parameters of the resource type. Their existing values are masked when the resource is shown. */
60+
sensitiveParameters?: string[];
61+
}

‎src/entities/resource-info.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { GetResourceInfoResponseData } from '@codifycli/schemas';
22

3+
import { findMissingParameters } from '../utils/missing-parameters.js';
4+
import { MissingParametersResult } from './missing-parameter.js';
35
import { ResourceConfig } from './resource-config.js';
46

57
interface ParameterInfo {
@@ -50,6 +52,14 @@ export class ResourceInfo implements GetResourceInfoResponseData {
5052
})
5153
}
5254

55+
/**
56+
* Values the resource still needs before it can be planned: required keys (per the schema) that are missing and
57+
* values set to the dashboard placeholder.
58+
*/
59+
findMissingParameters(resource: ResourceConfig): MissingParametersResult {
60+
return findMissingParameters(this.schema, resource.parameters);
61+
}
62+
5363
getParameterInfo(): ParameterInfo[] {
5464
if (!this.parametersCache) {
5565
const { schema } = this;

‎src/orchestrators/plan.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Config } from '@codifycli/schemas';
22

33
import { PluginInitOrchestrator } from '../common/initialize-plugins.js';
4+
import { MissingParametersResolver } from '../common/resolve-missing-parameters.js';
45
import { Plan } from '../entities/plan.js';
56
import { Project } from '../entities/project.js';
67
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
@@ -36,6 +37,8 @@ export class PlanOrchestrator {
3637

3738
await createStartupShellScriptsIfNotExists();
3839

40+
// Prompt for missing required values and dashboard placeholders. Must happen before validation.
41+
await MissingParametersResolver.run(initializationResult, reporter);
3942
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: args.noProgress }, reporter);
4043
project.resolveDependenciesAndCalculateEvalOrder(resourceDefinitions);
4144
if (OsUtils.isMacOS()) {

‎src/orchestrators/test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import path from 'node:path';
77
import semver from 'semver';
88

99
import { PluginInitOrchestrator } from '../common/initialize-plugins.js';
10+
import { MissingParametersResolver } from '../common/resolve-missing-parameters.js';
1011
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
1112
import { Reporter } from '../ui/reporters/reporter.js';
1213
import { StubReporter } from '../ui/reporters/stub-reporter.js';
@@ -36,6 +37,7 @@ export const TestOrchestrator = {
3637
ctx.subprocessStarted(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
3738
// Perform validation initially to ensure the project is valid
3839
const initializationResult = await PluginInitOrchestrator.run({ ...args, noProgress: true }, reporter);
40+
await MissingParametersResolver.run(initializationResult, reporter);
3941
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: true }, reporter);
4042
ctx.subprocessFinished(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
4143

‎src/ui/components/default-component.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ImportResultComponent } from './import/import-result.js';
1717
import { ImportWarning } from './import/import-warning.js';
1818
import { InitBanner } from './init/InitBanner.js';
1919
import { MultiSelect } from './multi-select/MultiSelect.js';
20+
import { ParameterList, ParameterListProps } from './parameter-list/parameter-list.js';
2021
import { PlanComponent } from './plan/plan.js';
2122
import { ProgressDisplay } from './progress/progress-display.js';
2223
import { ApplyComplete } from './widgets/ApplyComplete.js';
@@ -114,6 +115,15 @@ export function DefaultComponent(props: {
114115
}} { ...renderData as FormProps}/>
115116
)
116117
}
118+
{
119+
renderStatus === RenderStatus.PROMPT_MISSING_PARAMETERS && (
120+
<ParameterList
121+
{...renderData as Omit<ParameterListProps, 'onCancel' | 'onSubmit'>}
122+
onCancel={() => emitter.emit(RenderEvent.PROMPT_MISSING_PARAMETERS_RESULT, null)}
123+
onSubmit={(values) => emitter.emit(RenderEvent.PROMPT_MISSING_PARAMETERS_RESULT, values)}
124+
/>
125+
)
126+
}
117127
{
118128
renderStatus === RenderStatus.DISPLAY_IMPORT_RESULT && (
119129
<Static items={[renderData as { importResult: ImportResult; showConfigs: boolean }]}>{

0 commit comments

Comments
 (0)