@unscientificjszhai/howto 1.0.0 → 1.0.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.
Files changed (44) hide show
  1. package/README.CN.md +74 -8
  2. package/README.md +74 -8
  3. package/THIRD_PARTY_NOTICES.md +6 -4
  4. package/dist/ai/command-schema.js +7 -1
  5. package/dist/ai/errors.js +7 -23
  6. package/dist/ai/gemini.js +67 -27
  7. package/dist/ai/openai.js +35 -27
  8. package/dist/cli.js +12 -1
  9. package/dist/config-file.js +97 -51
  10. package/dist/config.js +8 -7
  11. package/dist/errors.js +19 -17
  12. package/dist/execute.js +12 -34
  13. package/dist/index.js +100 -98
  14. package/dist/init/InitializationApp.js +177 -32
  15. package/dist/init/index.js +35 -33
  16. package/dist/init/state.js +46 -23
  17. package/dist/prompt.js +24 -6
  18. package/dist/safety/command-risk.js +11 -0
  19. package/dist/safety/dangerous-command.js +470 -92
  20. package/dist/shell/command-analysis.js +373 -0
  21. package/dist/shell/execution-environment.js +25 -0
  22. package/dist/shell/runtime-environment.js +61 -0
  23. package/dist/terminal-text.js +33 -0
  24. package/dist/ui/App.js +67 -28
  25. package/dist/ui/ConfirmView.js +163 -67
  26. package/dist/ui/InteractiveSessionProvider.js +19 -0
  27. package/dist/ui/ResolvePlaceholdersView.js +107 -27
  28. package/dist/ui/SelectCommandView.js +73 -23
  29. package/dist/ui/SelectedCommandDisplay.js +11 -9
  30. package/dist/ui/interactive-session.js +476 -0
  31. package/dist/ui/paste-framing.js +105 -0
  32. package/dist/ui/placeholder-logic.js +28 -16
  33. package/dist/ui/resize-safe-output.js +73 -0
  34. package/dist/ui/run-interactive-command.js +55 -0
  35. package/dist/ui/single-line-preview.js +28 -0
  36. package/dist/ui/text-input.js +53 -0
  37. package/dist/ui/use-keyboard-input.js +22 -0
  38. package/dist/ui/use-paste-aware-input.js +11 -0
  39. package/dist/user-visible-error.js +19 -0
  40. package/dist/validation/ai-response.js +34 -6
  41. package/dist/validation/command-tool.js +31 -132
  42. package/dist/validation/generated-commands.js +5 -16
  43. package/dist/version.js +38 -0
  44. package/package.json +31 -16
package/dist/cli.js CHANGED
@@ -13,12 +13,14 @@ const VALUE_OPTIONS = new Set([
13
13
  "--openai-model",
14
14
  "--structured-output",
15
15
  ]);
