@iola_adm/iola-cli 0.1.38 → 0.1.39
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/package.json +1 -1
- package/src/cli.js +177 -13
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -330,14 +330,7 @@ const SLASH_COMMANDS = [
|
|
|
330
330
|
{ command: "/init", description: "проверить окружение" },
|
|
331
331
|
{ command: "/exit", description: "выйти" },
|
|
332
332
|
];
|
|
333
|
-
const
|
|
334
|
-
│ │
|
|
335
|
-
│\x1b[38;5;213m CLI-Йошкар-Ола \x1b[38;5;45m│
|
|
336
|
-
│ │
|
|
337
|
-
│\x1b[38;5;250m открытые данные • MCP • локальный AI \x1b[38;5;45m│
|
|
338
|
-
│ │
|
|
339
|
-
│\x1b[38;5;82m VERSION_LINE \x1b[38;5;45m│
|
|
340
|
-
└────────────────────────────────────────────────────────────────────────────┘\x1b[0m`;
|
|
333
|
+
const BANNER_WIDTH = 76;
|
|
341
334
|
|
|
342
335
|
const COMMANDS = new Map([
|
|
343
336
|
["help", showHelp],
|
|
@@ -632,6 +625,17 @@ async function startAgent() {
|
|
|
632
625
|
console.log("Интерактивный режим. Введите /help для списка команд, /exit для выхода.");
|
|
633
626
|
await runHooks("SessionStart", { mode: "agent" });
|
|
634
627
|
|
|
628
|
+
if (input.isTTY && output.isTTY) {
|
|
629
|
+
await startAgentRawInput();
|
|
630
|
+
await runHooks("SessionEnd", { mode: "agent" });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
await startAgentReadline();
|
|
635
|
+
await runHooks("SessionEnd", { mode: "agent" });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function startAgentReadline() {
|
|
635
639
|
const rl = readline.createInterface({ input, output, prompt: "iola> " });
|
|
636
640
|
const state = {
|
|
637
641
|
history: [],
|
|
@@ -667,7 +671,86 @@ async function startAgent() {
|
|
|
667
671
|
rl.close();
|
|
668
672
|
}
|
|
669
673
|
detachSlashSuggestions();
|
|
670
|
-
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function startAgentRawInput() {
|
|
677
|
+
const state = { history: [], buffer: "", selected: 0, slashOpen: false, running: false };
|
|
678
|
+
emitKeypressEvents(input);
|
|
679
|
+
const wasRaw = input.isRaw;
|
|
680
|
+
input.setRawMode(true);
|
|
681
|
+
input.resume();
|
|
682
|
+
|
|
683
|
+
const render = () => renderAgentInput(state);
|
|
684
|
+
render();
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
while (true) {
|
|
688
|
+
const { str, key } = await readKeypress();
|
|
689
|
+
if (key?.ctrl && key.name === "c") break;
|
|
690
|
+
if (key?.name === "escape") {
|
|
691
|
+
state.slashOpen = false;
|
|
692
|
+
render();
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (key?.name === "backspace") {
|
|
696
|
+
state.buffer = [...state.buffer].slice(0, -1).join("");
|
|
697
|
+
updateSlashState(state);
|
|
698
|
+
render();
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (key?.name === "up" && state.slashOpen) {
|
|
702
|
+
const matches = currentSlashMatches(state);
|
|
703
|
+
state.selected = Math.max(0, Math.min(matches.length - 1, state.selected - 1));
|
|
704
|
+
render();
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (key?.name === "down" && state.slashOpen) {
|
|
708
|
+
const matches = currentSlashMatches(state);
|
|
709
|
+
state.selected = Math.max(0, Math.min(matches.length - 1, state.selected + 1));
|
|
710
|
+
render();
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (isShiftEnter(str, key)) {
|
|
714
|
+
state.buffer += "\n";
|
|
715
|
+
state.slashOpen = false;
|
|
716
|
+
render();
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
720
|
+
const matches = currentSlashMatches(state);
|
|
721
|
+
const selected = matches[state.selected];
|
|
722
|
+
if (state.slashOpen && selected && state.buffer.trim() !== selected.command) {
|
|
723
|
+
state.buffer = selected.command;
|
|
724
|
+
state.slashOpen = false;
|
|
725
|
+
render();
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const line = state.buffer.trim();
|
|
729
|
+
state.buffer = "";
|
|
730
|
+
state.slashOpen = false;
|
|
731
|
+
clearAgentInputArea();
|
|
732
|
+
if (!line) {
|
|
733
|
+
render();
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
const shouldExit = await handleAgentLine(line, state);
|
|
738
|
+
if (shouldExit) break;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
741
|
+
}
|
|
742
|
+
render();
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (str && !key?.ctrl && !key?.meta) {
|
|
746
|
+
state.buffer += str;
|
|
747
|
+
updateSlashState(state);
|
|
748
|
+
render();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
} finally {
|
|
752
|
+
if (!wasRaw) input.setRawMode(false);
|
|
753
|
+
}
|
|
671
754
|
}
|
|
672
755
|
|
|
673
756
|
async function handleAgentLine(line, state) {
|
|
@@ -1085,6 +1168,59 @@ function getSlashCommandMatches(filter = "") {
|
|
|
1085
1168
|
|| item.description.toLocaleLowerCase("ru-RU").includes(normalized));
|
|
1086
1169
|
}
|
|
1087
1170
|
|
|
1171
|
+
function updateSlashState(state) {
|
|
1172
|
+
state.slashOpen = state.buffer.startsWith("/");
|
|
1173
|
+
state.selected = 0;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function currentSlashMatches(state) {
|
|
1177
|
+
if (!state.buffer.startsWith("/")) return [];
|
|
1178
|
+
return getSlashCommandMatches(state.buffer.slice(1)).slice(0, 10);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function renderAgentInput(state) {
|
|
1182
|
+
clearAgentInputArea();
|
|
1183
|
+
const prompt = "iola> ";
|
|
1184
|
+
const lines = state.buffer.split("\n");
|
|
1185
|
+
output.write(`${prompt}${lines[0] || ""}\n`);
|
|
1186
|
+
for (const line of lines.slice(1)) output.write(` ${line}\n`);
|
|
1187
|
+
if (state.slashOpen) {
|
|
1188
|
+
const matches = currentSlashMatches(state);
|
|
1189
|
+
if (matches.length === 0) {
|
|
1190
|
+
output.write(" нет команд\n");
|
|
1191
|
+
} else {
|
|
1192
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
1193
|
+
const marker = index === state.selected ? ">" : " ";
|
|
1194
|
+
output.write(`${marker} ${matches[index].command.padEnd(24)} ${matches[index].description}\n`);
|
|
1195
|
+
}
|
|
1196
|
+
output.write(" ↑/↓ выбрать • Enter вставить/выполнить • Esc закрыть\n");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
writePromptBottomPadding();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function clearAgentInputArea() {
|
|
1203
|
+
if (!output.isTTY) return;
|
|
1204
|
+
output.write("\x1b[2K\r");
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function readKeypress() {
|
|
1208
|
+
return new Promise((resolve) => {
|
|
1209
|
+
const handler = (str, key) => {
|
|
1210
|
+
input.off("keypress", handler);
|
|
1211
|
+
resolve({ str, key });
|
|
1212
|
+
};
|
|
1213
|
+
input.on("keypress", handler);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function isShiftEnter(str, key) {
|
|
1218
|
+
return (key?.name === "return" && key.shift)
|
|
1219
|
+
|| (key?.name === "enter" && key.shift)
|
|
1220
|
+
|| String(str || "").includes("[13;2")
|
|
1221
|
+
|| String(str || "").includes("[27;2;13");
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1088
1224
|
function printAgentHistory(history) {
|
|
1089
1225
|
if (history.length === 0) {
|
|
1090
1226
|
console.log("История пуста.");
|
|
@@ -1167,7 +1303,7 @@ async function showBanner(options = {}) {
|
|
|
1167
1303
|
const updateAvailable = latest && compareVersions(latest, version) > 0;
|
|
1168
1304
|
const versionLine = updateAvailable ? `v${version} -> v${latest} • npm install -g @iola_adm/iola-cli@latest` : `v${version} • iola help`;
|
|
1169
1305
|
if (process.stdout.isTTY && process.env.NO_COLOR !== "1") {
|
|
1170
|
-
console.log(
|
|
1306
|
+
console.log(renderBanner(versionLine, true));
|
|
1171
1307
|
if (updateAvailable) {
|
|
1172
1308
|
console.log(`Доступно обновление: v${version} -> v${latest}`);
|
|
1173
1309
|
console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
|
|
@@ -1180,9 +1316,37 @@ async function showBanner(options = {}) {
|
|
|
1180
1316
|
if (updateAvailable) console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
|
|
1181
1317
|
}
|
|
1182
1318
|
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1319
|
+
function renderBanner(versionLine, color = false) {
|
|
1320
|
+
const c = color ? {
|
|
1321
|
+
border: "\x1b[38;5;45m",
|
|
1322
|
+
title: "\x1b[38;5;213m",
|
|
1323
|
+
muted: "\x1b[38;5;250m",
|
|
1324
|
+
version: "\x1b[38;5;82m",
|
|
1325
|
+
reset: "\x1b[0m",
|
|
1326
|
+
} : { border: "", title: "", muted: "", version: "", reset: "" };
|
|
1327
|
+
const line = (text = "", style = "") => {
|
|
1328
|
+
const value = centerBannerText(text);
|
|
1329
|
+
return `${c.border}│${style}${value}${c.border}│`;
|
|
1330
|
+
};
|
|
1331
|
+
return [
|
|
1332
|
+
`${c.border}┌${"─".repeat(BANNER_WIDTH)}┐`,
|
|
1333
|
+
line(),
|
|
1334
|
+
line("CLI-Йошкар-Ола", c.title),
|
|
1335
|
+
line(),
|
|
1336
|
+
line("открытые данные • MCP • локальный AI", c.muted),
|
|
1337
|
+
line(),
|
|
1338
|
+
line(versionLine, c.version),
|
|
1339
|
+
`${c.border}└${"─".repeat(BANNER_WIDTH)}┘${c.reset}`,
|
|
1340
|
+
].join("\n");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function centerBannerText(value) {
|
|
1344
|
+
const text = String(value || "");
|
|
1345
|
+
const length = bannerVisibleLength(text);
|
|
1346
|
+
if (length >= BANNER_WIDTH) return [...text].slice(0, BANNER_WIDTH).join("");
|
|
1347
|
+
const left = Math.floor((BANNER_WIDTH - length) / 2);
|
|
1348
|
+
const right = BANNER_WIDTH - length - left;
|
|
1349
|
+
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
|
|
1186
1350
|
}
|
|
1187
1351
|
|
|
1188
1352
|
function bannerVisibleLength(value) {
|