@iola_adm/iola-cli 0.1.1 → 0.1.2

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 +6 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +193 -12
package/README.md CHANGED
@@ -25,14 +25,20 @@ iola help
25
25
  ## Команды
26
26
 
27
27
  ```bash
28
+ iola health
28
29
  iola layers
29
30
  iola schools --limit 10
31
+ iola schools get --inn 1215067180
30
32
  iola kindergartens --search "29"
33
+ iola kindergartens get --inn 1215077421 --json
31
34
  iola search "лицей"
32
35
  iola mcp-info
33
36
  iola setup codex
34
37
  ```
35
38
 
39
+ По умолчанию команды выводят компактную таблицу. Для полного ответа API
40
+ используйте `--json`.
41
+
36
42
  ## Назначение
37
43
 
38
44
  Первый релиз CLI дает прямой терминальный доступ к открытым данным и командам
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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
@@ -4,6 +4,7 @@ const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
4
4
  const COMMANDS = new Map([
5
5
  ["help", showHelp],
6
6
  ["version", showVersion],
7
+ ["health", checkHealth],
7
8
  ["layers", listLayers],
8
9
  ["schools", listSchools],
9
10
  ["kindergartens", listKindergartens],
@@ -27,11 +28,14 @@ async function showHelp() {
27
28
  console.log(`iola - CLI для открытых данных городского округа "Город Йошкар-Ола"
28
29
 
29
30
  Usage:
30
- iola layers
31
- iola schools [--limit 10] [--search TEXT]
32
- iola kindergartens [--limit 10] [--search TEXT]
33
- iola search TEXT [--limit 5]
34
- iola mcp-info
31
+ iola health [--json]
32
+ iola layers [--json]
33
+ iola schools [--limit 10] [--search TEXT] [--json]
34
+ iola schools get --inn INN [--json]
35
+ iola kindergartens [--limit 10] [--search TEXT] [--json]
36
+ iola kindergartens get --inn INN [--json]
37
+ iola search TEXT [--limit 5] [--json]
38
+ iola mcp-info [--json]
35
39
  iola setup codex
36
40
  iola version
37
41
 
@@ -46,14 +50,57 @@ async function showVersion() {
46
50
  console.log(packageJson.default.version);
47
51
  }
48
52
 
49
- async function listLayers() {
53
+ async function checkHealth(args) {
54
+ const options = parseOptions(args);
55
+ const health = await fetchJson(`${MCP_BASE_URL}/mcp-health`);
56
+
57
+ if (options.json) {
58
+ printJson(health);
59
+ return;
60
+ }
61
+
62
+ printKeyValue({
63
+ status: health.status,
64
+ server_version: health.server_version,
65
+ skill_version: health.skill_version,
66
+ mcp_endpoint: health.mcp_endpoint,
67
+ });
68
+ }
69
+
70
+ async function listLayers(args) {
71
+ const options = parseOptions(args);
50
72
  const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
51
- printJson(info.data_layers);
73
+
74
+ if (options.json) {
75
+ printJson(info.data_layers);
76
+ return;
77
+ }
78
+
79
+ printTable(info.data_layers, [
80
+ ["id", "ID"],
81
+ ["name", "Название"],
82
+ ["category", "Категория"],
83
+ ["status", "Статус"],
84
+ ]);
52
85
  }
53
86
 
54
- async function showMcpInfo() {
87
+ async function showMcpInfo(args) {
88
+ const options = parseOptions(args);
55
89
  const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
56
- printJson(info);
90
+
91
+ if (options.json) {
92
+ printJson(info);
93
+ return;
94
+ }
95
+
96
+ printKeyValue({
97
+ server_name: info.server_name,
98
+ server_version: info.server_version,
99
+ skill_version: info.skill_version,
100
+ npm_package: info.npm_package,
101
+ mcp_endpoint: info.mcp_endpoint,
102
+ layers: info.data_layers.map((layer) => layer.id).join(", "),
103
+ });
57
104
  }
58
105
 
59
106
  async function listSchools(args) {
@@ -66,6 +113,12 @@ async function listKindergartens(args) {
66
113
 
67
114
  async function listDataset(dataset, args) {
68
115
  const options = parseOptions(args);
116
+
117
+ if (options._[0] === "get") {
118
+ await getDatasetItem(dataset, options);
119
+ return;
120
+ }
121
+
69
122
  const params = new URLSearchParams();
70
123
  params.set("limit", options.limit || "20");
71
124
  params.set("offset", options.offset || "0");
@@ -73,7 +126,34 @@ async function listDataset(dataset, args) {
73
126
  const data = await fetchJson(`${API_BASE_URL}/${dataset}?${params}`);
74
127
  const items = normalizeItems(data);
75
128
  const filtered = options.search ? filterItems(items, options.search) : items;
76
- printJson(filtered.slice(0, Number(options.limit || 20)));
129
+ const limited = filtered.slice(0, Number(options.limit || 20));
130
+
131
+ if (options.json) {
132
+ printJson(limited);
133
+ return;
134
+ }
135
+
136
+ printDatasetTable(limited);
137
+ }
138
+
139
+ async function getDatasetItem(dataset, options) {
140
+ if (!options.inn) {
141
+ throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
142
+ }
143
+
144
+ const data = await fetchJson(`${API_BASE_URL}/${dataset}?limit=500&offset=0`);
145
+ const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
146
+
147
+ if (!item) {
148
+ throw new Error(`Record was not found in ${dataset}: inn=${options.inn}`);
149
+ }
150
+
151
+ if (options.json) {
152
+ printJson(item);
153
+ return;
154
+ }
155
+
156
+ printKeyValue(selectPublicSummary(item));
77
157
  }
78
158
 
79
159
  async function searchAll(args) {
@@ -95,7 +175,16 @@ async function searchAll(args) {
95
175
  kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
96
176
  };
97
177
 
98
- printJson(result);
178
+ if (options.json) {
179
+ printJson(result);
180
+ return;
181
+ }
182
+
183
+ console.log("Школы");
184
+ printDatasetTable(result.schools);
185
+ console.log("");
186
+ console.log("Детские сады");
187
+ printDatasetTable(result.kindergartens);
99
188
  }
100
189
 
101
190
  async function setupClient(args) {
@@ -115,7 +204,9 @@ function parseOptions(args) {
115
204
 
116
205
  for (let index = 0; index < args.length; index += 1) {
117
206
  const arg = args[index];
118
- if (arg === "--limit" || arg === "--offset" || arg === "--search") {
207
+ if (arg === "--json") {
208
+ result.json = true;
209
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn") {
119
210
  result[arg.slice(2)] = args[index + 1];
120
211
  index += 1;
121
212
  } else {
@@ -147,6 +238,20 @@ function normalizeItems(payload) {
147
238
  return [];
148
239
  }
149
240
 
241
+ function selectPublicSummary(item) {
242
+ return {
243
+ inn: item.inn,
244
+ name: item.fns_short_name || item.fns_full_name,
245
+ address: item.address || item.legal_address,
246
+ phone: item.phone,
247
+ email: item.email,
248
+ website: item.website,
249
+ head: item.fns_head_name,
250
+ license_number: item.license_number,
251
+ license_status: item.license_status,
252
+ };
253
+ }
254
+
150
255
  async function fetchJson(url) {
151
256
  const response = await fetch(url, {
152
257
  headers: {
@@ -165,3 +270,79 @@ async function fetchJson(url) {
165
270
  function printJson(value) {
166
271
  console.log(JSON.stringify(value, null, 2));
167
272
  }
273
+
274
+ function printDatasetTable(items) {
275
+ printTable(items.map(selectPublicSummary), [
276
+ ["inn", "ИНН"],
277
+ ["name", "Название"],
278
+ ["address", "Адрес"],
279
+ ["phone", "Телефон"],
280
+ ]);
281
+ }
282
+
283
+ function printKeyValue(value) {
284
+ const rows = Object.entries(value).map(([key, raw]) => ({
285
+ key,
286
+ value: raw == null || raw === "" ? "-" : String(raw),
287
+ }));
288
+
289
+ printTable(rows, [
290
+ ["key", "Поле"],
291
+ ["value", "Значение"],
292
+ ]);
293
+ }
294
+
295
+ function printTable(rows, columns) {
296
+ if (rows.length === 0) {
297
+ console.log("Нет данных.");
298
+ return;
299
+ }
300
+
301
+ const normalized = rows.map((row) =>
302
+ Object.fromEntries(
303
+ columns.map(([key]) => [key, formatCell(row[key])]),
304
+ ),
305
+ );
306
+ const widths = columns.map(([key, title]) =>
307
+ Math.min(
308
+ Math.max(
309
+ visibleLength(title),
310
+ ...normalized.map((row) => visibleLength(row[key])),
311
+ ),
312
+ 52,
313
+ ),
314
+ );
315
+ const header = columns.map(([, title], index) => padCell(title, widths[index])).join(" ");
316
+ const divider = widths.map((width) => "-".repeat(width)).join(" ");
317
+
318
+ console.log(header);
319
+ console.log(divider);
320
+
321
+ for (const row of normalized) {
322
+ console.log(columns.map(([key], index) => padCell(truncateCell(row[key], widths[index]), widths[index])).join(" "));
323
+ }
324
+ }
325
+
326
+ function formatCell(value) {
327
+ if (value == null || value === "") {
328
+ return "-";
329
+ }
330
+
331
+ return String(value).replace(/\s+/g, " ").trim();
332
+ }
333
+
334
+ function truncateCell(value, width) {
335
+ if (visibleLength(value) <= width) {
336
+ return value;
337
+ }
338
+
339
+ return `${value.slice(0, Math.max(0, width - 1))}…`;
340
+ }
341
+
342
+ function padCell(value, width) {
343
+ return value + " ".repeat(Math.max(0, width - visibleLength(value)));
344
+ }
345
+
346
+ function visibleLength(value) {
347
+ return String(value).length;
348
+ }