@iola_adm/iola-cli 0.1.38 → 0.1.40
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 +178 -23
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] || ""}`);
|
|
1186
|
+
for (const line of lines.slice(1)) output.write(` ${line}\n`);
|
|
1187
|
+
if (state.slashOpen) {
|
|
1188
|
+
output.write("\n");
|
|
1189
|
+
const matches = currentSlashMatches(state);
|
|
1190
|
+
if (matches.length === 0) {
|
|
1191
|
+
output.write(" нет команд\n");
|
|
1192
|
+
} else {
|
|
1193
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
1194
|
+
const marker = index === state.selected ? ">" : " ";
|
|
1195
|
+
output.write(`${marker} ${matches[index].command.padEnd(24)} ${matches[index].description}\n`);
|
|
1196
|
+
}
|
|
1197
|
+
output.write(" ↑/↓ выбрать • Enter вставить/выполнить • Esc закрыть\n");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
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("История пуста.");
|
|
@@ -1123,7 +1259,6 @@ function safePrompt(rl, closed = false) {
|
|
|
1123
1259
|
}
|
|
1124
1260
|
|
|
1125
1261
|
try {
|
|
1126
|
-
writePromptBottomPadding();
|
|
1127
1262
|
rl.prompt();
|
|
1128
1263
|
} catch {
|
|
1129
1264
|
// The input stream can close while an async slash-command is still running.
|
|
@@ -1146,7 +1281,6 @@ function attachSlashSuggestions(rl) {
|
|
|
1146
1281
|
lastFilter = filter;
|
|
1147
1282
|
output.write("\n");
|
|
1148
1283
|
printSlashMenu(filter, { compact: true, limit: 10 });
|
|
1149
|
-
writePromptBottomPadding();
|
|
1150
1284
|
rl.prompt(true);
|
|
1151
1285
|
}, 0);
|
|
1152
1286
|
};
|
|
@@ -1154,20 +1288,13 @@ function attachSlashSuggestions(rl) {
|
|
|
1154
1288
|
return () => input.off("keypress", onKeypress);
|
|
1155
1289
|
}
|
|
1156
1290
|
|
|
1157
|
-
function writePromptBottomPadding() {
|
|
1158
|
-
if (!output.isTTY) return;
|
|
1159
|
-
const padding = Math.max(0, Math.min(5, Number(process.env.IOLA_PROMPT_BOTTOM_PADDING || 2)));
|
|
1160
|
-
if (padding === 0) return;
|
|
1161
|
-
output.write(`${"\n".repeat(padding)}\x1b[${padding}A`);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
1291
|
async function showBanner(options = {}) {
|
|
1165
1292
|
const version = getPackageVersion();
|
|
1166
1293
|
const latest = options.skipUpdate ? null : await getLatestNpmVersion("@iola_adm/iola-cli");
|
|
1167
1294
|
const updateAvailable = latest && compareVersions(latest, version) > 0;
|
|
1168
1295
|
const versionLine = updateAvailable ? `v${version} -> v${latest} • npm install -g @iola_adm/iola-cli@latest` : `v${version} • iola help`;
|
|
1169
1296
|
if (process.stdout.isTTY && process.env.NO_COLOR !== "1") {
|
|
1170
|
-
console.log(
|
|
1297
|
+
console.log(renderBanner(versionLine, true));
|
|
1171
1298
|
if (updateAvailable) {
|
|
1172
1299
|
console.log(`Доступно обновление: v${version} -> v${latest}`);
|
|
1173
1300
|
console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
|
|
@@ -1176,13 +1303,41 @@ async function showBanner(options = {}) {
|
|
|
1176
1303
|
}
|
|
1177
1304
|
|
|
1178
1305
|
console.log(`CLI-Йошкар-Ола ${updateAvailable ? `v${version} -> v${latest}` : `v${version}`}`);
|
|
1179
|
-
console.log("
|
|
1306
|
+
console.log("Йошкар-Ола • MCP • локальный AI");
|
|
1180
1307
|
if (updateAvailable) console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
|
|
1181
1308
|
}
|
|
1182
1309
|
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1310
|
+
function renderBanner(versionLine, color = false) {
|
|
1311
|
+
const c = color ? {
|
|
1312
|
+
border: "\x1b[38;5;45m",
|
|
1313
|
+
title: "\x1b[38;5;213m",
|
|
1314
|
+
muted: "\x1b[38;5;250m",
|
|
1315
|
+
version: "\x1b[38;5;82m",
|
|
1316
|
+
reset: "\x1b[0m",
|
|
1317
|
+
} : { border: "", title: "", muted: "", version: "", reset: "" };
|
|
1318
|
+
const line = (text = "", style = "") => {
|
|
1319
|
+
const value = centerBannerText(text);
|
|
1320
|
+
return `${c.border}│${style}${value}${c.border}│`;
|
|
1321
|
+
};
|
|
1322
|
+
return [
|
|
1323
|
+
`${c.border}┌${"─".repeat(BANNER_WIDTH)}┐`,
|
|
1324
|
+
line(),
|
|
1325
|
+
line("CLI-Йошкар-Ола", c.title),
|
|
1326
|
+
line(),
|
|
1327
|
+
line("Йошкар-Ола • MCP • локальный AI", c.muted),
|
|
1328
|
+
line(),
|
|
1329
|
+
line(versionLine, c.version),
|
|
1330
|
+
`${c.border}└${"─".repeat(BANNER_WIDTH)}┘${c.reset}`,
|
|
1331
|
+
].join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function centerBannerText(value) {
|
|
1335
|
+
const text = String(value || "");
|
|
1336
|
+
const length = bannerVisibleLength(text);
|
|
1337
|
+
if (length >= BANNER_WIDTH) return [...text].slice(0, BANNER_WIDTH).join("");
|
|
1338
|
+
const left = Math.floor((BANNER_WIDTH - length) / 2);
|
|
1339
|
+
const right = BANNER_WIDTH - length - left;
|
|
1340
|
+
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
|
|
1186
1341
|
}
|
|
1187
1342
|
|
|
1188
1343
|
function bannerVisibleLength(value) {
|