@mikitasazan/notify 1.23.0 → 1.25.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/README.md CHANGED
@@ -200,7 +200,7 @@ notify report --project playhub --json < payload.json # весь объект
200
200
  - Автосоздания темы при первой отправке — сбой мог бы наплодить дублей;
201
201
  создание — явная команда `notify setup`.
202
202
  - Любых зависимостей, сборки, MarkdownV2, мультиязычности, троттла (троттл —
203
- забота вызывающего кода, он есть в `zabukai/web/src/server/alert.ts` и там
203
+ забота вызывающего кода, он есть в `Zabukai/app/web/src/server/alert.ts` и там
204
204
  и остаётся: работает только внутри долгоживущего процесса, а CLI стартует
205
205
  заново на каждый вызов).
206
206
 
package/dist/cli.js CHANGED
@@ -13,11 +13,11 @@
13
13
  * notify deploy --project playhub --status ok --commit "msg" [--commit-url "..."] --url "..."
14
14
  * notify job --project playhub --job "Game import" --status ok --stat "added=5" [--via vps] [--took "4m 12s"]
15
15
  * notify report --project playhub --title "Daily summary" --line "Games=1284"
16
- * notify ci --project zabukai --status fail --branch master
17
- * notify pr --project zabukai --action opened --number 142 --title "..."
18
- * notify incident --project zabukai --title "Redis is unreachable" --detail "$ERR"
16
+ * notify ci --project zabukai-app --status fail --branch master
17
+ * notify pr --project zabukai-app --action opened --number 142 --title "..."
18
+ * notify incident --project zabukai-app --title "Redis is unreachable" --detail "$ERR"
19
19
  * notify session --project mac-config --action "burning the limit" # DEPRECATED alias of incident
20
- * notify file --project zabukai --title "Full dialogues" --path ./out.txt [--filename name.txt]
20
+ * notify file --project zabukai-app --title "Full dialogues" --path ./out.txt [--filename name.txt]
21
21
  * notify <type> [--key stable-key] # the task's key on the card's last line
22
22
  * notify <type> --json < payload.json # the whole event object on stdin
23
23
  * notify setup <forum chat_id> <project key> # create the tabs, see setup.ts
package/dist/events.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - required fields are never added — only a new event type.
11
11
  * That keeps old caller code and a new package compatible both ways.
12
12
  */
13
- export type Project = 'playhub' | 'one-q' | 'zabukai' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools' | 'htmlg' | '2roles' | 'market-lens';
13
+ export type Project = 'playhub' | 'one-q' | 'zabukai-app' | 'zabukai-site' | 'game-publisher' | 'vault' | 'mac-config' | 'alitools' | 'htmlg' | '2roles' | 'market-lens';
14
14
  /**
15
15
  * The name of a project — THE NAME ITS REPOSITORY CARRIES, character for
16
16
  * character. One project, one name, everywhere: the repository, the folder on
@@ -27,6 +27,12 @@ export type Project = 'playhub' | 'one-q' | 'zabukai' | 'game-publisher' | 'vaul
27
27
  * Do not restore a prettified name here. To change how a project is written,
28
28
  * rename the REPOSITORY first, then bring this table and the chat title to it.
29
29
  *
30
+ * A product built from SEVERAL repositories gets one row per repository, not
31
+ * one row for the product: `zabukai-app` and `zabukai-site` are two rows that
32
+ * share one chat. The product's own name lives in `title` on their ROUTES
33
+ * rows, because that is the only place a name belongs to a chat rather than
34
+ * to a repository.
35
+ *
30
36
  * `cap()` guessed a name by capitalizing the key, which is how `htmlg` showed
31
37
  * up as «Htmlg»; this table exists so nothing guesses.
32
38
  *
package/dist/events.js CHANGED
@@ -26,6 +26,12 @@
26
26
  * Do not restore a prettified name here. To change how a project is written,
27
27
  * rename the REPOSITORY first, then bring this table and the chat title to it.
28
28
  *
29
+ * A product built from SEVERAL repositories gets one row per repository, not
30
+ * one row for the product: `zabukai-app` and `zabukai-site` are two rows that
31
+ * share one chat. The product's own name lives in `title` on their ROUTES
32
+ * rows, because that is the only place a name belongs to a chat rather than
33
+ * to a repository.
34
+ *
29
35
  * `cap()` guessed a name by capitalizing the key, which is how `htmlg` showed
30
36
  * up as «Htmlg»; this table exists so nothing guesses.
31
37
  *
@@ -35,7 +41,8 @@
35
41
  export const DISPLAY = {
36
42
  playhub: 'playhub',
37
43
  'one-q': 'one-q',
38
- zabukai: 'zabukai',
44
+ 'zabukai-app': 'zabukai-app',
45
+ 'zabukai-site': 'zabukai-site',
39
46
  'game-publisher': 'game-publisher',
40
47
  vault: 'vault',
41
48
  'mac-config': 'mac-config',
package/dist/routes.d.ts CHANGED
@@ -4,12 +4,12 @@
4
4
  * the projects themselves.
5
5
  *
6
6
  * The scheme: ONE CHAT PER PROJECT. A project with a team is a forum
7
- * supergroup with an "⚙️ Ops" tab (robot notifications) and a "💬 Dev" tab
8
- * (people's live chat). A project the owner runs alone is a plain
9
- * supergroup without topics — the robots post straight into it and there
10
- * is nobody to talk to in a Dev tab (owner's rule of 2026-09-05; the first
11
- * such chat is `htmlg`). A plain chat has no `ops` number: the message
12
- * goes to the chat itself.
7
+ * supergroup with an "Ops" tab (robot notifications, native 🤖 topic icon)
8
+ * and a "Dev" tab (people's live chat, 💬). A project the owner runs alone
9
+ * is a plain supergroup without topics — the robots post straight into it
10
+ * and there is nobody to talk to in a Dev tab (owner's rule of 2026-09-05;
11
+ * the first such chat is `htmlg`). A plain chat has no `ops` number: the
12
+ * message goes to the chat itself.
13
13
  *
14
14
  * Why not one shared forum with a topic per project — that was the first
15
15
  * version, and it turned out to be a mistake: Telegram cannot hide one
@@ -30,6 +30,15 @@ import type { NotifyEvent, Project } from './events.ts';
30
30
  type Forum = {
31
31
  /** The id of the project's supergroup (forum or plain). */
