theus-cli-linux-x64 1.2.1 → 1.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.
@@ -0,0 +1,400 @@
1
+ // Theus en Chrome — lógica PURA de la fase 2 (sin chrome.*, sin CDP en vivo).
2
+ //
3
+ // Este archivo se carga en el service worker con importScripts('cdp-core.js')
4
+ // y también desde los tests del CLI (chrome-extension/cdp-core.test.ts) vía
5
+ // import con efecto lateral: todo lo público cuelga de
6
+ // globalThis.TheusCdpCore. Mantenerlo libre de APIs de extensión.
7
+
8
+ ;(() => {
9
+ // -------------------------------------------------------------------------
10
+ // Buffers circulares
11
+ // -------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Añade `item` a `buffer` respetando el tope `max`. Si desborda, expulsa el
15
+ * más antiguo y lo devuelve (para que el llamador limpie índices auxiliares).
16
+ */
17
+ function pushCircular(buffer, item, max) {
18
+ buffer.push(item)
19
+ if (buffer.length > max) {
20
+ return buffer.shift()
21
+ }
22
+ return null
23
+ }
24
+
25
+ // -------------------------------------------------------------------------
26
+ // Consola: eventos CDP → entradas homogéneas {ts, nivel, texto, url?}
27
+ // -------------------------------------------------------------------------
28
+
29
+ function describirRemoteObject(obj) {
30
+ if (!obj || typeof obj !== 'object') return String(obj)
31
+ if (obj.value !== undefined) {
32
+ if (typeof obj.value === 'string') return obj.value
33
+ try {
34
+ return JSON.stringify(obj.value)
35
+ } catch (_e) {
36
+ return String(obj.value)
37
+ }
38
+ }
39
+ if (typeof obj.description === 'string') return obj.description
40
+ return obj.type || 'undefined'
41
+ }
42
+
43
+ /**
44
+ * Convierte un evento CDP de consola en una entrada del buffer, o null si el
45
+ * evento no es de consola. Eventos soportados: Runtime.consoleAPICalled,
46
+ * Runtime.exceptionThrown y Log.entryAdded.
47
+ */
48
+ function formatearEventoConsola(method, params, ahoraMs) {
49
+ const ts = typeof ahoraMs === 'number' ? ahoraMs : Date.now()
50
+ if (method === 'Runtime.consoleAPICalled') {
51
+ const args = Array.isArray(params.args) ? params.args : []
52
+ return {
53
+ ts,
54
+ nivel: params.type || 'log',
55
+ texto: args.map(describirRemoteObject).join(' '),
56
+ }
57
+ }
58
+ if (method === 'Runtime.exceptionThrown') {
59
+ const det = params.exceptionDetails || {}
60
+ const desc =
61
+ (det.exception && det.exception.description) ||
62
+ (det.exception && det.exception.value) ||
63
+ ''
64
+ const texto = [det.text, desc].filter(Boolean).join(' ')
65
+ const entrada = { ts, nivel: 'error', texto: texto || 'Excepción sin detalle' }
66
+ if (det.url) entrada.url = det.url
67
+ return entrada
68
+ }
69
+ if (method === 'Log.entryAdded') {
70
+ const e = params.entry || {}
71
+ const entrada = { ts, nivel: e.level || 'info', texto: e.text || '' }
72
+ if (e.url) entrada.url = e.url
73
+ return entrada
74
+ }
75
+ return null
76
+ }
77
+
78
+ const NIVELES_ERROR = new Set(['error', 'assert'])
79
+
80
+ // -------------------------------------------------------------------------
81
+ // Red: pliega Network.* en entradas {id, metodo, url, tipo, estado, tam, …}
82
+ // -------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Estado de red por pestaña: { entradas: [], porId: Map }. `entradas` es el
86
+ * buffer circular (orden de llegada); `porId` indexa por requestId para
87
+ * completar estado/tamaño/duración cuando llegan los eventos posteriores.
88
+ */
89
+ function crearEstadoRed() {
90
+ return { entradas: [], porId: new Map() }
91
+ }
92
+
93
+ /** Devuelve true si el evento era de red y fue plegado. */
94
+ function plegarEventoRed(estado, method, params, max, ahoraMs) {
95
+ const ts = typeof ahoraMs === 'number' ? ahoraMs : Date.now()
96
+ if (method === 'Network.requestWillBeSent') {
97
+ const entrada = {
98
+ id: params.requestId,
99
+ metodo: (params.request && params.request.method) || 'GET',
100
+ url: (params.request && params.request.url) || '',
101
+ tipo: params.type || 'Other',
102
+ estado: null,
103
+ mime: null,
104
+ tam: null,
105
+ inicioMs: ts,
106
+ duracionMs: null,
107
+ error: null,
108
+ }
109
+ estado.porId.set(params.requestId, entrada)
110
+ const expulsada = pushCircular(estado.entradas, entrada, max)
111
+ if (expulsada) estado.porId.delete(expulsada.id)
112
+ return true
113
+ }
114
+ const entrada = estado.porId.get(params && params.requestId)
115
+ if (!entrada) return method.startsWith('Network.')
116
+ if (method === 'Network.responseReceived') {
117
+ entrada.estado = params.response ? params.response.status : null
118
+ entrada.mime = params.response ? params.response.mimeType || null : null
119
+ return true
120
+ }
121
+ if (method === 'Network.loadingFinished') {
122
+ if (typeof params.encodedDataLength === 'number') {
123
+ entrada.tam = params.encodedDataLength
124
+ }
125
+ entrada.duracionMs = ts - entrada.inicioMs
126
+ return true
127
+ }
128
+ if (method === 'Network.loadingFailed') {
129
+ entrada.error = params.errorText || 'fallo de carga'
130
+ entrada.duracionMs = ts - entrada.inicioMs
131
+ return true
132
+ }
133
+ return method.startsWith('Network.')
134
+ }
135
+
136
+ // -------------------------------------------------------------------------
137
+ // Filtros de lectura (consola y red)
138
+ // -------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Filtra entradas por regex (`pattern` sobre `campoTexto`), opcionalmente
142
+ * solo errores (consola), y corta a las `limit` MÁS RECIENTES. Un pattern
143
+ * inválido lanza Error con mensaje honesto.
144
+ */
145
+ function filtrarEntradas(entradas, opciones) {
146
+ const { pattern, limit, onlyErrors, campoTexto } = opciones
147
+ let re = null
148
+ if (typeof pattern === 'string' && pattern.length > 0) {
149
+ try {
150
+ re = new RegExp(pattern)
151
+ } catch (e) {
152
+ throw new Error(`El pattern no es una regex válida: ${e.message}`)
153
+ }
154
+ }
155
+ let resultado = entradas
156
+ if (onlyErrors) {
157
+ resultado = resultado.filter(e => NIVELES_ERROR.has(e.nivel))
158
+ }
159
+ if (re) {
160
+ resultado = resultado.filter(e => re.test(String(e[campoTexto] ?? '')))
161
+ }
162
+ const tope = typeof limit === 'number' && limit > 0 ? Math.floor(limit) : 100
163
+ return resultado.slice(-tope)
164
+ }
165
+
166
+ // -------------------------------------------------------------------------
167
+ // Teclado: "ctrl+shift+t" → parámetros de Input.dispatchKeyEvent
168
+ // -------------------------------------------------------------------------
169
+
170
+ const MODIFICADORES = {
171
+ alt: 1,
172
+ option: 1,
173
+ ctrl: 2,
174
+ control: 2,
175
+ meta: 4,
176
+ cmd: 4,
177
+ command: 4,
178
+ win: 4,
179
+ shift: 8,
180
+ }
181
+
182
+ const TECLAS_ESPECIALES = {
183
+ enter: { key: 'Enter', code: 'Enter', keyCode: 13, text: '\r' },
184
+ return: { key: 'Enter', code: 'Enter', keyCode: 13, text: '\r' },
185
+ tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
186
+ escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
187
+ esc: { key: 'Escape', code: 'Escape', keyCode: 27 },
188
+ backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
189
+ delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
190
+ space: { key: ' ', code: 'Space', keyCode: 32, text: ' ' },
191
+ up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
192
+ arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
193
+ down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
194
+ arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
195
+ left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
196
+ arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
197
+ right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
198
+ arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
199
+ home: { key: 'Home', code: 'Home', keyCode: 36 },
200
+ end: { key: 'End', code: 'End', keyCode: 35 },
201
+ pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
202
+ pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
203
+ insert: { key: 'Insert', code: 'Insert', keyCode: 45 },
204
+ }
205
+ for (let i = 1; i <= 12; i++) {
206
+ TECLAS_ESPECIALES[`f${i}`] = { key: `F${i}`, code: `F${i}`, keyCode: 111 + i }
207
+ }
208
+
209
+ /**
210
+ * Parsea una combinación tipo "ctrl+a", "Enter", "ctrl+shift+t".
211
+ * Devuelve { modifiers, key, code, keyCode, text? } o null si la tecla base
212
+ * no se reconoce. Con Ctrl/Alt/Meta activos no se emite `text` (no imprime).
213
+ */
214
+ function parsearCombinacionTeclas(combo) {
215
+ if (typeof combo !== 'string' || combo.trim().length === 0) return null
216
+ const partes = combo
217
+ .trim()
218
+ .split('+')
219
+ .map(p => p.trim())
220
+ .filter(p => p.length > 0)
221
+ if (partes.length === 0) return null
222
+ let modifiers = 0
223
+ let base = null
224
+ for (const parte of partes) {
225
+ const mod = MODIFICADORES[parte.toLowerCase()]
226
+ if (mod !== undefined && parte !== partes[partes.length - 1]) {
227
+ modifiers |= mod
228
+ continue
229
+ }
230
+ // Última parte (o parte no-modificador): la tecla base.
231
+ if (base !== null) return null
232
+ base = parte
233
+ }
234
+ if (base === null) return null
235
+ const especial = TECLAS_ESPECIALES[base.toLowerCase()]
236
+ if (especial) {
237
+ const def = { modifiers, ...especial }
238
+ if ((modifiers & (1 | 2 | 4)) !== 0) delete def.text
239
+ return def
240
+ }
241
+ if (base.length === 1) {
242
+ const ch = base
243
+ const mayus = (modifiers & 8) !== 0 ? ch.toUpperCase() : ch
244
+ const esLetra = /^[a-zA-Z]$/.test(ch)
245
+ const esDigito = /^[0-9]$/.test(ch)
246
+ const def = {
247
+ modifiers,
248
+ key: mayus,
249
+ code: esLetra
250
+ ? `Key${ch.toUpperCase()}`
251
+ : esDigito
252
+ ? `Digit${ch}`
253
+ : '',
254
+ keyCode: esLetra
255
+ ? ch.toUpperCase().charCodeAt(0)
256
+ : esDigito
257
+ ? ch.charCodeAt(0)
258
+ : mayus.charCodeAt(0),
259
+ }
260
+ if ((modifiers & (1 | 2 | 4)) === 0) def.text = mayus
261
+ return def
262
+ }
263
+ return null
264
+ }
265
+
266
+ // -------------------------------------------------------------------------
267
+ // Árbol de accesibilidad → resumen con refs
268
+ // -------------------------------------------------------------------------
269
+
270
+ const ROLES_INTERACTIVOS = new Set([
271
+ 'button',
272
+ 'link',
273
+ 'textbox',
274
+ 'searchbox',
275
+ 'checkbox',
276
+ 'radio',
277
+ 'combobox',
278
+ 'listbox',
279
+ 'option',
280
+ 'menuitem',
281
+ 'menuitemcheckbox',
282
+ 'menuitemradio',
283
+ 'slider',
284
+ 'spinbutton',
285
+ 'switch',
286
+ 'tab',
287
+ 'textfield',
288
+ 'ToggleButton',
289
+ ])
290
+
291
+ const ROLES_ESTRUCTURA = new Set([
292
+ 'heading',
293
+ 'StaticText',
294
+ 'image',
295
+ 'img',
296
+ 'list',
297
+ 'listitem',
298
+ 'table',
299
+ 'row',
300
+ 'cell',
301
+ 'form',
302
+ 'navigation',
303
+ 'main',
304
+ 'banner',
305
+ 'dialog',
306
+ 'alert',
307
+ 'region',
308
+ 'article',
309
+ ])
310
+
311
+ function valorAX(campo) {
312
+ return campo && campo.value !== undefined && campo.value !== null
313
+ ? String(campo.value)
314
+ : ''
315
+ }
316
+
317
+ /**
318
+ * Resume los nodos de Accessibility.getFullAXTree: filtra por rol
319
+ * ('interactive' por defecto, 'all' añade estructura), asigna refs numéricos
320
+ * estables dentro de la lectura y trunca honestamente en `maxElementos`.
321
+ *
322
+ * Devuelve { elementos: [{ref, backendDOMNodeId, rol, nombre, valor?}],
323
+ * truncado, totalCandidatos }.
324
+ */
325
+ function resumirArbolAX(nodos, filtro, maxElementos) {
326
+ const modo = filtro === 'all' ? 'all' : 'interactive'
327
+ const max = typeof maxElementos === 'number' && maxElementos > 0 ? maxElementos : 150
328
+ const candidatos = []
329
+ for (const nodo of nodos || []) {
330
+ if (!nodo || nodo.ignored) continue
331
+ const rol = valorAX(nodo.role)
332
+ if (!rol) continue
333
+ const interesa =
334
+ ROLES_INTERACTIVOS.has(rol) || (modo === 'all' && ROLES_ESTRUCTURA.has(rol))
335
+ if (!interesa) continue
336
+ if (typeof nodo.backendDOMNodeId !== 'number') continue
337
+ const nombre = valorAX(nodo.name)
338
+ if (modo === 'all' && rol === 'StaticText' && nombre.trim().length === 0) continue
339
+ candidatos.push({
340
+ backendDOMNodeId: nodo.backendDOMNodeId,
341
+ rol,
342
+ nombre: nombre.slice(0, 160),
343
+ valor: valorAX(nodo.value).slice(0, 160),
344
+ })
345
+ }
346
+ const truncado = candidatos.length > max
347
+ const elementos = candidatos.slice(0, max).map((c, i) => {
348
+ const el = {
349
+ ref: String(i + 1),
350
+ backendDOMNodeId: c.backendDOMNodeId,
351
+ rol: c.rol,
352
+ nombre: c.nombre,
353
+ }
354
+ if (c.valor) el.valor = c.valor
355
+ return el
356
+ })
357
+ return { elementos, truncado, totalCandidatos: candidatos.length }
358
+ }
359
+
360
+ // -------------------------------------------------------------------------
361
+ // Capturas: decisión de reescalado/recompresión (la ejecución con canvas
362
+ // vive en cdp.js; aquí solo la aritmética, para poder testearla)
363
+ // -------------------------------------------------------------------------
364
+
365
+ /**
366
+ * Decide si una captura debe reprocesarse y con qué parámetros.
367
+ * base64Length: longitud de la cadena base64; anchoPx: ancho real del bitmap.
368
+ */
369
+ function planRecompresionCaptura(base64Length, anchoPx, opciones) {
370
+ const maxAncho = (opciones && opciones.maxAncho) || 1600
371
+ const maxBase64 = (opciones && opciones.maxBase64) || 12 * 1024 * 1024
372
+ const excedeAncho = anchoPx > maxAncho
373
+ const excedeTam = base64Length > maxBase64
374
+ if (!excedeAncho && !excedeTam) return { reprocesar: false }
375
+ const escala = excedeAncho ? maxAncho / anchoPx : 1
376
+ return {
377
+ reprocesar: true,
378
+ escala,
379
+ calidad: excedeTam ? 0.6 : 0.8,
380
+ }
381
+ }
382
+
383
+ function horaLocal(ms) {
384
+ const d = new Date(ms)
385
+ const dos = n => String(n).padStart(2, '0')
386
+ return `${dos(d.getHours())}:${dos(d.getMinutes())}:${dos(d.getSeconds())}`
387
+ }
388
+
389
+ globalThis.TheusCdpCore = {
390
+ pushCircular,
391
+ formatearEventoConsola,
392
+ crearEstadoRed,
393
+ plegarEventoRed,
394
+ filtrarEntradas,
395
+ parsearCombinacionTeclas,
396
+ resumirArbolAX,
397
+ planRecompresionCaptura,
398
+ horaLocal,
399
+ }
400
+ })()