@mikitasazan/notify 1.4.4 → 1.5.0

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/dist/cli-flags.js CHANGED
@@ -12,7 +12,8 @@ export const KNOWN_FLAGS = new Set([
12
12
  'action', 'actor', 'assignee', 'author', 'body', 'branch', 'commit',
13
13
  'id', 'opened', 'reason', 'workdir',
14
14
  'command', 'command-note', 'commit-body', 'commit-title', 'commit-url', 'detail',
15
- 'expected', 'filename', 'item', 'job', 'key', 'last-seen', 'line', 'logs', 'note',
15
+ 'expected', 'filename', 'item',
16
+ 'item-group', 'job', 'key', 'last-seen', 'line', 'logs', 'note',
16
17
  'number', 'path', 'period', 'project', 'reviewer', 'stat', 'status',
17
18
  'target', 'title', 'url', 'via', 'workflow-name', 'workflow-url',
18
19
  // Флаги без значения. Живут здесь же, чтобы разбор и список не разошлись.
package/dist/cli.js CHANGED
@@ -101,11 +101,22 @@ const num = (key) => {
101
101
  }
102
102
  return n;
103
103
  };
104
- // --item "текст" или --item "текст|https://ссылка"
105
- const items = () => (flags.get('item') ?? []).map((raw) => {
106
- const idx = raw.lastIndexOf('|');
107
- return idx === -1 ? { text: raw } : { text: raw.slice(0, idx), url: raw.slice(idx + 1) };
108
- });
104
+ /**
105
+ * `--item "текст"` или `--item "текст|https://ссылка"`.
106
+ *
107
+ * Имя группы у позиции нельзя передать так же, как у `--stat`: черта здесь
108
+ * уже занята ссылкой. Поэтому `--item-group "Red checks"` — одно имя на все
109
+ * позиции этого вызова. Списки у отправителей однородные (красные проверки,
110
+ * выключенные процессы), а разнородный список — это `--json`.
111
+ */
112
+ const items = () => {
113
+ const name = flags.get('item-group')?.[0];
114
+ return (flags.get('item') ?? []).map((raw) => {
115
+ const idx = raw.lastIndexOf('|');
116
+ const base = idx === -1 ? { text: raw } : { text: raw.slice(0, idx), url: raw.slice(idx + 1) };
117
+ return name ? { ...base, group: name } : base;
118
+ });
119
+ };
109
120
  /**
110
121
  * `--stat "label=value"`, и с 25.08.2026 — `--stat "Group | label=value"`:
111
122
  * имя группы, вертикальная черта, ярлык. Черта выбрана потому, что её нет ни
@@ -337,7 +348,7 @@ else {
337
348
  type: 'report',
338
349
  project: project(),
339
350
  title: one('title') ?? '(no title)',
340
- period: one('note'),
351
+ // A file card has no period; the caption is its title.
341
352
  lines: []
342
353
  };
343
354
  break;
package/dist/render.js CHANGED
@@ -164,7 +164,11 @@ const fieldCode = (label, value) => value ? `<b>${esc(cap(label))}:</b> <code>${
164
164
  */
165
165
  const fieldRun = (value, why) => {
166
166
  const explain = field('To do', why);
167
- return value && explain !== null ? [explain, `▶ <code>${esc(value)}</code>`] : [];
167
+ // Без значка. `▶` был единственным символом такого рода на все двадцать
168
+ // видов карточек, и владелец справедливо спросил, что он значит: ничего,
169
+ // чего не сказала бы строка `To do:` над ним и моноширинный шрифт под ним —
170
+ // Telegram делает такую строку копируемой по нажатию сам.
171
+ return value && explain !== null ? [explain, `<code>${esc(value)}</code>`] : [];
168
172
  };
169
173
  /** Заголовок группы: курсив + подчёркивание, без жирности, без двоеточия. */
170
174
  const group = (name) => `<i><u>${esc(cap(name))}</u></i>`;
@@ -341,14 +345,21 @@ const twoBlocks = (run, change) => {
341
345
  // report and asked what "open" was — the answer is the report itself, which was
342
346
  // sitting three lines above as dead text. So line 2 takes an optional URL and
343
347
  // the action text becomes the link: `Report: <a>Analytics for 12.08</a>`.
344
- const typeLine = (icon, type, action, url) => {
345
- // `field` возвращает null на пустом значении, а интерполяция null в шаблон
346
- // печатает слово «null». Так вторая строка карточки становилась `ℹ️ null`
347
- // достижимо через `--json` и прямой вызов на JS, где типов нет.
348
+ // `aside` is the one qualifier an identifier needs to be readable on its own —
349
+ // which day a report covers, which day its arrows are measured against. It
350
+ // rides in brackets ON the type line instead of taking a row of its own,
351
+ // because a row of its own reads as another fact about the subject rather than
352
+ // as part of the name.
353
+ const typeLine = (icon, type, action, url, aside) => {
354
+ // `field` returns null on an empty value, and interpolating null into a
355
+ // template prints the word "null". That is how line 2 of a card became
356
+ // `ℹ️ null` — reachable through `--json` and through a direct call from JS,
357
+ // where there are no types.
348
358
  // `action || 'open'` in the linked case: an empty title must not swallow the
349
359
  // link, which would be the one thing the card cannot afford to lose.
350
360
  const line = url ? fieldLink(type, url, action || 'open') : field(type, action);
351
- return line === null ? `${icon} <b>${esc(cap(type))}</b>` : `${icon} ${line}`;
361
+ const tail = aside ? ` (${esc(aside)})` : '';
362
+ return line === null ? `${icon} <b>${esc(cap(type))}</b>${tail}` : `${icon} ${line}${tail}`;
352
363
  };
353
364
  // `workflowUrl ?? url`: половина отправителей шлёт ссылку на прогон под именем
354
365
  // `--url` — это имя было в пакете раньше и осталось в вызовах. Рендер читал
@@ -382,8 +393,10 @@ const renderDeploy = (e) => {
382
393
  const icon = iconFor(e);
383
394
  const runUrl = e.workflowUrl ?? e.url;
384
395
  return join([
385
- typeLine(icon, 'Deploy', e.status),
386
- fieldLink('Via', runUrl, mechanism(e.workflowName, e.via, runUrl)),
396
+ // Имя того, чем выкатили, — на строке типа. Исход говорят значок и третий
397
+ // тег; повторять его словом нечего, и это тот же закон, что у задачи и у
398
+ // отчёта. Строки `Via` больше нет: она несла это имя этажом ниже.
399
+ typeLine(icon, 'Deploy', mechanism(e.workflowName, e.via, runUrl) ?? e.status, runUrl),
387
400
  ...twoBlocks([field('Target', e.target), field('Reason', e.note)], [fieldLink('Commit', e.commitUrl, e.commit), titleField(e.commitTitle), bodyQuote(e.commitBody)])
388
401
  ]);
389
402
  };
@@ -391,18 +404,23 @@ const renderJob = (e) => {
391
404
  const icon = iconFor(e);
392
405
  const hasItems = (e.items ?? []).length > 0;
393
406
  const disabledList = hasItems && e.status === 'disabled';
407
+ // The NAME goes on the type line, the way `Report:` and `Issue:` carry
408
+ // theirs. It used to sit a line below under a second label, `Task:`, and
409
+ // the owner asked what a task is doing on a card headed Job — nothing: they
410
+ // were two words for one thing, and the outcome that took the first line is
411
+ // already the icon, and now the third tag too.
412
+ //
413
+ // `disabled` and `silent` keep a word of their own. A red mark reads as
414
+ // "it broke", and neither of those is that: one was switched off on purpose
415
+ // and the other has not been heard from at all.
416
+ const state = e.status === 'disabled'
417
+ ? 'switched off, not broken'
418
+ : e.status === 'silent'
419
+ ? 'no word from it at all'
420
+ : undefined;
394
421
  return join([
395
- typeLine(icon, 'Job', e.status),
396
- '',
397
- // The name is NOT the type line: line 2 is `Job: fail` by the format's own
398
- // rule, so the name is its own field. Called `Task:` and not `Job:` because
399
- // repeating the label of the line right above it reads as a mistake.
400
- // Until now the name was dropped entirely — every caller passed it and the
401
- // owner only ever saw it as the small grey instance tag.
402
- // The run link rides on the task's own name. It used to sit at the bottom
403
- // as `Workflow: open` — every job caller passes a URL and none passes a
404
- // workflow name, so that row was the bare verb the owner objected to.
405
- fieldLink('Task', e.workflowUrl ?? e.url, e.job),
422
+ typeLine(icon, 'Job', e.job, e.workflowUrl ?? e.url),
423
+ field('State', state),
406
424
  field('Reason', e.note),
407
425
  field('Expected', e.expected),
408
426
  // `Last run` when the task is alive, `Last seen` when it is not: the same
@@ -427,18 +445,15 @@ const renderJob = (e) => {
427
445
  const renderReport = (e) => {
428
446
  if (e.groups && e.groups.length > 0) {
429
447
  const body = e.groups.flatMap((g, i) => (i === 0 ? renderGroup(g) : ['', ...renderGroup(g)]));
430
- // `lines` и `groups` вместе, а не «или»: раньше ветка с группами печатала
431
- // ТОЛЬКО группы, и цифры отчёта молча исчезали. Поймано 25.08.2026 при
432
- // переводе утреннего отчёта PlayHub на типизированное событие.
448
+ // `lines` AND `groups` together, not one or the other: the branch with
449
+ // groups used to print ONLY the groups, and the report's numbers vanished
450
+ // without a word.
433
451
  const numbers = labelled(e.lines);
434
452
  return join([
435
- typeLine(iconFor(e), 'Report', e.title, e.url),
436
- // Период стоит ВПЛОТНУЮ к названию, без пустой строки, по тому же
437
- // закону, что `Via` у выкатки и `Check` у проверки: строка, которая
438
- // уточняет вторую строку, живёт рядом с ней, а не в блоке фактов.
439
- // Владелец: «период пошёл не туда, он же должен быть рядом с датой».
440
- field('Period', e.period),
441
- '',
453
+ typeLine(iconFor(e), 'Report', e.title, e.url, e.period),
454
+ // Rows with no group of their own sit flush against the header instead of
455
+ // forming a separate slab under a blank line. `labelled` puts the blank
456
+ // line before the first group itself, so there is none here.
442
457
  ...numbers,
443
458
  body.length > 0 ? '' : null,
444
459
  ...body
@@ -448,10 +463,8 @@ const renderReport = (e) => {
448
463
  return join([
449
464
  // Both analytics jobs send a link to the day's snapshot in docs/. It used to
450
465
  // hang off a trailing `Details: open` row; now it is the report's own name.
451
- typeLine(iconFor(e), 'Report', e.title, e.url),
452
- // Вплотную к названиюсм. соседнюю ветку.
453
- field('Period', e.period),
454
- '',
466
+ typeLine(iconFor(e), 'Report', e.title, e.url, e.period),
467
+ // Flush against the header see the branch above.
455
468
  ...labelled(e.lines),
456
469
  items.length > 0 ? '' : null,
457
470
  ...items
@@ -465,61 +478,54 @@ const renderCi = (e) => {
465
478
  const icon = iconFor(e);
466
479
  const runUrl = e.workflowUrl ?? e.url;
467
480
  return join([
468
- typeLine(icon, 'CI', e.status),
469
- fieldLink('Check', runUrl, mechanism(e.workflowName, undefined, runUrl)),
481
+ typeLine(icon, 'CI', mechanism(e.workflowName, undefined, runUrl) ?? e.status, runUrl),
470
482
  ...twoBlocks([field('Actor', e.actor), field('Reason', e.note)], [fieldLink('Commit', e.commitUrl, e.commit), titleField(e.commitTitle), bodyQuote(e.commitBody)])
471
483
  ]);
472
484
  };
485
+ // A pull request and an issue are identified the way GitHub itself identifies
486
+ // them: `#118 <title>`, one string, and it is the link. It used to take three
487
+ // rows — the action on line 2, `Number:` under it, `Title:` under that — so
488
+ // the thing the card is about could not be read without reading three lines.
489
+ // The action is not repeated in words: the icon carries it, and no two actions
490
+ // of one type share an icon.
491
+ const named = (number, title) => title ? `#${number} ${title}` : `#${number}`;
473
492
  const renderPr = (e) => join([
474
- typeLine(iconFor(e), 'PR', e.action),
475
- '',
476
- // Идентификатор первым, заголовок под ним: так вещь читается «#118, вот
477
- // такая», а не «вот такая, кстати #118» — и так её пишет сам GitHub.
478
- fieldLink('Number', e.url, `#${e.number}`),
479
- titleField(e.title),
493
+ typeLine(iconFor(e), 'PR', named(e.number, e.title), e.url),
480
494
  bodyQuote(e.body),
481
- // Без пустой строки перед автором: у задачи её нет, и одно и то же поле
482
- // не должно стоять по-разному в двух соседних карточках. Пустая строка в
483
- // этом формате означает «дальше указатель, куда пойти» — автор не он.
484
495
  field('Author', e.author),
485
496
  field('Reviewer', e.reviewer)
486
497
  ]);
487
498
  const renderIssue = (e) => join([
488
- typeLine(iconFor(e), 'Issue', e.action),
489
- '',
490
- fieldLink('Number', e.url, `#${e.number}`),
491
- titleField(e.title),
499
+ typeLine(iconFor(e), 'Issue', named(e.number, e.title), e.url),
492
500
  bodyQuote(e.body),
493
501
  field('Author', e.author),
494
502
  field('Assignee', e.assignee)
495
503
  ]);
504
+ // The incident's own title IS line 2, exactly as an issue's is. It used to say
505
+ // the word `open` there — which the 🚨 already says, and no other card repeats
506
+ // its icon in words — with the real title one row below under a `Title:` label.
507
+ //
508
+ // `detail` is a diagnosis of several lines (vault greps three of them plus a
509
+ // log path). It used to go through `field`, which keeps only the first line, so
510
+ // every alarm this package ever sent arrived gutted. It is quoted now, the same
511
+ // shape a commit body takes.
496
512
  const renderIncident = (e) => join([
497
- typeLine(iconFor(e), 'Incident', 'open'),
498
- '',
499
- // `detail` is a diagnosis of several lines (vault greps three of them plus a
500
- // log path). It used to go through `field`, which keeps only the first line,
501
- // so every alarm this package ever sent arrived gutted. Same shape as a
502
- // commit now: short label, full text quoted under it.
503
- // Ярлык `Title`, а не `Reason`: у аварии заголовок — такой же заголовок,
504
- // как у коммита и задачи, и называться в одной карточке он должен так же.
505
- // Same rule as the report: the link rides on the incident's own title
506
- // rather than on a trailing row whose only text is the word `open`.
507
- fieldLink('Title', e.url, e.title),
513
+ typeLine(iconFor(e), 'Incident', e.title, e.url),
508
514
  e.detail && e.detail !== e.title ? note(e.detail) : null,
509
515
  e.logs ? '' : null,
510
516
  fieldCode('Logs', e.logs)
511
517
  ]);
512
- // Раньше всё это склеивалось в одну строку `Reason:` через тире: «имя — no
513
- // reports — expected X, last seen Y». Каждая другая карточка кладёт факт на
514
- // свою строку с ярлыком, и владелец справедливо спросил, зачем тут отдельный
515
- // формат. Отдельного формата больше нет.
516
518
  // A session in trouble. Same law as every other card: identifier first, then
517
519
  // the facts as fields, then his own words as a quote — never as a field, which
518
520
  // keeps one line and clipped the name of the very session the card is about.
521
+ //
522
+ // A session has no name, so line 2 says what happened to it. The 36-character
523
+ // id is not printed: he cannot type it, cannot search it and cannot act on it.
524
+ // It is still in the card — inside the `rm` command at the bottom, which is the
525
+ // one place it is of any use.
519
526
  const renderSession = (e) => join([
520
527
  typeLine(iconFor(e), 'Session', e.action),
521
528
  '',
522
- field('Id', e.id),
523
529
  field('Project', e.workdir),
524
530
  field('Reason', e.reason),
525
531
  e.opened ? '' : null,
@@ -531,9 +537,12 @@ const renderHeartbeatMiss = (e) => {
531
537
  const icon = iconFor(e);
532
538
  const action = e.recovered ? 'ok' : 'miss';
533
539
  return join([
534
- typeLine(icon, 'Heartbeat', action),
535
- '',
536
- field('Task', e.job),
540
+ // The task's name on the type line, exactly as a job card carries it. The
541
+ // `Task:` row said the same thing a floor below. No sender in any
542
+ // repository builds this event any more — the silence watchdog sends an
543
+ // ordinary job with `--status silent` — but a machine still running the old
544
+ // copy of that watchdog can, and the card it gets must obey the template.
545
+ typeLine(icon, 'Heartbeat', e.job, undefined, action),
537
546
  field('Reason', e.note),
538
547
  field('Expected', e.expected),
539
548
  field(e.recovered ? 'Last run' : 'Last seen', e.lastSeen)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",