@unscientificjszhai/howto 1.0.0 → 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.
Files changed (40) hide show
  1. package/README.CN.md +32 -3
  2. package/README.md +32 -3
  3. package/THIRD_PARTY_NOTICES.md +5 -4
  4. package/dist/ai/errors.js +7 -23
  5. package/dist/ai/gemini.js +67 -27
  6. package/dist/ai/openai.js +35 -27
  7. package/dist/cli.js +12 -1
  8. package/dist/config-file.js +97 -51
  9. package/dist/config.js +7 -6
  10. package/dist/errors.js +17 -16
  11. package/dist/execute.js +12 -25
  12. package/dist/index.js +84 -95
  13. package/dist/init/InitializationApp.js +175 -32
  14. package/dist/init/index.js +35 -33
  15. package/dist/init/state.js +46 -23
  16. package/dist/prompt.js +4 -2
  17. package/dist/safety/dangerous-command.js +262 -93
  18. package/dist/shell/command-analysis.js +373 -0
  19. package/dist/terminal-text.js +33 -0
  20. package/dist/ui/App.js +64 -25
  21. package/dist/ui/ConfirmView.js +163 -67
  22. package/dist/ui/InteractiveSessionProvider.js +19 -0
  23. package/dist/ui/ResolvePlaceholdersView.js +107 -27
  24. package/dist/ui/SelectCommandView.js +73 -23
  25. package/dist/ui/SelectedCommandDisplay.js +11 -9
  26. package/dist/ui/interactive-session.js +407 -0
  27. package/dist/ui/paste-framing.js +105 -0
  28. package/dist/ui/placeholder-logic.js +28 -16
  29. package/dist/ui/resize-safe-output.js +73 -0
  30. package/dist/ui/run-interactive-command.js +55 -0
  31. package/dist/ui/single-line-preview.js +28 -0
  32. package/dist/ui/text-input.js +53 -0
  33. package/dist/ui/use-keyboard-input.js +12 -0
  34. package/dist/ui/use-paste-aware-input.js +7 -0
  35. package/dist/user-visible-error.js +19 -0
  36. package/dist/validation/ai-response.js +11 -4
  37. package/dist/validation/command-tool.js +25 -127
  38. package/dist/validation/generated-commands.js +5 -16
  39. package/dist/version.js +38 -0
  40. package/package.json +18 -15
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { executeCommand } from "../execute.js";
4
+ import { App } from "./App.js";
5
+ import { InteractiveSessionProvider } from "./InteractiveSessionProvider.js";
6
+ export const MANUAL_EXECUTION_NOTICE = "本次会话包含粘贴,howto 不会自动执行命令。请检查输出内容后手动运行。";
7
+ export async function runInteractiveCommand({ session, provider, request, execute = executeCommand, print = (command) => {
8
+ console.error(MANUAL_EXECUTION_NOTICE);
9
+ console.log(command);
10
+ }, }) {
11
+ let finalCommand;
12
+ let appError;
13
+ const instance = render(React.createElement(InteractiveSessionProvider, { session: session },
14
+ React.createElement(App, { provider: provider, request: request, onSuccess: (command) => {
15
+ finalCommand = command;
16
+ session.suspendViewInput();
17
+ instance.clear();
18
+ instance.unmount();
19
+ }, onError: (error) => {
20
+ appError = error;
21
+ session.suspendViewInput();
22
+ instance.clear();
23
+ instance.unmount();
24
+ } })), {
25
+ stdin: session.input,
26
+ stdout: session.output,
27
+ exitOnCtrlC: false,
28
+ // 会话已确认 TTY,避免 CI 环境关闭 Ink 的实时渲染。
29
+ interactive: true,
30
+ });
31
+ const unsubscribe = session.subscribeFailure((error) => {
32
+ appError = error;
33
+ instance.clear();
34
+ instance.unmount();
35
+ });
36
+ try {
37
+ await instance.waitUntilExit();
38
+ if (appError)
39
+ throw appError;
40
+ if (finalCommand === undefined)
41
+ return 1;
42
+ const command = finalCommand;
43
+ return await session.handoff({
44
+ execute: () => execute(command),
45
+ print: () => {
46
+ print(command);
47
+ return Promise.resolve(0);
48
+ },
49
+ });
50
+ }
51
+ finally {
52
+ unsubscribe();
53
+ instance.unmount();
54
+ }
55
+ }
@@ -0,0 +1,28 @@
1
+ /// <reference lib="es2022.intl" />
2
+ import stringWidth from "string-width";
3
+ import { renderTerminalSafeText } from "../terminal-text.js";
4
+ const graphemeSegmenter = new Intl.Segmenter(undefined, {
5
+ granularity: "grapheme",
6
+ });
7
+ export function toSingleLinePreview(value) {
8
+ return renderTerminalSafeText(value);
9
+ }
10
+ export function toTailPreview(value, maxColumns) {
11
+ if (maxColumns <= 0) {
12
+ return "";
13
+ }
14
+ const preview = toSingleLinePreview(value);
15
+ const graphemes = Array.from(graphemeSegmenter.segment(preview), ({ segment }) => segment);
16
+ let usedColumns = 0;
17
+ let startIndex = graphemes.length;
18
+ while (startIndex > 0) {
19
+ const grapheme = graphemes[startIndex - 1];
20
+ const graphemeColumns = stringWidth(grapheme);
21
+ if (usedColumns + graphemeColumns > maxColumns) {
22
+ break;
23
+ }
24
+ usedColumns += graphemeColumns;
25
+ startIndex--;
26
+ }
27
+ return graphemes.slice(startIndex).join("");
28
+ }
@@ -0,0 +1,53 @@
1
+ /// <reference lib="es2022.intl" />
2
+ import { hasUnsafeTerminalControlCharacters } from "../terminal-text.js";
3
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
4
+ // eslint-disable-next-line no-control-regex -- 显式识别传统终端协议的按键控制字节。
5
+ const LEGACY_CONTROL_PATTERN = /([\r\n\t\u0003\u0008\u001b\u007f])/;
6
+ const LEGACY_CONTROL_KEYS = new Map([
7
+ ["\r", { input: "", key: { return: true } }],
8
+ ["\n", { input: "", key: { return: true } }],
9
+ ["\t", { input: "", key: { tab: true } }],
10
+ ["\u0003", { input: "c", key: { ctrl: true } }],
11
+ ["\u0008", { input: "", key: { backspace: true } }],
12
+ ["\u007f", { input: "", key: { backspace: true } }],
13
+ ["\u001b", { input: "", key: { escape: true } }],
14
+ ]);
15
+ export function splitKeyboardInput(input, key) {
16
+ const parts = input.split(LEGACY_CONTROL_PATTERN).filter((part) => part !== "");
17
+ const text = parts.map((part) => (LEGACY_CONTROL_KEYS.has(part) ? " " : part)).join("");
18
+ // Kitty 的文本码点已有事件边界;未知控制字符和快捷键继续交给原有规则整块处理。
19
+ if (key.eventType !== undefined || !isTextInputEvent(text, key))
20
+ return [{ input, key }];
21
+ return parts.map((part) => {
22
+ const control = LEGACY_CONTROL_KEYS.get(part);
23
+ return control === undefined
24
+ ? { input: part, key }
25
+ : { input: control.input, key: { ...key, shift: false, ...control.key } };
26
+ });
27
+ }
28
+ export function deleteLastGrapheme(value) {
29
+ const lastGrapheme = graphemeSegmenter.segment(value).containing(value.length - 1);
30
+ return lastGrapheme === undefined ? "" : value.slice(0, lastGrapheme.index);
31
+ }
32
+ export function isTextInputEvent(input, key) {
33
+ return (input.length > 0 &&
34
+ !hasUnsafeTerminalControlCharacters(input) &&
35
+ key.eventType !== "release" &&
36
+ !key.ctrl &&
37
+ !key.meta &&
38
+ !key.super &&
39
+ !key.hyper &&
40
+ !key.upArrow &&
41
+ !key.downArrow &&
42
+ !key.leftArrow &&
43
+ !key.rightArrow &&
44
+ !key.pageUp &&
45
+ !key.pageDown &&
46
+ !key.home &&
47
+ !key.end &&
48
+ !key.tab &&
49
+ !key.return &&
50
+ !key.escape &&
51
+ !key.backspace &&
52
+ !key.delete);
53
+ }
@@ -0,0 +1,12 @@
1
+ import { useInput } from "ink";
2
+ import { splitKeyboardInput } from "./text-input.js";
3
+ export function useKeyboardInput(onInput, options) {
4
+ useInput((input, key) => {
5
+ // 同一按键的释放可能到达下一个视图,必须在确认、取消、导航和删除之前丢弃。
6
+ if (key.eventType === "release")
7
+ return;
8
+ // 同步交付给本次视图的回调,不能把剩余按键排队到下一页。
9
+ for (const event of splitKeyboardInput(input, key))
10
+ onInput(event.input, event.key);
11
+ }, options);
12
+ }
@@ -0,0 +1,7 @@
1
+ import { usePaste } from "ink";
2
+ import { useKeyboardInput } from "./use-keyboard-input.js";
3
+ export function usePasteAwareInput({ onInput, onPaste, isInputActive = true, isPasteActive = true, }) {
4
+ // 原始会话按顺序交付完整粘贴;此 hook 不承担恢复或授予执行权限的职责。
5
+ useKeyboardInput(onInput, { isActive: isInputActive });
6
+ usePaste(onPaste, { isActive: isPasteActive });
7
+ }
@@ -0,0 +1,19 @@
1
+ import { renderTerminalSafeText } from "./terminal-text.js";
2
+ const AUTHORIZATION_CREDENTIAL_PATTERN = /((?<![-\w])a[�␍␊]*u[�␍␊]*t[�␍␊]*h[�␍␊]*o[�␍␊]*r[�␍␊]*i[�␍␊]*z[�␍␊]*a[�␍␊]*t[�␍␊]*i[�␍␊]*o[�␍␊]*n["'\s:=�␍␊]+)([\s\S]*)$/i;
3
+ const AUTHORIZATION_ASSIGNMENT_PATTERN = /((?<!\w)(?:--)?[\w-]*a[�␍␊]*u[�␍␊]*t[�␍␊]*h[�␍␊]*o[�␍␊]*r[�␍␊]*i[�␍␊]*z[�␍␊]*a[�␍␊]*t[�␍␊]*i[�␍␊]*o[�␍␊]*n["'\s�␍␊]*[:=]["'\s:=�␍␊]*)([\s\S]*)$/i;
4
+ const API_KEY_CREDENTIAL_PATTERN = /((?<![-\w])a[�␍␊]*p[�␍␊]*i[�␍␊]*(?:[-_ ][�␍␊]*)?k[�␍␊]*e[�␍␊]*y["'\s:=�␍␊]+)([\s\S]*)$/i;
5
+ const API_KEY_ASSIGNMENT_PATTERN = /((?<!\w)(?:--)?[\w-]*a[�␍␊]*p[�␍␊]*i[�␍␊]*(?:[-_ ][�␍␊]*)?k[�␍␊]*e[�␍␊]*y["'\s�␍␊]*[:=]["'\s:=�␍␊]*)([\s\S]*)$/i;
6
+ const BEARER_CREDENTIAL_PATTERN = /((?<![-\w])b[�␍␊]*e[�␍␊]*a[�␍␊]*r[�␍␊]*e[�␍␊]*r[\s�␍␊]+)([\s\S]*)$/i;
7
+ const URL_PATTERN = /(?<![-\w])h[�␍␊]*t[�␍␊]*t[�␍␊]*p[�␍␊]*s?[�␍␊]*:[�␍␊]*\/[�␍␊]*\/([\s\S]*)$/i;
8
+ export function sanitizeUserVisibleErrorMessage(message) {
9
+ const terminalSafe = renderTerminalSafeText(message);
10
+ const singleLine = terminalSafe.replace(/\s+/g, " ").trim();
11
+ const redacted = singleLine
12
+ .replace(URL_PATTERN, "[redacted URL]")
13
+ .replace(AUTHORIZATION_ASSIGNMENT_PATTERN, "$1[redacted]")
14
+ .replace(API_KEY_ASSIGNMENT_PATTERN, "$1[redacted]")
15
+ .replace(AUTHORIZATION_CREDENTIAL_PATTERN, "$1[redacted]")
16
+ .replace(API_KEY_CREDENTIAL_PATTERN, "$1[redacted]")
17
+ .replace(BEARER_CREDENTIAL_PATTERN, "Bearer [redacted]");
18
+ return redacted.length > 220 ? `${redacted.slice(0, 217)}...` : redacted;
19
+ }
@@ -1,3 +1,4 @@
1
+ import { hasUnsafeTerminalControlCharacters } from "../terminal-text.js";
1
2
  export class AiResponseValidationError extends Error {
2
3
  constructor(message) {
3
4
  super(message);
@@ -17,7 +18,7 @@ function parseJsonObject(rawText) {
17
18
  try {
18
19
  return JSON.parse(rawText);
19
20
  }
20
- catch (_a) {
21
+ catch {
21
22
  throw new AiResponseValidationError("AI response is not valid JSON");
22
23
  }
23
24
  }
@@ -68,20 +69,23 @@ function validatePlaceholder(value, path) {
68
69
  }
69
70
  function validatePlaceholderReferences(command, placeholders, candidatePath) {
70
71
  const declaredNames = new Set(placeholders.map((placeholder) => placeholder.name));
72
+ if (declaredNames.size !== placeholders.length) {
73
+ throw new AiResponseValidationError(`${candidatePath}.placeholders must have unique names`);
74
+ }
71
75
  const usedNames = new Set();
72
76
  for (const match of command.matchAll(PLACEHOLDER_REFERENCE_PATTERN)) {
73
77
  const name = match[1];
74
78
  if (!PLACEHOLDER_NAME_PATTERN.test(name)) {
75
- throw new AiResponseValidationError(`${candidatePath}.command contains invalid placeholder reference {{${name}}}`);
79
+ throw new AiResponseValidationError(`${candidatePath}.command contains an invalid placeholder reference`);
76
80
  }
77
81
  if (!declaredNames.has(name)) {
78
- throw new AiResponseValidationError(`${candidatePath}.command references undeclared placeholder {{${name}}}`);
82
+ throw new AiResponseValidationError(`${candidatePath}.command references an undeclared placeholder`);
79
83
  }
80
84
  usedNames.add(name);
81
85
  }
82
86
  for (const declaredName of declaredNames) {
83
87
  if (!usedNames.has(declaredName)) {
84
- throw new AiResponseValidationError(`${candidatePath}.placeholders declares unused placeholder ${declaredName}`);
88
+ throw new AiResponseValidationError(`${candidatePath}.placeholders contains an unused placeholder`);
85
89
  }
86
90
  }
87
91
  }
@@ -89,6 +93,9 @@ function readNonEmptyString(value, path) {
89
93
  if (typeof value !== "string") {
90
94
  throw new AiResponseValidationError(`${path} must be a string`);
91
95
  }
96
+ if (hasUnsafeTerminalControlCharacters(value)) {
97
+ throw new AiResponseValidationError(`${path} must not contain terminal control characters other than CR or LF`);
98
+ }
92
99
  if (value.trim() === "") {
93
100
  throw new AiResponseValidationError(`${path} must be a non-empty string`);
94
101
  }
@@ -1,8 +1,7 @@
1
1
  import { accessSync, constants } from "fs";
2
2
  import { delimiter, isAbsolute, join } from "path";
3
+ import { isShellExecutable, parseShellCommand, resolveCommandPrefix, } from "../shell/command-analysis.js";
3
4
  import { AiResponseValidationError } from "./ai-response.js";
4
- const ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*$/;
5
- const SHELL_WRAPPERS = new Set(["sh", "bash", "zsh", "fish", "csh", "ksh"]);
6
5
  export function checkCommandInPath(command, env = process.env) {
7
6
  const candidates = buildPathCandidates(command, env);
8
7
  for (const candidate of candidates) {
@@ -24,32 +23,41 @@ export function validateUseCommandCandidates(response, useCommand) {
24
23
  return;
25
24
  }
26
25
  response.commands.forEach((candidate, index) => {
27
- if (!candidateUsesRequestedCommand(candidate.command, useCommand)) {
26
+ if (!candidateUsesRequestedCommand(candidate.command, useCommand, candidate.placeholders.map((placeholder) => placeholder.name))) {
28
27
  throw new AiResponseValidationError(`commands[${index}].command must use ${useCommand} as the first executable token after optional environment assignments, sudo, or env`);
29
28
  }
30
29
  });
31
30
  }
32
- export function candidateUsesRequestedCommand(commandText, requestedCommand) {
33
- const tokens = tokenizeShellPrefix(commandText);
34
- if (tokens === undefined || tokens.length === 0) {
31
+ export function candidateUsesRequestedCommand(commandText, requestedCommand, declaredPlaceholderNames = []) {
32
+ const declaredNames = new Set(declaredPlaceholderNames);
33
+ const placeholders = [];
34
+ // 仅修改分析副本,等长替身让解析结果的位置仍对应原始模板。
35
+ const analysisText = commandText.replace(/{{([A-Za-z0-9_-]+)}}/g, (reference, name, start) => {
36
+ if (!declaredNames.has(name))
37
+ return reference;
38
+ placeholders.push({ start, end: start + reference.length });
39
+ return "x".repeat(reference.length);
40
+ });
41
+ const parsed = parseShellCommand(analysisText);
42
+ if (parsed.kind === "unsupported")
35
43
  return false;
36
- }
37
- const executable = findFirstExecutableToken(tokens);
38
- if (executable === undefined) {
44
+ const firstCommand = parsed.commands[0];
45
+ if (firstCommand === undefined)
39
46
  return false;
40
- }
41
- if (SHELL_WRAPPERS.has(executable)) {
47
+ const prefix = resolveCommandPrefix(firstCommand.words);
48
+ if (prefix.kind === "unsupported" || isShellExecutable(prefix.executable.value))
42
49
  return false;
43
- }
44
- return executable === requestedCommand;
50
+ // 首工具本身及之前的模板可能改变前缀边界,不能靠替身证明工具身份。
51
+ if (placeholders.some((placeholder) => placeholder.start < prefix.executable.end))
52
+ return false;
53
+ return prefix.executable.value === requestedCommand;
45
54
  }
46
55
  function buildPathCandidates(command, env) {
47
- var _a, _b;
48
56
  if (command.includes("/") || isAbsolute(command)) {
49
57
  return [command];
50
58
  }
51
- const pathValue = (_a = env.PATH) !== null && _a !== void 0 ? _a : "";
52
- const pathExt = process.platform === "win32" ? ((_b = env.PATHEXT) !== null && _b !== void 0 ? _b : ".EXE;.CMD;.BAT;.COM") : "";
59
+ const pathValue = env.PATH ?? "";
60
+ const pathExt = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM") : "";
53
61
  const extensions = process.platform === "win32" ? pathExt.split(";").filter(Boolean) : [""];
54
62
  const candidates = [];
55
63
  for (const directory of pathValue.split(delimiter)) {
@@ -69,117 +77,7 @@ function isExecutable(path) {
69
77
  accessSync(path, constants.X_OK);
70
78
  return true;
71
79
  }
72
- catch (_a) {
80
+ catch {
73
81
  return false;
74
82
  }
75
83
  }
76
- function findFirstExecutableToken(tokens) {
77
- let index = 0;
78
- while (index < tokens.length && ENV_ASSIGNMENT_PATTERN.test(tokens[index])) {
79
- index += 1;
80
- }
81
- while (index < tokens.length) {
82
- const token = tokens[index];
83
- if (token === "sudo") {
84
- index = skipSudo(tokens, index + 1);
85
- continue;
86
- }
87
- if (token === "env") {
88
- index = skipEnv(tokens, index + 1);
89
- continue;
90
- }
91
- if (ENV_ASSIGNMENT_PATTERN.test(token)) {
92
- index += 1;
93
- continue;
94
- }
95
- return token;
96
- }
97
- return undefined;
98
- }
99
- function skipSudo(tokens, startIndex) {
100
- let index = startIndex;
101
- while (index < tokens.length && tokens[index].startsWith("-")) {
102
- index += 1;
103
- }
104
- return index;
105
- }
106
- function skipEnv(tokens, startIndex) {
107
- let index = startIndex;
108
- while (index < tokens.length) {
109
- const token = tokens[index];
110
- if (ENV_ASSIGNMENT_PATTERN.test(token)) {
111
- index += 1;
112
- continue;
113
- }
114
- if (token.startsWith("-")) {
115
- index += 1;
116
- if ((token === "-u" || token === "--unset") && index < tokens.length) {
117
- index += 1;
118
- }
119
- continue;
120
- }
121
- return index;
122
- }
123
- return index;
124
- }
125
- function tokenizeShellPrefix(commandText) {
126
- const tokens = [];
127
- let current = "";
128
- let quote;
129
- for (let index = 0; index < commandText.length; index += 1) {
130
- const character = commandText[index];
131
- if (quote !== undefined) {
132
- if (character === quote) {
133
- quote = undefined;
134
- }
135
- else if (character === "\\" && quote === '"' && index + 1 < commandText.length) {
136
- index += 1;
137
- current += commandText[index];
138
- }
139
- else {
140
- current += character;
141
- }
142
- continue;
143
- }
144
- if (character === '"' || character === "'") {
145
- quote = character;
146
- continue;
147
- }
148
- if (character === "\\") {
149
- if (index + 1 >= commandText.length) {
150
- return undefined;
151
- }
152
- index += 1;
153
- current += commandText[index];
154
- continue;
155
- }
156
- if (/\s/.test(character)) {
157
- if (current !== "") {
158
- tokens.push(current);
159
- current = "";
160
- }
161
- continue;
162
- }
163
- if (isShellControlOperator(character)) {
164
- if (current !== "") {
165
- tokens.push(current);
166
- }
167
- break;
168
- }
169
- current += character;
170
- }
171
- if (quote !== undefined) {
172
- return undefined;
173
- }
174
- if (current !== "") {
175
- tokens.push(current);
176
- }
177
- return tokens;
178
- }
179
- function isShellControlOperator(character) {
180
- return (character === "|" ||
181
- character === ";" ||
182
- character === "&" ||
183
- character === "(" ||
184
- character === ")");
185
- }
@@ -1,19 +1,8 @@
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 { parseAndValidateAiResponse } from "./ai-response.js";
11
2
  import { validateUseCommandCandidates } from "./command-tool.js";
12
- export function generateValidatedCommandCandidates(provider, request) {
13
- return __awaiter(this, void 0, void 0, function* () {
14
- const result = yield provider.generateCommands(request);
15
- const response = parseAndValidateAiResponse(result.rawText);
16
- validateUseCommandCandidates(response, request.useCommand);
17
- return response.commands;
18
- });
3
+ export async function generateValidatedCommandCandidates(provider, request, signal) {
4
+ const result = await provider.generateCommands(request, signal);
5
+ const response = parseAndValidateAiResponse(result.rawText);
6
+ validateUseCommandCandidates(response, request.useCommand);
7
+ return response.commands;
19
8
  }
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function readPackageVersion() {
5
+ let directory = dirname(fileURLToPath(import.meta.url));
6
+ while (true) {
7
+ let content;
8
+ try {
9
+ content = readFileSync(join(directory, "package.json"), "utf8");
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
13
+ throw new Error("failed to read package version", { cause: error });
14
+ }
15
+ const parentDirectory = dirname(directory);
16
+ if (parentDirectory === directory) {
17
+ throw new Error("package.json not found", { cause: error });
18
+ }
19
+ directory = parentDirectory;
20
+ continue;
21
+ }
22
+ let metadata;
23
+ try {
24
+ metadata = JSON.parse(content);
25
+ }
26
+ catch {
27
+ throw new Error("package.json is not valid JSON");
28
+ }
29
+ if (typeof metadata !== "object" ||
30
+ metadata === null ||
31
+ !("version" in metadata) ||
32
+ typeof metadata.version !== "string" ||
33
+ metadata.version.trim() === "") {
34
+ throw new Error("package.json must contain a version string");
35
+ }
36
+ return metadata.version;
37
+ }
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unscientificjszhai/howto",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Use AI to quickly find commands within the terminal.",
6
6
  "license": "MIT",
@@ -21,7 +21,9 @@
21
21
  "THIRD_PARTY_NOTICES.md"
22
22
  ],
23
23
  "scripts": {
24
- "build": "tsc",
24
+ "build": "npm run clean:dist && tsc",
25
+ "clean:dist": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
26
+ "clean:dist-test": "node -e \"require('node:fs').rmSync('dist-test', { recursive: true, force: true })\"",
25
27
  "format": "prettier . --write",
26
28
  "format:check": "prettier . --check",
27
29
  "lint": "eslint .",
@@ -29,25 +31,26 @@
29
31
  "lint:staged": "lint-staged",
30
32
  "prepare": "husky",
31
33
  "test": "npm run test:unit",
32
- "test:unit": "tsc -p tsconfig.test.json && node --test dist-test/tests/unit/*.test.js dist-test/tests/unit/**/*.test.js"
34
+ "test:unit": "npm run clean:dist-test && tsc -p tsconfig.test.json && node --test dist-test/tests/unit/*.test.js dist-test/tests/unit/**/*.test.js"
33
35
  },
34
36
  "dependencies": {
35
- "@google/genai": "^2.6.0",
36
- "ink": "^7.0.4",
37
- "openai": "^6.39.0",
38
- "react": "^19.2.6"
37
+ "@google/genai": "^2.18.0",
38
+ "ink": "^7.1.1",
39
+ "openai": "^7.5.0",
40
+ "react": "^19.2.8",
41
+ "string-width": "^8.2.2"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@eslint/js": "^10.0.1",
42
- "@types/node": "^20.0.0",
43
- "@types/react": "^19.2.14",
44
- "@types/react-dom": "^19.2.3",
45
- "eslint": "^10.4.0",
45
+ "@types/node": "^26.2.0",
46
+ "@types/react": "^19.2.18",
47
+ "@types/react-dom": "^19.2.4",
48
+ "eslint": "^10.9.0",
46
49
  "husky": "^9.1.7",
47
- "lint-staged": "^16.4.0",
48
- "prettier": "^3.8.3",
49
- "typescript": "^5.5.3",
50
- "typescript-eslint": "^8.59.4"
50
+ "lint-staged": "^17.3.0",
51
+ "prettier": "^3.9.6",
52
+ "typescript": "^6.0.3",
53
+ "typescript-eslint": "^8.67.0"
51
54
  },
52
55
  "lint-staged": {
53
56
  "*.{ts,tsx}": [