16
- const BOOLEAN_OPTIONS = new Set(["--print", "--init"]);
16
+ const BOOLEAN_OPTIONS = new Set(["--print", "--init", "--version"]);
17
17
  export const USAGE = `Usage: howto [options] [use <command>] <question> [<argument>...]
18
18
  howto --init
19
+ howto --version
19
20
 
20
21
  Options:
21
22
  --init
23
+ --version
22
24
  --print
23
25
  --ai-provider <openai|gemini>
24
26
  --gemini-api-key <key>
@@ -62,6 +64,12 @@ export function parseCliArgs(argv) {
62
64
  }
63
65
  positionals.push(token);
64
66
  }
67
+ if (options.version) {
68
+ if (argv.length !== 1) {
69
+ throw new CliParseError("--version must be used alone");
70
+ }
71
+ return { options, arguments: [] };
72
+ }
65
73
  return parsePositionals(options, positionals);
66
74
  }
67
75
  function readOptionValue(argv, optionIndex, optionName) {
@@ -106,6 +114,9 @@ function assignBooleanOption(options, optionName) {
106
114
  case "--init":
107
115
  options.init = true;
108
116
  return;
117
+ case "--version":
118
+ options.version = true;
119
+ return;
109
120
  default:
110
121
  throw new CliParseError(`unsupported option: ${optionName}`);
111
122
  }
@@ -1,16 +1,6 @@
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
- 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
- var _a;
26
- return join((_a = env.HOME) !== null && _a !== void 0 ? _a : homedir(), ".howto", "config.json");
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
- return __awaiter(this, arguments, void 0, function* (path = getConfigFilePath()) {
30
- if (!existsSync(path)) {
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
- let parsed;
34
- try {
35
- parsed = JSON.parse(yield readFile(path, "utf8"));
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
- catch (error) {
38
- throw new ConfigError(`failed to read config file ${path}: ${getParseErrorMessage(error)}`);
70
+ if (typeof value !== "string") {
71
+ throw new ConfigError(`config file field ${key} must be a string`);
39
72
  }
40
- if (!isPlainObject(parsed)) {
41
- throw new ConfigError(`config file ${path} must contain a JSON object`);
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
- const config = {};
44
- for (const [key, value] of Object.entries(parsed)) {
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
- return config;
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
- export function writeUserConfigFile(config_1) {
64
- return __awaiter(this, arguments, void 0, function* (config, path = getConfigFilePath()) {
65
- yield mkdir(dirname(path), { recursive: true });
66
- yield writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
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 getParseErrorMessage(error) {
73
- return error instanceof Error ? error.message : "invalid JSON";
118
+ function isFileNotFoundError(error) {
119
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
74
120
  }
package/dist/config.js CHANGED
@@ -5,13 +5,12 @@ export class ConfigError extends Error {
5
5
  }
6
6
  }
7
7
  const DEFAULT_OPENAI_MODEL = "gpt-5.4-mini";
8
- const DEFAULT_GEMINI_MODEL = "gemini-3.1-flash-lite";
8
+ const DEFAULT_GEMINI_MODEL = "gemini-3.5-flash-lite";
9
9
  export { DEFAULT_GEMINI_MODEL, DEFAULT_OPENAI_MODEL };
10
10
  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: (_a = pickConfigValue(options.geminiModel, env.HOWTO_GEMINI_MODEL, fileConfig.geminiModel)) !== null && _a !== void 0 ? _a : DEFAULT_GEMINI_MODEL,
23
+ model: pickConfigValue(options.geminiModel, env.HOWTO_GEMINI_MODEL, fileConfig.geminiModel) ??
24
+ DEFAULT_GEMINI_MODEL,
25
25
  },
26
26
  openai: {
27
- apiKey: (_b = pickConfigValue(options.openaiApiKey, env.HOWTO_OPENAI_API_KEY, fileConfig.openaiApiKey)) !== null && _b !== void 0 ? _b : "",
28
- model: (_c = pickConfigValue(options.openaiModel, env.HOWTO_OPENAI_MODEL, fileConfig.openaiModel)) !== null && _c !== void 0 ? _c : DEFAULT_OPENAI_MODEL,
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
- var _a;
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,17 @@ 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
+ import { UnsupportedEnvironmentError } from "./shell/execution-environment.js";
9
+ export { sanitizeUserVisibleErrorMessage };
10
+ export function getUserVisibleErrorMessage(error) {
11
+ if (error instanceof AiProviderError) {
12
+ return `AI provider request failed (provider: ${error.provider}, model: ${sanitizeUserVisibleErrorMessage(error.model)})`;
13
+ }
14
+ return sanitizeUserVisibleErrorMessage(error.message);
15
+ }
7
16
  export class AppError extends Error {
17
+ exitCode;
8
18
  constructor(message, exitCode) {
9
19
  super(message);
10
20
  this.name = "AppError";
@@ -13,30 +23,30 @@ export class AppError extends Error {
13
23
  }
14
24
  export function toAppError(error) {
15
25
  if (error instanceof AppError) {
16
- return error;
26
+ return new AppError(sanitizeUserVisibleErrorMessage(error.message), error.exitCode);
17
27
  }
18
28
  if (error instanceof CliParseError) {
19
- return new AppError(`Error: ${error.message}\n${USAGE}`, 2);
29
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}\n${USAGE}`, 2);
20
30
  }
