@kaikybrofc/omnizap-system 2.2.0 → 2.2.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.
package/.env.example CHANGED
@@ -539,6 +539,10 @@ RELEASE_GIT_BRANCH=
539
539
  RELEASE_GIT_PRE_COMMIT_MESSAGE=chore(release): auto-commit before release
540
540
  RELEASE_GIT_COMMIT_VERSION=1
541
541
  RELEASE_GIT_VERSION_COMMIT_PREFIX=chore(release): v
542
+ # Geração automática de tag da versão no git
543
+ RELEASE_GIT_TAG_CREATE=1
544
+ RELEASE_GIT_TAG_PUSH=1
545
+ RELEASE_GIT_TAG_ANNOTATED=1
542
546
 
543
547
  # Cria/atualiza GitHub Release (aba Releases) automaticamente no npm run release
544
548
  RELEASE_GITHUB_RELEASE=1
@@ -549,6 +553,10 @@ RELEASE_GITHUB_TOKEN=
549
553
  RELEASE_GITHUB_TAG_PREFIX=v
550
554
  RELEASE_GITHUB_NAME_PREFIX=v
551
555
  RELEASE_GITHUB_GENERATE_NOTES=1
556
+ # Inclui seção markdown com arquivos alterados no corpo da release
557
+ RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES=1
558
+ # Limite de arquivos listados no markdown da release
559
+ RELEASE_GITHUB_RELEASE_MAX_FILES=300
552
560
  # Opcional: 1 para prerelease, 0 para release normal, vazio = auto (detecta '-' na versao)
553
561
  RELEASE_GITHUB_PRERELEASE=
554
562
  RELEASE_GITHUB_DRAFT=0
package/README.md CHANGED
@@ -26,6 +26,24 @@ O **OmniZap System** é uma plataforma de automação para WhatsApp usando **Nod
26
26
  - Termos de uso: https://omnizap.shop/termos-de-uso/
27
27
  - Licença: https://omnizap.shop/licenca/
28
28
 
29
+ ## Snapshot dinâmico para README/NPM
30
+
31
+ Use as rotas abaixo para consumir o bloco já renderizado com dados do sistema:
32
+
33
+ - JSON: `GET /api/sticker-packs/readme-summary`
34
+ - Markdown: `GET /api/sticker-packs/readme-markdown`
35
+
36
+ Conteúdo incluído no snapshot:
37
+
38
+ - total de usuários (`lid_map`)
39
+ - total de grupos
40
+ - total de packs e stickers
41
+ - total de mensagens registradas
42
+ - tipos de mensagem mais usados
43
+ - lista de comandos disponíveis no menu
44
+
45
+ Atualização em cache: **30 minutos** por padrão (`README_SUMMARY_CACHE_SECONDS=1800`).
46
+
29
47
  ## Recursos principais
30
48
 
31
49
  - Gerenciamento de grupos (admin, boas-vindas, despedida, anti-link, captcha).
@@ -239,6 +257,9 @@ Variáveis do fluxo de release (git):
239
257
  - `RELEASE_GIT_PRE_COMMIT_MESSAGE` (default: `chore(release): auto-commit before release`)
240
258
  - `RELEASE_GIT_COMMIT_VERSION` (default: `1`) - commita alteração da versão após sucesso
241
259
  - `RELEASE_GIT_VERSION_COMMIT_PREFIX` (default: `chore(release): v`)
260
+ - `RELEASE_GIT_TAG_CREATE` (default: `1`) - cria tag `vX.Y.Z` no release
261
+ - `RELEASE_GIT_TAG_PUSH` (default: `1`) - envia a tag para o remoto
262
+ - `RELEASE_GIT_TAG_ANNOTATED` (default: `1`) - usa tag anotada (`git tag -a`)
242
263
  - `RELEASE_GITHUB_RELEASE` (default: `1`) - cria/atualiza GitHub Release na aba Releases
243
264
  - `RELEASE_REQUIRE_GITHUB_RELEASE` (default: `1`) - falha se GitHub Release estiver desativado
244
265
  - `RELEASE_GITHUB_REPO` (opcional; ex.: `kaikybrofc/omnizap-system`)
@@ -246,6 +267,8 @@ Variáveis do fluxo de release (git):
246
267
  - `RELEASE_GITHUB_TAG_PREFIX` (default: `v`) - prefixo da tag (`v2.1.9`)
247
268
  - `RELEASE_GITHUB_NAME_PREFIX` (default: `v`) - prefixo do nome exibido na release
248
269
  - `RELEASE_GITHUB_GENERATE_NOTES` (default: `1`) - usa notas automáticas do GitHub
270
+ - `RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES` (default: `1`) - adiciona markdown com arquivos alterados no corpo da release
271
+ - `RELEASE_GITHUB_RELEASE_MAX_FILES` (default: `300`) - limite de arquivos listados no markdown
249
272
  - `RELEASE_GITHUB_PRERELEASE` (default: auto) - vazio detecta `-` na versão como prerelease
250
273
  - `RELEASE_GITHUB_DRAFT` (default: `0`)
251
274
  - `RELEASE_GITHUB_TARGET` (opcional; vazio usa `HEAD`)
@@ -63,6 +63,16 @@ import {
63
63
  buildViewerTagAffinity,
64
64
  computePackSignals,
65
65
  } from './stickerPackMarketplaceService.js';
66
+ import {
67
+ buildAdminMenu,
68
+ buildAiMenu,
69
+ buildAnimeMenu,
70
+ buildMediaMenu,
71
+ buildMenuCaption,
72
+ buildQuoteMenu,
73
+ buildStatsMenu,
74
+ buildStickerMenu,
75
+ } from '../menuModule/common.js';
66
76
  import { getMarketplaceDriftSnapshot } from './stickerMarketplaceDriftService.js';
