@unscientificjszhai/howto 1.0.0-alpha.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,8 +17,8 @@ import { ConfigError, hasExplicitAiProvider, loadConfig } from "./config.js";
17
17
  import { readUserConfigFile } from "./config-file.js";
18
18
  import { createCommandProvider } from "./ai/index.js";
19
19
  import { buildCommandGenerationPrompt, createProviderPromptRequest } from "./prompt.js";
20
- import { parseAndValidateAiResponse } from "./validation/ai-response.js";
21
20
  import { checkCommandInPath } from "./validation/command-tool.js";
21
+ import { generateValidatedCommandCandidates } from "./validation/generated-commands.js";
22
22
  import { ensureInteractiveTty } from "./ui/tty.js";
23
23
  import { executeCommand } from "./execute.js";
24
24
  import { toAppError } from "./errors.js";
@@ -66,14 +66,14 @@ function run(argv) {
66
66
  question: parsedCli.question,
67
67
  arguments: parsedCli.arguments,
68
68
  useCommand: parsedCli.useCommand,
69
+ structuredOutput: config.structuredOutput,
69
70
  });
70
71
  const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
71
72
  const provider = createCommandProvider(config);
72
73
  if (parsedCli.options.print) {
73
- const aiResult = yield provider.generateCommands(Object.assign(Object.assign({}, promptRequest), { systemPrompt,
74
+ const candidates = yield generateValidatedCommandCandidates(provider, Object.assign(Object.assign({}, promptRequest), { systemPrompt,
74
75
  userPrompt }));
75
- const aiResponse = parseAndValidateAiResponse(aiResult.rawText);
76
- aiResponse.commands.forEach((candidate) => {
76
+ candidates.forEach((candidate) => {
77
77
  console.log(candidate.command);
78
78
  });
79
79
  return { exitCode: 0 };
@@ -83,7 +83,7 @@ function run(argv) {
83
83
  }
84
84
  let finalCommand;
85
85
  let appError;
86
- const { waitUntilExit, unmount, clear } = render(React.createElement(App, { provider: provider, request: Object.assign(Object.assign({}, promptRequest), { systemPrompt, userPrompt }), useCommand: parsedCli.useCommand, onSuccess: (command) => {
86
+ const { waitUntilExit, unmount, clear } = render(React.createElement(App, { provider: provider, request: Object.assign(Object.assign({}, promptRequest), { systemPrompt, userPrompt }), onSuccess: (command) => {
87
87
  finalCommand = command;
88
88
  clear();
89
89
  unmount();
@@ -0,0 +1,97 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { applyInitializationInput, createInitialInitializationState, getProviderOptions, } from "./state.js";
4
+ export const InitializationApp = ({ onSubmit, onComplete, onCancel, onError }) => {
5
+ const [state, setState] = useState(createInitialInitializationState);
6
+ const [submitStatus, setSubmitStatus] = useState("editing");
7
+ const [errorMessage, setErrorMessage] = useState("");
8
+ useInput((input, key) => {
9
+ const update = applyInitializationInput(state, { input, key });
10
+ setState(update.state);
11
+ if (update.cancelled) {
12
+ setSubmitStatus("cancelled");
13
+ onCancel();
14
+ return;
15
+ }
16
+ if (update.completedValues !== undefined) {
17
+ setSubmitStatus("saving");
18
+ onSubmit(update.completedValues)
19
+ .then((config) => {
20
+ onComplete(config);
21
+ })
22
+ .catch((error) => {
23
+ const appError = normalizeError(error);
24
+ setErrorMessage(appError.message);
25
+ setSubmitStatus("error");
26
+ onError(appError);
27
+ });
28
+ }
29
+ }, { isActive: submitStatus === "editing" });
30
+ if (submitStatus === "saving") {
31
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
32
+ React.createElement(Text, { color: "green" }, "Saving howto configuration...")));
33
+ }
34
+ if (submitStatus === "error") {
35
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
36
+ React.createElement(Text, { color: "red" },
37
+ "Error: ",
38
+ errorMessage)));
39
+ }
40
+ if (submitStatus === "cancelled") {
41
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
42
+ React.createElement(Text, { dimColor: true }, "Initialization cancelled.")));
43
+ }
44
+ if (state.step === "provider") {
45
+ return React.createElement(ProviderStep, { selectedIndex: state.selectedIndex });
46
+ }
47
+ return React.createElement(InputStep, { state: state });
48
+ };
49
+ const ProviderStep = ({ selectedIndex }) => {
50
+ const providers = getProviderOptions();
51
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
52
+ React.createElement(Text, null, "howto needs an AI provider before it can call AI."),
53
+ React.createElement(Box, { height: 1 },
54
+ React.createElement(Text, null, " ")),
55
+ React.createElement(Text, { color: "green" }, "? Choose AI provider"),
56
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, providers.map((provider, index) => {
57
+ const isSelected = selectedIndex === index;
58
+ return (React.createElement(Box, { key: provider },
59
+ React.createElement(Text, { color: isSelected ? "cyan" : undefined }, isSelected ? "> " : " "),
60
+ React.createElement(Text, { inverse: isSelected, bold: isSelected },
61
+ index + 1,
62
+ ". ",
63
+ provider)));
64
+ })),
65
+ React.createElement(Box, { flexShrink: 0, marginTop: 1 },
66
+ React.createElement(Text, { dimColor: true }, "Use Up/Down or 1/2 to choose, Enter to continue, Esc or Ctrl+C to cancel."))));
67
+ };
68
+ const InputStep = ({ state, }) => {
69
+ const currentField = state.fields[state.fieldIndex];
70
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
71
+ React.createElement(Text, { color: "green" },
72
+ "? Configure ",
73
+ state.provider),
74
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 }, state.fields.map((field, index) => (React.createElement(FieldRow, { key: field.name, field: field, isActive: index === state.fieldIndex })))),
75
+ currentField.defaultValue !== undefined && currentField.value === "" && (React.createElement(Text, { dimColor: true },
76
+ "Default: ",
77
+ currentField.defaultValue)),
78
+ state.errorMessage !== undefined && React.createElement(Text, { color: "red" }, state.errorMessage),
79
+ React.createElement(Box, { flexShrink: 0, marginTop: 1 },
80
+ React.createElement(Text, { dimColor: true }, "Press Enter for next value, Esc to go back, Ctrl+C to cancel."))));
81
+ };
82
+ const FieldRow = ({ field, isActive, }) => (React.createElement(Box, null,
83
+ React.createElement(Text, { color: isActive ? "cyan" : undefined }, isActive ? "> " : " "),
84
+ React.createElement(Text, null,
85
+ field.label,
86
+ ": "),
87
+ React.createElement(Text, null, field.value),
88
+ isActive && React.createElement(Text, { backgroundColor: "white" }, " ")));
89
+ function normalizeError(error) {
90
+ if (error instanceof Error) {
91
+ return error;
92
+ }
93
+ if (typeof error === "string" && error.trim() !== "") {
94
+ return new Error(error);
95
+ }
96
+ return new Error("unknown error");
97
+ }
@@ -7,37 +7,48 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { input as promptInput, select } from "@inquirer/prompts";
11
- import { DEFAULT_GEMINI_MODEL, DEFAULT_OPENAI_MODEL, loadConfig } from "../config.js";
10
+ import React from "react";
11
+ import { render } from "ink";
12
+ import { loadConfig } from "../config.js";
12
13
  import { getConfigFilePath, writeUserConfigFile } from "../config-file.js";
13
14
  import { InteractionCancelledError } from "../ui/tty.js";
15
+ import { InitializationApp } from "./InitializationApp.js";
14
16
  export function initializeConfig(_a) {
15
17
  return __awaiter(this, arguments, void 0, function* ({ cliOptions, env, input, output, }) {
16
- try {
17
- let initializationRows = 0;
18
- const introduction = "howto needs an AI provider before it can call AI.\n\n";
19
- output.write(introduction);
20
- initializationRows += countTerminalRows(output, introduction);
21
- const values = yield collectInitializationValues(input, output);
22
- initializationRows += countInitializationPromptRows(output, values);
23
- const fileConfig = createFileConfig(values);
24
- const configFilePath = getConfigFilePath(env);
25
- yield writeUserConfigFile(fileConfig, configFilePath);
26
- const savedMessage = `\nSaved howto configuration to ${configFilePath}.\n`;
27
- output.write(savedMessage);
28
- initializationRows += countTerminalRows(output, savedMessage);
29
- yield promptInput({ message: "Press Enter to start..." }, { input, output });
30
- initializationRows += countTerminalRows(output, "✔ Press Enter to start...\n");
31
- clearInitializationOutput(output, initializationRows);
32
- return loadConfig(cliOptions, env, fileConfig);
33
- }
34
- catch (error) {
35
- if (error instanceof Error &&
36
- (error.name === "ExitPromptError" || error.message.includes("force closed"))) {
37
- throw new InteractionCancelledError();
38
- }
39
- throw error;
40
- }
18
+ return yield new Promise((resolve, reject) => {
19
+ let settled = false;
20
+ const settle = (result) => {
21
+ if (settled) {
22
+ return;
23
+ }
24
+ settled = true;
25
+ instance.clear();
26
+ instance.unmount();
27
+ void instance.waitUntilExit().then(result, result);
28
+ };
29
+ const instance = render(React.createElement(InitializationApp, { onSubmit: (values) => __awaiter(this, void 0, void 0, function* () {
30
+ const fileConfig = createFileConfig(values);
31
+ const configFilePath = getConfigFilePath(env);
32
+ yield writeUserConfigFile(fileConfig, configFilePath);
33
+ return loadConfig(cliOptions, env, fileConfig);
34
+ }), onComplete: (config) => {
35
+ settle(() => {
36
+ resolve(config);
37
+ });
38
+ }, onCancel: () => {
39
+ settle(() => {
40
+ reject(new InteractionCancelledError());
41
+ });
42
+ }, onError: (error) => {
43
+ settle(() => {
44
+ reject(error);
45
+ });
46
+ } }), {
47
+ stdin: input,
48
+ stdout: output,
49
+ exitOnCtrlC: false,
50
+ });
51
+ });
41
52
  });
42
53
  }
43
54
  export function clearInitializationOutput(output, rows) {
@@ -55,22 +66,6 @@ export function erasePreviousTerminalRows(rows) {
55
66
  }
56
67
  return `${sequence}\x1b[G`;
57
68
  }
58
- export function countInitializationPromptRows(output, values) {
59
- var _a;
60
- const promptLines = values.provider === "gemini"
61
- ? [
62
- "✔ Choose AI provider: gemini",
63
- `✔ Gemini API key: ${values.apiKey}`,
64
- `✔ Gemini model: ${values.model}`,
65
- ]
66
- : [
67
- "✔ Choose AI provider: openai",
68
- `✔ OpenAI API key (optional): ${values.apiKey}`,
69
- `✔ OpenAI model: ${values.model}`,
70
- `✔ OpenAI base URL (optional, Enter for official default): ${(_a = values.openaiBaseUrl) !== null && _a !== void 0 ? _a : ""}`,
71
- ];
72
- return promptLines.reduce((rows, line) => rows + countTerminalRows(output, `${line}\n`), 0);
73
- }
74
69
  export function countTerminalRows(output, text) {
75
70
  const columns = getOutputColumns(output);
76
71
  const lines = text.split("\n");
@@ -101,46 +96,8 @@ export function createFileConfig(values) {
101
96
  openaiApiKey: values.apiKey,
102
97
  openaiModel: values.model,
103
98
  };
104
- if (values.openaiBaseUrl !== undefined) {
99
+ if (values.openaiBaseUrl !== undefined && values.openaiBaseUrl.trim() !== "") {
105
100
  config.openaiApiUrl = values.openaiBaseUrl;
106
101
  }
107
102
  return config;
108
103
  }
109
- function collectInitializationValues(input, output) {
110
- return __awaiter(this, void 0, void 0, function* () {
111
- const provider = yield select({
112
- message: "Choose AI provider:",
113
- choices: [
114
- { name: "openai", value: "openai" },
115
- { name: "gemini", value: "gemini" },
116
- ],
117
- }, { input, output });
118
- if (provider === "gemini") {
119
- const apiKey = yield promptInput({
120
- message: "Gemini API key:",
121
- validate: (val) => (val.trim() !== "" ? true : "This value is required."),
122
- }, { input, output });
123
- const model = yield promptInput({
124
- message: "Gemini model:",
125
- default: DEFAULT_GEMINI_MODEL,
126
- }, { input, output });
127
- return { provider, apiKey, model };
128
- }
129
- const apiKey = yield promptInput({
130
- message: "OpenAI API key (optional):",
131
- }, { input, output });
132
- const model = yield promptInput({
133
- message: "OpenAI model:",
134
- default: DEFAULT_OPENAI_MODEL,
135
- }, { input, output });
136
- const baseUrl = yield promptInput({
137
- message: "OpenAI base URL (optional, Enter for official default):",
138
- }, { input, output });
139
- return {
140
- provider,
141
- apiKey,
142
- model,
143
- openaiBaseUrl: baseUrl.trim() === "" ? undefined : baseUrl,
144
- };
145
- });
146
- }
@@ -0,0 +1,172 @@
1
+ import { DEFAULT_GEMINI_MODEL, DEFAULT_OPENAI_MODEL } from "../config.js";
2
+ const PROVIDERS = ["openai", "gemini"];
3
+ export function createInitialInitializationState() {
4
+ return {
5
+ step: "provider",
6
+ selectedIndex: null,
7
+ };
8
+ }
9
+ export function getProviderOptions() {
10
+ return PROVIDERS;
11
+ }
12
+ export function applyInitializationInput(state, event) {
13
+ if (event.key.ctrl && event.input === "c") {
14
+ return { state, cancelled: true };
15
+ }
16
+ if (state.step === "provider") {
17
+ return applyProviderInput(state, event);
18
+ }
19
+ return applyFieldInput(state, event);
20
+ }
21
+ function applyProviderInput(state, event) {
22
+ if (event.key.escape) {
23
+ return { state, cancelled: true };
24
+ }
25
+ const numericSelection = getNumericProviderSelection(event.input);
26
+ if (numericSelection !== null) {
27
+ return {
28
+ state: createInputState(PROVIDERS[numericSelection]),
29
+ };
30
+ }
31
+ if (event.key.upArrow) {
32
+ return {
33
+ state: Object.assign(Object.assign({}, state), { selectedIndex: state.selectedIndex === null
34
+ ? PROVIDERS.length - 1
35
+ : (state.selectedIndex - 1 + PROVIDERS.length) % PROVIDERS.length }),
36
+ };
37
+ }
38
+ if (event.key.downArrow) {
39
+ return {
40
+ state: Object.assign(Object.assign({}, state), { selectedIndex: state.selectedIndex === null ? 0 : (state.selectedIndex + 1) % PROVIDERS.length }),
41
+ };
42
+ }
43
+ if (event.key.return && state.selectedIndex !== null) {
44
+ return {
45
+ state: createInputState(PROVIDERS[state.selectedIndex]),
46
+ };
47
+ }
48
+ return { state };
49
+ }
50
+ function getNumericProviderSelection(input) {
51
+ if (input === "1") {
52
+ return 0;
53
+ }
54
+ if (input === "2") {
55
+ return 1;
56
+ }
57
+ return null;
58
+ }
59
+ function applyFieldInput(state, event) {
60
+ if (event.key.escape) {
61
+ return { state: createInitialInitializationState() };
62
+ }
63
+ if (event.key.return) {
64
+ const validationError = validateCurrentField(state);
65
+ if (validationError !== undefined) {
66
+ return {
67
+ state: Object.assign(Object.assign({}, state), { errorMessage: validationError }),
68
+ };
69
+ }
70
+ if (state.fieldIndex === state.fields.length - 1) {
71
+ return {
72
+ state,
73
+ completedValues: createInitializationValues(state),
74
+ };
75
+ }
76
+ return {
77
+ state: Object.assign(Object.assign({}, state), { fieldIndex: state.fieldIndex + 1, errorMessage: undefined }),
78
+ };
79
+ }
80
+ if (event.key.backspace || event.key.delete) {
81
+ return updateCurrentField(state, (value) => value.slice(0, -1));
82
+ }
83
+ if (event.input !== "" && !hasControlKey(event)) {
84
+ return updateCurrentField(state, (value) => value + event.input);
85
+ }
86
+ return { state };
87
+ }
88
+ function hasControlKey(event) {
89
+ return (event.key.upArrow ||
90
+ event.key.downArrow ||
91
+ event.key.return ||
92
+ event.key.escape ||
93
+ event.key.ctrl ||
94
+ event.key.backspace ||
95
+ event.key.delete);
96
+ }
97
+ function createInputState(provider) {
98
+ return {
99
+ step: "input",
100
+ provider,
101
+ fieldIndex: 0,
102
+ fields: provider === "gemini"
103
+ ? [
104
+ {
105
+ name: "apiKey",
106
+ label: "Gemini API key",
107
+ value: "",
108
+ required: true,
109
+ },
110
+ {
111
+ name: "model",
112
+ label: "Gemini model",
113
+ value: "",
114
+ defaultValue: DEFAULT_GEMINI_MODEL,
115
+ required: false,
116
+ },
117
+ ]
118
+ : [
119
+ {
120
+ name: "apiKey",
121
+ label: "OpenAI API key (optional)",
122
+ value: "",
123
+ required: false,
124
+ },
125
+ {
126
+ name: "model",
127
+ label: "OpenAI model",
128
+ value: "",
129
+ defaultValue: DEFAULT_OPENAI_MODEL,
130
+ required: false,
131
+ },
132
+ {
133
+ name: "openaiBaseUrl",
134
+ label: "OpenAI base URL (optional, Enter for official default)",
135
+ value: "",
136
+ required: false,
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ function updateCurrentField(state, updateValue) {
142
+ return {
143
+ state: Object.assign(Object.assign({}, state), { fields: state.fields.map((field, index) => index === state.fieldIndex
144
+ ? Object.assign(Object.assign({}, field), { value: updateValue(field.value) }) : field), errorMessage: undefined }),
145
+ };
146
+ }
147
+ function validateCurrentField(state) {
148
+ const field = state.fields[state.fieldIndex];
149
+ if (field.required && field.value.trim() === "") {
150
+ return "This value is required.";
151
+ }
152
+ return undefined;
153
+ }
154
+ function createInitializationValues(state) {
155
+ var _a, _b, _c;
156
+ const fieldValues = new Map(state.fields.map((field) => [field.name, field.value]));
157
+ const modelValue = (_a = fieldValues.get("model")) === null || _a === void 0 ? void 0 : _a.trim();
158
+ if (state.provider === "gemini") {
159
+ return {
160
+ provider: "gemini",
161
+ apiKey: (_b = fieldValues.get("apiKey")) !== null && _b !== void 0 ? _b : "",
162
+ model: modelValue === "" || modelValue === undefined ? DEFAULT_GEMINI_MODEL : modelValue,
163
+ };
164
+ }
165
+ const baseUrl = fieldValues.get("openaiBaseUrl");
166
+ return {
167
+ provider: "openai",
168
+ apiKey: (_c = fieldValues.get("apiKey")) !== null && _c !== void 0 ? _c : "",
169
+ model: modelValue === "" || modelValue === undefined ? DEFAULT_OPENAI_MODEL : modelValue,
170
+ openaiBaseUrl: baseUrl === undefined || baseUrl.trim() === "" ? undefined : baseUrl,
171
+ };
172
+ }
package/dist/prompt.js CHANGED
@@ -18,6 +18,10 @@ The JSON object must match this schema:
18
18
  The commands array must contain 1 to 3 items. Do not return more than 3 candidate commands.
19
19
  Detect the primary natural language of the user's question. Return title, description, and placeholders[].description in that language. Keep placeholder name values English-compatible ASCII using only letters, numbers, underscores, or hyphens.
20
20
  Use placeholders in commands only as {{name}}, and declare every placeholder in the placeholders array. User may provide argument. If the user's intent is clear, try to use the provided arguments as parameters in the generated commands instead of placeholders. If the intent is unclear, do not fill them.`;
21
+ export const STRUCTURED_OUTPUT_CONTRACT = `Return only the JSON object requested by the response schema. Do not include a natural-language body, markdown, or code fences.
22
+ The commands array must contain 1 to 3 items. Do not return more than 3 candidate commands.
23
+ Detect the primary natural language of the user's question. Return title, description, and placeholders[].description in that language. Keep placeholder name values English-compatible ASCII using only letters, numbers, underscores, or hyphens.
24
+ Use placeholders in commands only as {{name}}, and declare every placeholder in the placeholders array. User may provide argument. If the user's intent is clear, try to use the provided arguments as parameters in the generated commands instead of placeholders. If the intent is unclear, do not fill them.`;
21
25
  export const SAFETY_CONSTRAINTS = `Prefer read-only, reversible, and low-risk commands.
22
26
  When a task could involve deletion, overwrite, privilege escalation, network download, or executing downloaded content, prefer a safer alternative or inspection command when possible.
23
27
  Do not include safety metadata or claim that a command is safe; the local CLI will perform its own validation and dangerous-command checks.`;
@@ -26,7 +30,8 @@ export function createProviderPromptRequest(request) {
26
30
  question: request.question,
27
31
  arguments: request.arguments,
28
32
  useCommand: request.useCommand,
29
- outputContract: OUTPUT_CONTRACT,
33
+ structuredOutput: request.structuredOutput,
34
+ outputContract: request.structuredOutput ? STRUCTURED_OUTPUT_CONTRACT : OUTPUT_CONTRACT,
30
35
  safetyConstraints: SAFETY_CONSTRAINTS,
31
36
  };
32
37
  }
package/dist/ui/App.js CHANGED
@@ -4,12 +4,11 @@ import { LoadingView } from "./LoadingView.js";
4
4
  import { SelectCommandView } from "./SelectCommandView.js";
5
5
  import { ResolvePlaceholdersView } from "./ResolvePlaceholdersView.js";
6
6
  import { ConfirmView } from "./ConfirmView.js";
7
- import { parseAndValidateAiResponse } from "../validation/ai-response.js";
8
- import { validateUseCommandCandidates } from "../validation/command-tool.js";
7
+ import { generateValidatedCommandCandidates } from "../validation/generated-commands.js";
9
8
  import { detectDangerousCommand } from "../safety/dangerous-command.js";
10
- import { replaceCommandPlaceholders } from "./placeholder-logic.js";
9
+ import { resolveCandidatePlaceholders } from "./placeholder-logic.js";
11
10
  import { InteractionCancelledError } from "./tty.js";
12
- export const App = ({ provider, request, useCommand, onSuccess, onError }) => {
11
+ export const App = ({ provider, request, onSuccess, onError }) => {
13
12
  const { rows } = useWindowSize();
14
13
  const [status, setStatus] = useState("loading");
15
14
  const [candidates, setCandidates] = useState([]);
@@ -20,12 +19,9 @@ export const App = ({ provider, request, useCommand, onSuccess, onError }) => {
20
19
  const [errorMessage, setErrorMessage] = useState("");
21
20
  useEffect(() => {
22
21
  if (status === "loading") {
23
- provider
24
- .generateCommands(request)
25
- .then((result) => {
26
- const aiResponse = parseAndValidateAiResponse(result.rawText);
27
- validateUseCommandCandidates(aiResponse, useCommand);
28
- setCandidates(aiResponse.commands);
22
+ generateValidatedCommandCandidates(provider, request)
23
+ .then((generatedCandidates) => {
24
+ setCandidates(generatedCandidates);
29
25
  setStatus("selecting");
30
26
  })
31
27
  .catch((error) => {
@@ -35,7 +31,7 @@ export const App = ({ provider, request, useCommand, onSuccess, onError }) => {
35
31
  onError(appError);
36
32
  });
37
33
  }
38
- }, [status, provider, request, useCommand, onError]);
34
+ }, [status, provider, request, onError]);
39
35
  const handleSelect = (candidate) => {
40
36
  setSelectedCandidate(candidate);
41
37
  setResolvedValues(new Map());
@@ -43,19 +39,27 @@ export const App = ({ provider, request, useCommand, onSuccess, onError }) => {
43
39
  setStatus("resolving");
44
40
  }
45
41
  else {
46
- const command = candidate.command;
47
- setFinalCommand(command);
48
- setDanger(detectDangerousCommand(command));
49
- setStatus("confirming");
42
+ try {
43
+ const resolved = resolveCandidatePlaceholders(candidate, new Map());
44
+ setFinalCommand(resolved.command);
45
+ setResolvedValues(resolved.values);
46
+ setDanger(detectDangerousCommand(resolved.command));
47
+ setStatus("confirming");
48
+ }
49
+ catch (error) {
50
+ const appError = normalizeError(error);
51
+ setErrorMessage(appError.message);
52
+ setStatus("error");
53
+ onError(appError);
54
+ }
50
55
  }
51
56
  };
52
- const handleResolve = (values) => {
57
+ const handleResolve = (resolved) => {
53
58
  if (!selectedCandidate)
54
59
  return;
55
- setResolvedValues(values);
56
- const command = replaceCommandPlaceholders(selectedCandidate.command, values);
57
- setFinalCommand(command);
58
- setDanger(detectDangerousCommand(command));
60
+ setResolvedValues(resolved.values);
61
+ setFinalCommand(resolved.command);
62
+ setDanger(detectDangerousCommand(resolved.command));
59
63
  setStatus("confirming");
60
64
  };
61
65
  useEffect(() => {
@@ -76,8 +80,8 @@ export const App = ({ provider, request, useCommand, onSuccess, onError }) => {
76
80
  return (React.createElement(Box, { flexDirection: "column", maxHeight: maxFrameRows, overflowY: "hidden" },
77
81
  status === "loading" && React.createElement(LoadingView, null),
78
82
  status === "selecting" && (React.createElement(SelectCommandView, { candidates: candidates, onSelect: handleSelect, onCancel: handleCancel })),
79
- status === "resolving" && selectedCandidate && (React.createElement(ResolvePlaceholdersView, { candidate: selectedCandidate, placeholders: selectedCandidate.placeholders, onResolve: handleResolve, onBack: handleBackToSelection, onCancel: handleCancel })),
80
- (status === "confirming" || status === "done") && selectedCandidate && (React.createElement(ConfirmView, { candidate: selectedCandidate, resolvedValues: resolvedValues, danger: danger, onConfirm: handleConfirm, onCancel: handleCancel, isDone: status === "done" })),
83
+ status === "resolving" && selectedCandidate && (React.createElement(ResolvePlaceholdersView, { candidate: selectedCandidate, onResolve: handleResolve, onBack: handleBackToSelection, onCancel: handleCancel })),
84
+ (status === "confirming" || status === "done") && selectedCandidate && (React.createElement(ConfirmView, { candidate: selectedCandidate, command: finalCommand, resolvedValues: resolvedValues, danger: danger, onConfirm: handleConfirm, onCancel: handleCancel, isDone: status === "done" })),
81
85
  status === "error" && React.createElement(Text, { color: "red" },
82
86
  "Error: ",
83
87
  errorMessage)));
@@ -1,7 +1,10 @@
1
1
  import React, { useState } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import { SelectedCommandDisplay } from "./SelectedCommandDisplay.js";
4
- export const ConfirmView = ({ candidate, resolvedValues, danger, onConfirm, onCancel, isDone = false, }) => {
4
+ export function isDangerConfirmationInput(input) {
5
+ return input.toUpperCase() === "EXECUTE";
6
+ }
7
+ export const ConfirmView = ({ candidate, command, resolvedValues, danger, onConfirm, onCancel, isDone = false, }) => {
5
8
  const [buffer, setBuffer] = useState("");
6
9
  useInput((input, key) => {
7
10
  if (isDone)
@@ -16,7 +19,7 @@ export const ConfirmView = ({ candidate, resolvedValues, danger, onConfirm, onCa
16
19
  }
17
20
  if (danger) {
18
21
  if (key.return) {
19
- if (buffer === "EXECUTE") {
22
+ if (isDangerConfirmationInput(buffer)) {
20
23
  onConfirm();
21
24
  }
22
25
  else {
@@ -48,7 +51,10 @@ export const ConfirmView = ({ candidate, resolvedValues, danger, onConfirm, onCa
48
51
  "Risk: ",
49
52
  danger.reason),
50
53
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
51
- React.createElement(SelectedCommandDisplay, { candidate: candidate, resolvedValues: resolvedValues })),
54
+ React.createElement(SelectedCommandDisplay, { candidate: candidate, resolvedValues: resolvedValues }),
55
+ React.createElement(Text, null,
56
+ "Final command: ",
57
+ React.createElement(Text, { color: "yellow" }, command))),
52
58
  !isDone && (React.createElement(React.Fragment, null,
53
59
  React.createElement(Box, { marginTop: 1 },
54
60
  React.createElement(Text, null,
@@ -62,6 +68,9 @@ export const ConfirmView = ({ candidate, resolvedValues, danger, onConfirm, onCa
62
68
  }
63
69
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
64
70
  React.createElement(SelectedCommandDisplay, { candidate: candidate, resolvedValues: resolvedValues }),
71
+ React.createElement(Text, null,
72
+ "Final command: ",
73
+ React.createElement(Text, { color: "yellow" }, command)),
65
74
  !isDone && (React.createElement(Box, { marginTop: 1 },
66
75
  React.createElement(Text, null,
67
76
  "Press ",