@iola_adm/iola-cli 0.1.1 → 0.1.3
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 +57 -0
- package/package.json +1 -1
- package/src/cli.js +506 -12
package/README.md
CHANGED
|
@@ -9,6 +9,55 @@ CLI для работы с открытыми данными городског
|
|
|
9
9
|
- `https://apiiola.yasg.ru/api/v1`
|
|
10
10
|
- `https://apiiola.yasg.ru/mcp`
|
|
11
11
|
|
|
12
|
+
## Необходимые компоненты
|
|
13
|
+
|
|
14
|
+
Проверьте Node.js и npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node --version
|
|
18
|
+
npm --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Нужен Node.js `18` или новее. Если Node.js не установлен:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Windows
|
|
25
|
+
winget install OpenJS.NodeJS.LTS
|
|
26
|
+
|
|
27
|
+
# macOS
|
|
28
|
+
brew install node
|
|
29
|
+
|
|
30
|
+
# Linux, вариант через NodeSource
|
|
31
|
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
|
32
|
+
sudo apt-get install -y nodejs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Для локального AI-режима нужен Ollama. Проверка:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ollama --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Установка Ollama:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Windows
|
|
45
|
+
winget install Ollama.Ollama
|
|
46
|
+
|
|
47
|
+
# macOS
|
|
48
|
+
brew install --cask ollama
|
|
49
|
+
|
|
50
|
+
# Linux
|
|
51
|
+
curl -fsSL https://ollama.com/install.sh | sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Диагностика ПК и подбор локальной модели:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y @iola_adm/iola-cli ai doctor
|
|
58
|
+
npx -y @iola_adm/iola-cli ai setup ollama
|
|
59
|
+
```
|
|
60
|
+
|
|
12
61
|
## Установка и запуск
|
|
13
62
|
|
|
14
63
|
```bash
|
|
@@ -25,14 +74,22 @@ iola help
|
|
|
25
74
|
## Команды
|
|
26
75
|
|
|
27
76
|
```bash
|
|
77
|
+
iola ai doctor
|
|
78
|
+
iola ai setup ollama
|
|
79
|
+
iola health
|
|
28
80
|
iola layers
|
|
29
81
|
iola schools --limit 10
|
|
82
|
+
iola schools get --inn 1215067180
|
|
30
83
|
iola kindergartens --search "29"
|
|
84
|
+
iola kindergartens get --inn 1215077421 --json
|
|
31
85
|
iola search "лицей"
|
|
32
86
|
iola mcp-info
|
|
33
87
|
iola setup codex
|
|
34
88
|
```
|
|
35
89
|
|
|
90
|
+
По умолчанию команды выводят компактную таблицу. Для полного ответа API
|
|
91
|
+
используйте `--json`.
|
|
92
|
+
|
|
36
93
|
## Назначение
|
|
37
94
|
|
|
38
95
|
Первый релиз CLI дает прямой терминальный доступ к открытым данным и командам
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
|
|
1
8
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
2
9
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
3
12
|
|
|
4
13
|
const COMMANDS = new Map([
|
|
5
14
|
["help", showHelp],
|
|
6
15
|
["version", showVersion],
|
|
16
|
+
["ai", handleAi],
|
|
17
|
+
["health", checkHealth],
|
|
7
18
|
["layers", listLayers],
|
|
8
19
|
["schools", listSchools],
|
|
9
20
|
["kindergartens", listKindergartens],
|
|
@@ -27,11 +38,17 @@ async function showHelp() {
|
|
|
27
38
|
console.log(`iola - CLI для открытых данных городского округа "Город Йошкар-Ола"
|
|
28
39
|
|
|
29
40
|
Usage:
|
|
30
|
-
iola
|
|
31
|
-
iola
|
|
32
|
-
iola
|
|
33
|
-
iola
|
|
34
|
-
iola
|
|
41
|
+
iola ai doctor [--json]
|
|
42
|
+
iola ai setup
|
|
43
|
+
iola ai setup ollama [--yes] [--model MODEL]
|
|
44
|
+
iola health [--json]
|
|
45
|
+
iola layers [--json]
|
|
46
|
+
iola schools [--limit 10] [--search TEXT] [--json]
|
|
47
|
+
iola schools get --inn INN [--json]
|
|
48
|
+
iola kindergartens [--limit 10] [--search TEXT] [--json]
|
|
49
|
+
iola kindergartens get --inn INN [--json]
|
|
50
|
+
iola search TEXT [--limit 5] [--json]
|
|
51
|
+
iola mcp-info [--json]
|
|
35
52
|
iola setup codex
|
|
36
53
|
iola version
|
|
37
54
|
|
|
@@ -46,14 +63,185 @@ async function showVersion() {
|
|
|
46
63
|
console.log(packageJson.default.version);
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
async function
|
|
66
|
+
async function checkHealth(args) {
|
|
67
|
+
const options = parseOptions(args);
|
|
68
|
+
const health = await fetchJson(`${MCP_BASE_URL}/mcp-health`);
|
|
69
|
+
|
|
70
|
+
if (options.json) {
|
|
71
|
+
printJson(health);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
printKeyValue({
|
|
76
|
+
status: health.status,
|
|
77
|
+
server_version: health.server_version,
|
|
78
|
+
skill_version: health.skill_version,
|
|
79
|
+
mcp_endpoint: health.mcp_endpoint,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleAi(args) {
|
|
84
|
+
const [subcommand = "help", ...rest] = args;
|
|
85
|
+
|
|
86
|
+
if (subcommand === "help") {
|
|
87
|
+
console.log(`AI-команды:
|
|
88
|
+
iola ai doctor [--json]
|
|
89
|
+
iola ai setup
|
|
90
|
+
iola ai setup ollama [--yes] [--model MODEL]
|
|
91
|
+
|
|
92
|
+
Локальная настройка сохраняется в ${CONFIG_FILE}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (subcommand === "doctor") {
|
|
97
|
+
await aiDoctor(rest);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (subcommand === "setup") {
|
|
102
|
+
await aiSetup(rest);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unknown AI command: ${subcommand}\nRun "iola ai help" to see available commands.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function aiDoctor(args) {
|
|
110
|
+
const options = parseOptions(args);
|
|
111
|
+
const diagnostics = await getLocalDiagnostics();
|
|
112
|
+
const recommendation = recommendOllamaModel(diagnostics);
|
|
113
|
+
|
|
114
|
+
if (options.json) {
|
|
115
|
+
printJson({ ...diagnostics, recommendation });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
printDiagnostics(diagnostics, recommendation);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function aiSetup(args) {
|
|
123
|
+
const [provider] = args;
|
|
124
|
+
|
|
125
|
+
if (!provider) {
|
|
126
|
+
const selected = await chooseAiProvider();
|
|
127
|
+
await aiSetup([selected]);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (provider === "ollama") {
|
|
132
|
+
await setupOllama(args.slice(1));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (provider === "openai" || provider === "openrouter") {
|
|
137
|
+
console.log(`Для ${provider} используйте переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
138
|
+
console.log("AI-запросы через API будут добавлены отдельной командой iola ai ask.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (provider === "codex") {
|
|
143
|
+
await setupClient(["codex"]);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function chooseAiProvider() {
|
|
151
|
+
console.log("Выберите режим AI:");
|
|
152
|
+
console.log("1. Локальная модель через Ollama");
|
|
153
|
+
console.log("2. OpenAI API");
|
|
154
|
+
console.log("3. OpenRouter API");
|
|
155
|
+
console.log("4. Codex/MCP");
|
|
156
|
+
|
|
157
|
+
const rl = readline.createInterface({ input, output });
|
|
158
|
+
try {
|
|
159
|
+
const answer = (await rl.question("Введите номер [1]: ")).trim() || "1";
|
|
160
|
+
return {
|
|
161
|
+
1: "ollama",
|
|
162
|
+
2: "openai",
|
|
163
|
+
3: "openrouter",
|
|
164
|
+
4: "codex",
|
|
165
|
+
}[answer] || "ollama";
|
|
166
|
+
} finally {
|
|
167
|
+
rl.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function setupOllama(args) {
|
|
172
|
+
const options = parseOptions(args);
|
|
173
|
+
const diagnostics = await getLocalDiagnostics();
|
|
174
|
+
const recommendation = recommendOllamaModel(diagnostics);
|
|
175
|
+
const model = options.model || recommendation.model;
|
|
176
|
+
|
|
177
|
+
printDiagnostics(diagnostics, { ...recommendation, model });
|
|
178
|
+
|
|
179
|
+
if (!diagnostics.ollama.installed) {
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log("Ollama не найден. Установите Ollama, затем повторите команду:");
|
|
182
|
+
console.log(" iola ai setup ollama");
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log("Windows:");
|
|
185
|
+
console.log(" winget install Ollama.Ollama");
|
|
186
|
+
console.log("macOS:");
|
|
187
|
+
console.log(" brew install --cask ollama");
|
|
188
|
+
console.log("Linux:");
|
|
189
|
+
console.log(" curl -fsSL https://ollama.com/install.sh | sh");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const shouldInstall = options.yes || (await confirm(`Установить модель ${model} через "ollama pull ${model}"? [Y/n] `));
|
|
194
|
+
|
|
195
|
+
if (shouldInstall) {
|
|
196
|
+
await runCommand("ollama", ["pull", model], { inherit: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await saveConfig({
|
|
200
|
+
ai: {
|
|
201
|
+
provider: "ollama",
|
|
202
|
+
model,
|
|
203
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
console.log("");
|
|
208
|
+
console.log(`Готово. Локальный AI-профиль сохранен в ${CONFIG_FILE}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function listLayers(args) {
|
|
212
|
+
const options = parseOptions(args);
|
|
50
213
|
const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
|
|
51
|
-
|
|
214
|
+
|
|
215
|
+
if (options.json) {
|
|
216
|
+
printJson(info.data_layers);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printTable(info.data_layers, [
|
|
221
|
+
["id", "ID"],
|
|
222
|
+
["name", "Название"],
|
|
223
|
+
["category", "Категория"],
|
|
224
|
+
["status", "Статус"],
|
|
225
|
+
]);
|
|
52
226
|
}
|
|
53
227
|
|
|
54
|
-
async function showMcpInfo() {
|
|
228
|
+
async function showMcpInfo(args) {
|
|
229
|
+
const options = parseOptions(args);
|
|
55
230
|
const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
|
|
56
|
-
|
|
231
|
+
|
|
232
|
+
if (options.json) {
|
|
233
|
+
printJson(info);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
printKeyValue({
|
|
238
|
+
server_name: info.server_name,
|
|
239
|
+
server_version: info.server_version,
|
|
240
|
+
skill_version: info.skill_version,
|
|
241
|
+
npm_package: info.npm_package,
|
|
242
|
+
mcp_endpoint: info.mcp_endpoint,
|
|
243
|
+
layers: info.data_layers.map((layer) => layer.id).join(", "),
|
|
244
|
+
});
|
|
57
245
|
}
|
|
58
246
|
|
|
59
247
|
async function listSchools(args) {
|
|
@@ -66,6 +254,12 @@ async function listKindergartens(args) {
|
|
|
66
254
|
|
|
67
255
|
async function listDataset(dataset, args) {
|
|
68
256
|
const options = parseOptions(args);
|
|
257
|
+
|
|
258
|
+
if (options._[0] === "get") {
|
|
259
|
+
await getDatasetItem(dataset, options);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
69
263
|
const params = new URLSearchParams();
|
|
70
264
|
params.set("limit", options.limit || "20");
|
|
71
265
|
params.set("offset", options.offset || "0");
|
|
@@ -73,7 +267,34 @@ async function listDataset(dataset, args) {
|
|
|
73
267
|
const data = await fetchJson(`${API_BASE_URL}/${dataset}?${params}`);
|
|
74
268
|
const items = normalizeItems(data);
|
|
75
269
|
const filtered = options.search ? filterItems(items, options.search) : items;
|
|
76
|
-
|
|
270
|
+
const limited = filtered.slice(0, Number(options.limit || 20));
|
|
271
|
+
|
|
272
|
+
if (options.json) {
|
|
273
|
+
printJson(limited);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
printDatasetTable(limited);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function getDatasetItem(dataset, options) {
|
|
281
|
+
if (!options.inn) {
|
|
282
|
+
throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const data = await fetchJson(`${API_BASE_URL}/${dataset}?limit=500&offset=0`);
|
|
286
|
+
const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
|
|
287
|
+
|
|
288
|
+
if (!item) {
|
|
289
|
+
throw new Error(`Record was not found in ${dataset}: inn=${options.inn}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (options.json) {
|
|
293
|
+
printJson(item);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
printKeyValue(selectPublicSummary(item));
|
|
77
298
|
}
|
|
78
299
|
|
|
79
300
|
async function searchAll(args) {
|
|
@@ -95,7 +316,16 @@ async function searchAll(args) {
|
|
|
95
316
|
kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
|
|
96
317
|
};
|
|
97
318
|
|
|
98
|
-
|
|
319
|
+
if (options.json) {
|
|
320
|
+
printJson(result);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log("Школы");
|
|
325
|
+
printDatasetTable(result.schools);
|
|
326
|
+
console.log("");
|
|
327
|
+
console.log("Детские сады");
|
|
328
|
+
printDatasetTable(result.kindergartens);
|
|
99
329
|
}
|
|
100
330
|
|
|
101
331
|
async function setupClient(args) {
|
|
@@ -115,7 +345,9 @@ function parseOptions(args) {
|
|
|
115
345
|
|
|
116
346
|
for (let index = 0; index < args.length; index += 1) {
|
|
117
347
|
const arg = args[index];
|
|
118
|
-
if (arg === "--
|
|
348
|
+
if (arg === "--json" || arg === "--yes") {
|
|
349
|
+
result[arg.slice(2)] = true;
|
|
350
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn") {
|
|
119
351
|
result[arg.slice(2)] = args[index + 1];
|
|
120
352
|
index += 1;
|
|
121
353
|
} else {
|
|
@@ -147,6 +379,192 @@ function normalizeItems(payload) {
|
|
|
147
379
|
return [];
|
|
148
380
|
}
|
|
149
381
|
|
|
382
|
+
function selectPublicSummary(item) {
|
|
383
|
+
return {
|
|
384
|
+
inn: item.inn,
|
|
385
|
+
name: item.fns_short_name || item.fns_full_name,
|
|
386
|
+
address: item.address || item.legal_address,
|
|
387
|
+
phone: item.phone,
|
|
388
|
+
email: item.email,
|
|
389
|
+
website: item.website,
|
|
390
|
+
head: item.fns_head_name,
|
|
391
|
+
license_number: item.license_number,
|
|
392
|
+
license_status: item.license_status,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function getLocalDiagnostics() {
|
|
397
|
+
const [nvidia, windowsGpu, ollamaVersion] = await Promise.all([
|
|
398
|
+
getNvidiaGpu(),
|
|
399
|
+
process.platform === "win32" ? getWindowsGpu() : Promise.resolve(null),
|
|
400
|
+
getOllamaVersion(),
|
|
401
|
+
]);
|
|
402
|
+
const gpu = nvidia || windowsGpu || { name: "-", vramGb: null, source: "not-detected" };
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
os: `${os.type()} ${os.release()} (${process.arch})`,
|
|
406
|
+
cpu: os.cpus()?.[0]?.model || "-",
|
|
407
|
+
ramGb: roundGb(os.totalmem()),
|
|
408
|
+
gpu,
|
|
409
|
+
ollama: {
|
|
410
|
+
installed: Boolean(ollamaVersion),
|
|
411
|
+
version: ollamaVersion || "-",
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function getNvidiaGpu() {
|
|
417
|
+
try {
|
|
418
|
+
const { stdout } = await runCommand("nvidia-smi", [
|
|
419
|
+
"--query-gpu=name,memory.total",
|
|
420
|
+
"--format=csv,noheader,nounits",
|
|
421
|
+
]);
|
|
422
|
+
const [line] = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
423
|
+
|
|
424
|
+
if (!line) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const [name, memoryMb] = line.split(",").map((value) => value.trim());
|
|
429
|
+
return {
|
|
430
|
+
name,
|
|
431
|
+
vramGb: Math.round((Number(memoryMb) / 1024) * 10) / 10,
|
|
432
|
+
source: "nvidia-smi",
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function getWindowsGpu() {
|
|
440
|
+
try {
|
|
441
|
+
const command = [
|
|
442
|
+
"$gpu = Get-CimInstance Win32_VideoController |",
|
|
443
|
+
"Sort-Object AdapterRAM -Descending |",
|
|
444
|
+
"Select-Object -First 1 Name,AdapterRAM;",
|
|
445
|
+
"$gpu | ConvertTo-Json -Compress",
|
|
446
|
+
].join(" ");
|
|
447
|
+
const { stdout } = await runCommand("powershell.exe", ["-NoProfile", "-Command", command]);
|
|
448
|
+
const parsed = JSON.parse(stdout.trim());
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
name: parsed.Name || "-",
|
|
452
|
+
vramGb: parsed.AdapterRAM ? roundGb(Number(parsed.AdapterRAM)) : null,
|
|
453
|
+
source: "Win32_VideoController",
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function getOllamaVersion() {
|
|
461
|
+
try {
|
|
462
|
+
const { stdout } = await runCommand("ollama", ["--version"]);
|
|
463
|
+
return stdout.trim();
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function recommendOllamaModel(diagnostics) {
|
|
470
|
+
const ramGb = diagnostics.ramGb || 0;
|
|
471
|
+
const vramGb = diagnostics.gpu.vramGb || 0;
|
|
472
|
+
|
|
473
|
+
if (ramGb >= 32 && vramGb >= 8) {
|
|
474
|
+
return {
|
|
475
|
+
profile: "good",
|
|
476
|
+
model: "qwen3:8b",
|
|
477
|
+
reason: "достаточно RAM/VRAM для более качественной локальной модели.",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (ramGb >= 16 && vramGb >= 4) {
|
|
482
|
+
return {
|
|
483
|
+
profile: "balanced",
|
|
484
|
+
model: "qwen3:4b",
|
|
485
|
+
reason: "баланс качества, скорости и памяти для работы с вопросами по данным.",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (ramGb >= 12) {
|
|
490
|
+
return {
|
|
491
|
+
profile: "standard",
|
|
492
|
+
model: "llama3.2:3b",
|
|
493
|
+
reason: "достаточно оперативной памяти для компактной универсальной модели.",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
profile: "low",
|
|
499
|
+
model: "llama3.2:1b",
|
|
500
|
+
reason: "минимальная модель для слабого ПК или CPU-only режима.",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function printDiagnostics(diagnostics, recommendation) {
|
|
505
|
+
console.log("Диагностика системы");
|
|
506
|
+
printKeyValue({
|
|
507
|
+
os: diagnostics.os,
|
|
508
|
+
cpu: diagnostics.cpu,
|
|
509
|
+
ram: `${diagnostics.ramGb} GB`,
|
|
510
|
+
gpu: diagnostics.gpu.name,
|
|
511
|
+
vram: diagnostics.gpu.vramGb ? `${diagnostics.gpu.vramGb} GB` : "-",
|
|
512
|
+
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "не установлен",
|
|
513
|
+
});
|
|
514
|
+
console.log("");
|
|
515
|
+
console.log("Рекомендация");
|
|
516
|
+
printKeyValue({
|
|
517
|
+
profile: recommendation.profile,
|
|
518
|
+
model: recommendation.model,
|
|
519
|
+
reason: recommendation.reason,
|
|
520
|
+
install: `ollama pull ${recommendation.model}`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function confirm(question) {
|
|
525
|
+
if (!process.stdin.isTTY) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const rl = readline.createInterface({ input, output });
|
|
530
|
+
try {
|
|
531
|
+
const answer = (await rl.question(question)).trim().toLocaleLowerCase("ru-RU");
|
|
532
|
+
return answer === "" || answer === "y" || answer === "yes" || answer === "д" || answer === "да";
|
|
533
|
+
} finally {
|
|
534
|
+
rl.close();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function saveConfig(value) {
|
|
539
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
540
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function runCommand(command, args, options = {}) {
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
const child = execFile(command, args, {
|
|
546
|
+
windowsHide: true,
|
|
547
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
548
|
+
}, (error, stdout, stderr) => {
|
|
549
|
+
if (error) {
|
|
550
|
+
reject(error);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
resolve({ stdout, stderr });
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (options.inherit) {
|
|
558
|
+
child.stdout?.pipe(process.stdout);
|
|
559
|
+
child.stderr?.pipe(process.stderr);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function roundGb(bytes) {
|
|
565
|
+
return Math.round((bytes / 1024 / 1024 / 1024) * 10) / 10;
|
|
566
|
+
}
|
|
567
|
+
|
|
150
568
|
async function fetchJson(url) {
|
|
151
569
|
const response = await fetch(url, {
|
|
152
570
|
headers: {
|
|
@@ -165,3 +583,79 @@ async function fetchJson(url) {
|
|
|
165
583
|
function printJson(value) {
|
|
166
584
|
console.log(JSON.stringify(value, null, 2));
|
|
167
585
|
}
|
|
586
|
+
|
|
587
|
+
function printDatasetTable(items) {
|
|
588
|
+
printTable(items.map(selectPublicSummary), [
|
|
589
|
+
["inn", "ИНН"],
|
|
590
|
+
["name", "Название"],
|
|
591
|
+
["address", "Адрес"],
|
|
592
|
+
["phone", "Телефон"],
|
|
593
|
+
]);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function printKeyValue(value) {
|
|
597
|
+
const rows = Object.entries(value).map(([key, raw]) => ({
|
|
598
|
+
key,
|
|
599
|
+
value: raw == null || raw === "" ? "-" : String(raw),
|
|
600
|
+
}));
|
|
601
|
+
|
|
602
|
+
printTable(rows, [
|
|
603
|
+
["key", "Поле"],
|
|
604
|
+
["value", "Значение"],
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function printTable(rows, columns) {
|
|
609
|
+
if (rows.length === 0) {
|
|
610
|
+
console.log("Нет данных.");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const normalized = rows.map((row) =>
|
|
615
|
+
Object.fromEntries(
|
|
616
|
+
columns.map(([key]) => [key, formatCell(row[key])]),
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
const widths = columns.map(([key, title]) =>
|
|
620
|
+
Math.min(
|
|
621
|
+
Math.max(
|
|
622
|
+
visibleLength(title),
|
|
623
|
+
...normalized.map((row) => visibleLength(row[key])),
|
|
624
|
+
),
|
|
625
|
+
52,
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
const header = columns.map(([, title], index) => padCell(title, widths[index])).join(" ");
|
|
629
|
+
const divider = widths.map((width) => "-".repeat(width)).join(" ");
|
|
630
|
+
|
|
631
|
+
console.log(header);
|
|
632
|
+
console.log(divider);
|
|
633
|
+
|
|
634
|
+
for (const row of normalized) {
|
|
635
|
+
console.log(columns.map(([key], index) => padCell(truncateCell(row[key], widths[index]), widths[index])).join(" "));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function formatCell(value) {
|
|
640
|
+
if (value == null || value === "") {
|
|
641
|
+
return "-";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return String(value).replace(/\s+/g, " ").trim();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function truncateCell(value, width) {
|
|
648
|
+
if (visibleLength(value) <= width) {
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function padCell(value, width) {
|
|
656
|
+
return value + " ".repeat(Math.max(0, width - visibleLength(value)));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function visibleLength(value) {
|
|
660
|
+
return String(value).length;
|
|
661
|
+
}
|