@iola_adm/iola-cli 0.1.9 → 0.1.10

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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +278 -31
package/README.md CHANGED
@@ -80,10 +80,16 @@ iola banner
80
80
  iola agent
81
81
  iola chat
82
82
  iola init
83
+ iola doctor
84
+ iola config get
85
+ iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
86
+ iola config reset
83
87
  iola update
84
88
  iola version --check
89
+ iola ask "Найди школу 29"
85
90
  iola data schools --limit 10
86
91
  iola data kindergartens --search "29"
92
+ iola data schools --where address=Петрова --columns name,address,phone
87
93
  iola data schools --format csv
88
94
  iola ai doctor
89
95
  iola ai setup ollama
@@ -120,6 +126,8 @@ iola agent
120
126
  ```text
121
127
  /help
122
128
  /health
129
+ /doctor
130
+ /config get
123
131
  /layers
124
132
  /data schools --limit 10
125
133
  /schools --limit 10
@@ -153,6 +161,7 @@ iola agent
153
161
  ```bash
154
162
  iola ai setup ollama
155
163
  iola ai ask "Какие школы есть на улице Петрова?"
164
+ iola ask "Какие школы есть на улице Петрова?"
156
165
  ```
157
166
 
158
167
  OpenAI:
@@ -219,3 +228,12 @@ CLI дает прямой терминальный доступ к открыт
219
228
  IOLA_API_BASE_URL=https://apiiola.yasg.ru/api/v1
220
229
  IOLA_MCP_BASE_URL=https://apiiola.yasg.ru
221
230
  ```
231
+
232
+ Переменные окружения имеют приоритет над локальной конфигурацией. Локальные
233
+ endpoints можно настроить так:
234
+
235
+ ```bash
236
+ iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
237
+ iola config set api.mcpBaseUrl https://apiiola.yasg.ru
238
+ iola config get
239
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -11,6 +11,10 @@ const CONFIG_DIR = path.join(os.homedir(), ".iola");
11
11
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
12
12
  const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
13
13
  const DEFAULT_AI_CONFIG = {
14
+ api: {
15
+ baseUrl: "https://apiiola.yasg.ru/api/v1",
16
+ mcpBaseUrl: "https://apiiola.yasg.ru",
17
+ },
14
18
  ai: {
15
19
  provider: "ollama",
16
20
  model: "llama3.2:1b",
@@ -45,9 +49,12 @@ const COMMANDS = new Map([
45
49
  ["help", showHelp],
46
50
  ["version", showVersion],
47
51
  ["update", checkUpdate],
52
+ ["doctor", doctor],
53
+ ["config", handleConfig],
48
54
  ["banner", showBanner],
49
55
  ["agent", startAgent],
50
56
  ["chat", startAgent],
57
+ ["ask", aiAsk],
51
58
  ["ai", handleAi],
52
59
  ["init", initCli],
53
60
  ["health", checkHealth],
@@ -80,8 +87,14 @@ Usage:
80
87
  iola agent
81
88
  iola chat
82
89
  iola init
90
+ iola doctor
91
+ iola config get
92
+ iola config set api.baseUrl URL
93
+ iola config set api.mcpBaseUrl URL
94
+ iola config reset
83
95
  iola update
84
- iola data LAYER [--limit 10] [--search TEXT] [--format table|json|csv]
96
+ iola ask TEXT
97
+ iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
85
98
  iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
86
99
  iola ai context TEXT [--json]
87
100
  iola ai key set openai
@@ -93,9 +106,9 @@ Usage:
93
106
  iola ai setup ollama [--yes] [--model MODEL]
94
107
  iola health [--json]
95
108
  iola layers [--json]
96
- iola schools [--limit 10] [--search TEXT] [--format table|json|csv]
109
+ iola schools [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
97
110
  iola schools get --inn INN [--json]
98
- iola kindergartens [--limit 10] [--search TEXT] [--format table|json|csv]
111
+ iola kindergartens [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
99
112
  iola kindergartens get --inn INN [--json]
100
113
  iola search TEXT [--limit 5] [--format table|json|csv]
101
114
  iola mcp-info [--json]
@@ -178,7 +191,17 @@ async function handleAgentLine(line, state) {
178
191
  }
179
192
 
180
193
  if (command === "config") {
181
- await printAiConfig();
194
+ await handleConfig(args.length > 0 ? args : ["get"]);
195
+ return false;
196
+ }
197
+
198
+ if (command === "doctor") {
199
+ await doctor(args);
200
+ return false;
201
+ }
202
+
203
+ if (command === "cfg" || command === "settings") {
204
+ await handleConfig(args);
182
205
  return false;
183
206
  }
184
207
 
@@ -229,6 +252,8 @@ async function handleAgentLine(line, state) {
229
252
 
230
253
  const mapped = {
231
254
  health: ["health", args],
255
+ doctor: ["doctor", args],
256
+ config: ["config", args],
232
257
  layers: ["layers", args],
233
258
  data: ["data", args],
234
259
  schools: ["schools", args],
@@ -253,6 +278,9 @@ function printAgentHelp() {
253
278
  console.log(`Slash-команды:
