@unscientificjszhai/howto 1.0.0-alpha.2
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/LICENSE +21 -0
- package/dist/ai/errors.js +30 -0
- package/dist/ai/gemini.js +39 -0
- package/dist/ai/index.js +15 -0
- package/dist/ai/openai.js +43 -0
- package/dist/ai/types.js +1 -0
- package/dist/cli.js +140 -0
- package/dist/config-file.js +66 -0
- package/dist/config.js +49 -0
- package/dist/env.js +10 -0
- package/dist/errors.js +57 -0
- package/dist/execute.js +47 -0
- package/dist/index.js +137 -0
- package/dist/init/index.js +146 -0
- package/dist/prompt.js +55 -0
- package/dist/safety/dangerous-command.js +104 -0
- package/dist/ui/App.js +93 -0
- package/dist/ui/ConfirmView.js +75 -0
- package/dist/ui/LoadingView.js +17 -0
- package/dist/ui/ResolvePlaceholdersView.js +80 -0
- package/dist/ui/SelectCommandView.js +39 -0
- package/dist/ui/SelectedCommandDisplay.js +31 -0
- package/dist/ui/confirm.js +117 -0
- package/dist/ui/interactive.js +115 -0
- package/dist/ui/key-input.js +23 -0
- package/dist/ui/loading.js +40 -0
- package/dist/ui/placeholder-logic.js +19 -0
- package/dist/ui/placeholders.js +168 -0
- package/dist/ui/tty.js +17 -0
- package/dist/validation/ai-response.js +102 -0
- package/dist/validation/command-tool.js +185 -0
- package/package.json +50 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.confirmFinalCommand = confirmFinalCommand;
|
|
16
|
+
exports.confirmDangerousCommand = confirmDangerousCommand;
|
|
17
|
+
exports.formatFinalCommandConfirmation = formatFinalCommandConfirmation;
|
|
18
|
+
exports.formatDangerousCommandConfirmation = formatDangerousCommandConfirmation;
|
|
19
|
+
const readline_1 = __importDefault(require("readline"));
|
|
20
|
+
const interactive_1 = require("./interactive");
|
|
21
|
+
const key_input_1 = require("./key-input");
|
|
22
|
+
function confirmFinalCommand(command_1) {
|
|
23
|
+
return __awaiter(this, arguments, void 0, function* (command, streams = {}) {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
|
|
26
|
+
const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
|
|
27
|
+
(0, interactive_1.ensureInteractiveTty)(input, output);
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let cleanupKeypressSession;
|
|
30
|
+
const cleanup = () => {
|
|
31
|
+
cleanupKeypressSession === null || cleanupKeypressSession === void 0 ? void 0 : cleanupKeypressSession();
|
|
32
|
+
cleanupKeypressSession = undefined;
|
|
33
|
+
};
|
|
34
|
+
const finish = () => {
|
|
35
|
+
cleanup();
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const cancel = () => {
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new interactive_1.InteractionCancelledError("Final command confirmation cancelled"));
|
|
41
|
+
};
|
|
42
|
+
const onKeypress = (_inputText, key = {}) => {
|
|
43
|
+
if (key.ctrl === true && key.name === "c") {
|
|
44
|
+
cancel();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (key.name === "escape") {
|
|
48
|
+
cancel();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (key.name === "return" || key.name === "enter") {
|
|
52
|
+
finish();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
output.write(formatFinalCommandConfirmation(command));
|
|
56
|
+
cleanupKeypressSession = (0, key_input_1.beginKeypressSession)(input, onKeypress);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function confirmDangerousCommand(command_1, match_1) {
|
|
61
|
+
return __awaiter(this, arguments, void 0, function* (command, match, streams = {}) {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
|
|
64
|
+
const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
|
|
65
|
+
(0, interactive_1.ensureInteractiveTty)(input, output);
|
|
66
|
+
const rl = readline_1.default.createInterface({
|
|
67
|
+
input,
|
|
68
|
+
output,
|
|
69
|
+
terminal: input.isTTY === true && output.isTTY === true,
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
output.write(formatDangerousCommandConfirmation(command, match));
|
|
73
|
+
const answer = yield readConfirmationLine(rl, input);
|
|
74
|
+
if (answer !== "EXECUTE") {
|
|
75
|
+
throw new interactive_1.InteractionCancelledError("Dangerous command confirmation cancelled");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rl.close();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function formatFinalCommandConfirmation(command) {
|
|
84
|
+
return `\nFinal command:\n${command}\n\nPress Enter to execute, Esc or Ctrl+C to cancel.\n`;
|
|
85
|
+
}
|
|
86
|
+
function formatDangerousCommandConfirmation(command, match) {
|
|
87
|
+
return [
|
|
88
|
+
"\nDangerous command detected.",
|
|
89
|
+
`Rule: ${match.rule}`,
|
|
90
|
+
`Risk: ${match.reason}`,
|
|
91
|
+
"",
|
|
92
|
+
"Final command:",
|
|
93
|
+
command,
|
|
94
|
+
"",
|
|
95
|
+
'Type EXECUTE to continue, or anything else to cancel.',
|
|
96
|
+
"> ",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
function readConfirmationLine(rl, input) {
|
|
100
|
+
input.resume();
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
rl.off("line", onLine);
|
|
104
|
+
rl.off("SIGINT", onSigint);
|
|
105
|
+
};
|
|
106
|
+
const onLine = (line) => {
|
|
107
|
+
cleanup();
|
|
108
|
+
resolve(line);
|
|
109
|
+
};
|
|
110
|
+
const onSigint = () => {
|
|
111
|
+
cleanup();
|
|
112
|
+
reject(new interactive_1.InteractionCancelledError("Dangerous command confirmation cancelled"));
|
|
113
|
+
};
|
|
114
|
+
rl.on("line", onLine);
|
|
115
|
+
rl.on("SIGINT", onSigint);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.InteractionCancelledError = exports.InteractiveTtyError = void 0;
|
|
13
|
+
exports.selectCommandCandidate = selectCommandCandidate;
|
|
14
|
+
exports.ensureInteractiveTty = ensureInteractiveTty;
|
|
15
|
+
exports.formatCandidateSelection = formatCandidateSelection;
|
|
16
|
+
const key_input_1 = require("./key-input");
|
|
17
|
+
class InteractiveTtyError extends Error {
|
|
18
|
+
constructor(message = "Interactive mode requires a TTY. Run howto in an interactive terminal or use --print.") {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "InteractiveTtyError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.InteractiveTtyError = InteractiveTtyError;
|
|
24
|
+
class InteractionCancelledError extends Error {
|
|
25
|
+
constructor(message = "Interactive selection cancelled") {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "InteractionCancelledError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.InteractionCancelledError = InteractionCancelledError;
|
|
31
|
+
function selectCommandCandidate(candidates_1) {
|
|
32
|
+
return __awaiter(this, arguments, void 0, function* (candidates, streams = {}) {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
|
|
35
|
+
const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
|
|
36
|
+
ensureInteractiveTty(input, output);
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let activeIndex = 0;
|
|
39
|
+
let renderedLineCount = 0;
|
|
40
|
+
let cleanupKeypressSession;
|
|
41
|
+
const clearRenderedSelection = () => {
|
|
42
|
+
if (renderedLineCount > 0) {
|
|
43
|
+
output.write(`\x1B[${renderedLineCount}A\x1B[J`);
|
|
44
|
+
renderedLineCount = 0;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
cleanupKeypressSession === null || cleanupKeypressSession === void 0 ? void 0 : cleanupKeypressSession();
|
|
49
|
+
cleanupKeypressSession = undefined;
|
|
50
|
+
clearRenderedSelection();
|
|
51
|
+
output.write("\x1B[?25h");
|
|
52
|
+
};
|
|
53
|
+
const finish = (candidate) => {
|
|
54
|
+
cleanup();
|
|
55
|
+
resolve(candidate);
|
|
56
|
+
};
|
|
57
|
+
const cancel = () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
reject(new InteractionCancelledError());
|
|
60
|
+
};
|
|
61
|
+
const render = () => {
|
|
62
|
+
const text = formatCandidateSelection(candidates, activeIndex);
|
|
63
|
+
clearRenderedSelection();
|
|
64
|
+
output.write(`\x1B[?25l${text}\n`);
|
|
65
|
+
renderedLineCount = text.split("\n").length + 1;
|
|
66
|
+
};
|
|
67
|
+
const onKeypress = (_inputText, key = {}) => {
|
|
68
|
+
if (key.ctrl === true && key.name === "c") {
|
|
69
|
+
cancel();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.name === "escape") {
|
|
73
|
+
cancel();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (key.name === "return" || key.name === "enter") {
|
|
77
|
+
finish(candidates[activeIndex]);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.name === "up") {
|
|
81
|
+
activeIndex = (activeIndex - 1 + candidates.length) % candidates.length;
|
|
82
|
+
render();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.name === "down") {
|
|
86
|
+
activeIndex = (activeIndex + 1) % candidates.length;
|
|
87
|
+
render();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
cleanupKeypressSession = (0, key_input_1.beginKeypressSession)(input, onKeypress);
|
|
91
|
+
render();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function ensureInteractiveTty(input, output) {
|
|
96
|
+
if (input.isTTY !== true || output.isTTY !== true) {
|
|
97
|
+
throw new InteractiveTtyError();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function formatCandidateSelection(candidates, activeIndex) {
|
|
101
|
+
const lines = ["? Select a command", ""];
|
|
102
|
+
candidates.forEach((candidate, index) => {
|
|
103
|
+
const prefix = index === activeIndex ? ">" : " ";
|
|
104
|
+
const titleLine = `${prefix} ${candidate.title}`;
|
|
105
|
+
lines.push(index === activeIndex ? `\x1B[7m${titleLine}\x1B[0m` : titleLine);
|
|
106
|
+
lines.push(` ${candidate.command}`);
|
|
107
|
+
lines.push(` ${candidate.description}`);
|
|
108
|
+
if (index < candidates.length - 1) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push("Use Up/Down to move, Enter to select, Esc or Ctrl+C to cancel.");
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.beginKeypressSession = beginKeypressSession;
|
|
7
|
+
const readline_1 = __importDefault(require("readline"));
|
|
8
|
+
function beginKeypressSession(input, onKeypress) {
|
|
9
|
+
const wasRaw = input.isRaw === true;
|
|
10
|
+
readline_1.default.emitKeypressEvents(input);
|
|
11
|
+
input.on("keypress", onKeypress);
|
|
12
|
+
if (typeof input.setRawMode === "function") {
|
|
13
|
+
input.setRawMode(true);
|
|
14
|
+
}
|
|
15
|
+
input.resume();
|
|
16
|
+
return () => {
|
|
17
|
+
input.off("keypress", onKeypress);
|
|
18
|
+
if (typeof input.setRawMode === "function" && !wasRaw) {
|
|
19
|
+
input.setRawMode(false);
|
|
20
|
+
}
|
|
21
|
+
input.pause();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAiLoadingIndicator = createAiLoadingIndicator;
|
|
4
|
+
const FRAMES = ["|", "/", "-", "\\"];
|
|
5
|
+
const MESSAGE = "Waiting for AI response...";
|
|
6
|
+
function createAiLoadingIndicator(options) {
|
|
7
|
+
var _a, _b;
|
|
8
|
+
const output = (_a = options.output) !== null && _a !== void 0 ? _a : process.stdout;
|
|
9
|
+
const intervalMs = (_b = options.intervalMs) !== null && _b !== void 0 ? _b : 120;
|
|
10
|
+
let timer;
|
|
11
|
+
let frameIndex = 0;
|
|
12
|
+
let running = false;
|
|
13
|
+
const shouldRender = () => options.enabled && output.isTTY === true;
|
|
14
|
+
const render = () => {
|
|
15
|
+
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
16
|
+
frameIndex += 1;
|
|
17
|
+
output.write(`\r${frame} ${MESSAGE}`);
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
start() {
|
|
21
|
+
if (!shouldRender() || running) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
running = true;
|
|
25
|
+
render();
|
|
26
|
+
timer = setInterval(render, intervalMs);
|
|
27
|
+
},
|
|
28
|
+
stop() {
|
|
29
|
+
if (!running) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (timer !== undefined) {
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
timer = undefined;
|
|
35
|
+
}
|
|
36
|
+
running = false;
|
|
37
|
+
output.write("\r\x1B[2K");
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const UNRESOLVED_PLACEHOLDER_PATTERN = /{{[^{}]*}}/;
|
|
2
|
+
export class PlaceholderResolutionError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PlaceholderResolutionError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function replaceCommandPlaceholders(command, values) {
|
|
9
|
+
let resolvedCommand = command;
|
|
10
|
+
for (const [name, value] of values) {
|
|
11
|
+
resolvedCommand = resolvedCommand.split(`{{${name}}}`).join(value);
|
|
12
|
+
}
|
|
13
|
+
return resolvedCommand;
|
|
14
|
+
}
|
|
15
|
+
export function assertNoUnresolvedPlaceholders(command) {
|
|
16
|
+
if (UNRESOLVED_PLACEHOLDER_PATTERN.test(command)) {
|
|
17
|
+
throw new PlaceholderResolutionError("final command contains unresolved placeholders");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PlaceholderSelectionBackError = exports.PlaceholderResolutionError = void 0;
|
|
13
|
+
exports.resolveCommandPlaceholders = resolveCommandPlaceholders;
|
|
14
|
+
exports.promptPlaceholderValues = promptPlaceholderValues;
|
|
15
|
+
exports.replaceCommandPlaceholders = replaceCommandPlaceholders;
|
|
16
|
+
exports.assertNoUnresolvedPlaceholders = assertNoUnresolvedPlaceholders;
|
|
17
|
+
exports.formatPlaceholderPrompt = formatPlaceholderPrompt;
|
|
18
|
+
const interactive_1 = require("./interactive");
|
|
19
|
+
const key_input_1 = require("./key-input");
|
|
20
|
+
class PlaceholderResolutionError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PlaceholderResolutionError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.PlaceholderResolutionError = PlaceholderResolutionError;
|
|
27
|
+
class PlaceholderSelectionBackError extends Error {
|
|
28
|
+
constructor(message = "Return to command selection") {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "PlaceholderSelectionBackError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.PlaceholderSelectionBackError = PlaceholderSelectionBackError;
|
|
34
|
+
const UNRESOLVED_PLACEHOLDER_PATTERN = /{{[^{}]*}}/;
|
|
35
|
+
function resolveCommandPlaceholders(candidate_1) {
|
|
36
|
+
return __awaiter(this, arguments, void 0, function* (candidate, streams = {}) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
const input = (_a = streams.input) !== null && _a !== void 0 ? _a : process.stdin;
|
|
39
|
+
const output = (_b = streams.output) !== null && _b !== void 0 ? _b : process.stdout;
|
|
40
|
+
const values = yield promptPlaceholderValues(candidate.placeholders, { input, output });
|
|
41
|
+
const command = replaceCommandPlaceholders(candidate.command, values);
|
|
42
|
+
assertNoUnresolvedPlaceholders(command);
|
|
43
|
+
return command;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function promptPlaceholderValues(placeholders, streams) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
if (placeholders.length === 0) {
|
|
49
|
+
return new Map();
|
|
50
|
+
}
|
|
51
|
+
return readPlaceholderValuesWithRawInput(placeholders, streams);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function replaceCommandPlaceholders(command, values) {
|
|
55
|
+
let resolvedCommand = command;
|
|
56
|
+
for (const [name, value] of values) {
|
|
57
|
+
resolvedCommand = resolvedCommand.split(`{{${name}}}`).join(value);
|
|
58
|
+
}
|
|
59
|
+
return resolvedCommand;
|
|
60
|
+
}
|
|
61
|
+
function assertNoUnresolvedPlaceholders(command) {
|
|
62
|
+
if (UNRESOLVED_PLACEHOLDER_PATTERN.test(command)) {
|
|
63
|
+
throw new PlaceholderResolutionError("final command contains unresolved placeholders");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readPlaceholderValuesWithRawInput(placeholders, streams) {
|
|
67
|
+
const { input, output } = streams;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let activeIndex = 0;
|
|
70
|
+
let renderedLineCount = 0;
|
|
71
|
+
const values = placeholders.map(() => "");
|
|
72
|
+
let buffer = "";
|
|
73
|
+
let cleanupKeypressSession;
|
|
74
|
+
const clearRenderedPrompt = () => {
|
|
75
|
+
if (renderedLineCount > 0) {
|
|
76
|
+
output.write(`\x1B[${renderedLineCount}A\x1B[J`);
|
|
77
|
+
renderedLineCount = 0;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const cleanup = () => {
|
|
81
|
+
cleanupKeypressSession === null || cleanupKeypressSession === void 0 ? void 0 : cleanupKeypressSession();
|
|
82
|
+
cleanupKeypressSession = undefined;
|
|
83
|
+
clearRenderedPrompt();
|
|
84
|
+
};
|
|
85
|
+
const finish = () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve(new Map(placeholders.map((placeholder, index) => [placeholder.name, values[index]])));
|
|
88
|
+
};
|
|
89
|
+
const cancel = () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(new interactive_1.InteractionCancelledError("Placeholder input cancelled"));
|
|
92
|
+
};
|
|
93
|
+
const backToSelection = () => {
|
|
94
|
+
cleanup();
|
|
95
|
+
reject(new PlaceholderSelectionBackError());
|
|
96
|
+
};
|
|
97
|
+
const render = () => {
|
|
98
|
+
const text = formatPlaceholderPrompt(placeholders, values, activeIndex, buffer);
|
|
99
|
+
clearRenderedPrompt();
|
|
100
|
+
output.write(`${text}\n`);
|
|
101
|
+
renderedLineCount = text.split("\n").length + 1;
|
|
102
|
+
};
|
|
103
|
+
const onKeypress = (inputText, key = {}) => {
|
|
104
|
+
if (key.ctrl === true && key.name === "c") {
|
|
105
|
+
cancel();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (key.name === "escape") {
|
|
109
|
+
if (activeIndex === 0) {
|
|
110
|
+
values[0] = "";
|
|
111
|
+
buffer = "";
|
|
112
|
+
render();
|
|
113
|
+
backToSelection();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
values[activeIndex] = "";
|
|
117
|
+
activeIndex -= 1;
|
|
118
|
+
buffer = values[activeIndex];
|
|
119
|
+
render();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (key.name === "return" || key.name === "enter") {
|
|
123
|
+
values[activeIndex] = buffer;
|
|
124
|
+
if (activeIndex === placeholders.length - 1) {
|
|
125
|
+
render();
|
|
126
|
+
finish();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
activeIndex += 1;
|
|
130
|
+
buffer = values[activeIndex];
|
|
131
|
+
render();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (key.name === "backspace") {
|
|
135
|
+
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
136
|
+
render();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (isPrintableInput(inputText)) {
|
|
140
|
+
buffer += inputText;
|
|
141
|
+
render();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
cleanupKeypressSession = (0, key_input_1.beginKeypressSession)(input, onKeypress);
|
|
145
|
+
render();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function formatPlaceholderPrompt(placeholders, values, activeIndex, buffer) {
|
|
149
|
+
const lines = ["", "? Fill command placeholders", ""];
|
|
150
|
+
placeholders.slice(0, activeIndex + 1).forEach((placeholder, index) => {
|
|
151
|
+
lines.push(`${placeholder.name}: ${placeholder.description}`);
|
|
152
|
+
if (index < activeIndex) {
|
|
153
|
+
lines.push(`> ${values[index]}`);
|
|
154
|
+
}
|
|
155
|
+
else if (index === activeIndex) {
|
|
156
|
+
lines.push(`> ${buffer}`);
|
|
157
|
+
}
|
|
158
|
+
if (index <= activeIndex && index < placeholders.length - 1) {
|
|
159
|
+
lines.push("");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push("Press Enter for next value, Esc to go back, Ctrl+C to cancel.");
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
function isPrintableInput(inputText) {
|
|
167
|
+
return inputText.length > 0 && !/[\x00-\x1F\x7F]/.test(inputText);
|
|
168
|
+
}
|
package/dist/ui/tty.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class InteractiveTtyError extends Error {
|
|
2
|
+
constructor(message = "Interactive mode requires a TTY. Run howto in an interactive terminal or use --print.") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "InteractiveTtyError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class InteractionCancelledError extends Error {
|
|
8
|
+
constructor(message = "Interactive selection cancelled") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "InteractionCancelledError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function ensureInteractiveTty(input, output) {
|
|
14
|
+
if (input.isTTY !== true || output.isTTY !== true) {
|
|
15
|
+
throw new InteractiveTtyError();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class AiResponseValidationError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "AiResponseValidationError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
const PLACEHOLDER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
8
|
+
const PLACEHOLDER_REFERENCE_PATTERN = /{{([^{}]*)}}/g;
|
|
9
|
+
export function parseAndValidateAiResponse(rawText) {
|
|
10
|
+
const parsed = parseJsonObject(rawText);
|
|
11
|
+
const commands = readCommands(parsed);
|
|
12
|
+
return {
|
|
13
|
+
commands: commands.map((command, index) => validateCommandCandidate(command, index)),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function parseJsonObject(rawText) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(rawText);
|
|
19
|
+
}
|
|
20
|
+
catch (_a) {
|
|
21
|
+
throw new AiResponseValidationError("AI response is not valid JSON");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function readCommands(value) {
|
|
25
|
+
const root = asRecord(value, "AI response root must be a JSON object");
|
|
26
|
+
const commands = root.commands;
|
|
27
|
+
if (!Array.isArray(commands)) {
|
|
28
|
+
throw new AiResponseValidationError("AI response must contain a commands array");
|
|
29
|
+
}
|
|
30
|
+
if (commands.length < 1 || commands.length > 3) {
|
|
31
|
+
throw new AiResponseValidationError("commands array must contain 1 to 3 items");
|
|
32
|
+
}
|
|
33
|
+
return commands;
|
|
34
|
+
}
|
|
35
|
+
function validateCommandCandidate(value, index) {
|
|
36
|
+
const path = `commands[${index}]`;
|
|
37
|
+
const candidate = asRecord(value, `${path} must be an object`);
|
|
38
|
+
const title = readNonEmptyString(candidate.title, `${path}.title`);
|
|
39
|
+
const command = readNonEmptyString(candidate.command, `${path}.command`);
|
|
40
|
+
const description = readNonEmptyString(candidate.description, `${path}.description`);
|
|
41
|
+
const placeholders = readPlaceholders(candidate.placeholders, path);
|
|
42
|
+
validatePlaceholderReferences(command, placeholders, path);
|
|
43
|
+
return {
|
|
44
|
+
title,
|
|
45
|
+
command,
|
|
46
|
+
description,
|
|
47
|
+
placeholders,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function readPlaceholders(value, candidatePath) {
|
|
51
|
+
const path = `${candidatePath}.placeholders`;
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
throw new AiResponseValidationError(`${path} must be an array`);
|
|
54
|
+
}
|
|
55
|
+
return value.map((placeholder, index) => validatePlaceholder(placeholder, `${path}[${index}]`));
|
|
56
|
+
}
|
|
57
|
+
function validatePlaceholder(value, path) {
|
|
58
|
+
const placeholder = asRecord(value, `${path} must be an object`);
|
|
59
|
+
const name = readNonEmptyString(placeholder.name, `${path}.name`);
|
|
60
|
+
const description = readNonEmptyString(placeholder.description, `${path}.description`);
|
|
61
|
+
if (!PLACEHOLDER_NAME_PATTERN.test(name)) {
|
|
62
|
+
throw new AiResponseValidationError(`${path}.name may contain only letters, numbers, underscores, or hyphens`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
description,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function validatePlaceholderReferences(command, placeholders, candidatePath) {
|
|
70
|
+
const declaredNames = new Set(placeholders.map((placeholder) => placeholder.name));
|
|
71
|
+
const usedNames = new Set();
|
|
72
|
+
for (const match of command.matchAll(PLACEHOLDER_REFERENCE_PATTERN)) {
|
|
73
|
+
const name = match[1];
|
|
74
|
+
if (!PLACEHOLDER_NAME_PATTERN.test(name)) {
|
|
75
|
+
throw new AiResponseValidationError(`${candidatePath}.command contains invalid placeholder reference {{${name}}}`);
|
|
76
|
+
}
|
|
77
|
+
if (!declaredNames.has(name)) {
|
|
78
|
+
throw new AiResponseValidationError(`${candidatePath}.command references undeclared placeholder {{${name}}}`);
|
|
79
|
+
}
|
|
80
|
+
usedNames.add(name);
|
|
81
|
+
}
|
|
82
|
+
for (const declaredName of declaredNames) {
|
|
83
|
+
if (!usedNames.has(declaredName)) {
|
|
84
|
+
throw new AiResponseValidationError(`${candidatePath}.placeholders declares unused placeholder ${declaredName}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function readNonEmptyString(value, path) {
|
|
89
|
+
if (typeof value !== "string") {
|
|
90
|
+
throw new AiResponseValidationError(`${path} must be a string`);
|
|
91
|
+
}
|
|
92
|
+
if (value.trim() === "") {
|
|
93
|
+
throw new AiResponseValidationError(`${path} must be a non-empty string`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function asRecord(value, message) {
|
|
98
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
99
|
+
throw new AiResponseValidationError(message);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|