21
31
  if (error instanceof ConfigError) {
22
- return new AppError(`Configuration error: ${error.message}`, 2);
32
+ return new AppError(`Configuration error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
23
33
  }
24
34
  if (error instanceof AiProviderError) {
25
- return new AppError(error.message, 1);
35
+ return new AppError(getUserVisibleErrorMessage(error), 1);
26
36
  }
27
37
  if (error instanceof AiResponseValidationError) {
28
- return new AppError(`AI response format error: ${error.message}`, 2);
38
+ return new AppError(`AI response format error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
29
39
  }
30
- if (error instanceof InteractiveTtyError) {
31
- return new AppError(`Error: ${error.message}`, 2);
40
+ if (error instanceof InteractiveTtyError || error instanceof UnsupportedEnvironmentError) {
41
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
32
42
  }
33
43
  if (error instanceof PlaceholderResolutionError) {
34
- return new AppError(`Error: ${error.message}`, 2);
44
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(error.message)}`, 2);
35
45
  }
36
46
  if (error instanceof InteractionCancelledError) {
37
47
  return new AppError("", 130);
38
48
  }
39
- return new AppError(`Error: ${sanitizeErrorMessage(getErrorMessage(error))}`, 1);
49
+ return new AppError(`Error: ${sanitizeUserVisibleErrorMessage(getErrorMessage(error))}`, 1);
40
50
  }
41
51
  function getErrorMessage(error) {
42
52
  if (error instanceof Error && error.message.trim() !== "") {
@@ -47,11 +57,3 @@ function getErrorMessage(error) {
47
57
  }
48
58
  return "unknown error";
49
59
  }
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,47 +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(command_1) {
13
- return __awaiter(this, arguments, void 0, function* (command, options = {}) {
14
- var _a, _b, _c;
15
- const env = (_a = options.env) !== null && _a !== void 0 ? _a : process.env;
16
- const platform = (_b = options.platform) !== null && _b !== void 0 ? _b : process.platform;
17
- const spawnCommand = (_c = options.spawnCommand) !== null && _c !== void 0 ? _c : spawnCommandWithNodeSpawn;
18
- const child = platform === "win32"
19
- ? spawnCommand(command, { shell: env.SHELL || true, stdio: "inherit" }, { stdio: "inherit" })
20
- : spawnCommand(resolveShell(env), ["-lc", `exec ${command}`], { stdio: "inherit" });
21
- return new Promise((resolve, reject) => {
22
- child.once("error", reject);
23
- child.once("close", (code, signal) => {
24
- resolve(resolveProcessExitCode(code, signal));
25
- });
3
+ import { resolveExecutionShell } from "./shell/execution-environment.js";
4
+ export async function executeCommand(command, options = {}) {
5
+ const env = options.env ?? process.env;
6
+ const platform = options.platform ?? process.platform;
7
+ const shell = resolveExecutionShell(env, platform);
8
+ const spawnCommand = options.spawnCommand ?? spawn;
9
+ const child = spawnCommand(shell, ["-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 + ((_a = osConstants.signals[signal]) !== null && _a !== void 0 ? _a : 1);
22
+ return 128 + (osConstants.signals[signal] ?? 1);
36
23
  }
37
24
  return 1;
38
25
  }
39
- function resolveShell(env) {
40
- return env.SHELL && env.SHELL.trim() !== "" ? env.SHELL : "/bin/sh";
41
- }
42
- const spawnCommandWithNodeSpawn = (command, args, options) => {
43
- if (Array.isArray(args)) {
44
- return options === undefined ? spawn(command, args) : spawn(command, args, options);
45
- }
46
- return spawn(command, args);
47
- };
package/dist/index.js CHANGED
@@ -1,17 +1,6 @@
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";
@@ -20,118 +9,131 @@ import { buildCommandGenerationPrompt, createProviderPromptRequest } from "./pro
20
9
  import { checkCommandInPath } from "./validation/command-tool.js";
21
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 { App } from "./ui/App.js";
26
- import { initializeConfig } from "./init/index.js";
27
- function run(argv) {
28
- return __awaiter(this, void 0, void 0, function* () {
29
- try {
30
- const parsedCli = parseCliArgs(argv);
31
- if (parsedCli.options.init) {
32
- ensureInteractiveTty(process.stdin, process.stdout);
33
- yield initializeConfig({
34
- cliOptions: parsedCli.options,
35
- env: process.env,
36
- input: process.stdin,
37
- output: process.stdout,
38
- });
39
- return { exitCode: 0 };
40
- }
41
- const fileConfig = yield readUserConfigFile();
42
- if (parsedCli.question === undefined) {
43
- throw new CliParseError("missing question");
44
- }
45
- if (!hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)) {
46
- if (parsedCli.options.print) {
47
- 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.");
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,
13
+ import { renderTerminalSafeText } from "./terminal-text.js";
14
+ import { readPackageVersion } from "./version.js";
15
+ import { assertSupportedPlatform, resolveExecutionShell } from "./shell/execution-environment.js";
16
+ async function run(argv) {
17
+ let session;
18
+ const getSession = async () => {
19
+ ensureInteractiveTty(process.stdin, process.stdout);
20
+ const { createInteractiveSession } = await import("./ui/interactive-session.js");
21
+ session ??= createInteractiveSession({ input: process.stdin, output: process.stdout });
22
+ return session;
23
+ };
24
+ try {
25
+ assertSupportedPlatform();
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
+ const { initializeConfig } = await import("./init/index.js");
34
+ await initializeConfig({
35
+ cliOptions: parsedCli.options,
36
+ env: process.env,
37
+ session: await getSession(),
70
38
  });
71
- const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
72
- const provider = createCommandProvider(config);
39
+ return { exitCode: 0 };
40
+ }
41
+ if (!parsedCli.options.print) {
42
+ resolveExecutionShell();
43
+ }
44
+ const fileConfig = await readUserConfigFile();
45
+ if (parsedCli.question === undefined) {
46
+ throw new CliParseError("missing question");
47
+ }
48
+ if (!hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)) {
73
49
  if (parsedCli.options.print) {
74
- const candidates = yield generateValidatedCommandCandidates(provider, Object.assign(Object.assign({}, promptRequest), { systemPrompt,
75
- userPrompt }));
76
- candidates.forEach((candidate) => {
77
- console.log(candidate.command);
78
- });
79
- return { exitCode: 0 };
80
- }
81
- if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
82
- console.error(`Warning: requested command "${useCommandPathCheck.command}" was not found in PATH. Review before executing any generated command.`);
83
- }
84
- let finalCommand;
85
- let appError;
86
- const { waitUntilExit, unmount, clear } = render(React.createElement(App, { provider: provider, request: Object.assign(Object.assign({}, promptRequest), { systemPrompt, userPrompt }), onSuccess: (command) => {
87
- finalCommand = command;
88
- clear();
89
- unmount();
90
- }, onError: (error) => {
91
- appError = error;
92
- clear();
93
- unmount();
94
- } }));
95
- yield waitUntilExit();
96
- if (appError) {
97
- throw appError;
50
+ 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.");
98
51
  }
99
- if (finalCommand === undefined) {
100
- return { exitCode: 1 };
101
- }
102
- const exitCode = yield executeCommand(finalCommand);
103
- return { exitCode };
52
+ ensureInteractiveTty(process.stdin, process.stdout);
104
53
  }
105
- catch (error) {
106
- const appError = toAppError(error);
107
- if (appError.message !== "") {
108
- console.error(appError.message);
109
- }
110
- return { exitCode: appError.exitCode };
54
+ const config = hasExplicitAiProvider(parsedCli.options, process.env, fileConfig)
55
+ ? loadConfig(parsedCli.options, process.env, fileConfig)
56
+ : await (await import("./init/index.js")).initializeConfig({
57
+ cliOptions: parsedCli.options,
58
+ env: process.env,
59
+ session: await getSession(),
60
+ });
61
+ const useCommandPathCheck = parsedCli.useCommand === undefined
62
+ ? undefined
63
+ : checkCommandInPath(parsedCli.useCommand, process.env);
64
+ if (!parsedCli.options.print) {
65
+ ensureInteractiveTty(process.stdin, process.stdout);
111
66
  }
112
- });
67
+ const promptRequest = createProviderPromptRequest({
68
+ question: parsedCli.question,
69
+ arguments: parsedCli.arguments,
70
+ useCommand: parsedCli.useCommand,
71
+ structuredOutput: config.structuredOutput,
72
+ });
73
+ const { systemPrompt, userPrompt } = buildCommandGenerationPrompt(promptRequest);
74
+ const provider = createCommandProvider(config);
75
+ if (parsedCli.options.print) {
76
+ const candidates = await generateValidatedCommandCandidates(provider, {
77
+ ...promptRequest,
78
+ systemPrompt,
79
+ userPrompt,
80
+ });
81
+ candidates.forEach((candidate) => {
82
+ console.log(candidate.command);
83
+ });
84
+ return { exitCode: 0 };
85
+ }
86
+ if (useCommandPathCheck !== undefined && !useCommandPathCheck.found) {
87
+ console.error(`Warning: requested command "${renderTerminalSafeText(useCommandPathCheck.command)}" was not found in PATH. Review before executing any generated command.`);
88
+ }
89
+ const { runInteractiveCommand } = await import("./ui/run-interactive-command.js");
90
+ const exitCode = await runInteractiveCommand({
91
+ session: await getSession(),
92
+ provider,
93
+ request: { ...promptRequest, systemPrompt, userPrompt },
94
+ });
95
+ return { exitCode };
96
+ }
97
+ catch (error) {
98
+ const appError = toAppError(error);
99
+ if (appError.message !== "") {
100
+ console.error(appError.message);
101
+ }
102
+ return { exitCode: appError.exitCode };
103
+ }
104
+ finally {
105
+ session?.dispose();
106
+ }
113
107
  }
114
108
  function resolveEntrypointPath(path) {
115
109
  try {
116
110
  return realpathSync(path);
117
111
  }
118
- catch (_a) {
112
+ catch {
119
113
  return path;
120
114
  }
121
115
  }
122
116
  const isMain = process.argv[1] !== undefined &&
123
117
  fileURLToPath(import.meta.url) === resolveEntrypointPath(process.argv[1]);
124
118
  if (isMain) {
119
+ let outputFailed = false;
120
+ process.stdout.on("error", () => {
121
+ if (!outputFailed) {
122
+ outputFailed = true;
123
+ console.error("Error: failed to write standard output.");
124
+ }
125
+ process.exitCode = 1;
126
+ });
125
127
  run(process.argv.slice(2))
126
128
  .then((result) => {
127
- process.exitCode = result.exitCode;
129
+ process.exitCode = outputFailed ? 1 : result.exitCode;
128
130
  })
129
131
  .catch((error) => {
130
132
  const appError = toAppError(error);
131
133
  if (appError.message !== "") {
132
134
  console.error(appError.message);
133
135
  }
134
- process.exitCode = appError.exitCode;
136
+ process.exitCode = outputFailed ? 1 : appError.exitCode;
135
137
  });
136
138
  }
137
139
  export { run };