67
77
  import { getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from './stickerStorageService.js';
68
78
  import { convertToWebp } from '../stickerModule/convertToWebp.js';
@@ -193,6 +203,9 @@ const MARKETPLACE_GLOBAL_STATS_API_PATH = '/api/marketplace/stats';
193
203
  const MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS = clampInt(process.env.MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS, 45, 30, 60);
194
204
  const HOME_MARKETPLACE_STATS_CACHE_SECONDS = clampInt(process.env.HOME_MARKETPLACE_STATS_CACHE_SECONDS, 45, 10, 300);
195
205
  const SYSTEM_SUMMARY_CACHE_SECONDS = clampInt(process.env.SYSTEM_SUMMARY_CACHE_SECONDS, 20, 5, 120);
206
+ const README_SUMMARY_CACHE_SECONDS = clampInt(process.env.README_SUMMARY_CACHE_SECONDS, 60 * 30, 60, 60 * 60 * 6);
207
+ const README_MESSAGE_TYPE_SAMPLE_LIMIT = clampInt(process.env.README_MESSAGE_TYPE_SAMPLE_LIMIT, 25000, 500, 250000);
208
+ const README_COMMAND_PREFIX = String(process.env.README_COMMAND_PREFIX || PACK_COMMAND_PREFIX).trim() || PACK_COMMAND_PREFIX;
196
209
  const SITE_CANONICAL_HOST = String(process.env.SITE_CANONICAL_HOST || 'omnizap.shop').trim().toLowerCase() || 'omnizap.shop';
197
210
  const SITE_CANONICAL_SCHEME = String(process.env.SITE_CANONICAL_SCHEME || 'https').trim().toLowerCase() === 'http'
198
211
  ? 'http'
@@ -283,6 +296,11 @@ const SYSTEM_SUMMARY_CACHE = {
283
296
  value: null,
284
297
  pending: null,
285
298
  };
299
+ const README_SUMMARY_CACHE = {
300
+ expiresAt: 0,
301
+ value: null,
302
+ pending: null,
303
+ };
286
304
  const SITEMAP_CACHE = {
287
305
  expiresAt: 0,
288
306
  xml: '',
@@ -3700,6 +3718,9 @@ const invalidateStickerCatalogDerivedCaches = () => {
3700
3718
  SYSTEM_SUMMARY_CACHE.expiresAt = 0;
3701
3719
  SYSTEM_SUMMARY_CACHE.value = null;
3702
3720
  SYSTEM_SUMMARY_CACHE.pending = null;
3721
+ README_SUMMARY_CACHE.expiresAt = 0;
3722
+ README_SUMMARY_CACHE.value = null;
3723
+ README_SUMMARY_CACHE.pending = null;
3703
3724
  };
3704
3725
 
3705
3726
  const sendManagedMutationStatus = (req, res, status, extra = {}, statusCode = 200) => {
@@ -5736,6 +5757,253 @@ const parseMessageTypeFromRaw = (rawMessage) => {
5736
5757
  }
5737
5758
  };
5738
5759
 
5760
+ const formatPtBrInteger = (value) => Number(value || 0).toLocaleString('pt-BR');
5761
+
5762
+ const extractCommandsFromMenuLine = (line, commandPrefix) => {
5763
+ const normalizedLine = String(line || '').trim();
5764
+ if (!normalizedLine.startsWith('→')) return [];
5765
+
5766
+ const commandParts = normalizedLine
5767
+ .replace(/^→\s*/, '')
5768
+ .split('|')
5769
+ .map((part) => part.trim())
5770
+ .filter((part) => part.startsWith(commandPrefix));
5771
+
5772
+ return commandParts
5773
+ .map((part) => {
5774
+ const normalized = part.replace(/\s{2,}.*/, '').trim();
5775
+ const withoutPrefix = normalized.slice(commandPrefix.length).trim();
5776
+ if (!withoutPrefix) return '';
5777
+
5778
+ const tokens = withoutPrefix.split(/\s+/).filter(Boolean);
5779
+ const selectedTokens = [];
5780
+ for (const token of tokens) {
5781
+ if (/^[<[(]/.test(token)) break;
5782
+ selectedTokens.push(token);
5783
+ }
5784
+ if (!selectedTokens.length) return '';
5785
+ return `${commandPrefix}${selectedTokens.join(' ')}`;
5786
+ })
5787
+ .filter(Boolean);
5788
+ };
5789
+
5790
+ const collectAvailableMenuCommands = (commandPrefix = README_COMMAND_PREFIX) => {
5791
+ const sections = [
5792
+ buildMenuCaption('OmniZap', commandPrefix),
5793
+ buildStickerMenu(commandPrefix),
5794
+ buildMediaMenu(commandPrefix),
5795
+ buildQuoteMenu(commandPrefix),
5796
+ buildAnimeMenu(commandPrefix),
5797
+ buildAiMenu(commandPrefix),
5798
+ buildStatsMenu(commandPrefix),
5799
+ buildAdminMenu(commandPrefix),
5800
+ ];
5801
+
5802
+ const commands = new Set();
5803
+ for (const section of sections) {
5804
+ for (const line of String(section || '').split('\n')) {
5805
+ const extracted = extractCommandsFromMenuLine(line, commandPrefix);
5806
+ for (const command of extracted) {
5807
+ commands.add(command);
5808
+ }
5809
+ }
5810
+ }
5811
+
5812
+ return Array.from(commands).sort((left, right) => left.localeCompare(right, 'pt-BR'));
5813
+ };
5814
+
5815
+ const renderReadmeSnapshotMarkdown = ({ generatedAt, totals, topMessageTypes, commands }) => {
5816
+ const typeLines = topMessageTypes.length
5817
+ ? topMessageTypes.map((entry, index) => `${index + 1}. \`${entry.type}\` - **${formatPtBrInteger(entry.total)}**`)
5818
+ : ['1. `outros` - **0**'];
5819
+
5820
+ const commandLines = commands.length
5821
+ ? commands.map((command) => `- \`${command}\``)
5822
+ : ['- Nenhum comando identificado no menu atual.'];
5823
+
5824
+ return [
5825
+ '## OmniZap System Snapshot',
5826
+ '',
5827
+ `Atualizado em: **${generatedAt}**`,
5828
+ `Janela de atualização: **${README_SUMMARY_CACHE_SECONDS} segundos**`,
5829
+ '',
5830
+ '### Totais do sistema',
5831
+ `- Usuários (lid_map): **${formatPtBrInteger(totals.total_users)}**`,
5832
+ `- Grupos: **${formatPtBrInteger(totals.total_groups)}**`,
5833
+ `- Packs: **${formatPtBrInteger(totals.total_packs)}**`,
5834
+ `- Stickers: **${formatPtBrInteger(totals.total_stickers)}**`,
5835
+ `- Mensagens registradas: **${formatPtBrInteger(totals.total_messages)}**`,
5836
+ '',
5837
+ `### Tipos de mensagem mais usados (amostra de ${formatPtBrInteger(totals.message_types_sample_size)} mensagens)`,
5838
+ ...typeLines,
5839
+ '',
5840
+ '### Comandos disponíveis',
5841
+ ...commandLines,
5842
+ '',
5843
+ ].join('\n');
5844
+ };
5845
+
5846
+ const buildReadmeSummarySnapshot = async () => {
5847
+ const [
5848
+ lidMapTotalsRows,
5849
+ chatsTotalsRows,
5850
+ groupsMetadataTotalsRows,
5851
+ packTotalsRows,
5852
+ stickerTotalsRows,
5853
+ messageTotalsRows,
5854
+ messageTypeRows,
5855
+ ] = await Promise.all([
5856
+ executeQuery(`SELECT COUNT(*) AS total_users FROM ${TABLES.LID_MAP}`),
5857
+ executeQuery(
5858
+ `SELECT
5859
+ COUNT(*) AS total_chats,
5860
+ SUM(CASE WHEN id LIKE '%@g.us' THEN 1 ELSE 0 END) AS total_groups
5861
+ FROM ${TABLES.CHATS}`,
5862
+ ),
5863
+ executeQuery(`SELECT COUNT(*) AS total_groups FROM ${TABLES.GROUPS_METADATA}`),
5864
+ executeQuery(`SELECT COUNT(*) AS total_packs FROM ${TABLES.STICKER_PACK} WHERE deleted_at IS NULL`),
5865
+ executeQuery(`SELECT COUNT(*) AS total_stickers FROM ${TABLES.STICKER_ASSET}`),
5866
+ executeQuery(`SELECT COUNT(*) AS total_messages FROM ${TABLES.MESSAGES}`),
5867
+ executeQuery(
5868
+ `SELECT raw_message
5869
+ FROM ${TABLES.MESSAGES}
5870
+ WHERE raw_message IS NOT NULL
5871
+ ORDER BY id DESC
5872
+ LIMIT ${README_MESSAGE_TYPE_SAMPLE_LIMIT}`,
5873
+ ),
5874
+ ]);
5875
+
5876
+ const lidMapTotals = lidMapTotalsRows?.[0] || {};
5877
+ const chatsTotals = chatsTotalsRows?.[0] || {};
5878
+ const groupsMetadataTotals = groupsMetadataTotalsRows?.[0] || {};
5879
+ const packTotals = packTotalsRows?.[0] || {};
5880
+ const stickerTotals = stickerTotalsRows?.[0] || {};
5881
+ const messageTotals = messageTotalsRows?.[0] || {};
5882
+
5883
+ const totalGroupsFromChats = Number(chatsTotals?.total_groups || 0);
5884
+ const totalGroupsFromMetadata = Number(groupsMetadataTotals?.total_groups || 0);
5885
+ const totalGroups = Math.max(totalGroupsFromChats, totalGroupsFromMetadata);
5886
+ const totalMessages = Number(messageTotals?.total_messages || 0);
5887
+
5888
+ const typeCounts = new Map();
5889
+ const sampledMessages = Array.isArray(messageTypeRows) ? messageTypeRows.length : 0;
5890
+ for (const row of messageTypeRows || []) {
5891
+ const type = parseMessageTypeFromRaw(row?.raw_message);
5892
+ typeCounts.set(type, (typeCounts.get(type) || 0) + 1);
5893
+ }
5894
+ const topMessageTypes = Array.from(typeCounts.entries())
5895
+ .map(([type, total]) => ({ type, total: Number(total || 0) }))
5896
+ .sort((left, right) => Number(right.total || 0) - Number(left.total || 0))
5897
+ .slice(0, 8);
5898
+
5899
+ const commands = collectAvailableMenuCommands(README_COMMAND_PREFIX);
5900
+ const generatedAt = new Date().toISOString();
5901
+
5902
+ const totals = {
5903
+ total_users: Number(lidMapTotals?.total_users || 0),
5904
+ total_groups: totalGroups,
5905
+ total_groups_from_chats: totalGroupsFromChats,
5906
+ total_groups_from_metadata: totalGroupsFromMetadata,
5907
+ total_chats: Number(chatsTotals?.total_chats || 0),
5908
+ total_packs: Number(packTotals?.total_packs || 0),
5909
+ total_stickers: Number(stickerTotals?.total_stickers || 0),
5910
+ total_messages: totalMessages,
5911
+ message_types_sample_size: sampledMessages,
5912
+ message_types_total_coverage_percent: totalMessages > 0 ? Number(((sampledMessages / totalMessages) * 100).toFixed(2)) : 0,
5913
+ };
5914
+
5915
+ const markdown = renderReadmeSnapshotMarkdown({
5916
+ generatedAt,
5917
+ totals,
5918
+ topMessageTypes,
5919
+ commands,
5920
+ });
5921
+
5922
+ return {
5923
+ data: {
5924
+ generated_at: generatedAt,
5925
+ cache_seconds: README_SUMMARY_CACHE_SECONDS,
5926
+ command_prefix: README_COMMAND_PREFIX,
5927
+ totals,
5928
+ top_message_types: topMessageTypes,
5929
+ commands,
5930
+ markdown,
5931
+ },
5932
+ };
5933
+ };
5934
+
5935
+ const getReadmeSummaryCached = async () => {
5936
+ const now = Date.now();
5937
+ const hasValue = Boolean(README_SUMMARY_CACHE.value);
5938
+
5939
+ if (hasValue && now < README_SUMMARY_CACHE.expiresAt) {
5940
+ return README_SUMMARY_CACHE.value;
5941
+ }
5942
+
5943
+ if (!README_SUMMARY_CACHE.pending) {
5944
+ README_SUMMARY_CACHE.pending = withTimeout(buildReadmeSummarySnapshot(), 7000)
5945
+ .then((payload) => {
5946
+ README_SUMMARY_CACHE.value = payload;
5947
+ README_SUMMARY_CACHE.expiresAt = Date.now() + README_SUMMARY_CACHE_SECONDS * 1000;
5948
+ return payload;
5949
+ })
5950
+ .finally(() => {
5951
+ README_SUMMARY_CACHE.pending = null;
5952
+ });
5953
+ }
5954
+
5955
+ if (hasValue) return README_SUMMARY_CACHE.value;
5956
+ return README_SUMMARY_CACHE.pending;
5957
+ };
5958
+
5959
+ const handleReadmeSummaryRequest = async (req, res) => {
5960
+ try {
5961
+ const payload = await getReadmeSummaryCached();
5962
+ sendJson(req, res, 200, payload);
5963
+ } catch (error) {
5964
+ logger.warn('Falha ao montar resumo markdown para README.', {
5965
+ action: 'readme_summary_error',
5966
+ error: error?.message,
5967
+ });
5968
+ if (README_SUMMARY_CACHE.value) {
5969
+ sendJson(req, res, 200, {
5970
+ ...README_SUMMARY_CACHE.value,
5971
+ meta: {
5972
+ stale: true,
5973
+ error: error?.message || 'fallback_cache',
5974
+ },
5975
+ });
5976
+ return;
5977
+ }
5978
+ sendJson(req, res, 503, { error: 'Resumo markdown indisponível no momento.' });
5979
+ }
5980
+ };
5981
+
5982
+ const handleReadmeMarkdownRequest = async (req, res) => {
5983
+ try {
5984
+ const payload = await getReadmeSummaryCached();
5985
+ const markdown = String(payload?.data?.markdown || '').trim();
5986
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
5987
+ res.setHeader('Cache-Control', `public, max-age=${Math.min(README_SUMMARY_CACHE_SECONDS, 300)}`);
5988
+ res.setHeader('X-Cache-Seconds', String(README_SUMMARY_CACHE_SECONDS));
5989
+ sendText(req, res, 200, markdown ? `${markdown}\n` : '', 'text/markdown; charset=utf-8');
5990
+ } catch (error) {
5991
+ logger.warn('Falha ao renderizar markdown para README.', {
5992
+ action: 'readme_markdown_error',
5993
+ error: error?.message,
5994
+ });
5995
+ if (README_SUMMARY_CACHE.value) {
5996
+ const markdown = String(README_SUMMARY_CACHE.value?.data?.markdown || '').trim();
5997
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
5998
+ res.setHeader('Cache-Control', `public, max-age=${Math.min(README_SUMMARY_CACHE_SECONDS, 300)}`);
5999
+ res.setHeader('X-Cache-Seconds', String(README_SUMMARY_CACHE_SECONDS));
6000
+ sendText(req, res, 200, markdown ? `${markdown}\n` : '', 'text/markdown; charset=utf-8');
6001
+ return;
6002
+ }
6003
+ sendText(req, res, 503, 'Resumo markdown indisponivel no momento.\n', 'text/plain; charset=utf-8');
6004
+ }
6005
+ };
6006
+
5739
6007
  const resolveBotUserCandidates = (activeSocket) => {
5740
6008
  const candidates = new Set();
5741
6009
  const botJidFromSocket = resolveBotJid(activeSocket?.user?.id);
@@ -7155,6 +7423,24 @@ const handleCatalogApiRequest = async (req, res, pathname, url) => {
7155
7423
  return true;
7156
7424
  }
7157
7425
 
7426
+ if (segments.length === 1 && segments[0] === 'readme-summary') {
7427
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
7428
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7429
+ return true;
7430
+ }
7431
+ await handleReadmeSummaryRequest(req, res);
7432
+ return true;
7433
+ }
7434
+
7435
+ if (segments.length === 1 && segments[0] === 'readme-markdown') {
7436
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
7437
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7438
+ return true;
7439
+ }
7440
+ await handleReadmeMarkdownRequest(req, res);
7441
+ return true;
7442
+ }
7443
+
7158
7444
  if (segments.length === 1 && segments[0] === 'support') {
7159
7445
  if (!['GET', 'HEAD'].includes(req.method || '')) {
7160
7446
  sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaikybrofc/omnizap-system",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Sistema profissional de automação WhatsApp com tecnologia Baileys",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'node:child_process';
4
+ import fs from 'node:fs';
4
5
  import path from 'node:path';
5
6
  import process from 'node:process';
6
7
  import { fileURLToPath } from 'node:url';
@@ -80,11 +81,22 @@ if (!repoOwner || !repoName) {
80
81
  const tag = getArg('--tag');
81
82
  const target = getArg('--target');
82
83
  const name = getArg('--name', tag);
83
- const body = getArg('--body', '');
84
+ const bodyArg = getArg('--body', '');
85
+ const bodyFile = getArg('--body-file', '');
84
86
  const generateNotes = toBool(getArg('--generate-notes', 'true'), true);
85
87
  const prerelease = toBool(getArg('--prerelease', 'false'), false);
86
88
  const draft = toBool(getArg('--draft', 'false'), false);
87
89
 
90
+ let body = bodyArg;
91
+ if (!body && bodyFile) {
92
+ try {
93
+ body = fs.readFileSync(bodyFile, 'utf8');
94
+ } catch (error) {
95
+ console.error(`Falha ao ler --body-file (${bodyFile}): ${error?.message || error}`);
96
+ process.exit(1);
97
+ }
98
+ }
99
+
88
100
  if (!tag) {
89
101
  console.error('Parâmetro obrigatório ausente: --tag');
90
102
  process.exit(1);
@@ -13,6 +13,9 @@ RELEASE_GIT_BRANCH="${RELEASE_GIT_BRANCH:-}"
13
13
  RELEASE_GIT_PRE_COMMIT_MESSAGE="${RELEASE_GIT_PRE_COMMIT_MESSAGE:-chore(release): auto-commit before release}"
14
14
  RELEASE_GIT_COMMIT_VERSION="${RELEASE_GIT_COMMIT_VERSION:-1}"
15
15
  RELEASE_GIT_VERSION_COMMIT_PREFIX="${RELEASE_GIT_VERSION_COMMIT_PREFIX:-chore(release): v}"
16
+ RELEASE_GIT_TAG_CREATE="${RELEASE_GIT_TAG_CREATE:-1}"
17
+ RELEASE_GIT_TAG_PUSH="${RELEASE_GIT_TAG_PUSH:-1}"
18
+ RELEASE_GIT_TAG_ANNOTATED="${RELEASE_GIT_TAG_ANNOTATED:-1}"
16
19
  RELEASE_GITHUB_RELEASE="${RELEASE_GITHUB_RELEASE:-1}"
17
20
  RELEASE_REQUIRE_GITHUB_RELEASE="${RELEASE_REQUIRE_GITHUB_RELEASE:-1}"
18
21
  RELEASE_GITHUB_TAG_PREFIX="${RELEASE_GITHUB_TAG_PREFIX:-v}"
@@ -21,6 +24,8 @@ RELEASE_GITHUB_GENERATE_NOTES="${RELEASE_GITHUB_GENERATE_NOTES:-1}"
21
24
  RELEASE_GITHUB_PRERELEASE="${RELEASE_GITHUB_PRERELEASE:-}"
22
25
  RELEASE_GITHUB_DRAFT="${RELEASE_GITHUB_DRAFT:-0}"
23
26
  RELEASE_GITHUB_TARGET="${RELEASE_GITHUB_TARGET:-}"
27
+ RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES="${RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES:-1}"
28
+ RELEASE_GITHUB_RELEASE_MAX_FILES="${RELEASE_GITHUB_RELEASE_MAX_FILES:-300}"
24
29
  RELEASE_REQUIRE_DUAL_PUBLISH="${RELEASE_REQUIRE_DUAL_PUBLISH:-1}"
25
30
  RELEASE_VERIFY_UNIFIED_VERSION="${RELEASE_VERIFY_UNIFIED_VERSION:-1}"
26
31
  RELEASE_VERIFY_PRIMARY_REGISTRY="${RELEASE_VERIFY_PRIMARY_REGISTRY:-${DEPLOY_PACKAGE_REGISTRY:-https://npm.pkg.github.com}}"
@@ -28,6 +33,7 @@ RELEASE_VERIFY_SECONDARY_REGISTRY="${RELEASE_VERIFY_SECONDARY_REGISTRY:-${DEPLOY
28
33
  RELEASE_VERIFY_PRIMARY_TOKEN_KEYS="${RELEASE_VERIFY_PRIMARY_TOKEN_KEYS:-DEPLOY_PACKAGE_TOKEN,DEPLOY_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,NPM_TOKEN,NODE_AUTH_TOKEN}"
29
34
  RELEASE_VERIFY_SECONDARY_TOKEN_KEYS="${RELEASE_VERIFY_SECONDARY_TOKEN_KEYS:-DEPLOY_PACKAGE_SECONDARY_TOKEN,NPM_TOKEN,NODE_AUTH_TOKEN}"
30
35
  TMP_NPMRC_FILES=()
36
+ TMP_MISC_FILES=()
31
37
 
32
38
  case "$RELEASE_TYPE" in
33
39
  patch|minor|major|prepatch|preminor|premajor|prerelease)
@@ -43,14 +49,19 @@ log() {
43
49
  printf '[release] %s\n' "$*"
44
50
  }
45
51
 
46
- cleanup_tmp_npmrcs() {
52
+ cleanup_tmp_files() {
47
53
  for npmrc_tmp in "${TMP_NPMRC_FILES[@]:-}"; do
48
54
  if [ -n "$npmrc_tmp" ] && [ -f "$npmrc_tmp" ]; then
49
55
  rm -f "$npmrc_tmp"
50
56
  fi
51
57
  done
58
+ for tmp_file in "${TMP_MISC_FILES[@]:-}"; do
59
+ if [ -n "$tmp_file" ] && [ -f "$tmp_file" ]; then
60
+ rm -f "$tmp_file"
61
+ fi
62
+ done
52
63
  }
53
- trap cleanup_tmp_npmrcs EXIT
64
+ trap cleanup_tmp_files EXIT
54
65
 
55
66
  require_cmd() {
56
67
  if ! command -v "$1" >/dev/null 2>&1; then
@@ -217,6 +228,119 @@ compute_target_version() {
217
228
  printf ''
218
229
  }
219
230
 
231
+ resolve_previous_release_tag() {
232
+ local current_tag="$1"
233
+ (
234
+ cd "$PROJECT_ROOT" &&
235
+ git tag --list "${RELEASE_GITHUB_TAG_PREFIX}[0-9]*" --sort=-version:refname |
236
+ grep -Fvx "$current_tag" |
237
+ head -n 1
238
+ )
239
+ }
240
+
241
+ build_release_body_file() {
242
+ local current_tag="$1"
243
+ local target_ref="$2"
244
+ local body_file=""
245
+ body_file="$(mktemp /tmp/omnizap-release-body.XXXXXX.md)"
246
+ TMP_MISC_FILES+=("$body_file")
247
+
248
+ local previous_tag=""
249
+ previous_tag="$(resolve_previous_release_tag "$current_tag")"
250
+
251
+ local max_files=300
252
+ if [[ "$RELEASE_GITHUB_RELEASE_MAX_FILES" =~ ^[0-9]+$ ]] && [ "$RELEASE_GITHUB_RELEASE_MAX_FILES" -gt 0 ]; then
253
+ max_files="$RELEASE_GITHUB_RELEASE_MAX_FILES"
254
+ fi
255
+
256
+ local -a changed_files=()
257
+ if [ -n "$previous_tag" ]; then
258
+ mapfile -t changed_files < <(
259
+ cd "$PROJECT_ROOT" &&
260
+ git diff --name-only --diff-filter=ACMRTUXB "${previous_tag}..${target_ref}" |
261
+ sed '/^$/d'
262
+ )
263
+ fi
264
+
265
+ {
266
+ printf '## Arquivos alterados\n\n'
267
+ if [ -n "$previous_tag" ]; then
268
+ printf 'Comparação: `%s...%s`\n\n' "$previous_tag" "$current_tag"
269
+ else
270
+ printf 'Release inicial (sem tag anterior para comparação).\n\n'
271
+ fi
272
+
273
+ if [ "${#changed_files[@]}" -eq 0 ]; then
274
+ printf -- '- Nenhum arquivo alterado detectado.\n'
275
+ else
276
+ local total="${#changed_files[@]}"
277
+ local limit="$total"
278
+ if [ "$total" -gt "$max_files" ]; then
279
+ limit="$max_files"
280
+ fi
281
+
282
+ local i=0
283
+ while [ "$i" -lt "$limit" ]; do
284
+ printf -- '- `%s`\n' "${changed_files[$i]}"
285
+ i=$((i + 1))
286
+ done
287
+
288
+ if [ "$total" -gt "$max_files" ]; then
289
+ printf '\n_...e mais %s arquivo(s)._\n' "$((total - max_files))"
290
+ fi
291
+ fi
292
+ } > "$body_file"
293
+
294
+ printf '%s' "$body_file"
295
+ }
296
+
297
+ ensure_release_tag() {
298
+ local tag_name="$1"
299
+ local target_ref="$2"
300
+
301
+ local local_target_sha=""
302
+ local_target_sha="$(cd "$PROJECT_ROOT" && git rev-parse "${target_ref}^{}")"
303
+
304
+ if (cd "$PROJECT_ROOT" && git rev-parse --verify "refs/tags/${tag_name}" >/dev/null 2>&1); then
305
+ local local_tag_sha=""
306
+ local_tag_sha="$(cd "$PROJECT_ROOT" && git rev-parse "${tag_name}^{}")"
307
+ if [ "$local_tag_sha" != "$local_target_sha" ]; then
308
+ printf '[release] Tag %s já existe e aponta para outro commit (%s).\n' "$tag_name" "$local_tag_sha" >&2
309
+ exit 1
310
+ fi
311
+ log "Tag ${tag_name} já existe localmente."
312
+ else
313
+ if [ "$RELEASE_GIT_TAG_CREATE" != "1" ]; then
314
+ printf '[release] Tag %s não existe e RELEASE_GIT_TAG_CREATE=0.\n' "$tag_name" >&2
315
+ exit 1
316
+ fi
317
+ log "Criando tag ${tag_name}"
318
+ if [ "$RELEASE_GIT_TAG_ANNOTATED" = "1" ]; then
319
+ (cd "$PROJECT_ROOT" && git tag -a "$tag_name" -m "Release ${tag_name}" "$target_ref")
320
+ else
321
+ (cd "$PROJECT_ROOT" && git tag "$tag_name" "$target_ref")
322
+ fi
323
+ fi
324
+
325
+ if [ "$RELEASE_GIT_TAG_PUSH" = "1" ]; then
326
+ local remote_sha=""
327
+ remote_sha="$(cd "$PROJECT_ROOT" && git ls-remote --tags "$RELEASE_GIT_REMOTE" "refs/tags/${tag_name}^{}" | awk 'NR==1{print $1}')"
328
+ if [ -z "$remote_sha" ]; then
329
+ remote_sha="$(cd "$PROJECT_ROOT" && git ls-remote --tags "$RELEASE_GIT_REMOTE" "refs/tags/${tag_name}" | awk 'NR==1{print $1}')"
330
+ fi
331
+
332
+ if [ -z "$remote_sha" ]; then
333
+ log "Enviando tag ${tag_name} para ${RELEASE_GIT_REMOTE}"
334
+ (cd "$PROJECT_ROOT" && git push "$RELEASE_GIT_REMOTE" "refs/tags/${tag_name}")
335
+ elif [ "$remote_sha" != "$local_target_sha" ]; then
336
+ printf '[release] Tag remota %s já existe e aponta para outro commit (%s).\n' "$tag_name" "$remote_sha" >&2
337
+ exit 1
338
+ else
339
+ log "Tag ${tag_name} já existe no remoto ${RELEASE_GIT_REMOTE}."
340
+ fi
341
+ fi
342
+ }
343
+
220
344
  commit_and_push_if_dirty() {
221
345
  local commit_message="$1"
222
346
 
@@ -320,6 +444,14 @@ if [ "$RELEASE_GIT_COMMIT_VERSION" = "1" ]; then
320
444
  fi
321
445
 
322
446
  release_tag="${RELEASE_GITHUB_TAG_PREFIX}${new_version}"
447
+ release_target_ref="$(cd "$PROJECT_ROOT" && git rev-parse HEAD)"
448
+
449
+ if [ -n "$(cd "$PROJECT_ROOT" && git status --porcelain --untracked-files=no)" ]; then
450
+ printf '[release] Working tree com alterações rastreadas antes de criar tag/release. Ajuste RELEASE_GIT_COMMIT_VERSION ou commite manualmente.\n' >&2
451
+ exit 1
452
+ fi
453
+
454
+ ensure_release_tag "$release_tag" "$release_target_ref"
323
455
 
324
456
  if [ "$RELEASE_GITHUB_RELEASE" = "1" ]; then
325
457
  if [ "$RELEASE_GIT_AUTO_PUSH" != "1" ]; then
@@ -330,7 +462,7 @@ if [ "$RELEASE_GITHUB_RELEASE" = "1" ]; then
330
462
  local_name="${RELEASE_GITHUB_NAME_PREFIX}${new_version}"
331
463
  local_target="$RELEASE_GITHUB_TARGET"
332
464
  if [ -z "$local_target" ]; then
333
- local_target="$(cd "$PROJECT_ROOT" && git rev-parse HEAD)"
465
+ local_target="$release_target_ref"
334
466
  fi
335
467
 
336
468
  local_prerelease="$RELEASE_GITHUB_PRERELEASE"
@@ -342,23 +474,40 @@ if [ "$RELEASE_GITHUB_RELEASE" = "1" ]; then
342
474
  fi
343
475
  fi
344
476
 
345
- local generate_notes_bool=""
477
+ generate_notes_bool=""
346
478
  generate_notes_bool="$(to_bool "$RELEASE_GITHUB_GENERATE_NOTES")"
347
- local prerelease_bool=""
479
+ prerelease_bool=""
348
480
  prerelease_bool="$(to_bool "$local_prerelease")"
349
- local draft_bool=""
481
+ draft_bool=""
350
482
  draft_bool="$(to_bool "$RELEASE_GITHUB_DRAFT")"
483
+ release_body_file=""
484
+ if [ "$RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES" = "1" ]; then
485
+ release_body_file="$(build_release_body_file "$release_tag" "$release_target_ref")"
486
+ fi
351
487
 
352
488
  log "Criando/atualizando GitHub Release ($release_tag)"
353
- release_output="$(
354
- cd "$PROJECT_ROOT" && node ./scripts/github-release-notify.mjs upsert \
355
- --tag "$release_tag" \
356
- --target "$local_target" \
357
- --name "$local_name" \
358
- --generate-notes "$generate_notes_bool" \
359
- --prerelease "$prerelease_bool" \
360
- --draft "$draft_bool"
361
- )"
489
+ if [ -n "$release_body_file" ]; then
490
+ release_output="$(
491
+ cd "$PROJECT_ROOT" && node ./scripts/github-release-notify.mjs upsert \
492
+ --tag "$release_tag" \
493
+ --target "$local_target" \
494
+ --name "$local_name" \
495
+ --body-file "$release_body_file" \
496
+ --generate-notes "$generate_notes_bool" \
497
+ --prerelease "$prerelease_bool" \
498
+ --draft "$draft_bool"
499
+ )"
500
+ else
501
+ release_output="$(
502
+ cd "$PROJECT_ROOT" && node ./scripts/github-release-notify.mjs upsert \
503
+ --tag "$release_tag" \
504
+ --target "$local_target" \
505
+ --name "$local_name" \
506
+ --generate-notes "$generate_notes_bool" \
507
+ --prerelease "$prerelease_bool" \
508
+ --draft "$draft_bool"
509
+ )"
510
+ fi
362
511
  log "GitHub Release atualizado: $release_output"
363
512
  fi
364
513
 
@@ -376,6 +525,33 @@ if [ "$RELEASE_VERIFY_UNIFIED_VERSION" = "1" ]; then
376
525
  fi
377
526
  log "Verificado localmente: versão=$local_version_now"
378
527
 
528
+ tag_commit_now="$(cd "$PROJECT_ROOT" && git rev-parse "${release_tag}^{}" 2>/dev/null || true)"
529
+ if [ -z "$tag_commit_now" ]; then
530
+ printf '[release] Tag local ausente: %s\n' "$release_tag" >&2
531
+ exit 1
532
+ fi
533
+ if [ "$tag_commit_now" != "$release_target_ref" ]; then
534
+ printf '[release] Tag local %s aponta para commit divergente (%s).\n' "$release_tag" "$tag_commit_now" >&2
535
+ exit 1
536
+ fi
537
+ log "Verificada tag local: ${release_tag} -> ${tag_commit_now}"
538
+
539
+ if [ "$RELEASE_GIT_TAG_PUSH" = "1" ]; then
540
+ remote_tag_sha="$(cd "$PROJECT_ROOT" && git ls-remote --tags "$RELEASE_GIT_REMOTE" "refs/tags/${release_tag}^{}" | awk 'NR==1{print $1}')"
541
+ if [ -z "$remote_tag_sha" ]; then
542
+ remote_tag_sha="$(cd "$PROJECT_ROOT" && git ls-remote --tags "$RELEASE_GIT_REMOTE" "refs/tags/${release_tag}" | awk 'NR==1{print $1}')"
543
+ fi
544
+ if [ -z "$remote_tag_sha" ]; then
545
+ printf '[release] Tag remota ausente: %s em %s\n' "$release_tag" "$RELEASE_GIT_REMOTE" >&2
546
+ exit 1
547
+ fi
548
+ if [ "$remote_tag_sha" != "$release_target_ref" ]; then
549
+ printf '[release] Tag remota %s divergente (%s).\n' "$release_tag" "$remote_tag_sha" >&2
550
+ exit 1
551
+ fi
552
+ log "Verificada tag remota: ${release_tag} -> ${remote_tag_sha}"
553
+ fi
554
+
379
555
  verify_registry_version "$pkg_name" "$new_version" "$RELEASE_VERIFY_PRIMARY_REGISTRY" "$RELEASE_VERIFY_PRIMARY_TOKEN_KEYS" "1"
380
556
  verify_registry_version "$pkg_name" "$new_version" "$RELEASE_VERIFY_SECONDARY_REGISTRY" "$RELEASE_VERIFY_SECONDARY_TOKEN_KEYS" "0"
381
557
 
package/RELEASE-v2.1.2.md DELETED
@@ -1,83 +0,0 @@
1
- # v2.1.3
2
-
3
- Atualiza a versão para `2.1.3` e consolida o ciclo de entregas desde a `v2.1.2`.
4
-
5
- Período do ciclo: `2026-02-26` a `2026-02-27`
6
- Comparação base: `v2.1.2..HEAD`
7
- Escopo: **39 commits** e **71 arquivos alterados**
8
-
9
- ## Destaques
10
-
11
- - Plataforma web de stickers expandida de ponta a ponta: criação de packs no browser, upload de mídia (imagem/vídeo), fluxo de publicação e dashboard de criador.
12
- - Marketplace e descoberta evoluídos: categorias, sorting, ranking de criadores, métricas globais, cards/UX mobile e melhorias de navegação.
13
- - SEO e indexação: `sitemap`, ajustes de páginas SEO por pack e controles de exposição NSFW no catálogo.
14
- - Classificação inteligente e curadoria automática reforçadas: pipeline CLIP/MobileCLIP/OpenCLIP, fila de reprocessamento, clustering semântico e otimizações do auto-pack por tags.
15
- - Admin panel consolidado com moderação: gestão de bans e role de moderador, com persistência de sessão Google Web.
16
- - Backend mais robusto: limpeza segura de órfãos, proteção de mutações, idempotência em uploads/publicação e novos caches para stats/sumários.
17
- - Runtime e resiliência: reconexão de socket aprimorada, tratamento de rejeições transitórias e ajustes de agendamento em background.
18
- - Banco e deploy: novas migrations para classificação/engajamento/workers/admin, e ajuste no nome do banco com sufixo por ambiente.
19
-
20
- ## Banco de dados e migrations
21
-
22
- Este ciclo adiciona migrations estruturais importantes:
23
-
24
- - `20260226_0011_sticker_asset_classification.sql`
25
- - `20260226_0012_sticker_pack_engagement.sql`
26
- - `20260226_0013_sticker_marketplace_intelligence.sql`
27
- - `20260226_0014_sticker_pack_publish_flow.sql`
28
- - `20260226_0014_sticker_worker_queues.sql`
29
- - `20260226_0015_sticker_auto_pack_curation_integrity.sql`
30
- - `20260226_0016_sticker_web_google_auth_persistence.sql`
31
- - `20260226_0017_sticker_web_admin_ban.sql`
32
- - `20260226_0018_sticker_web_admin_moderator.sql`
33
- - `20260227_0019_sticker_classification_v2_signals.sql`
34
- - `20260227_0020_semantic_theme_clusters.sql`
35
-
36
- ## Commits incluídos (v2.1.2..HEAD)
37
-
38
- - `b46a33f` Improve background scheduling, add cached stats and UI/status enhancements
39
- - `c43decb` Enable semantic clustering and improve auto-pack scheduler
40
- - `91f2950` Enhance socket reconnection and prioritize complete sticker packs
41
- - `64f8e73` Improve classification and auto-pack optimization
42
- - `d11d78b` Appends environment-based suffix to DB name
43
- - `b8ede5f` Add advanced auto-pack optimization and CLIP classification pipeline
44
- - `32e169f` Add SEO, sitemap and NSFW gating to sticker catalog
45
- - `7271a9b` Adds web links for sticker packs and sets auto-packs to unlisted
46
- - `7cd1151` Ignore transient rejections and force process exit
47
- - `5072a0e` Add admin moderator role and revamp admin panel UI
48
- - `84c08dd` Add admin panel and ban management
49
- - `27b387b` Persist Google web sessions and add client-side Google auth cache
50
- - `6c07feb` Improve mobile category-chip touch and scroll behavior
51
- - `c3ca0ea` Improve auto-pack-by-tags curation and pack metadata
52
- - `3f6c258` Fix mobile discover tabs behavior
53
- - `831c279` Adds marketplace global stats API and panel
54
- - `08fdafe` Add catalog sorting, creators ranking and pack UI improvements
55
- - `082ba82` Add safe pack management, orphan asset cleanup, and UX/network robustness
56
- - `d928227` Improve marketplace discovery UX
57
- - `97fe5f4` Bumps stickers app asset version
58
- - `d2d1843` Adds sticker upload API and improves creator dashboard UX
59
- - `be5506b` Adds creator pack management endpoints and dashboard UI
60
- - `589f671` Add creator profile with Google sign-in
61
- - `122c1e8` Switches CLIP classifier to MobileCLIP/OpenCLIP
62
- - `7385d30` Allow spaces in pack names and add tag typeahead
63
- - `2c5589c` Stops trimming input on field change
64
- - `067b6f9` Add web pack publish flow with Google auth and idempotent uploads
65
- - `6953768` Allow image/video uploads with WebP conversion
66
- - `b33c1b8` Add web pack creation and sticker upload UI/API
67
- - `2b23441` Add reprocess queue, cohesion scoring and ranking
68
- - `6342d2b` Add marketplace stats endpoint and homepage preview
69
- - `55cf8b7` Batch-scans and deduplicates catalog listings
70
- - `7819fe6` Adds automatic sticker pack curation by tags
71
- - `8ed61bc` Improve API docs and dynamic sidebar/UI for stickers
72
- - `d0fd86d` Adds support contact API and enhances sticker catalog UI
73
- - `c5cb106` Improves pack cards and mobile header UX
74
- - `3da0375` Adds categories filtering and pack engagement tracking
75
- - `d9bb604` Adds CLIP-based sticker classification
76
- - `fffc205` Enhances catalog and API docs UI
77
-
78
- ## Notas de atualização
79
-
80
- - `package.json` atualizado para `2.1.3`.
81
- - `README.md` atualizado para refletir a versão atual.
82
- - Recomendado reiniciar o processo de produção após deploy (`pm2 restart ... --update-env`).
83
- - Atenção para comportamento de nome de banco por ambiente (`DB_NAME` com sufixo `_dev`/`_prod` quando aplicável).