theus-cli-linux-x64 1.2.0 → 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.
- package/README.md +16 -1
- package/RELEASE.json +6 -6
- package/bin/theus +0 -0
- package/package.json +1 -1
- package/vendor/chrome-extension/README.md +142 -0
- package/vendor/chrome-extension/background.js +540 -0
- package/vendor/chrome-extension/cdp-core.js +400 -0
- package/vendor/chrome-extension/cdp-core.test.ts +321 -0
- package/vendor/chrome-extension/cdp.js +672 -0
- package/vendor/chrome-extension/icons/icon128.png +0 -0
- package/vendor/chrome-extension/icons/icon16.png +0 -0
- package/vendor/chrome-extension/icons/icon32.png +0 -0
- package/vendor/chrome-extension/icons/icon48.png +0 -0
- package/vendor/chrome-extension/manifest.json +36 -0
- package/vendor/chrome-extension/popup.html +78 -0
- package/vendor/chrome-extension/popup.js +47 -0
- package/vendor/chrome-extension/recorder.js +316 -0
- package/vendor/theus-arsenal/.theus-plugin/plugin.json +1 -1
- package/vendor/theus-arsenal/README.md +1 -1
|
@@ -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
|
+
})()
|