32
32
  chat: string;
33
+ /**
34
+ * The chat's own title, wherever it cannot be derived from the project key:
35
+ * a product of several repositories (every row sharing that chat carries
36
+ * the same `title`), and a team forum whose product name is prettier than
37
+ * its repository key (owner's rule of 2026-09-10: a FORUM wears the
38
+ * product's own name; a solo chat wears the repository's). Left out
39
+ * everywhere else, where `chatTitle()` derives the name from the project.
40
+ */
41
+ title?: string;
33
42
  /**
34
43
  * The "⚙️ Ops" tab — robots write here. Absent for a plain chat without
35
44
  * topics (a solo project): the message then goes to the chat itself.
@@ -47,9 +56,13 @@ export declare const ROUTES: Record<Project, Forum>;
47
56
  /**
48
57
  * A solo project's chat is «<Name> · Ops»; a team forum is just the name — the
49
58
  * forum already carries its own «Ops»/«Dev» tabs, so the chat's own title
50
- * does not need to repeat the word. The real Telegram chat title and this
51
- * function must agree: the avatar scripts and the name-drift check both read
52
- * it through `notify routes --json`, not by asking Telegram.
59
+ * does not need to repeat the word. A chat shared by several repositories of
60
+ * one product, or a forum whose product name differs from the repo key,
61
+ * carries the product's name in `title`, and every row sharing that chat
62
+ * repeats it, so any of them answers with the same title. The real
63
+ * Telegram chat title and this function must agree: the avatar scripts and the
64
+ * name-drift check both read it through `notify routes --json`, not by asking
65
+ * Telegram.
53
66
  */
54
67
  export declare const chatTitle: (p: Project) => string;
