@iola_adm/iola-cli 0.1.8 → 0.1.9
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 +24 -5
- package/package.json +1 -1
- package/src/cli.js +248 -12
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ npx -y @iola_adm/iola-cli ai setup ollama
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
npx -y @iola_adm/iola-cli help
|
|
65
|
+
npx -y @iola_adm/iola-cli init --yes
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
Глобальная установка:
|
|
@@ -69,6 +70,7 @@ npx -y @iola_adm/iola-cli help
|
|
|
69
70
|
```bash
|
|
70
71
|
npm install -g @iola_adm/iola-cli
|
|
71
72
|
iola help
|
|
73
|
+
iola init
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
## Команды
|
|
@@ -76,6 +78,13 @@ iola help
|
|
|
76
78
|
```bash
|
|
77
79
|
iola banner
|
|
78
80
|
iola agent
|
|
81
|
+
iola chat
|
|
82
|
+
iola init
|
|
83
|
+
iola update
|
|
84
|
+
iola version --check
|
|
85
|
+
iola data schools --limit 10
|
|
86
|
+
iola data kindergartens --search "29"
|
|
87
|
+
iola data schools --format csv
|
|
79
88
|
iola ai doctor
|
|
80
89
|
iola ai setup ollama
|
|
81
90
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
@@ -87,6 +96,7 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
87
96
|
iola health
|
|
88
97
|
iola layers
|
|
89
98
|
iola schools --limit 10
|
|
99
|
+
iola schools --format csv
|
|
90
100
|
iola schools get --inn 1215067180
|
|
91
101
|
iola kindergartens --search "29"
|
|
92
102
|
iola kindergartens get --inn 1215077421 --json
|
|
@@ -96,7 +106,8 @@ iola setup codex
|
|
|
96
106
|
```
|
|
97
107
|
|
|
98
108
|
По умолчанию команды выводят компактную таблицу. Для полного ответа API
|
|
99
|
-
используйте `--json`.
|
|
109
|
+
используйте `--json` или `--format json`. Для выгрузки используйте
|
|
110
|
+
`--format csv`.
|
|
100
111
|
|
|
101
112
|
## Интерактивный режим
|
|
102
113
|
|
|
@@ -110,6 +121,7 @@ iola agent
|
|
|
110
121
|
/help
|
|
111
122
|
/health
|
|
112
123
|
/layers
|
|
124
|
+
/data schools --limit 10
|
|
113
125
|
/schools --limit 10
|
|
114
126
|
/schools get --inn 1215067180
|
|
115
127
|
/kindergartens --search 29
|
|
@@ -126,10 +138,13 @@ iola agent
|
|
|
126
138
|
/config
|
|
127
139
|
/history
|
|
128
140
|
/clear
|
|
141
|
+
/update
|
|
142
|
+
/init
|
|
129
143
|
/exit
|
|
130
144
|
```
|
|
131
145
|
|
|
132
146
|
Обычный текст без `/` в `iola agent` отправляется в настроенный AI-провайдер.
|
|
147
|
+
`iola chat` запускает тот же интерактивный режим.
|
|
133
148
|
|
|
134
149
|
## AI-запросы
|
|
135
150
|
|
|
@@ -166,6 +181,10 @@ iola ai context "улица Петрова"
|
|
|
166
181
|
|
|
167
182
|
Поиск контекста учитывает номера учреждений, ИНН и улицы.
|
|
168
183
|
|
|
184
|
+
AI-ответ строится по контексту из публичного API. Ассистент получает краткий
|
|
185
|
+
список источников контекста и должен указывать слой, название и ИНН, если
|
|
186
|
+
отвечает по конкретным организациям.
|
|
187
|
+
|
|
169
188
|
Ключи OpenAI/OpenRouter сохраняются локально на компьютере пользователя:
|
|
170
189
|
|
|
171
190
|
```text
|
|
@@ -185,14 +204,14 @@ iola ai key delete openai
|
|
|
185
204
|
`OPENROUTER_API_KEY`) и сохранен локальный ключ, CLI использует переменную
|
|
186
205
|
окружения как более приоритетную.
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
Если данных в контексте недостаточно, ассистент должен сообщить об этом, а не
|
|
208
|
+
выдумывать сведения.
|
|
190
209
|
|
|
191
210
|
## Назначение
|
|
192
211
|
|
|
193
212
|
CLI дает прямой терминальный доступ к открытым данным городского округа,
|
|
194
|
-
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter
|
|
195
|
-
интерактивному агентному
|
|
213
|
+
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter,
|
|
214
|
+
интерактивному агентному режиму, экспорту данных и проверке обновлений.
|
|
196
215
|
|
|
197
216
|
## Переменные окружения
|
|
198
217
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,6 +17,16 @@ const DEFAULT_AI_CONFIG = {
|
|
|
17
17
|
baseUrl: "http://127.0.0.1:11434",
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
|
+
const DATASETS = {
|
|
21
|
+
schools: {
|
|
22
|
+
title: "Школы",
|
|
23
|
+
endpoint: "schools",
|
|
24
|
+
},
|
|
25
|
+
kindergartens: {
|
|
26
|
+
title: "Детские сады",
|
|
27
|
+
endpoint: "kindergartens",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
20
30
|
const BANNER = `\x1b[38;5;45m┌────────────────────────────────────────────────────────────────────────────┐
|
|
21
31
|
│\x1b[38;5;51m ____ _ ___ ____ ____ ___ _____ _ _______ \x1b[38;5;45m│
|
|
22
32
|
│\x1b[38;5;51m / ___| | |_ _| | _ \\| _ \\ / _ \\| ____| |/ /_ _| \x1b[38;5;45m│
|
|
@@ -34,11 +44,15 @@ const BANNER = `\x1b[38;5;45m┌────────────────
|
|
|
34
44
|
const COMMANDS = new Map([
|
|
35
45
|
["help", showHelp],
|
|
36
46
|
["version", showVersion],
|
|
47
|
+
["update", checkUpdate],
|
|
37
48
|
["banner", showBanner],
|
|
38
49
|
["agent", startAgent],
|
|
50
|
+
["chat", startAgent],
|
|
39
51
|
["ai", handleAi],
|
|
52
|
+
["init", initCli],
|
|
40
53
|
["health", checkHealth],
|
|
41
54
|
["layers", listLayers],
|
|
55
|
+
["data", handleData],
|
|
42
56
|
["schools", listSchools],
|
|
43
57
|
["kindergartens", listKindergartens],
|
|
44
58
|
["search", searchAll],
|
|
@@ -64,6 +78,10 @@ async function showHelp() {
|
|
|
64
78
|
Usage:
|
|
65
79
|
iola banner
|
|
66
80
|
iola agent
|
|
81
|
+
iola chat
|
|
82
|
+
iola init
|
|
83
|
+
iola update
|
|
84
|
+
iola data LAYER [--limit 10] [--search TEXT] [--format table|json|csv]
|
|
67
85
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
68
86
|
iola ai context TEXT [--json]
|
|
69
87
|
iola ai key set openai
|
|
@@ -75,11 +93,11 @@ Usage:
|
|
|
75
93
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
76
94
|
iola health [--json]
|
|
77
95
|
iola layers [--json]
|
|
78
|
-
iola schools [--limit 10] [--search TEXT] [--json]
|
|
96
|
+
iola schools [--limit 10] [--search TEXT] [--format table|json|csv]
|
|
79
97
|
iola schools get --inn INN [--json]
|
|
80
|
-
iola kindergartens [--limit 10] [--search TEXT] [--json]
|
|
98
|
+
iola kindergartens [--limit 10] [--search TEXT] [--format table|json|csv]
|
|
81
99
|
iola kindergartens get --inn INN [--json]
|
|
82
|
-
iola search TEXT [--limit 5] [--json]
|
|
100
|
+
iola search TEXT [--limit 5] [--format table|json|csv]
|
|
83
101
|
iola mcp-info [--json]
|
|
84
102
|
iola setup codex
|
|
85
103
|
iola version
|
|
@@ -194,6 +212,16 @@ async function handleAgentLine(line, state) {
|
|
|
194
212
|
return false;
|
|
195
213
|
}
|
|
196
214
|
|
|
215
|
+
if (command === "update") {
|
|
216
|
+
await checkUpdate(args);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (command === "init") {
|
|
221
|
+
await initCli(args);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
197
225
|
if (command === "ai") {
|
|
198
226
|
await handleAi(args);
|
|
199
227
|
return false;
|
|
@@ -202,6 +230,7 @@ async function handleAgentLine(line, state) {
|
|
|
202
230
|
const mapped = {
|
|
203
231
|
health: ["health", args],
|
|
204
232
|
layers: ["layers", args],
|
|
233
|
+
data: ["data", args],
|
|
205
234
|
schools: ["schools", args],
|
|
206
235
|
kindergartens: ["kindergartens", args],
|
|
207
236
|
search: ["search", args],
|
|
@@ -225,6 +254,7 @@ function printAgentHelp() {
|
|
|
225
254
|
/help
|
|
226
255
|
/health
|
|
227
256
|
/layers
|
|
257
|
+
/data schools --limit 10
|
|
228
258
|
/schools --limit 10
|
|
229
259
|
/schools get --inn 1215067180
|
|
230
260
|
/kindergartens --search 29
|
|
@@ -244,6 +274,8 @@ function printAgentHelp() {
|
|
|
244
274
|
/history
|
|
245
275
|
/clear
|
|
246
276
|
/banner
|
|
277
|
+
/update
|
|
278
|
+
/init
|
|
247
279
|
/exit
|
|
248
280
|
|
|
249
281
|
Обычный текст без slash-команды отправляется в настроенный AI-провайдер.`);
|
|
@@ -282,9 +314,43 @@ function showBanner() {
|
|
|
282
314
|
console.log("открытые данные • MCP • локальный AI");
|
|
283
315
|
}
|
|
284
316
|
|
|
285
|
-
async function showVersion() {
|
|
317
|
+
async function showVersion(args = []) {
|
|
318
|
+
const options = parseOptions(args);
|
|
286
319
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
287
320
|
console.log(packageJson.default.version);
|
|
321
|
+
|
|
322
|
+
if (options.check) {
|
|
323
|
+
await checkUpdate([]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function checkUpdate() {
|
|
328
|
+
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
329
|
+
const current = packageJson.default.version;
|
|
330
|
+
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
331
|
+
|
|
332
|
+
if (!latest) {
|
|
333
|
+
console.log("Не удалось проверить npm-версию.");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const comparison = compareVersions(latest, current);
|
|
338
|
+
|
|
339
|
+
if (comparison > 0) {
|
|
340
|
+
console.log(`Доступна новая версия: ${latest}`);
|
|
341
|
+
console.log("Обновление:");
|
|
342
|
+
console.log(` npm install -g ${packageJson.default.name}@latest`);
|
|
343
|
+
console.log("Или запуск без установки:");
|
|
344
|
+
console.log(` npx -y ${packageJson.default.name}@latest help`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (comparison < 0) {
|
|
349
|
+
console.log(`Локальная версия ${current} новее опубликованной npm latest ${latest}.`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`Установлена актуальная версия: ${current}`);
|
|
288
354
|
}
|
|
289
355
|
|
|
290
356
|
async function checkHealth(args) {
|
|
@@ -304,6 +370,41 @@ async function checkHealth(args) {
|
|
|
304
370
|
});
|
|
305
371
|
}
|
|
306
372
|
|
|
373
|
+
async function initCli(args = []) {
|
|
374
|
+
const options = parseOptions(args);
|
|
375
|
+
|
|
376
|
+
showBanner();
|
|
377
|
+
console.log("Проверка окружения");
|
|
378
|
+
printKeyValue({
|
|
379
|
+
node: process.version,
|
|
380
|
+
npm: await getCommandVersion("npm", ["--version"]),
|
|
381
|
+
api: await probeEndpoint(`${MCP_BASE_URL}/mcp-health`),
|
|
382
|
+
mcp: MCP_BASE_URL,
|
|
383
|
+
});
|
|
384
|
+
console.log("");
|
|
385
|
+
|
|
386
|
+
await aiDoctor(options.json ? ["--json"] : []);
|
|
387
|
+
|
|
388
|
+
if (!process.stdin.isTTY || options.yes) {
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log("Для настройки AI используйте:");
|
|
391
|
+
console.log(" iola ai setup ollama");
|
|
392
|
+
console.log(" iola ai key set openai");
|
|
393
|
+
console.log(" iola ai setup openai --model gpt-4.1-mini");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log("");
|
|
398
|
+
const configureAi = await confirm("Настроить AI-провайдер сейчас? [Y/n] ");
|
|
399
|
+
|
|
400
|
+
if (configureAi) {
|
|
401
|
+
await aiSetup([]);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log("");
|
|
405
|
+
await checkUpdate();
|
|
406
|
+
}
|
|
407
|
+
|
|
307
408
|
async function handleAi(args) {
|
|
308
409
|
const [subcommand = "help", ...rest] = args;
|
|
309
410
|
|
|
@@ -393,7 +494,8 @@ async function aiSetup(args) {
|
|
|
393
494
|
},
|
|
394
495
|
});
|
|
395
496
|
console.log(`AI-профиль ${provider} сохранен в ${CONFIG_FILE}`);
|
|
396
|
-
console.log(`Ключ
|
|
497
|
+
console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
|
|
498
|
+
console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
397
499
|
return;
|
|
398
500
|
}
|
|
399
501
|
|
|
@@ -738,12 +840,14 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
738
840
|
}
|
|
739
841
|
|
|
740
842
|
function buildAiMessages(question, dataContext, history) {
|
|
843
|
+
const sourceLines = buildSourceLines(dataContext);
|
|
741
844
|
const system = [
|
|
742
845
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
743
846
|
"Отвечай на русском языке.",
|
|
744
847
|
"Используй только данные из переданного контекста.",
|
|
745
848
|
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
746
849
|
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
850
|
+
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
747
851
|
"Отвечай кратко и по делу.",
|
|
748
852
|
].join(" ");
|
|
749
853
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
@@ -754,11 +858,26 @@ function buildAiMessages(question, dataContext, history) {
|
|
|
754
858
|
...recentHistory,
|
|
755
859
|
{
|
|
756
860
|
role: "user",
|
|
757
|
-
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nВопрос пользователя: ${question}`,
|
|
861
|
+
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nКраткие источники контекста:\n${sourceLines}\n\nВопрос пользователя: ${question}`,
|
|
758
862
|
},
|
|
759
863
|
];
|
|
760
864
|
}
|
|
761
865
|
|
|
866
|
+
function buildSourceLines(dataContext) {
|
|
867
|
+
const rows = [
|
|
868
|
+
...dataContext.schools.map((item) => ({ layer: "schools", ...item })),
|
|
869
|
+
...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
if (rows.length === 0) {
|
|
873
|
+
return "Совпавших организаций нет.";
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return rows
|
|
877
|
+
.map((item) => `- ${item.layer}: ${item.name || "-"}; ИНН ${item.inn || "-"}; адрес ${item.address || "-"}`)
|
|
878
|
+
.join("\n");
|
|
879
|
+
}
|
|
880
|
+
|
|
762
881
|
async function callAiProvider(config, messages) {
|
|
763
882
|
if (config.provider === "ollama") {
|
|
764
883
|
return callOllama(config, messages);
|
|
@@ -886,6 +1005,28 @@ async function listKindergartens(args) {
|
|
|
886
1005
|
await listDataset("kindergartens", args);
|
|
887
1006
|
}
|
|
888
1007
|
|
|
1008
|
+
async function handleData(args) {
|
|
1009
|
+
const [dataset, ...rest] = args;
|
|
1010
|
+
|
|
1011
|
+
if (!dataset) {
|
|
1012
|
+
console.log("Доступные слои:");
|
|
1013
|
+
printTable(Object.entries(DATASETS).map(([id, value]) => ({ id, name: value.title })), [
|
|
1014
|
+
["id", "ID"],
|
|
1015
|
+
["name", "Название"],
|
|
1016
|
+
]);
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log("Пример:");
|
|
1019
|
+
console.log(" iola data schools --limit 10");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (!DATASETS[dataset]) {
|
|
1024
|
+
throw new Error(`Неизвестный слой: ${dataset}. Доступно: ${Object.keys(DATASETS).join(", ")}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
await listDataset(dataset, rest);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
889
1030
|
async function listDataset(dataset, args) {
|
|
890
1031
|
const options = parseOptions(args);
|
|
891
1032
|
|
|
@@ -898,16 +1039,21 @@ async function listDataset(dataset, args) {
|
|
|
898
1039
|
params.set("limit", options.limit || "20");
|
|
899
1040
|
params.set("offset", options.offset || "0");
|
|
900
1041
|
|
|
901
|
-
const data = await fetchJson(`${API_BASE_URL}/${dataset}?${params}`);
|
|
1042
|
+
const data = await fetchJson(`${API_BASE_URL}/${DATASETS[dataset].endpoint}?${params}`);
|
|
902
1043
|
const items = normalizeItems(data);
|
|
903
1044
|
const filtered = options.search ? filterItems(items, options.search) : items;
|
|
904
1045
|
const limited = filtered.slice(0, Number(options.limit || 20));
|
|
905
1046
|
|
|
906
|
-
if (options.json) {
|
|
1047
|
+
if (options.json || options.format === "json") {
|
|
907
1048
|
printJson(limited);
|
|
908
1049
|
return;
|
|
909
1050
|
}
|
|
910
1051
|
|
|
1052
|
+
if (options.format === "csv") {
|
|
1053
|
+
printCsv(limited.map(selectPublicSummary));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
911
1057
|
printDatasetTable(limited);
|
|
912
1058
|
}
|
|
913
1059
|
|
|
@@ -916,7 +1062,7 @@ async function getDatasetItem(dataset, options) {
|
|
|
916
1062
|
throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
|
|
917
1063
|
}
|
|
918
1064
|
|
|
919
|
-
const data = await fetchJson(`${API_BASE_URL}/${dataset}?limit=500&offset=0`);
|
|
1065
|
+
const data = await fetchJson(`${API_BASE_URL}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
920
1066
|
const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
|
|
921
1067
|
|
|
922
1068
|
if (!item) {
|
|
@@ -950,11 +1096,19 @@ async function searchAll(args) {
|
|
|
950
1096
|
kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
|
|
951
1097
|
};
|
|
952
1098
|
|
|
953
|
-
if (options.json) {
|
|
1099
|
+
if (options.json || options.format === "json") {
|
|
954
1100
|
printJson(result);
|
|
955
1101
|
return;
|
|
956
1102
|
}
|
|
957
1103
|
|
|
1104
|
+
if (options.format === "csv") {
|
|
1105
|
+
printCsv([
|
|
1106
|
+
...result.schools.map((item) => ({ layer: "schools", ...selectPublicSummary(item) })),
|
|
1107
|
+
...result.kindergartens.map((item) => ({ layer: "kindergartens", ...selectPublicSummary(item) })),
|
|
1108
|
+
]);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
958
1112
|
console.log("Школы");
|
|
959
1113
|
printDatasetTable(result.schools);
|
|
960
1114
|
console.log("");
|
|
@@ -981,7 +1135,9 @@ function parseOptions(args) {
|
|
|
981
1135
|
const arg = args[index];
|
|
982
1136
|
if (arg === "--json" || arg === "--yes") {
|
|
983
1137
|
result[arg.slice(2)] = true;
|
|
984
|
-
} else if (arg === "--
|
|
1138
|
+
} else if (arg === "--check") {
|
|
1139
|
+
result.check = true;
|
|
1140
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--format") {
|
|
985
1141
|
result[arg.slice(2)] = args[index + 1];
|
|
986
1142
|
index += 1;
|
|
987
1143
|
} else {
|
|
@@ -1134,6 +1290,66 @@ async function getOllamaVersion() {
|
|
|
1134
1290
|
}
|
|
1135
1291
|
}
|
|
1136
1292
|
|
|
1293
|
+
async function getCommandVersion(command, args) {
|
|
1294
|
+
try {
|
|
1295
|
+
const { stdout } = await runCommand(command, args);
|
|
1296
|
+
return stdout.trim() || "installed";
|
|
1297
|
+
} catch {
|
|
1298
|
+
if (process.platform === "win32" && !command.endsWith(".cmd")) {
|
|
1299
|
+
try {
|
|
1300
|
+
const { stdout } = await runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `${command} ${args.join(" ")}`]);
|
|
1301
|
+
return stdout.trim() || "installed";
|
|
1302
|
+
} catch {
|
|
1303
|
+
return "не найден";
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return "не найден";
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async function probeEndpoint(url) {
|
|
1312
|
+
try {
|
|
1313
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
1314
|
+
return response.ok ? "доступен" : `${response.status} ${response.statusText}`;
|
|
1315
|
+
} catch {
|
|
1316
|
+
return "недоступен";
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async function getLatestNpmVersion(packageName) {
|
|
1321
|
+
try {
|
|
1322
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
|
|
1323
|
+
headers: { accept: "application/json" },
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
if (!response.ok) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const payload = await response.json();
|
|
1331
|
+
return payload.version || null;
|
|
1332
|
+
} catch {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function compareVersions(left, right) {
|
|
1338
|
+
const leftParts = String(left).split(".").map(Number);
|
|
1339
|
+
const rightParts = String(right).split(".").map(Number);
|
|
1340
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1341
|
+
|
|
1342
|
+
for (let index = 0; index < length; index += 1) {
|
|
1343
|
+
const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
1344
|
+
|
|
1345
|
+
if (diff !== 0) {
|
|
1346
|
+
return diff > 0 ? 1 : -1;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return 0;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1137
1353
|
function recommendOllamaModel(diagnostics) {
|
|
1138
1354
|
const ramGb = diagnostics.ramGb || 0;
|
|
1139
1355
|
const vramGb = diagnostics.gpu.vramGb || 0;
|
|
@@ -1204,8 +1420,10 @@ async function confirm(question) {
|
|
|
1204
1420
|
}
|
|
1205
1421
|
|
|
1206
1422
|
async function saveConfig(value) {
|
|
1423
|
+
const current = await loadConfig();
|
|
1424
|
+
const merged = mergeConfig(current, value);
|
|
1207
1425
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
1208
|
-
await writeFile(CONFIG_FILE, `${JSON.stringify(
|
|
1426
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
1209
1427
|
}
|
|
1210
1428
|
|
|
1211
1429
|
async function loadConfig() {
|
|
@@ -1337,6 +1555,24 @@ function printJson(value) {
|
|
|
1337
1555
|
console.log(JSON.stringify(value, null, 2));
|
|
1338
1556
|
}
|
|
1339
1557
|
|
|
1558
|
+
function printCsv(rows) {
|
|
1559
|
+
if (rows.length === 0) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
1564
|
+
console.log(columns.map(csvCell).join(","));
|
|
1565
|
+
|
|
1566
|
+
for (const row of rows) {
|
|
1567
|
+
console.log(columns.map((column) => csvCell(row[column])).join(","));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function csvCell(value) {
|
|
1572
|
+
const text = value == null ? "" : String(value);
|
|
1573
|
+
return `"${text.replace(/"/g, "\"\"")}"`;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1340
1576
|
function printDatasetTable(items) {
|
|
1341
1577
|
printTable(items.map(selectPublicSummary), [
|
|
1342
1578
|
["inn", "ИНН"],
|