254
279
  /help
255
280
  /health
281
+ /doctor
282
+ /config get
283
+ /config set api.baseUrl URL
256
284
  /layers
257
285
  /data schools --limit 10
258
286
  /schools --limit 10
@@ -355,7 +383,7 @@ async function checkUpdate() {
355
383
 
356
384
  async function checkHealth(args) {
357
385
  const options = parseOptions(args);
358
- const health = await fetchJson(`${MCP_BASE_URL}/mcp-health`);
386
+ const health = await fetchJson(`${await getMcpBaseUrl()}/mcp-health`);
359
387
 
360
388
  if (options.json) {
361
389
  printJson(health);
@@ -370,6 +398,92 @@ async function checkHealth(args) {
370
398
  });
371
399
  }
372
400
 
401
+ async function doctor(args = []) {
402
+ const options = parseOptions(args);
403
+ const packageJson = await import("../package.json", { with: { type: "json" } });
404
+ const config = await loadConfig();
405
+ const secrets = await loadSecrets();
406
+ const diagnostics = await getLocalDiagnostics();
407
+ const latest = await getLatestNpmVersion(packageJson.default.name);
408
+ const apiBaseUrl = await getApiBaseUrl();
409
+ const mcpBaseUrl = await getMcpBaseUrl();
410
+ const report = {
411
+ cli: {
412
+ version: packageJson.default.version,
413
+ npmLatest: latest || "-",
414
+ update: getUpdateStatus(packageJson.default.version, latest),
415
+ },
416
+ api: {
417
+ baseUrl: apiBaseUrl,
418
+ mcpBaseUrl,
419
+ health: await probeEndpoint(`${mcpBaseUrl}/mcp-health`),
420
+ },
421
+ ai: {
422
+ provider: config.ai.provider,
423
+ model: config.ai.model,
424
+ modelAvailable: await checkConfiguredModel(config),
425
+ openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
426
+ openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
427
+ ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
428
+ },
429
+ system: diagnostics,
430
+ };
431
+
432
+ if (options.json) {
433
+ printJson(report);
434
+ return;
435
+ }
436
+
437
+ console.log("CLI");
438
+ printKeyValue(report.cli);
439
+ console.log("");
440
+ console.log("API/MCP");
441
+ printKeyValue(report.api);
442
+ console.log("");
443
+ console.log("AI");
444
+ printKeyValue(report.ai);
445
+ console.log("");
446
+ printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
447
+ }
448
+
449
+ function getUpdateStatus(current, latest) {
450
+ if (!latest) {
451
+ return "unknown";
452
+ }
453
+
454
+ const comparison = compareVersions(latest, current);
455
+
456
+ if (comparison > 0) {
457
+ return "available";
458
+ }
459
+
460
+ if (comparison < 0) {
461
+ return "local-newer";
462
+ }
463
+
464
+ return "ok";
465
+ }
466
+
467
+ async function checkConfiguredModel(config) {
468
+ if (config.ai.provider !== "ollama") {
469
+ return "external-api";
470
+ }
471
+
472
+ try {
473
+ const response = await fetch(`${config.ai.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
474
+
475
+ if (!response.ok) {
476
+ return "unknown";
477
+ }
478
+
479
+ const payload = await response.json();
480
+ const models = payload.models || [];
481
+ return models.some((model) => model.name === config.ai.model) ? "installed" : "missing";
482
+ } catch {
483
+ return "ollama-unavailable";
484
+ }
485
+ }
486
+
373
487
  async function initCli(args = []) {
374
488
  const options = parseOptions(args);
375
489
 
@@ -378,8 +492,8 @@ async function initCli(args = []) {
378
492
  printKeyValue({
379
493
  node: process.version,
380
494
  npm: await getCommandVersion("npm", ["--version"]),
381
- api: await probeEndpoint(`${MCP_BASE_URL}/mcp-health`),
382
- mcp: MCP_BASE_URL,
495
+ api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
496
+ mcp: await getMcpBaseUrl(),
383
497
  });
384
498
  console.log("");
385
499
 
@@ -455,6 +569,47 @@ async function handleAi(args) {
455
569
  throw new Error(`Unknown AI command: ${subcommand}\nRun "iola ai help" to see available commands.`);
456
570
  }
457
571
 
572
+ async function handleConfig(args) {
573
+ const [action = "get", key, ...rest] = args;
574
+
575
+ if (action === "get") {
576
+ const config = await loadConfig();
577
+ if (key) {
578
+ console.log(getConfigValue(config, key) ?? "-");
579
+ return;
580
+ }
581
+ printJson({
582
+ file: CONFIG_FILE,
583
+ config,
584
+ effective: {
585
+ apiBaseUrl: await getApiBaseUrl(),
586
+ mcpBaseUrl: await getMcpBaseUrl(),
587
+ },
588
+ });
589
+ return;
590
+ }
591
+
592
+ if (action === "set") {
593
+ const value = rest.join(" ").trim();
594
+ if (!key || !value) {
595
+ throw new Error("Пример: iola config set api.baseUrl https://apiiola.yasg.ru/api/v1");
596
+ }
597
+ const config = await loadConfig();
598
+ setConfigValue(config, key, value);
599
+ await saveConfig(config);
600
+ console.log(`Сохранено: ${key} = ${value}`);
601
+ return;
602
+ }
603
+
604
+ if (action === "reset") {
605
+ await writeConfig(DEFAULT_AI_CONFIG);
606
+ console.log(`Конфигурация сброшена: ${CONFIG_FILE}`);
607
+ return;
608
+ }
609
+
610
+ throw new Error("Команды config: get, set, reset.");
611
+ }
612
+
458
613
  async function aiDoctor(args) {
459
614
  const options = parseOptions(args);
460
615
  const diagnostics = await getLocalDiagnostics();
@@ -720,10 +875,12 @@ async function aiAsk(args, context = {}) {
720
875
  }
721
876
 
722
877
  async function buildDataContext(question) {
878
+ const apiBaseUrl = await getApiBaseUrl();
879
+ const mcpBaseUrl = await getMcpBaseUrl();
723
880
  const [layers, schools, kindergartens] = await Promise.all([
724
- fetchJson(`${MCP_BASE_URL}/mcp-version`),
725
- fetchJson(`${API_BASE_URL}/schools?limit=100&offset=0`),
726
- fetchJson(`${API_BASE_URL}/kindergartens?limit=100&offset=0`),
881
+ fetchJson(`${mcpBaseUrl}/mcp-version`),
882
+ fetchJson(`${apiBaseUrl}/schools?limit=100&offset=0`),
883
+ fetchJson(`${apiBaseUrl}/kindergartens?limit=100&offset=0`),
727
884
  ]);
728
885
  const queryTerms = extractSearchTerms(question);
729
886
  const patterns = extractStructuredPatterns(question);
@@ -921,7 +1078,7 @@ async function callOllama(config, messages) {
921
1078
 
922
1079
  async function callOpenAiCompatible(config, messages, apiKey, providerName) {
923
1080
  if (!apiKey) {
924
- throw new Error(`${providerName} API key не найден. Задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
1081
+ throw new Error(`${providerName} API key не найден. Выполните iola ai key set ${providerName === "OpenAI" ? "openai" : "openrouter"} или задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
925
1082
  }
926
1083
 
927
1084
  const response = await fetch(`${config.baseUrl}/chat/completions`, {
@@ -963,7 +1120,7 @@ async function getApiKey(provider) {
963
1120
 
964
1121
  async function listLayers(args) {
965
1122
  const options = parseOptions(args);
966
- const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
1123
+ const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
967
1124
 
968
1125
  if (options.json) {
969
1126
  printJson(info.data_layers);
@@ -980,7 +1137,7 @@ async function listLayers(args) {
980
1137
 
981
1138
  async function showMcpInfo(args) {
982
1139
  const options = parseOptions(args);
983
- const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
1140
+ const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
984
1141
 
985
1142
  if (options.json) {
986
1143
  printJson(info);
@@ -1039,22 +1196,24 @@ async function listDataset(dataset, args) {
1039
1196
  params.set("limit", options.limit || "20");
1040
1197
  params.set("offset", options.offset || "0");
1041
1198
 
1042
- const data = await fetchJson(`${API_BASE_URL}/${DATASETS[dataset].endpoint}?${params}`);
1199
+ const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`);
1043
1200
  const items = normalizeItems(data);
1044
- const filtered = options.search ? filterItems(items, options.search) : items;
1201
+ const filtered = applyDatasetFilters(items, options);
1045
1202
  const limited = filtered.slice(0, Number(options.limit || 20));
1203
+ const summarized = limited.map(selectPublicSummary);
1204
+ const projected = projectColumns(summarized, options.columns);
1046
1205
 
1047
1206
  if (options.json || options.format === "json") {
1048
- printJson(limited);
1207
+ printJson(projected);
1049
1208
  return;
1050
1209
  }
1051
1210
 
1052
1211
  if (options.format === "csv") {
1053
- printCsv(limited.map(selectPublicSummary));
1212
+ printCsv(projected);
1054
1213
  return;
1055
1214
  }
1056
1215
 
1057
- printDatasetTable(limited);
1216
+ printDatasetTable(projected, options.columns);
1058
1217
  }
1059
1218
 
1060
1219
  async function getDatasetItem(dataset, options) {
@@ -1062,7 +1221,7 @@ async function getDatasetItem(dataset, options) {
1062
1221
  throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
1063
1222
  }
1064
1223
 
1065
- const data = await fetchJson(`${API_BASE_URL}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
1224
+ const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
1066
1225
  const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
1067
1226
 
1068
1227
  if (!item) {
@@ -1086,14 +1245,14 @@ async function searchAll(args) {
1086
1245
  }
1087
1246
 
1088
1247
  const [schools, kindergartens] = await Promise.all([
1089
- fetchJson(`${API_BASE_URL}/schools?limit=100&offset=0`),
1090
- fetchJson(`${API_BASE_URL}/kindergartens?limit=100&offset=0`),
1248
+ fetchJson(`${await getApiBaseUrl()}/schools?limit=100&offset=0`),
1249
+ fetchJson(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`),
1091
1250
  ]);
1092
1251
 
1093
1252
  const limit = Number(options.limit || 5);
1094
1253
  const result = {
1095
- schools: filterItems(normalizeItems(schools), query).slice(0, limit),
1096
- kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
1254
+ schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
1255
+ kindergartens: projectColumns(filterItems(normalizeItems(kindergartens), query).slice(0, limit).map(selectPublicSummary), options.columns),
1097
1256
  };
1098
1257
 
1099
1258
  if (options.json || options.format === "json") {
@@ -1103,17 +1262,17 @@ async function searchAll(args) {
1103
1262
 
1104
1263
  if (options.format === "csv") {
1105
1264
  printCsv([
1106
- ...result.schools.map((item) => ({ layer: "schools", ...selectPublicSummary(item) })),
1107
- ...result.kindergartens.map((item) => ({ layer: "kindergartens", ...selectPublicSummary(item) })),
1265
+ ...result.schools.map((item) => ({ layer: "schools", ...item })),
1266
+ ...result.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
1108
1267
  ]);
1109
1268
  return;
1110
1269
  }
1111
1270
 
1112
1271
  console.log("Школы");
1113
- printDatasetTable(result.schools);
1272
+ printDatasetTable(result.schools, options.columns);
1114
1273
  console.log("");
1115
1274
  console.log("Детские сады");
1116
- printDatasetTable(result.kindergartens);
1275
+ printDatasetTable(result.kindergartens, options.columns);
1117
1276
  }
1118
1277
 
1119
1278
  async function setupClient(args) {
@@ -1137,7 +1296,7 @@ function parseOptions(args) {
1137
1296
  result[arg.slice(2)] = true;
1138
1297
  } else if (arg === "--check") {
1139
1298
  result.check = true;
1140
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--format") {
1299
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--format") {
1141
1300
  result[arg.slice(2)] = args[index + 1];
1142
1301
  index += 1;
1143
1302
  } else {
@@ -1187,6 +1346,42 @@ function filterItems(items, query) {
1187
1346
  return items.filter((item) => JSON.stringify(item).toLocaleLowerCase("ru-RU").includes(normalized));
1188
1347
  }
1189
1348
 
1349
+ function applyDatasetFilters(items, options) {
1350
+ let result = options.search ? filterItems(items, options.search) : items;
1351
+
1352
+ if (options.where) {
1353
+ const [field, ...valueParts] = String(options.where).split("=");
1354
+ const value = valueParts.join("=").trim().toLocaleLowerCase("ru-RU");
1355
+ const key = field.trim();
1356
+
1357
+ if (!key || !value) {
1358
+ throw new Error('Фильтр --where должен быть в формате field=value. Пример: --where address=Петрова');
1359
+ }
1360
+
1361
+ result = result.filter((item) => {
1362
+ const summary = selectPublicSummary(item);
1363
+ const raw = summary[key] ?? item[key];
1364
+ return String(raw ?? "").toLocaleLowerCase("ru-RU").includes(value);
1365
+ });
1366
+ }
1367
+
1368
+ return result;
1369
+ }
1370
+
1371
+ function projectColumns(rows, columnsValue) {
1372
+ if (!columnsValue) {
1373
+ return rows;
1374
+ }
1375
+
1376
+ const columns = String(columnsValue).split(",").map((column) => column.trim()).filter(Boolean);
1377
+
1378
+ if (columns.length === 0) {
1379
+ return rows;
1380
+ }
1381
+
1382
+ return rows.map((row) => Object.fromEntries(columns.map((column) => [column, row[column] ?? ""])));
1383
+ }
1384
+
1190
1385
  function normalizeItems(payload) {
1191
1386
  if (Array.isArray(payload)) {
1192
1387
  return payload;
@@ -1206,7 +1401,7 @@ function normalizeItems(payload) {
1206
1401
  function selectPublicSummary(item) {
1207
1402
  return {
1208
1403
  inn: item.inn,
1209
- name: item.fns_short_name || item.fns_full_name,
1404
+ name: item.name || item.fns_short_name || item.fns_full_name,
1210
1405
  address: item.address || item.legal_address,
1211
1406
  phone: item.phone,
1212
1407
  email: item.email,
@@ -1422,8 +1617,12 @@ async function confirm(question) {
1422
1617
  async function saveConfig(value) {
1423
1618
  const current = await loadConfig();
1424
1619
  const merged = mergeConfig(current, value);
1620
+ await writeConfig(merged);
1621
+ }
1622
+
1623
+ async function writeConfig(value) {
1425
1624
  await mkdir(CONFIG_DIR, { recursive: true });
1426
- await writeFile(CONFIG_FILE, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
1625
+ await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1427
1626
  }
1428
1627
 
1429
1628
  async function loadConfig() {
@@ -1439,6 +1638,10 @@ function mergeConfig(base, override) {
1439
1638
  return {
1440
1639
  ...base,
1441
1640
  ...override,
1641
+ api: {
1642
+ ...base.api,
1643
+ ...(override.api || {}),
1644
+ },
1442
1645
  ai: {
1443
1646
  ...base.ai,
1444
1647
  ...(override.ai || {}),
@@ -1446,6 +1649,40 @@ function mergeConfig(base, override) {
1446
1649
  };
1447
1650
  }
1448
1651
 
1652
+ async function getApiBaseUrl() {
1653
+ if (process.env.IOLA_API_BASE_URL) {
1654
+ return process.env.IOLA_API_BASE_URL;
1655
+ }
1656
+
1657
+ const config = await loadConfig();
1658
+ return config.api.baseUrl;
1659
+ }
1660
+
1661
+ async function getMcpBaseUrl() {
1662
+ if (process.env.IOLA_MCP_BASE_URL) {
1663
+ return process.env.IOLA_MCP_BASE_URL;
1664
+ }
1665
+
1666
+ const config = await loadConfig();
1667
+ return config.api.mcpBaseUrl;
1668
+ }
1669
+
1670
+ function getConfigValue(config, key) {
1671
+ return key.split(".").reduce((value, part) => value?.[part], config);
1672
+ }
1673
+
1674
+ function setConfigValue(config, key, value) {
1675
+ const parts = key.split(".");
1676
+ let current = config;
1677
+
1678
+ for (const part of parts.slice(0, -1)) {
1679
+ current[part] = current[part] && typeof current[part] === "object" ? current[part] : {};
1680
+ current = current[part];
1681
+ }
1682
+
1683
+ current[parts.at(-1)] = value;
1684
+ }
1685
+
1449
1686
  async function loadSecrets() {
1450
1687
  try {
1451
1688
  return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
@@ -1573,7 +1810,17 @@ function csvCell(value) {
1573
1810
  return `"${text.replace(/"/g, "\"\"")}"`;
1574
1811
  }
1575
1812
 
1576
- function printDatasetTable(items) {
1813
+ function printDatasetTable(items, columnsValue) {
1814
+ if (columnsValue) {
1815
+ const columns = String(columnsValue)
1816
+ .split(",")
1817
+ .map((column) => column.trim())
1818
+ .filter(Boolean)
1819
+ .map((column) => [column, column]);
1820
+ printTable(items, columns);
1821
+ return;
1822
+ }
1823
+
1577
1824
  printTable(items.map(selectPublicSummary), [
1578
1825
  ["inn", "ИНН"],
1579
1826
  ["name", "Название"],