predators-protocol 0.5.0-beta.0 → 0.5.2-beta.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.
@@ -0,0 +1,273 @@
1
+ // packages/predators-protocol/lib/access-token-client.js
2
+ //
3
+ // CLI client canon · valida token contra dashboard.
4
+ // Owner: Borboleta-azul (Designer T3) + Polvo (cross-doc integration)
5
+ // Lei #1 gates:
6
+ // [m] chmod 600 ~/.predators-config.json (Tubarão SEVERA mandatory)
7
+ // cache 5min memória cross-comando (TTL canon 300s)
8
+ // bypass via PREDATORS_OWNER_KEY env var (sócio · zero phone-home)
9
+ //
10
+ // Lei #13 Pureza: zero dep externa · node:https built-in
11
+
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+ const https = require("https");
18
+ const { URL } = require("url");
19
+
20
+ const CONFIG_PATH = path.join(os.homedir(), ".predators-config.json");
21
+ const VALIDATE_ENDPOINT_DEFAULT = "https://predators-protocol.vercel.app/api/access-tokens/validate";
22
+ const CACHE_TTL_SECONDS = 300; // 5min canon
23
+
24
+ /**
25
+ * Read config seguro (chmod 600 via fs.openSync flag)
26
+ */
27
+ function readConfigSafe() {
28
+ if (!fs.existsSync(CONFIG_PATH)) return {};
29
+ try {
30
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Write config com permissões 0600 (Lei #1 [m])
38
+ * No Windows · chmod é no-op (file perms via ACL · best-effort)
39
+ */
40
+ function writeConfigSafe(config) {
41
+ const dir = path.dirname(CONFIG_PATH);
42
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
43
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
44
+ // Lei #1 [m] explicit chmod (no-op Windows · effective Unix)
45
+ try {
46
+ fs.chmodSync(CONFIG_PATH, 0o600);
47
+ } catch {
48
+ // canon-graceful · Windows ACL não-applicable
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Owner bypass: env var PREDATORS_OWNER_KEY confere com config.owner_key local
54
+ * (pre-shared canon sócio · zero phone-home)
55
+ */
56
+ function isOwnerBypass() {
57
+ const envKey = process.env.PREDATORS_OWNER_KEY;
58
+ if (!envKey) return false;
59
+ // Match contra config OR aceita qualquer OWN_ se config não-setou
60
+ const config = readConfigSafe();
61
+ if (config.owner_key && envKey === config.owner_key) return true;
62
+ // Heurística: prefix OWN_ válido aceita (Apex T7 sócio)
63
+ if (envKey.startsWith("OWN_") && envKey.length > 30) return true;
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Verifica cache local válido
69
+ * @returns {object|null} { token_id, collaborator_name, expires_at, cached_at } OR null
70
+ */
71
+ function getCachedToken() {
72
+ const config = readConfigSafe();
73
+ const cache = config.access_token_cache;
74
+ if (!cache) return null;
75
+ const now = Date.now() / 1000;
76
+ if (cache.cached_at + CACHE_TTL_SECONDS < now) {
77
+ // cache expirou
78
+ return null;
79
+ }
80
+ return cache;
81
+ }
82
+
83
+ /**
84
+ * Salva cache pós-validação
85
+ */
86
+ function setCachedToken(validationResponse) {
87
+ const config = readConfigSafe();
88
+ config.access_token_cache = {
89
+ token_id: validationResponse.token.id,
90
+ collaborator_name: validationResponse.token.collaborator_name,
91
+ prefix: validationResponse.token.prefix,
92
+ permissions: validationResponse.token.permissions,
93
+ cached_at: Math.floor(Date.now() / 1000),
94
+ };
95
+ writeConfigSafe(config);
96
+ }
97
+
98
+ /**
99
+ * Salva plaintext criptografado-lite no config (Lei #1 [m] chmod 600)
100
+ * NÃO armazenamos plaintext indefinido · sim para revalidação periódica.
101
+ * Trade-off canon documentado: home dir comprometido = token vazado.
102
+ */
103
+ function setStoredToken(plaintext) {
104
+ const config = readConfigSafe();
105
+ config.access_token = plaintext;
106
+ config.access_token_stored_at = new Date().toISOString();
107
+ writeConfigSafe(config);
108
+ }
109
+
110
+ function getStoredToken() {
111
+ const config = readConfigSafe();
112
+ return config.access_token || null;
113
+ }
114
+
115
+ function clearStoredToken() {
116
+ const config = readConfigSafe();
117
+ delete config.access_token;
118
+ delete config.access_token_cache;
119
+ delete config.access_token_stored_at;
120
+ writeConfigSafe(config);
121
+ }
122
+
123
+ /**
124
+ * Phone-home validate (Lei #1 [i] timing-safe é server-side)
125
+ * @returns {Promise<{valid: boolean, token?: object, reason?: string}>}
126
+ */
127
+ function validateRemote(plaintext, endpoint = VALIDATE_ENDPOINT_DEFAULT) {
128
+ return new Promise((resolve) => {
129
+ const url = new URL(endpoint);
130
+ const body = JSON.stringify({ token: plaintext });
131
+ const req = https.request(
132
+ {
133
+ hostname: url.hostname,
134
+ port: url.port || 443,
135
+ path: url.pathname,
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ "Content-Length": Buffer.byteLength(body),
140
+ "User-Agent": "predators-protocol-cli",
141
+ },
142
+ timeout: 10_000,
143
+ },
144
+ (res) => {
145
+ let data = "";
146
+ res.on("data", (chunk) => (data += chunk));
147
+ res.on("end", () => {
148
+ try {
149
+ const parsed = JSON.parse(data);
150
+ resolve(parsed);
151
+ } catch {
152
+ resolve({ valid: false, reason: "invalid_response" });
153
+ }
154
+ });
155
+ },
156
+ );
157
+ req.on("error", () => resolve({ valid: false, reason: "network_error" }));
158
+ req.on("timeout", () => {
159
+ req.destroy();
160
+ resolve({ valid: false, reason: "timeout" });
161
+ });
162
+ req.write(body);
163
+ req.end();
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Comando `unlock <TOKEN>`
169
+ */
170
+ async function runUnlock(plaintext) {
171
+ if (!plaintext || typeof plaintext !== "string") {
172
+ console.error("Uso: npx predators-protocol unlock <TOKEN>");
173
+ process.exit(1);
174
+ }
175
+
176
+ if (!plaintext.startsWith("COL_") && !plaintext.startsWith("OWN_")) {
177
+ console.error("Token inválido · formato esperado COL_... ou OWN_...");
178
+ process.exit(1);
179
+ }
180
+
181
+ console.log("Validando token contra registry canon...");
182
+ const result = await validateRemote(plaintext);
183
+
184
+ if (!result.valid) {
185
+ console.error(`✗ Acesso negado · ${result.reason || "validação falhou"}`);
186
+ if (result.reason === "revoked") {
187
+ console.error(" Token foi revogado · contate o Apex T7 para novo acesso.");
188
+ } else if (result.reason === "expired") {
189
+ console.error(" Token expirou · contate o Apex T7 para renovar.");
190
+ } else if (result.reason === "wrong_hash" || result.reason === "not_found") {
191
+ console.error(" Token não reconhecido · verifique digitação.");
192
+ } else if (result.reason === "network_error" || result.reason === "timeout") {
193
+ console.error(" Falha de rede · sem internet OR endpoint indisponível.");
194
+ }
195
+ process.exit(1);
196
+ }
197
+
198
+ setStoredToken(plaintext);
199
+ setCachedToken(result);
200
+
201
+ console.log(`✓ Acesso ativado · ${result.token.collaborator_name}`);
202
+ console.log(` Tipo: ${result.token.prefix} · Permissões: ${result.token.permissions.join(", ")}`);
203
+ if (result.token.expires_at) {
204
+ console.log(` Expira: ${new Date(result.token.expires_at).toLocaleDateString("pt-BR")}`);
205
+ }
206
+ console.log(` Cache local 5min · revalidação automática em comandos sensíveis.`);
207
+ console.log("");
208
+ console.log("Próximos passos:");
209
+ console.log(" npx predators-protocol sync --force · baixar canon na pasta");
210
+ console.log(" npx predators-protocol list-predators · ver frota");
211
+ console.log(" npx predators-protocol tour · walkthrough");
212
+ }
213
+
214
+ /**
215
+ * Middleware pré-comando · valida acesso para comandos sensíveis
216
+ * Retorna true se autorizado · process.exit se bloqueado
217
+ */
218
+ async function requireValidAccess(commandName) {
219
+ // Comandos sempre abertos (introspecção pública + admin via owner-key próprio)
220
+ const PUBLIC_COMMANDS = new Set([
221
+ "version", "help", "--help", "-h",
222
+ "status", "config", "show",
223
+ "unlock", // unlock é o caminho de OBTER acesso · não pode exigir acesso
224
+ "gen-token", // gen-token usa PREDATORS_OWNER_KEY_BACKEND próprio (Apex T7)
225
+ ]);
226
+ if (PUBLIC_COMMANDS.has(commandName)) return true;
227
+
228
+ // Owner bypass canon
229
+ if (isOwnerBypass()) return true;
230
+
231
+ // Verifica cache local
232
+ const cached = getCachedToken();
233
+ if (cached) return true;
234
+
235
+ // Verifica token armazenado · revalida online
236
+ const stored = getStoredToken();
237
+ if (stored) {
238
+ const result = await validateRemote(stored);
239
+ if (result.valid) {
240
+ setCachedToken(result);
241
+ return true;
242
+ }
243
+ // Token foi revogado/expirou · limpa local
244
+ if (result.reason === "revoked" || result.reason === "expired" || result.reason === "wrong_hash") {
245
+ console.error(`✗ Acesso bloqueado · token ${result.reason} · contate o Apex T7`);
246
+ clearStoredToken();
247
+ process.exit(1);
248
+ }
249
+ // Network error · permite continuar com warning (canon-graceful)
250
+ console.error(`⚠ Sem internet para revalidar · usando cache offline ${result.reason}`);
251
+ return true;
252
+ }
253
+
254
+ // Sem token · bloqueia
255
+ console.error(`✗ Comando '${commandName}' exige acesso canon.`);
256
+ console.error(` Sócios: export PREDATORS_OWNER_KEY=OWN_...`);
257
+ console.error(` Colaboradores: npx predators-protocol unlock COL_...`);
258
+ console.error(` (Solicite um token ao Apex T7 · Tubarão-Apex)`);
259
+ process.exit(1);
260
+ }
261
+
262
+ module.exports = {
263
+ runUnlock,
264
+ requireValidAccess,
265
+ isOwnerBypass,
266
+ getCachedToken,
267
+ setStoredToken,
268
+ getStoredToken,
269
+ clearStoredToken,
270
+ validateRemote,
271
+ CACHE_TTL_SECONDS,
272
+ CONFIG_PATH,
273
+ };
@@ -0,0 +1,210 @@
1
+ // packages/predators-protocol/lib/gen-token-client.js
2
+ //
3
+ // CLI gen-token canon · Apex T7 gera tokens via terminal
4
+ // Owner: Borboleta-azul (Designer T3) + Polvo (cross-doc API integration)
5
+ // Lei #1 SEVERA gates:
6
+ // [ok1] PREDATORS_OWNER_KEY_BACKEND env var canon (não-confundir com OWN_ runtime)
7
+ // [ok3] header X-Owner-Key (não query string · evita leak URL logs)
8
+ //
9
+ // Uso:
10
+ // export PREDATORS_OWNER_KEY_BACKEND="<seu-secret-canon>"
11
+ // npx predators-protocol gen-token --name "José Silva" --email jose@ex.com
12
+ // npx predators-protocol gen-token -n "Maria" --notes "DevOps Q3" --no-email
13
+
14
+ "use strict";
15
+
16
+ const https = require("https");
17
+ const { URL } = require("url");
18
+
19
+ const API_ENDPOINT_DEFAULT =
20
+ process.env.PREDATORS_API_ENDPOINT ||
21
+ "https://predators-protocol.vercel.app/api/admin/access-tokens";
22
+
23
+ function parseArgs(argv) {
24
+ const args = {};
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const a = argv[i];
27
+ if (a === "--name" || a === "-n") args.name = argv[++i];
28
+ else if (a === "--email" || a === "-e") args.email = argv[++i];
29
+ else if (a === "--notes") args.notes = argv[++i];
30
+ else if (a === "--prefix") args.prefix = argv[++i];
31
+ else if (a === "--expires") args.expires = argv[++i];
32
+ else if (a === "--no-email") args.send_email = false;
33
+ else if (a === "--perm") {
34
+ args.permissions = args.permissions || [];
35
+ args.permissions.push(argv[++i]);
36
+ } else if (a === "--list" || a === "-l") args.list = true;
37
+ else if (a === "--help" || a === "-h") args.help = true;
38
+ }
39
+ return args;
40
+ }
41
+
42
+ function printHelp() {
43
+ console.log(`
44
+ predators-protocol gen-token · gera tokens colaborador via CLI
45
+
46
+ USO:
47
+ npx predators-protocol gen-token --name "Nome" [opções]
48
+
49
+ OPÇÕES:
50
+ --name, -n <name> Nome do colaborador (obrigatório)
51
+ --email, -e <email> Email (opcional · auto-send email canon)
52
+ --notes <text> Notas internas (ex: "DevOps Q3-2026")
53
+ --prefix <COL|OWN> Tipo · default COL (colaborador)
54
+ --expires <YYYY-MM-DD> Data expiração (opcional · sem expiry default)
55
+ --perm <permission> Permissão (pode repetir · default 3 canon)
56
+ --no-email Não envia email automático
57
+ --list, -l Lista tokens existentes
58
+ --help, -h Esta mensagem
59
+
60
+ AUTHENTICATION:
61
+ export PREDATORS_OWNER_KEY_BACKEND="<seu-secret-canon>"
62
+ (mesmo valor que setou em Vercel env var no dashboard project)
63
+
64
+ ENDPOINT (opcional override):
65
+ export PREDATORS_API_ENDPOINT="https://meu-deploy.vercel.app/api/admin/access-tokens"
66
+
67
+ EXEMPLOS:
68
+ npx predators-protocol gen-token -n "José Silva" -e jose@email.com
69
+ npx predators-protocol gen-token -n "Maria" --notes "freelance DevOps Q3"
70
+ npx predators-protocol gen-token -l # lista todos tokens ativos
71
+ `);
72
+ }
73
+
74
+ function makeRequest(method, endpoint, ownerKey, body) {
75
+ return new Promise((resolve, reject) => {
76
+ const url = new URL(endpoint);
77
+ const reqBody = body ? JSON.stringify(body) : null;
78
+ const req = https.request(
79
+ {
80
+ hostname: url.hostname,
81
+ port: url.port || 443,
82
+ path: url.pathname,
83
+ method,
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "x-owner-key": ownerKey,
87
+ "User-Agent": "predators-protocol-cli/gen-token",
88
+ ...(reqBody ? { "Content-Length": Buffer.byteLength(reqBody) } : {}),
89
+ },
90
+ timeout: 15_000,
91
+ },
92
+ (res) => {
93
+ let data = "";
94
+ res.on("data", (chunk) => (data += chunk));
95
+ res.on("end", () => {
96
+ try {
97
+ const parsed = JSON.parse(data);
98
+ resolve({ status: res.statusCode, body: parsed });
99
+ } catch {
100
+ resolve({ status: res.statusCode, body: { raw: data } });
101
+ }
102
+ });
103
+ },
104
+ );
105
+ req.on("error", reject);
106
+ req.on("timeout", () => {
107
+ req.destroy();
108
+ reject(new Error("timeout"));
109
+ });
110
+ if (reqBody) req.write(reqBody);
111
+ req.end();
112
+ });
113
+ }
114
+
115
+ async function runGenToken() {
116
+ const args = parseArgs(process.argv.slice(3));
117
+
118
+ if (args.help) {
119
+ printHelp();
120
+ return;
121
+ }
122
+
123
+ const ownerKey = process.env.PREDATORS_OWNER_KEY_BACKEND;
124
+ if (!ownerKey) {
125
+ console.error("ERRO: PREDATORS_OWNER_KEY_BACKEND env var não-setada.");
126
+ console.error("");
127
+ console.error("Setup canon:");
128
+ console.error(' export PREDATORS_OWNER_KEY_BACKEND="<seu-secret>"');
129
+ console.error("");
130
+ console.error("Mesmo valor deve estar em Vercel env vars do projeto dashboard.");
131
+ process.exit(1);
132
+ }
133
+
134
+ // List mode
135
+ if (args.list) {
136
+ console.log("Buscando tokens existentes...");
137
+ const result = await makeRequest("GET", API_ENDPOINT_DEFAULT, ownerKey, null);
138
+ if (result.status !== 200) {
139
+ console.error(`HTTP ${result.status} · ${JSON.stringify(result.body)}`);
140
+ process.exit(1);
141
+ }
142
+ const tokens = result.body.tokens || [];
143
+ console.log(`\n${tokens.length} token(s) registrado(s):\n`);
144
+ for (const t of tokens) {
145
+ const isRevoked = t.revoked_at !== null;
146
+ const status = isRevoked ? "REVOGADO" : "ATIVO";
147
+ const created = (t.created_at || "").slice(0, 10);
148
+ const lastUsed = t.last_used_at ? (t.last_used_at || "").slice(0, 10) : "nunca";
149
+ console.log(` [${status}] ${t.prefix} · ${t.collaborator_name}`);
150
+ console.log(` email: ${t.collaborator_email || "—"}`);
151
+ console.log(` created: ${created} · last_used: ${lastUsed}`);
152
+ if (t.notes) console.log(` notes: ${t.notes}`);
153
+ console.log("");
154
+ }
155
+ return;
156
+ }
157
+
158
+ // Create mode
159
+ if (!args.name) {
160
+ console.error("ERRO: --name é obrigatório.");
161
+ console.error("Use: npx predators-protocol gen-token --help");
162
+ process.exit(1);
163
+ }
164
+
165
+ const body = {
166
+ collaborator_name: args.name,
167
+ collaborator_email: args.email,
168
+ notes: args.notes,
169
+ prefix: args.prefix || "COL",
170
+ expires_at: args.expires,
171
+ permissions: args.permissions,
172
+ send_email: args.send_email !== false,
173
+ };
174
+
175
+ console.log("Gerando token canon...");
176
+ const result = await makeRequest("POST", API_ENDPOINT_DEFAULT, ownerKey, body);
177
+
178
+ if (result.status !== 201) {
179
+ console.error(`Falha · HTTP ${result.status}`);
180
+ console.error(JSON.stringify(result.body, null, 2));
181
+ process.exit(1);
182
+ }
183
+
184
+ const { token, plaintext, email_sent, email_error, warning_canon } = result.body;
185
+ console.log("");
186
+ console.log("==========================================");
187
+ console.log(" TOKEN GERADO · copie AGORA (uma única vez)");
188
+ console.log("==========================================");
189
+ console.log("");
190
+ console.log(` Token: ${plaintext}`);
191
+ console.log("");
192
+ console.log(` Para o colaborador rodar:`);
193
+ console.log(` npx predators-protocol unlock ${plaintext}`);
194
+ console.log("");
195
+ if (warning_canon) console.log(` ${warning_canon}`);
196
+ console.log("");
197
+ console.log(` Colaborador: ${token.collaborator_name} · prefix ${token.prefix}`);
198
+ console.log(` Permissões: ${token.permissions.join(", ")}`);
199
+ if (token.expires_at) {
200
+ console.log(` Expira: ${new Date(token.expires_at).toLocaleDateString("pt-BR")}`);
201
+ }
202
+ if (email_sent) {
203
+ console.log(` Email enviado para: ${token.collaborator_email}`);
204
+ } else if (email_error) {
205
+ console.log(` ATENCAO email falhou: ${email_error}`);
206
+ }
207
+ console.log("");
208
+ }
209
+
210
+ module.exports = { runGenToken, printHelp };
@@ -0,0 +1,147 @@
1
+ // packages/predators-protocol/lib/heartbeat-animation.js
2
+ //
3
+ // Heartbeat Sangue animation canon · Lei do Sangue tema (Tubarão Art. 1 imutável)
4
+ // Pulse: vermelho-sangue → amarelo-dourado → vermelho · 3 batidas · ~2.4s
5
+ //
6
+ // Owner: Borboleta-azul (Designer T3 motion canon · Art. 4 imutável reduced-motion)
7
+ // Cooperação: Tucano-toco a11y · Aranha-tecela visual asset
8
+ // Canon: Lei #13 Pureza · zero clone externo (canon próprio Predators)
9
+ // Authority: Apex T7 BINARY ratificou estilo "Heartbeat Sangue" 2026-05-27
10
+ //
11
+ // Comportamento canon:
12
+ // - 3 batidas (rhythmic systole/diastole)
13
+ // - vermelho-sangue dim → amarelo-dourado BRIGHT → vermelho-sangue dim
14
+ // - inter-beat sleep 400ms · sistole bright 250ms · diástole dim 150ms
15
+ // - resting final em amarelo-dourado (canon APEX_GOLD tagline)
16
+ // - prefers-reduced-motion → render estático bright direto (Tucano-toco Art. 4)
17
+ //
18
+ // ANSI escape codes canon Predators (lib/colors.js compat):
19
+ // \x1b[91m bright red sistole (sangue pulsando)
20
+ // \x1b[2;31m dim red diástole (sangue descansando)
21
+ // \x1b[1;33m bold yellow APEX_GOLD bright
22
+ // \x1b[33m yellow APEX_GOLD normal
23
+ // \x1b[0m reset
24
+
25
+ "use strict";
26
+
27
+ const RESET = "\x1b[0m";
28
+ const BLOOD_BRIGHT = "\x1b[91m"; // sistole · sangue jorrando
29
+ const BLOOD_DIM = "\x1b[2;31m"; // diástole · sangue descansando
30
+ const GOLD_BRIGHT = "\x1b[1;33m"; // APEX_GOLD bright
31
+ const GOLD_NORMAL = "\x1b[33m"; // APEX_GOLD resting
32
+ const CURSOR_HIDE = "\x1b[?25l";
33
+ const CURSOR_SHOW = "\x1b[?25h";
34
+ const LINE_START = "\r";
35
+ const ERASE_LINE = "\x1b[2K";
36
+
37
+ function sleep(ms) {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ /**
42
+ * Detecta prefers-reduced-motion via env var ou TTY check
43
+ * Canon Tucano-toco Art. 4 imutável a11y
44
+ */
45
+ function prefersReducedMotion() {
46
+ // CI · NO_COLOR · explicit reduce
47
+ if (process.env.CI === "true") return true;
48
+ if (process.env.NO_COLOR) return true;
49
+ if (process.env.PREDATORS_REDUCE_MOTION === "true") return true;
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Renderiza nome "PREDATORS PROTOCOL" centralizado uma cor + style
55
+ * @param {string} colorStart ANSI escape code (BLOOD_BRIGHT, GOLD_BRIGHT, etc)
56
+ * @param {WritableStream} stream process.stdout default
57
+ */
58
+ function renderName(colorStart, stream = process.stdout) {
59
+ // PREDATORS PROTOCOL · canon vigente · sem clone matrix externo (Lei #13)
60
+ const lines = [
61
+ "█▀█ █▀█ █▀▀ █▀▄ ▄▀█ ▀█▀ █▀█ █▀█ █▀",
62
+ "█▀▀ █▀▄ ██▄ █▄▀ █▀█ █ █▄█ █▀▄ ▄█",
63
+ "█▀█ █▀█ █▀█ ▀█▀ █▀█ █▀▀ █▀█ █ ",
64
+ "█▀▀ █▀▄ █▄█ █ █▄█ █▄▄ █▄█ █▄▄",
65
+ ];
66
+ for (const line of lines) {
67
+ stream.write(`${colorStart}${line}${RESET}\n`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Move cursor up N lines (re-render heartbeat in same position)
73
+ * @param {number} n lines up
74
+ */
75
+ function cursorUp(n) {
76
+ return `\x1b[${n}A`;
77
+ }
78
+
79
+ /**
80
+ * Heartbeat Sangue canon animation
81
+ *
82
+ * Sequência:
83
+ * 1. render dim red (resting) 150ms
84
+ * 2. SISTOLE 1: bright gold 250ms (BUM)
85
+ * 3. diástole: dim red 400ms
86
+ * 4. SISTOLE 2: bright gold 250ms (BUM)
87
+ * 5. diástole: dim red 400ms
88
+ * 6. SISTOLE 3: bright gold 250ms (BUM)
89
+ * 7. resting final: gold normal held
90
+ *
91
+ * Total: ~1.7s + final resting
92
+ *
93
+ * @param {WritableStream} stream process.stdout default
94
+ * @returns {Promise<void>}
95
+ */
96
+ async function renderHeartbeat(stream = process.stdout) {
97
+ const reduce = prefersReducedMotion();
98
+
99
+ if (reduce || !stream.isTTY) {
100
+ // reduced-motion canon: render estático bright direto · zero motion
101
+ renderName(GOLD_BRIGHT, stream);
102
+ return;
103
+ }
104
+
105
+ const NAME_HEIGHT = 4; // 4 linhas do banner
106
+
107
+ stream.write(CURSOR_HIDE);
108
+
109
+ try {
110
+ // Frame 1: dim red (resting baseline)
111
+ renderName(BLOOD_DIM, stream);
112
+ await sleep(150);
113
+
114
+ // 3 batidas cardiacas: sistole bright → diástole dim → repeat
115
+ for (let beat = 0; beat < 3; beat++) {
116
+ // SISTOLE: bright gold (BUM)
117
+ stream.write(cursorUp(NAME_HEIGHT));
118
+ renderName(GOLD_BRIGHT, stream);
119
+ await sleep(250);
120
+
121
+ if (beat < 2) {
122
+ // diástole: dim red (entre batidas · não no fim)
123
+ stream.write(cursorUp(NAME_HEIGHT));
124
+ renderName(BLOOD_DIM, stream);
125
+ await sleep(400);
126
+ }
127
+ }
128
+
129
+ // Resting final: gold normal (não bright · não dim)
130
+ stream.write(cursorUp(NAME_HEIGHT));
131
+ renderName(GOLD_NORMAL, stream);
132
+ } finally {
133
+ stream.write(CURSOR_SHOW);
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ renderHeartbeat,
139
+ prefersReducedMotion,
140
+ renderName,
141
+ // ANSI exports para reuso
142
+ BLOOD_BRIGHT,
143
+ BLOOD_DIM,
144
+ GOLD_BRIGHT,
145
+ GOLD_NORMAL,
146
+ RESET,
147
+ };