@unscientificjszhai/howto 1.0.0-alpha.3 → 1.0.1
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/README.CN.md +37 -5
- package/README.md +37 -5
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/dist/ai/command-schema.js +2 -0
- package/dist/ai/errors.js +7 -23
- package/dist/ai/gemini.js +67 -27
- package/dist/ai/openai.js +35 -27
- package/dist/cli.js +12 -1
- package/dist/config-file.js +97 -51
- package/dist/config.js +7 -6
- package/dist/errors.js +17 -16
- package/dist/execute.js +12 -25
- package/dist/index.js +85 -97
- package/dist/init/InitializationApp.js +175 -32
- package/dist/init/index.js +35 -33
- package/dist/init/state.js +46 -23
- package/dist/prompt.js +5 -2
- package/dist/safety/dangerous-command.js +262 -93
- package/dist/shell/command-analysis.js +373 -0
- package/dist/terminal-text.js +33 -0
- package/dist/ui/App.js +84 -41
- package/dist/ui/ConfirmView.js +166 -61
- package/dist/ui/InteractiveSessionProvider.js +19 -0
- package/dist/ui/ResolvePlaceholdersView.js +131 -72
- package/dist/ui/SelectCommandView.js +73 -23
- package/dist/ui/SelectedCommandDisplay.js +11 -9
- package/dist/ui/interactive-session.js +407 -0
- package/dist/ui/paste-framing.js +105 -0
- package/dist/ui/placeholder-logic.js +109 -8
- package/dist/ui/resize-safe-output.js +73 -0
- package/dist/ui/run-interactive-command.js +55 -0
- package/dist/ui/single-line-preview.js +28 -0
- package/dist/ui/text-input.js +53 -0
- package/dist/ui/use-keyboard-input.js +12 -0
- package/dist/ui/use-paste-aware-input.js +7 -0
- package/dist/user-visible-error.js +19 -0
- package/dist/validation/ai-response.js +11 -4
- package/dist/validation/command-tool.js +25 -127
- package/dist/validation/generated-commands.js +8 -0
- package/dist/version.js +38 -0
- package/package.json +21 -16
package/dist/config-file.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
13
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
14
4
|
import { ConfigError } from "./config.js";
|
|
15
5
|
const CONFIG_FILE_FIELDS = new Set([
|
|
16
6
|
"aiProvider",
|
|
@@ -21,54 +11,110 @@ const CONFIG_FILE_FIELDS = new Set([
|
|
|
21
11
|
"openaiModel",
|
|
22
12
|
"structuredOutput",
|
|
23
13
|
]);
|
|
24
|
-
export function getConfigFilePath(env = process.env) {
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
export function getConfigFilePath(env = process.env, getSystemHomeDirectory = () => userInfo().homedir) {
|
|
15
|
+
const configuredHome = env.HOME;
|
|
16
|
+
let homeDirectory;
|
|
17
|
+
if (configuredHome === undefined || configuredHome.trim() === "") {
|
|
18
|
+
try {
|
|
19
|
+
homeDirectory = getSystemHomeDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new ConfigError("failed to resolve user home directory");
|
|
23
|
+
}
|
|
24
|
+
if (typeof homeDirectory !== "string" ||
|
|
25
|
+
homeDirectory.trim() === "" ||
|
|
26
|
+
!isAbsolute(homeDirectory)) {
|
|
27
|
+
throw new ConfigError("failed to resolve user home directory");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
homeDirectory = configuredHome;
|
|
32
|
+
}
|
|
33
|
+
const configDirectory = join(homeDirectory, ".howto");
|
|
34
|
+
assertAbsoluteConfigDirectory(configDirectory);
|
|
35
|
+
return join(configDirectory, "config.json");
|
|
27
36
|
}
|
|
28
|
-
export function readUserConfigFile() {
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
export async function readUserConfigFile(path = getConfigFilePath()) {
|
|
38
|
+
let contents;
|
|
39
|
+
try {
|
|
40
|
+
contents = await readFile(path, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isFileNotFoundError(error)) {
|
|
31
44
|
return {};
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
throw new ConfigError("failed to read user config file");
|
|
47
|
+
}
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(contents);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new ConfigError("user config file is not valid JSON");
|
|
54
|
+
}
|
|
55
|
+
if (!isPlainObject(parsed)) {
|
|
56
|
+
throw new ConfigError("user config file must contain a JSON object");
|
|
57
|
+
}
|
|
58
|
+
const config = {};
|
|
59
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
60
|
+
if (!CONFIG_FILE_FIELDS.has(key)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (key === "structuredOutput") {
|
|
64
|
+
if (typeof value !== "string" && typeof value !== "boolean") {
|
|
65
|
+
throw new ConfigError(`config file field ${key} must be a boolean or string`);
|
|
66
|
+
}
|
|
67
|
+
config.structuredOutput = value;
|
|
68
|
+
continue;
|
|
36
69
|
}
|
|
37
|
-
|
|
38
|
-
throw new ConfigError(`
|
|
70
|
+
if (typeof value !== "string") {
|
|
71
|
+
throw new ConfigError(`config file field ${key} must be a string`);
|
|
39
72
|
}
|
|
40
|
-
|
|
41
|
-
|
|
73
|
+
config[key] = value;
|
|
74
|
+
}
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
export async function writeUserConfigFile(config, path = getConfigFilePath()) {
|
|
78
|
+
const configDirectory = dirname(path);
|
|
79
|
+
assertAbsoluteConfigDirectory(configDirectory);
|
|
80
|
+
const serializedConfig = `${JSON.stringify(config, null, 2)}\n`;
|
|
81
|
+
let temporaryDirectory;
|
|
82
|
+
try {
|
|
83
|
+
await mkdir(configDirectory, { recursive: true });
|
|
84
|
+
temporaryDirectory = await mkdtemp(join(configDirectory, `.${basename(path)}-`));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new ConfigError("failed to save user config file");
|
|
88
|
+
}
|
|
89
|
+
const temporaryPath = join(temporaryDirectory, basename(path));
|
|
90
|
+
try {
|
|
91
|
+
await writeFile(temporaryPath, serializedConfig, { encoding: "utf8", mode: 0o600 });
|
|
92
|
+
await rename(temporaryPath, path);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
try {
|
|
96
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
42
97
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!CONFIG_FILE_FIELDS.has(key)) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (key === "structuredOutput") {
|
|
49
|
-
if (typeof value !== "string" && typeof value !== "boolean") {
|
|
50
|
-
throw new ConfigError(`config file field ${key} must be a boolean or string`);
|
|
51
|
-
}
|
|
52
|
-
config.structuredOutput = value;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (typeof value !== "string") {
|
|
56
|
-
throw new ConfigError(`config file field ${key} must be a string`);
|
|
57
|
-
}
|
|
58
|
-
config[key] = value;
|
|
98
|
+
catch {
|
|
99
|
+
throw new ConfigError("failed to save user config file and remove temporary config data");
|
|
59
100
|
}
|
|
60
|
-
|
|
61
|
-
}
|
|
101
|
+
throw new ConfigError("failed to save user config file");
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// 配置已经通过原子重命名提交;空临时目录清理失败不能把成功写入报告为失败。
|
|
108
|
+
}
|
|
62
109
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
110
|
+
function assertAbsoluteConfigDirectory(path) {
|
|
111
|
+
if (!isAbsolute(path)) {
|
|
112
|
+
throw new ConfigError("config directory must be an absolute path");
|
|
113
|
+
}
|
|
68
114
|
}
|
|
69
115
|
function isPlainObject(value) {
|
|
70
116
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
117
|
}
|
|
72
|
-
function
|
|
73
|
-
return error instanceof Error
|
|
118
|
+
function isFileNotFoundError(error) {
|
|
119
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
74
120
|
}
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,6 @@ export function hasExplicitAiProvider(options, env, fileConfig = {}) {
|
|
|
11
11
|
return (pickConfigValue(options.aiProvider, env.HOWTO_AI_PROVIDER, fileConfig.aiProvider) !== undefined);
|
|
12
12
|
}
|
|
13
13
|
export function loadConfig(options, env, fileConfig = {}) {
|
|
14
|
-
var _a, _b, _c;
|
|
15
14
|
const aiProviderValue = pickConfigValue(options.aiProvider, env.HOWTO_AI_PROVIDER, fileConfig.aiProvider);
|
|
16
15
|
if (aiProviderValue === undefined) {
|
|
17
16
|
throw new ConfigError("AI provider is not configured. Set --ai-provider or HOWTO_AI_PROVIDER, or run howto in an interactive terminal to initialize it.");
|
|
@@ -21,11 +20,14 @@ export function loadConfig(options, env, fileConfig = {}) {
|
|
|
21
20
|
aiProvider,
|
|
22
21
|
gemini: {
|
|
23
22
|
apiKey: pickConfigValue(options.geminiApiKey, env.HOWTO_GEMINI_API_KEY, fileConfig.geminiApiKey),
|
|
24
|
-
model:
|
|
23
|
+
model: pickConfigValue(options.geminiModel, env.HOWTO_GEMINI_MODEL, fileConfig.geminiModel) ??
|
|
24
|
+
DEFAULT_GEMINI_MODEL,
|
|
25
25
|
},
|
|
26
26
|
openai: {
|
|
27
|
-
apiKey:
|
|
28
|
-
|
|
27
|
+
apiKey: pickConfigValue(options.openaiApiKey, env.HOWTO_OPENAI_API_KEY, fileConfig.openaiApiKey) ??
|
|
28
|
+
"",
|
|
29
|
+
model: pickConfigValue(options.openaiModel, env.HOWTO_OPENAI_MODEL, fileConfig.openaiModel) ??
|
|
30
|
+
DEFAULT_OPENAI_MODEL,
|
|
29
31
|
baseUrl: pickConfigValue(options.openaiApiUrl, env.HOWTO_OPENAI_API_URL, fileConfig.openaiApiUrl),
|
|
30
32
|
},
|
|
31
33
|
structuredOutput: parseStructuredOutput(pickConfigValue(options.structuredOutput, env.HOWTO_STRUCTURED_OUTPUT, fileConfig.structuredOutput)),
|
|
@@ -36,8 +38,7 @@ export function loadConfig(options, env, fileConfig = {}) {
|
|
|
36
38
|
return config;
|
|
37
39
|
}
|
|
38
40
|
function pickConfigValue(cliValue, envValue, fileValue) {
|
|
39
|
-
|
|
40
|
-
return (_a = cliValue !== null && cliValue !== void 0 ? cliValue : envValue) !== null && _a !== void 0 ? _a : fileValue;
|
|
41
|
+
return cliValue ?? envValue ?? fileValue;
|
|
41
42
|
}
|
|
42
43
|
function parseAiProvider(value) {
|
|
43
44
|
if (value === "openai" || value === "gemini") {
|
package/dist/errors.js
CHANGED
|
@@ -4,7 +4,16 @@ import { ConfigError } from "./config.js";
|
|
|
4
4
|
import { AiResponseValidationError } from "./validation/ai-response.js";
|
|
5
5
|
import { InteractionCancelledError, InteractiveTtyError } from "./ui/tty.js";
|
|
6
6
|
import { PlaceholderResolutionError } from "./ui/placeholder-logic.js";
|
|
7
|
+
import { sanitizeUserVisibleErrorMessage } from "./user-visible-error.js";
|
|
8
|
+
export { sanitizeUserVisibleErrorMessage };
|
|
9
|
+
export function getUserVisibleErrorMessage(error) {
|
|
10
|
+
if (error instanceof AiProviderError) {
|
|
11
|
+
return `AI provider request failed (provider: ${error.provider}, model: ${sanitizeUserVisibleErrorMessage(error.model)})`;
|
|
12
|
+
}
|
|
13
|
+
return sanitizeUserVisibleErrorMessage(error.message);
|
|
14
|
+
}
|
|
7
15
|
export class AppError extends Error {
|
|
16
|
+
exitCode;
|
|
8
17
|
constructor(message, exitCode) {
|
|
9
18
|
super(message);
|
|
10
19
|
this.name = "AppError";
|
|
@@ -13,30 +22,30 @@ export class AppError extends Error {
|
|
|
13
22
|
}
|
|
14
23
|
export function toAppError(error) {
|
|
15
24
|
if (error instanceof AppError) {
|
|
16
|
-
return error;
|
|
25
|
+
return new AppError(sanitizeUserVisibleErrorMessage(error.message), error.exitCode);
|
|
17
26
|
}
|
|
18
27
|
if (error instanceof CliParseError) {
|
|
19
|
-
return new AppError(`Error: ${error.message}\n${USAGE}`, 2);
|
|
28
|
+
return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}\n${USAGE}`, 2);
|
|
20
29
|
}
|
|
21
30
|
if (error instanceof ConfigError) {
|
|
22
|
-
return new AppError(`Configuration error: ${error.message}`, 2);
|
|
31
|
+
return new AppError(`Configuration error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
|
|
23
32
|
}
|
|
24
33
|
if (error instanceof AiProviderError) {
|
|
25
|
-
return new AppError(error
|
|
34
|
+
return new AppError(getUserVisibleErrorMessage(error), 1);
|
|
26
35
|
}
|
|
27
36
|
if (error instanceof AiResponseValidationError) {
|
|
28
|
-
return new AppError(`AI response format error: ${error.message}`, 2);
|
|
37
|
+
return new AppError(`AI response format error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
|
|
29
38
|
}
|
|
30
39
|
if (error instanceof InteractiveTtyError) {
|
|
31
|
-
return new AppError(`Error: ${error.message}`, 2);
|
|
40
|
+
return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
|
|
32
41
|
}
|
|
33
42
|
if (error instanceof PlaceholderResolutionError) {
|
|
34
|
-
return new AppError(`Error: ${error.message}`, 2);
|
|
43
|
+
return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
|
|
35
44
|
}
|
|
36
45
|
if (error instanceof InteractionCancelledError) {
|
|
37
46
|
return new AppError("", 130);
|
|
38
47
|
}
|
|
39
|
-
return new AppError(`Error: ${
|
|
48
|
+
return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(getErrorMessage(error))}`, 1);
|
|
40
49
|
}
|
|
41
50
|
function getErrorMessage(error) {
|
|
42
51
|
if (error instanceof Error && error.message.trim() !== "") {
|
|
@@ -47,11 +56,3 @@ function getErrorMessage(error) {
|
|
|
47
56
|
}
|
|
48
57
|
return "unknown error";
|
|
49
58
|
}
|
|
50
|
-
function sanitizeErrorMessage(message) {
|
|
51
|
-
const singleLine = message.replace(/\s+/g, " ").trim();
|
|
52
|
-
const redacted = singleLine
|
|
53
|
-
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
54
|
-
.replace(/(api[-_ ]?key["'\s:=]+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]")
|
|
55
|
-
.replace(/(authorization["'\s:=]+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]");
|
|
56
|
-
return redacted.length > 220 ? `${redacted.slice(0, 217)}...` : redacted;
|
|
57
|
-
}
|
package/dist/execute.js
CHANGED
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import { spawn } from "child_process";
|
|
11
2
|
import { constants as osConstants } from "os";
|
|
12
|
-
export function executeCommand(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
child.once("close", (code, signal) => {
|
|
24
|
-
resolve(resolveProcessExitCode(code, signal));
|
|
25
|
-
});
|
|
3
|
+
export async function executeCommand(command, options = {}) {
|
|
4
|
+
const env = options.env ?? process.env;
|
|
5
|
+
const platform = options.platform ?? process.platform;
|
|
6
|
+
const spawnCommand = options.spawnCommand ?? spawnCommandWithNodeSpawn;
|
|
7
|
+
const child = platform === "win32"
|
|
8
|
+
? spawnCommand(command, { shell: env.SHELL || true, stdio: "inherit" }, { stdio: "inherit" })
|
|
9
|
+
: spawnCommand(resolveShell(env), ["-c", command], { stdio: "inherit" });
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
child.once("error", reject);
|
|
12
|
+
child.once("close", (code, signal) => {
|
|
13
|
+
resolve(resolveProcessExitCode(code, signal));
|
|
26
14
|
});
|
|
27
15
|
});
|
|
28
16
|
}
|
|
29
17
|
export function resolveProcessExitCode(code, signal) {
|
|
30
|
-
var _a;
|
|
31
18
|
if (typeof code === "number") {
|
|
32
19
|
return code;
|
|
33
20
|
}
|
|
34
21
|
if (signal !== null) {
|
|
35
|
-
return 128 + (
|
|
22
|
+
return 128 + (osConstants.signals[signal] ?? 1);
|
|
36
23
|
}
|
|
37
24
|
return 1;
|
|
38
25
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,122 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
-
import React from "react";
|
|
12
2
|
import { realpathSync } from "node:fs";
|
|
13
3
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { render } from "ink";
|
|
15
4
|
import { CliParseError, parseCliArgs } from "./cli.js";
|
|
16
5
|
import { ConfigError, hasExplicitAiProvider, loadConfig } from "./config.js";
|
|
17
6
|
import { readUserConfigFile } from "./config-file.js";
|
|
18
7
|
import { createCommandProvider } from "./ai/index.js";
|
|
19
8
|
import { buildCommandGenerationPrompt, createProviderPromptRequest } from "./prompt.js";
|
|
20
|
-
import { parseAndValidateAiResponse } from "./validation/ai-response.js";
|
|
21
9
|
import { checkCommandInPath } from "./validation/command-tool.js";
|
|
10
|
+
import { generateValidatedCommandCandidates } from "./validation/generated-commands.js";
|
|
22
11
|
import { ensureInteractiveTty } from "./ui/tty.js";
|
|
23
|
-
import { executeCommand } from "./execute.js";
|
|
24
12
|
import { toAppError } from "./errors.js";
|
|
25
|
-
import {
|
|
13
|
+
import { createInteractiveSession } from "./ui/interactive-session.js";
|
|
14
|
+
import { runInteractiveCommand } from "./ui/run-interactive-command.js";
|
|
26
15
|
import { initializeConfig } from "./init/index.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
ensureInteractiveTty(process.stdin, process.stdout);
|
|
50
|
-
}
|
|
51
|
-
const config = hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)
|
|
52
|
-
? loadConfig(parsedCli.options, process.env, fileConfig)
|
|
53
|
-
: yield initializeConfig({
|
|
54
|
-
cliOptions: parsedCli.options,
|
|
55
|
-
env: process.env,
|
|
56
|
-
input: process.stdin,
|
|
57
|
-
output: process.stdout,
|
|
58
|
-
});
|
|
59
|
-
const useCommandPathCheck = parsedCli.useCommand === undefined
|
|
60
|
-
? undefined
|
|
61
|
-
: checkCommandInPath(parsedCli.useCommand, process.env);
|
|
62
|
-
if (!parsedCli.options.print) {
|
|
63
|
-
ensureInteractiveTty(process.stdin, process.stdout);
|
|
64
|
-
}
|
|
65
|
-
const promptRequest = createProviderPromptRequest({
|
|
66
|
-
question: parsedCli.question,
|
|
67
|
-
arguments: parsedCli.arguments,
|
|
68
|
-
useCommand: parsedCli.useCommand,
|
|
69
|
-
structuredOutput: config.structuredOutput,
|
|
16
|
+
import { renderTerminalSafeText } from "./terminal-text.js";
|
|
17
|
+
import { readPackageVersion } from "./version.js";
|
|
18
|
+
async function run(argv) {
|
|
19
|
+
let session;
|
|
20
|
+
const getSession = () => {
|
|
21
|
+
ensureInteractiveTty(process.stdin, process.stdout);
|
|
22
|
+
session ??= createInteractiveSession({ input: process.stdin, output: process.stdout });
|
|
23
|
+
return session;
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
const parsedCli = parseCliArgs(argv);
|
|
27
|
+
if (parsedCli.options.version) {
|
|
28
|
+
console.log(readPackageVersion());
|
|
29
|
+
return { exitCode: 0 };
|
|
30
|
+
}
|
|
31
|
+
if (parsedCli.options.init) {
|
|
32
|
+
ensureInteractiveTty(process.stdin, process.stdout);
|
|
33
|
+
await initializeConfig({
|
|
34
|
+
cliOptions: parsedCli.options,
|
|
35
|
+
env: process.env,
|
|
36
|
+
session: getSession(),
|
|
70
37
|
});
|
|
71
|
-
|
|
72
|
-
|
|
38
|
+
return { exitCode: 0 };
|
|
39
|
+
}
|
|
40
|
+
const fileConfig = await readUserConfigFile();
|
|
41
|
+
if (parsedCli.question === undefined) {
|
|
42
|
+
throw new CliParseError("missing question");
|
|
43
|
+
}
|
|
44
|
+
if (!hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)) {
|
|
73
45
|
if (parsedCli.options.print) {
|
|
74
|
-
|
|
75
|
-
userPrompt }));
|
|
76
|
-
const aiResponse = parseAndValidateAiResponse(aiResult.rawText);
|
|
77
|
-
aiResponse.commands.forEach((candidate) => {
|
|
78
|
-
console.log(candidate.command);
|
|
79
|
-
});
|
|
80
|
-
return { exitCode: 0 };
|
|
81
|
-
}
|
|
82
|
-
if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
|
|
83
|
-
console.error(`Warning: requested command "${useCommandPathCheck.command}" was not found in PATH. Review before executing any generated command.`);
|
|
46
|
+
throw new ConfigError("AI provider is not configured. --print cannot run initialization; run howto --init or set --ai-provider, HOWTO_AI_PROVIDER, or ~/.howto/config.json.");
|
|
84
47
|
}
|
|
85
|
-
|
|
86
|
-
let appError;
|
|
87
|
-
const { waitUntilExit, unmount, clear } = render(React.createElement(App, { provider: provider, request: Object.assign(Object.assign({}, promptRequest), { systemPrompt, userPrompt }), useCommand: parsedCli.useCommand, onSuccess: (command) => {
|
|
88
|
-
finalCommand = command;
|
|
89
|
-
clear();
|
|
90
|
-
unmount();
|
|
91
|
-
}, onError: (error) => {
|
|
92
|
-
appError = error;
|
|
93
|
-
clear();
|
|
94
|
-
unmount();
|
|
95
|
-
} }));
|
|
96
|
-
yield waitUntilExit();
|
|
97
|
-
if (appError) {
|
|
98
|
-
throw appError;
|
|
99
|
-
}
|
|
100
|
-
if (finalCommand === undefined) {
|
|
101
|
-
return { exitCode: 1 };
|
|
102
|
-
}
|
|
103
|
-
const exitCode = yield executeCommand(finalCommand);
|
|
104
|
-
return { exitCode };
|
|
48
|
+
ensureInteractiveTty(process.stdin, process.stdout);
|
|
105
49
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
50
|
+
const config = hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)
|
|
51
|
+
? loadConfig(parsedCli.options, process.env, fileConfig)
|
|
52
|
+
: await initializeConfig({
|
|
53
|
+
cliOptions: parsedCli.options,
|
|
54
|
+
env: process.env,
|
|
55
|
+
session: getSession(),
|
|
56
|
+
});
|
|
57
|
+
const useCommandPathCheck = parsedCli.useCommand === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: checkCommandInPath(parsedCli.useCommand, process.env);
|
|
60
|
+
if (!parsedCli.options.print) {
|
|
61
|
+
ensureInteractiveTty(process.stdin, process.stdout);
|
|
112
62
|
}
|
|
113
|
-
|
|
63
|
+
const promptRequest = createProviderPromptRequest({
|
|
64
|
+
question: parsedCli.question,
|
|
65
|
+
arguments: parsedCli.arguments,
|
|
66
|
+
useCommand: parsedCli.useCommand,
|
|
67
|
+
structuredOutput: config.structuredOutput,
|
|
68
|
+
});
|
|
69
|
+
const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
|
|
70
|
+
const provider = createCommandProvider(config);
|
|
71
|
+
if (parsedCli.options.print) {
|
|
72
|
+
const candidates = await generateValidatedCommandCandidates(provider, {
|
|
73
|
+
...promptRequest,
|
|
74
|
+
systemPrompt,
|
|
75
|
+
userPrompt,
|
|
76
|
+
});
|
|
77
|
+
candidates.forEach((candidate) => {
|
|
78
|
+
console.log(candidate.command);
|
|
79
|
+
});
|
|
80
|
+
return { exitCode: 0 };
|
|
81
|
+
}
|
|
82
|
+
if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
|
|
83
|
+
console.error(`Warning: requested command "${renderTerminalSafeText(useCommandPathCheck.command)}" was not found in PATH. Review before executing any generated command.`);
|
|
84
|
+
}
|
|
85
|
+
const exitCode = await runInteractiveCommand({
|
|
86
|
+
session: getSession(),
|
|
87
|
+
provider,
|
|
88
|
+
request: { ...promptRequest, systemPrompt, userPrompt },
|
|
89
|
+
});
|
|
90
|
+
return { exitCode };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const appError = toAppError(error);
|
|
94
|
+
if (appError.message !== "") {
|
|
95
|
+
console.error(appError.message);
|
|
96
|
+
}
|
|
97
|
+
return { exitCode: appError.exitCode };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
session?.dispose();
|
|
101
|
+
}
|
|
114
102
|
}
|
|
115
103
|
function resolveEntrypointPath(path) {
|
|
116
104
|
try {
|
|
117
105
|
return realpathSync(path);
|
|
118
106
|
}
|
|
119
|
-
catch
|
|
107
|
+
catch {
|
|
120
108
|
return path;
|
|
121
109
|
}
|
|
122
110
|
}
|