@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.
@@ -0,0 +1,185 @@
1
+ import { accessSync, constants } from "fs";
2
+ import { delimiter, isAbsolute, join } from "path";
3
+ 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
+ export function checkCommandInPath(command, env = process.env) {
7
+ const candidates = buildPathCandidates(command, env);
8
+ for (const candidate of candidates) {
9
+ if (isExecutable(candidate)) {
10
+ return {
11
+ command,
12
+ found: true,
13
+ resolvedPath: candidate,
14
+ };
15
+ }
16
+ }
17
+ return {
18
+ command,
19
+ found: false,
20
+ };
21
+ }
22
+ export function validateUseCommandCandidates(response, useCommand) {
23
+ if (useCommand === undefined) {
24
+ return;
25
+ }
26
+ response.commands.forEach((candidate, index) => {
27
+ if (!candidateUsesRequestedCommand(candidate.command, useCommand)) {
28
+ throw new AiResponseValidationError(`commands[${index}].command must use ${useCommand} as the first executable token after optional environment assignments, sudo, or env`);
29
+ }
30
+ });
31
+ }
32
+ export function candidateUsesRequestedCommand(commandText, requestedCommand) {
33
+ const tokens = tokenizeShellPrefix(commandText);
34
+ if (tokens === undefined || tokens.length === 0) {
35
+ return false;
36
+ }
37
+ const executable = findFirstExecutableToken(tokens);
38
+ if (executable === undefined) {
39
+ return false;
40
+ }
41
+ if (SHELL_WRAPPERS.has(executable)) {
42
+ return false;
43
+ }
44
+ return executable === requestedCommand;
45
+ }
46
+ function buildPathCandidates(command, env) {
47
+ var _a, _b;
48
+ if (command.includes("/") || isAbsolute(command)) {
49
+ return [command];
50
+ }
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") : "";
53
+ const extensions = process.platform === "win32" ? pathExt.split(";").filter(Boolean) : [""];
54
+ const candidates = [];
55
+ for (const directory of pathValue.split(delimiter)) {
56
+ if (directory === "") {
57
+ continue;
58
+ }
59
+ for (const extension of extensions) {
60
+ candidates.push(command.toLowerCase().endsWith(extension.toLowerCase())
61
+ ? join(directory, command)
62
+ : join(directory, `${command}${extension}`));
63
+ }
64
+ }
65
+ return candidates;
66
+ }
67
+ function isExecutable(path) {
68
+ try {
69
+ accessSync(path, constants.X_OK);
70
+ return true;
71
+ }
72
+ catch (_a) {
73
+ return false;
74
+ }
75
+ }
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
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@unscientificjszhai/howto",
3
+ "version": "1.0.0-alpha.2",
4
+ "type": "module",
5
+ "description": "Use AI to quickly find commands within the terminal.",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "howto": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "format": "prettier . --write",
16
+ "format:check": "prettier . --check",
17
+ "lint": "eslint .",
18
+ "lint:fix": "eslint . --fix",
19
+ "lint:staged": "lint-staged",
20
+ "prepare": "husky",
21
+ "test": "npm run test:unit",
22
+ "test:unit": "tsc -p tsconfig.test.json && node --test dist-test/tests/unit/*.test.js dist-test/tests/unit/**/*.test.js"
23
+ },
24
+ "dependencies": {
25
+ "@google/genai": "^1.0.0",
26
+ "@inquirer/prompts": "^7.0.0",
27
+ "ink": "^7.0.3",
28
+ "openai": "^4.0.0",
29
+ "react": "^19.2.6"
30
+ },
31
+ "devDependencies": {
32
+ "@eslint/js": "^10.0.1",
33
+ "@types/node": "^20.0.0",
34
+ "@types/react": "^19.2.14",
35
+ "@types/react-dom": "^19.2.3",
36
+ "eslint": "^10.4.0",
37
+ "husky": "^9.1.7",
38
+ "lint-staged": "^16.4.0",
39
+ "prettier": "^3.8.3",
40
+ "typescript": "^5.5.3",
41
+ "typescript-eslint": "^8.59.4"
42
+ },
43
+ "lint-staged": {
44
+ "*.{ts,tsx}": [
45
+ "eslint --fix",
46
+ "prettier --write"
47
+ ],
48
+ "*.{js,json,md,yml,yaml}": "prettier --write"
49
+ }
50
+ }