55
68
  export type Target = {
package/dist/routes.js CHANGED
@@ -1,11 +1,17 @@
1
1
  import { DISPLAY, severity } from "./events.js";
2
2
  export const ROUTES = {
3
- // The only project with a team, and so the only forum left. ops/dev =
4
- // 22/23, not 3/4: the old tabs were deleted by hand on 27.07.2026, and
5
- // Telegram removes a topic's messages along with it. A recreated topic
6
- // gets a NEW id — a topic's id is the id of its first message, it is
7
- // never reused.
8
- zabukai: { chat: '-1004299939100', ops: 22, dev: 23 },
3
+ // Zabukai is one product built from two repositories, so it is two rows
4
+ // sharing one forum: an Ops tab EACH, because a card belongs to the
5
+ // repository it came from, and ONE Dev tab, because the people talking in
6
+ // it are one team working on one product (owner's rule, 09.09.2026).
7
+ //
8
+ // ops/dev = 22/23, not 3/4: the old tabs were deleted by hand on
9
+ // 27.07.2026, and Telegram removes a topic's messages along with it. A
10
+ // recreated topic gets a NEW id — a topic's id is the id of its first
11
+ // message, it is never reused. 962 is the site's Ops tab, created
12
+ // 09.09.2026.
13
+ 'zabukai-app': { chat: '-1004299939100', title: 'Zabukai', ops: 22, dev: 23 },
14
+ 'zabukai-site': { chat: '-1004299939100', title: 'Zabukai', ops: 962, dev: 23 },
9
15
  // Every row below is a project the owner runs alone. Their Topics were
10
16
  // turned off on 06.09.2026 and their `ops` numbers went with them: a card
11
17
  // now goes to the chat itself. Sending a thread id into a chat that is no
@@ -25,17 +31,23 @@ export const ROUTES = {
25
31
  // runs it alone.
26
32
  '2roles': { chat: '-1004314188744' },
27
33
  // Created 09.09.2026 by create-ops-chat.py as «Market Lens»: the owner and
28
- // Semyon, so a forum; the tab numbers come from `notify setup`.
29
- 'market-lens': { chat: '-1004489147617', ops: 4, dev: 5 }
34
+ // Semyon, so a forum; the tab numbers come from `notify setup`. A forum
35
+ // wears the product's own name even when it is one repository (owner's
36
+ // rule of 2026-09-10) — hence `title`, like Zabukai above.
37
+ 'market-lens': { chat: '-1004489147617', title: 'Market Lens', ops: 4, dev: 5 }
30
38
  };
31
39
  /**
32
40
  * A solo project's chat is «<Name> · Ops»; a team forum is just the name — the
33
41
  * forum already carries its own «Ops»/«Dev» tabs, so the chat's own title
34
- * does not need to repeat the word. The real Telegram chat title and this
35
- * function must agree: the avatar scripts and the name-drift check both read
36
- * it through `notify routes --json`, not by asking Telegram.
42
+ * does not need to repeat the word. A chat shared by several repositories of
43
+ * one product, or a forum whose product name differs from the repo key,
44
+ * carries the product's name in `title`, and every row sharing that chat
45
+ * repeats it, so any of them answers with the same title. The real
46
+ * Telegram chat title and this function must agree: the avatar scripts and the
47
+ * name-drift check both read it through `notify routes --json`, not by asking
48
+ * Telegram.
37
49
  */
38
- export const chatTitle = (p) => ROUTES[p].ops === undefined ? `${DISPLAY[p]} · Ops` : DISPLAY[p];
50
+ export const chatTitle = (p) => ROUTES[p].title ?? (ROUTES[p].ops === undefined ? `${DISPLAY[p]} · Ops` : DISPLAY[p]);
39
51
  /**
40
52
  * Where an event goes. Everything goes to its own project's "Ops" tab; a
41
53
  * red one arrives there too, just with sound.
package/dist/setup.js CHANGED
@@ -1,24 +1,42 @@
1
1
  /**
2
- * `notify setup "<Project name>"` creates the "⚙️ Ops" and "💬 Dev" tabs in a
3
- * forum that already exists, and prints the ready line for `ROUTES`.
2
+ * `notify setup <forum chat_id> <project key>` creates the "Ops" and "Dev"
3
+ * tabs in a forum that already exists, and prints the ready line for `ROUTES`.
4
+ *
5
+ * Tab shape (the standard tests/check-telegram-shape.py enforces): plain
6
+ * titles with a NATIVE topic icon, never an emoji in the title text — Ops
7
+ * carries 🤖 (robots post there), Dev carries 💬 (people talk there). The
8
+ * icon ids below are documents of Telegram's topic-icon sticker set,
9
+ * resolved live on 2026-09-10; ⚙️ is NOT in that set, which is why the
10
+ * robot glyph is 🤖.
11
+ *
12
+ * A product of SEVERAL repositories shares one forum, one Ops tab PER
13
+ * repository (a card belongs to the repo it came from) and ONE Dev tab.
14
+ * The Bot API cannot list a forum's topics, so the shape is decided from
15
+ * `ROUTES` itself: peers are rows that already point at this chat.
4
16
  *
5
17
  * The bot cannot create the forum supergroup itself — Telegram only allows a
6
- * real account to do that. So the order for a new project is:
18
+ * real account to do that. So the order for a new project is
19
+ * (mac-config's tg-forums skill, scripts/create-ops-chat.py --forum does all
20
+ * of step 1):
7
21
  * 1. create a group in Telegram, turn on "Topics" in it, add
8
22
  * @mikita_ops_bot as an admin with the "Manage topics" right;
9
- * 2. run `notify setup <chat_id>` — it creates both tabs and prints the line;
23
+ * 2. run `notify setup <chat_id> <project key>` — it creates the tabs and
24
+ * prints the line;
10
25
  * 3. paste the line into `src/routes.ts`.
11
- *
12
- * Step 1 happens once per project and takes half a minute. Steps 2 and 3 are
13
- * mechanical.
14
26
  */
27
+ import { ROUTES } from "./routes.js";
15
28
  const log = (msg) => console.error(`[notify] ${msg}`);
16
- const createTopic = async (token, chat, name, color) => {
29
+ // Documents of Telegram's topic-icon set (InputStickerSetEmojiDefaultTopicIcons):
30
+ // 🤖 for Ops, 💬 for Dev. A wrong id here writes a dead icon into every new
31
+ // forum — tests/check-telegram-shape.py catches a dropped icon live.
32
+ const OPS_ICON_ID = '5309832892262654231'; // 🤖
33
+ const DEV_ICON_ID = '5417915203100613993'; // 💬
34
+ const createTopic = async (token, chat, name, iconId, color) => {
17
35
  try {
18
36
  const res = await fetch(`https://api.telegram.org/bot${token}/createForumTopic`, {
19
37
  method: 'POST',
20
38
  headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify({ chat_id: chat, name, icon_color: color }),
39
+ body: JSON.stringify({ chat_id: chat, name, icon_color: color, icon_custom_emoji_id: iconId }),
22
40
  signal: AbortSignal.timeout(10_000)
23
41
  });
24
42
  const body = (await res.json());
@@ -40,21 +58,35 @@ export const setupTopic = async (chatId, projectKey) => {
40
58
  log('no OPS_BOT_TOKEN — cannot create the tabs');
41
59
  return;
42
60
  }
43
- const ops = await createTopic(token, chatId, '⚙️ Ops', 9367192);
44
- const dev = await createTopic(token, chatId, '💬 Dev', 7322096);
61
+ // The Bot API cannot enumerate topics, so the forum's shape is read from
62
+ // ROUTES: a row already pointing here is a peer repository of the same
63
+ // product.
64
+ const existing = ROUTES[projectKey];
65
+ if (existing && existing.chat === chatId && existing.ops !== undefined) {
66
+ log(`${projectKey} is already in ROUTES with ops=${existing.ops} — nothing to create`);
67
+ return;
68
+ }
69
+ const peers = Object.entries(ROUTES).filter(([key, row]) => key !== projectKey && row.chat === chatId);
70
+ // A second repository of the same product gets its name on its own Ops tab;
71
+ // the product's FIRST tab is plain "Ops" (rename it when a second repo
72
+ // joins — tests/check-telegram-shape.py goes red until it is done).
73
+ const opsTitle = peers.length > 0 ? `Ops · ${projectKey}` : 'Ops';
74
+ const sharedDev = peers.map(([, row]) => row.dev).find((id) => id !== undefined);
75
+ const ops = await createTopic(token, chatId, opsTitle, OPS_ICON_ID, 9367192);
76
+ const dev = sharedDev ?? (await createTopic(token, chatId, 'Dev', DEV_ICON_ID, 7322096));
45
77
  // Partial success: if only Ops was created, print it. Otherwise running the
46
78
  // command again would create A DIFFERENT Ops topic, and the old id would be lost.
47
79
  if (ops === null) {
48
80
  log('Ops was not created — check: is the bot a group admin with "Manage topics", and are topics on?');
49
81
  return;
50
82
  }
51
- if (dev === null) {
83
+ if (dev === null || dev === undefined) {
52
84
  log(`Ops created (id=${ops}), Dev was not — add Dev by hand or run again, and keep ops=${ops}`);
53
85
  log('add to src/routes.ts (fill dev in afterwards):');
54
86
  log(` ${JSON.stringify(projectKey)}: { chat: '${chatId}', ops: ${ops}, dev: <fill in> },`);
55
87
  return;
56
88
  }
57
- log(`tabs created: Ops=${ops}, Dev=${dev}`);
89
+ log(`tabs created: Ops="${opsTitle}" (${ops}), Dev (${dev})${sharedDev ? ' [shared with a peer repo]' : ''}`);
58
90
  log('add to src/routes.ts:');
59
91
  log(` ${JSON.stringify(projectKey)}: { chat: '${chatId}', ops: ${ops}, dev: ${dev} },`);
60
92
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",