ai-cmds 0.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.md +17 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +304 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Typed Fetch
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40ls-stack%2Ftyped-fetch)
|
|
4
|
+
|
|
5
|
+
A cli tool
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ...
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @ls-stack/ai-cli
|
|
15
|
+
# or
|
|
16
|
+
yarn add @ls-stack/ai-cli
|
|
17
|
+
```
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { cancel, intro, isCancel, log, outro, select } from "@clack/prompts";
|
|
2
|
+
import { getAscIndexOrder, sortBy } from "@ls-stack/utils/arrayUtils";
|
|
3
|
+
import { exhaustiveCheck } from "@ls-stack/utils/assertions";
|
|
4
|
+
import { dedent } from "@ls-stack/utils/dedent";
|
|
5
|
+
import { removeANSIColors } from "@ls-stack/utils/stringUtils";
|
|
6
|
+
import { isObjKey, typedObjectEntries } from "@ls-stack/utils/typingFnUtils";
|
|
7
|
+
import { styleText } from "node:util";
|
|
8
|
+
|
|
9
|
+
//#region src/createCli.ts
|
|
10
|
+
const [, , cmdFromTerminal, ...cmdArgs] = process.argv;
|
|
11
|
+
function createCmd({ short, description, run, args, examples }) {
|
|
12
|
+
return {
|
|
13
|
+
short,
|
|
14
|
+
description,
|
|
15
|
+
run,
|
|
16
|
+
args,
|
|
17
|
+
examples
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function createCLI({ name, sort, baseCmd }, cmds) {
|
|
21
|
+
function getCmdId(cmd) {
|
|
22
|
+
if (isObjKey(cmd, cmds)) return cmd;
|
|
23
|
+
log.error(styleText(["red", "bold"], `Command '${cmd}' not found`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
console.clear();
|
|
27
|
+
intro(styleText(["blue", "bold"], name));
|
|
28
|
+
const addedShortCmds = /* @__PURE__ */ new Set();
|
|
29
|
+
let runCmdId = cmdFromTerminal;
|
|
30
|
+
for (const [, cmd] of typedObjectEntries(cmds)) if (cmd.short) {
|
|
31
|
+
if (addedShortCmds.has(cmd.short)) {
|
|
32
|
+
console.error(styleText(["red", "bold"], `Short cmd "${cmd.short}" is duplicated`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
addedShortCmds.add(cmd.short);
|
|
36
|
+
}
|
|
37
|
+
function printHelp() {
|
|
38
|
+
const pipeChar = styleText(["dim"], " or ");
|
|
39
|
+
const fmtCmd = (c) => styleText(["blue", "bold"], c);
|
|
40
|
+
const beforeDescription = styleText(["dim"], "->");
|
|
41
|
+
const largestCmdTextLength = Math.max(...typedObjectEntries(cmds).map(([cmd, { short }]) => `${cmd}${short ? ` or ${short}` : ""}`.length));
|
|
42
|
+
log.info(dedent`
|
|
43
|
+
${styleText(["blue", "bold"], "Docs:")}
|
|
44
|
+
|
|
45
|
+
${styleText(["bold", "underline"], "Usage:")} ${baseCmd} <command> [command-args...]
|
|
46
|
+
|
|
47
|
+
${styleText(["bold", "underline"], "Commands:")}
|
|
48
|
+
|
|
49
|
+
${typedObjectEntries(cmds).map(([cmd, { description, short, args }]) => {
|
|
50
|
+
const cmdText = `${fmtCmd(cmd)}${short ? `${pipeChar}${fmtCmd(short)}` : ""}`;
|
|
51
|
+
const unformattedCmdText = removeANSIColors(cmdText);
|
|
52
|
+
let result = `${cmdText}${" ".repeat(largestCmdTextLength - unformattedCmdText.length + 1)}${beforeDescription} ${description}`;
|
|
53
|
+
if (args && Object.keys(args).length > 0) {
|
|
54
|
+
const briefArgs = typedObjectEntries(args).sort(([, a], [, b]) => {
|
|
55
|
+
if (a.type.startsWith("positional") && !b.type.startsWith("positional")) return -1;
|
|
56
|
+
if (!a.type.startsWith("positional") && b.type.startsWith("positional")) return 1;
|
|
57
|
+
return 0;
|
|
58
|
+
}).map(([, arg]) => {
|
|
59
|
+
switch (arg.type) {
|
|
60
|
+
case "positional-string":
|
|
61
|
+
case "positional-number": return `[${arg.name}]`;
|
|
62
|
+
case "flag": return `[--${arg.name}]`;
|
|
63
|
+
case "value-string-flag":
|
|
64
|
+
case "value-number-flag": return `[--${arg.name}]`;
|
|
65
|
+
default: throw exhaustiveCheck(arg);
|
|
66
|
+
}
|
|
67
|
+
}).join(" ");
|
|
68
|
+
result += `\n${styleText(["dim"], ` └─ args: ${briefArgs}`)}`;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}).join("\n")}
|
|
72
|
+
|
|
73
|
+
${styleText(["dim"], `Use ${baseCmd} <cmd> -h for more details about a command`)}
|
|
74
|
+
|
|
75
|
+
${fmtCmd("i")} ${beforeDescription} Starts in interactive mode
|
|
76
|
+
${fmtCmd("h")} ${beforeDescription} Prints this help message
|
|
77
|
+
`);
|
|
78
|
+
outro(styleText(["dim"], "Use a command to get started!"));
|
|
79
|
+
}
|
|
80
|
+
function printCmdHelp(cmdId) {
|
|
81
|
+
const cmd = cmds[getCmdId(cmdId)];
|
|
82
|
+
function formatArg(arg) {
|
|
83
|
+
switch (arg.type) {
|
|
84
|
+
case "positional-string":
|
|
85
|
+
case "positional-number": return `[${arg.name}]`;
|
|
86
|
+
case "flag": return `[--${arg.name}]`;
|
|
87
|
+
case "value-string-flag": return `[--${arg.name} <value>]`;
|
|
88
|
+
case "value-number-flag": return `[--${arg.name} <number>]`;
|
|
89
|
+
default: throw exhaustiveCheck(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function getArgsUsage(args) {
|
|
93
|
+
if (!args) return "";
|
|
94
|
+
const sortedArgs = typedObjectEntries(args).sort(([, a], [, b]) => {
|
|
95
|
+
if (a.type.startsWith("positional") && !b.type.startsWith("positional")) return -1;
|
|
96
|
+
if (!a.type.startsWith("positional") && b.type.startsWith("positional")) return 1;
|
|
97
|
+
return 0;
|
|
98
|
+
});
|
|
99
|
+
const colors = [
|
|
100
|
+
"cyan",
|
|
101
|
+
"yellow",
|
|
102
|
+
"magenta",
|
|
103
|
+
"blue",
|
|
104
|
+
"green",
|
|
105
|
+
"red"
|
|
106
|
+
];
|
|
107
|
+
return sortedArgs.map(([, arg], index) => {
|
|
108
|
+
const color = colors[index % colors.length];
|
|
109
|
+
if (!color) return formatArg(arg);
|
|
110
|
+
return styleText([color], formatArg(arg)) || formatArg(arg);
|
|
111
|
+
}).join(" ");
|
|
112
|
+
}
|
|
113
|
+
const cmdTitle = cmd.short ? `${cmdId} (${cmd.short})` : cmdId;
|
|
114
|
+
let helpText = `${styleText(["blue", "bold"], `${cmdTitle}:`) || `${cmdTitle}:`} ${cmd.description}\n\n${styleText(["bold", "underline"], "Usage:") || "Usage:"} ${styleText(["dim"], baseCmd) || baseCmd} ${styleText(["bold"], cmdId) || cmdId}`;
|
|
115
|
+
if (cmd.args) {
|
|
116
|
+
const argsUsage = getArgsUsage(cmd.args);
|
|
117
|
+
if (argsUsage) helpText += ` ${argsUsage}`;
|
|
118
|
+
}
|
|
119
|
+
if (cmd.short) helpText += `\n ${styleText(["dim"], baseCmd) || baseCmd} ${styleText(["bold"], cmd.short) || cmd.short} ...`;
|
|
120
|
+
if (cmd.args) {
|
|
121
|
+
const argEntries = typedObjectEntries(cmd.args);
|
|
122
|
+
if (argEntries.length > 0) {
|
|
123
|
+
helpText += `\n\n${styleText(["bold", "underline"], "Arguments:") || "Arguments:"}`;
|
|
124
|
+
const colors = [
|
|
125
|
+
"cyan",
|
|
126
|
+
"yellow",
|
|
127
|
+
"magenta",
|
|
128
|
+
"blue",
|
|
129
|
+
"green",
|
|
130
|
+
"red"
|
|
131
|
+
];
|
|
132
|
+
argEntries.forEach(([, arg], index) => {
|
|
133
|
+
const color = colors[index % colors.length];
|
|
134
|
+
if (!color) return;
|
|
135
|
+
const argDisplayName = arg.type === "flag" ? `--${arg.name}` : arg.type === "value-string-flag" ? `--${arg.name}` : arg.type === "value-number-flag" ? `--${arg.name}` : arg.name;
|
|
136
|
+
const argName = styleText([color], argDisplayName) || argDisplayName;
|
|
137
|
+
const required = "";
|
|
138
|
+
helpText += `\n ${argName}${required} - ${arg.description}`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (cmd.examples) {
|
|
143
|
+
helpText += `\n\n${styleText(["bold", "underline"], "Examples:") || "Examples:"}`;
|
|
144
|
+
cmd.examples.forEach(({ args: exampleArgs, description: exampleDesc }) => {
|
|
145
|
+
helpText += `\n ${baseCmd} ${cmdId} ${exampleArgs.join(" ")} ${styleText(["dim"], `# ${exampleDesc}`) || `# ${exampleDesc}`}`;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
log.info(helpText);
|
|
149
|
+
outro(styleText(["dim"], "Use a command to get started!"));
|
|
150
|
+
}
|
|
151
|
+
if (!cmdFromTerminal) {
|
|
152
|
+
const response = await select({
|
|
153
|
+
message: "Choose an action",
|
|
154
|
+
options: [{
|
|
155
|
+
value: "run-cmd",
|
|
156
|
+
label: `Start interactive mode`,
|
|
157
|
+
hint: `Select a command to run from a list | ${baseCmd} i`
|
|
158
|
+
}, {
|
|
159
|
+
value: "print-help",
|
|
160
|
+
label: `Print help`,
|
|
161
|
+
hint: `${baseCmd} h`
|
|
162
|
+
}]
|
|
163
|
+
});
|
|
164
|
+
if (isCancel(response)) {
|
|
165
|
+
cancel("Bye!");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
if (response === "print-help") {
|
|
169
|
+
printHelp();
|
|
170
|
+
process.exit(0);
|
|
171
|
+
} else runCmdId = "i";
|
|
172
|
+
}
|
|
173
|
+
if (runCmdId === "-h" || runCmdId === "--help" || runCmdId === "help" || runCmdId === "h") {
|
|
174
|
+
printHelp();
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
function parseArgs(rawArgs, commandArgs) {
|
|
178
|
+
if (!commandArgs) return {};
|
|
179
|
+
const parsed = {};
|
|
180
|
+
const argEntries = typedObjectEntries(commandArgs);
|
|
181
|
+
argEntries.forEach(([key, argDef]) => {
|
|
182
|
+
if ("default" in argDef && argDef.default !== void 0) parsed[key] = argDef.default;
|
|
183
|
+
});
|
|
184
|
+
const positionalArgs = argEntries.filter(([, argDef]) => argDef.type.startsWith("positional"));
|
|
185
|
+
let positionalIndex = 0;
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < rawArgs.length) {
|
|
188
|
+
const currentArg = rawArgs[i];
|
|
189
|
+
if (currentArg?.startsWith("--")) {
|
|
190
|
+
const flagName = currentArg.slice(2);
|
|
191
|
+
const flagEntry = argEntries.find(([, argDef]) => argDef.name === flagName);
|
|
192
|
+
if (flagEntry) {
|
|
193
|
+
const [key, argDef] = flagEntry;
|
|
194
|
+
if (argDef.type === "flag") {
|
|
195
|
+
parsed[key] = true;
|
|
196
|
+
i++;
|
|
197
|
+
} else if (argDef.type === "value-string-flag" || argDef.type === "value-number-flag") {
|
|
198
|
+
const value = rawArgs[i + 1];
|
|
199
|
+
if (value && !value.startsWith("--")) {
|
|
200
|
+
if (argDef.type === "value-number-flag") {
|
|
201
|
+
const numValue = Number(value);
|
|
202
|
+
if (isNaN(numValue)) {
|
|
203
|
+
log.error(styleText(["red", "bold"], `Error: Invalid number "${value}" for --${argDef.name}`));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
parsed[key] = numValue;
|
|
207
|
+
} else parsed[key] = value;
|
|
208
|
+
i += 2;
|
|
209
|
+
} else {
|
|
210
|
+
log.error(styleText(["red", "bold"], `Error: Missing value for --${argDef.name}`));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
} else i++;
|
|
214
|
+
} else {
|
|
215
|
+
log.error(styleText(["red", "bold"], `Error: Unknown flag --${flagName}`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
if (positionalIndex < positionalArgs.length) {
|
|
220
|
+
const positionalEntry = positionalArgs[positionalIndex];
|
|
221
|
+
if (positionalEntry) {
|
|
222
|
+
const [key, argDef] = positionalEntry;
|
|
223
|
+
if (argDef.type === "positional-number") {
|
|
224
|
+
const numValue = Number(currentArg);
|
|
225
|
+
if (isNaN(numValue)) {
|
|
226
|
+
log.error(styleText(["red", "bold"], `Error: Invalid number "${currentArg}" for ${argDef.name}`));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
parsed[key] = numValue;
|
|
230
|
+
} else parsed[key] = currentArg;
|
|
231
|
+
positionalIndex++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
async function runCmd(cmd, args) {
|
|
240
|
+
console.clear();
|
|
241
|
+
for (const [cmdId, { short, run: fn, args: commandArgs }] of typedObjectEntries(cmds)) if (cmd === short || cmd === cmdId) {
|
|
242
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
243
|
+
printCmdHelp(cmdId);
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
log.info(`Running ${styleText(["blue", "bold"], cmdId)}${short ? styleText(["dim"], `|${short}`) : ""}:\n`);
|
|
247
|
+
const parsedArgs = parseArgs(args, commandArgs);
|
|
248
|
+
await fn(parsedArgs);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
log.error(styleText(["red", "bold"], `Command '${cmd}' not found`));
|
|
252
|
+
printHelp();
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
if (runCmdId === "i") {
|
|
256
|
+
let cmdEntries = typedObjectEntries(cmds);
|
|
257
|
+
if (sort) cmdEntries = sortBy(cmdEntries, ([cmd]) => getAscIndexOrder(sort.indexOf(cmd)));
|
|
258
|
+
const response = await select({
|
|
259
|
+
message: "Select a command",
|
|
260
|
+
options: cmdEntries.map(([cmd, { short, description }]) => ({
|
|
261
|
+
value: cmd,
|
|
262
|
+
label: short ? `${cmd} ${styleText(["dim"], "|")} ${short}` : cmd,
|
|
263
|
+
hint: description
|
|
264
|
+
}))
|
|
265
|
+
});
|
|
266
|
+
if (isCancel(response)) {
|
|
267
|
+
cancel("Cancelled!");
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
await runCmd(response, []);
|
|
271
|
+
} else {
|
|
272
|
+
if (!runCmdId) {
|
|
273
|
+
log.error(styleText(["red", "bold"], `Command not found, use \`${baseCmd} h\` to list all supported commands`));
|
|
274
|
+
outro(styleText(["dim"], "Use a command to get started!"));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
await runCmd(runCmdId, cmdArgs);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/main.ts
|
|
283
|
+
await createCLI({
|
|
284
|
+
name: "✨ ai-cli",
|
|
285
|
+
baseCmd: "ai-cli"
|
|
286
|
+
}, { "review-pr": createCmd({
|
|
287
|
+
description: "Review a pull request with AI",
|
|
288
|
+
short: "rpr",
|
|
289
|
+
args: { pr: {
|
|
290
|
+
type: "positional-number",
|
|
291
|
+
name: "pr",
|
|
292
|
+
description: "The pull request to review"
|
|
293
|
+
} },
|
|
294
|
+
examples: [{
|
|
295
|
+
args: ["123"],
|
|
296
|
+
description: "Review pull request 123"
|
|
297
|
+
}],
|
|
298
|
+
run: async () => {
|
|
299
|
+
console.log("Reviewing pull request");
|
|
300
|
+
}
|
|
301
|
+
}) });
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-cmds",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"repository": "github:lucaslos/ai-cli",
|
|
10
|
+
"author": "Lucas Santos",
|
|
11
|
+
"main": "./dist/main.js",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=23.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@ls-stack/eslint-cfg": "^0.1.3",
|
|
17
|
+
"@types/node": "^24.3.0",
|
|
18
|
+
"@vitest/ui": "^3.2.4",
|
|
19
|
+
"eslint": "^9.33.0",
|
|
20
|
+
"fetch-mock": "^12.5.3",
|
|
21
|
+
"prettier": "^3.6.2",
|
|
22
|
+
"tsdown": "^0.14.1",
|
|
23
|
+
"typescript": "^5.9.2",
|
|
24
|
+
"vite": "^7.1.2",
|
|
25
|
+
"vitest": "^3.2.4",
|
|
26
|
+
"zod": "^4.0.17"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@clack/prompts": "^0.11.0",
|
|
30
|
+
"@ls-stack/utils": "^3.34.1",
|
|
31
|
+
"t-result": "^0.6.1"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"lint": "pnpm tsc && pnpm eslint && pnpm format",
|
|
35
|
+
"tsc": "tsc -p tsconfig.prod.json",
|
|
36
|
+
"tsc:watch": "tsc -p tsconfig.prod.json --watch",
|
|
37
|
+
"format": "prettier src/ --write --list-different",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"eslint": "eslint src/",
|
|
40
|
+
"test:ui": "vitest --ui",
|
|
41
|
+
"build": "pnpm test && pnpm lint && pnpm run build:no-test",
|
|
42
|
+
"build:no-test": "tsdown",
|
|
43
|
+
"build:watch": "tsdown --watch",
|
|
44
|
+
"pre-publish": "./scripts/check-if-is-sync.sh && pnpm build",
|
|
45
|
+
"npm-publish": "pnpm publish --access=public"
|
|
46
|
+
}
|
|
47
|
+
}
|