primocode 7.0.3 → 7.2.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 +78 -76
- package/bin/primocode.js +270 -223
- package/lib/act.js +126 -116
- package/lib/api.js +110 -17
- package/lib/browser.js +492 -492
- package/lib/config.js +106 -52
- package/lib/desktop.js +142 -142
- package/lib/tools.js +147 -104
- package/lib/ui.js +172 -161
- package/package.json +5 -3
package/lib/config.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// ~/primocode/
|
|
5
|
-
// ~/primocode
|
|
6
|
-
// ~/primocode/<projeto>/
|
|
1
|
+
// Config + estrutura de pastas e memória do PrimoCode.
|
|
2
|
+
//
|
|
3
|
+
// Estrutura:
|
|
4
|
+
// ~/primocode/ <- raiz de trabalho
|
|
5
|
+
// ~/primocode/memory-geral.md <- memória global (usuário, preferências)
|
|
6
|
+
// ~/primocode/<projeto>/ <- uma pasta por projeto
|
|
7
|
+
// ~/primocode/<projeto>/memory.md <- memória daquele projeto
|
|
8
|
+
//
|
|
9
|
+
// Config e histórico ficam fora da raiz de trabalho, em ~/.primocode/.
|
|
7
10
|
|
|
8
11
|
'use strict';
|
|
9
12
|
|
|
@@ -13,94 +16,142 @@ const os = require('os');
|
|
|
13
16
|
|
|
14
17
|
const HOME = os.homedir();
|
|
15
18
|
const ROOT_DIR = path.join(HOME, 'primocode');
|
|
16
|
-
const INTERNAL_DIR = path.join(ROOT_DIR, 'primocode');
|
|
17
19
|
const CONFIG_DIR = path.join(HOME, '.primocode');
|
|
18
20
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
21
|
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.jsonl');
|
|
22
|
+
const GENERAL_MEMORY = path.join(ROOT_DIR, 'memory-geral.md');
|
|
20
23
|
|
|
21
|
-
const
|
|
24
|
+
const GENERAL_MEMORY_TEMPLATE = `# Memória Geral
|
|
25
|
+
|
|
26
|
+
Informações que valem para todos os projetos. O PrimoCode lê este arquivo
|
|
27
|
+
no início de cada sessão e escreve aqui o que descobrir sobre você.
|
|
22
28
|
|
|
23
29
|
## Sobre mim
|
|
24
|
-
|
|
30
|
+
- Nome:
|
|
31
|
+
- Como prefiro que o PrimoCode fale comigo:
|
|
25
32
|
|
|
26
|
-
##
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- Automacao de desktop e navegador
|
|
30
|
-
- Controle via CDP (Chrome DevTools Protocol)
|
|
33
|
+
## Preferências técnicas
|
|
34
|
+
- Linguagens/stacks preferidas:
|
|
35
|
+
- Convenções que sigo:
|
|
31
36
|
|
|
32
|
-
##
|
|
33
|
-
Este arquivo contem informacoes sobre mim e projetos em geral.
|
|
34
|
-
Cada pasta de projeto tem suas memorias em memory.md.
|
|
37
|
+
## Notas
|
|
35
38
|
`;
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
function memoryTemplate(projectName, date) {
|
|
41
|
+
return `# ${projectName}
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
Memória deste projeto. O PrimoCode lê no início da sessão e atualiza ao final,
|
|
44
|
+
para que nada se perca entre uma sessão e outra.
|
|
41
45
|
|
|
42
|
-
##
|
|
43
|
-
|
|
46
|
+
## Descrição
|
|
47
|
+
(a definir)
|
|
44
48
|
|
|
45
|
-
##
|
|
46
|
-
|
|
49
|
+
## Tecnologias
|
|
50
|
+
(a definir)
|
|
47
51
|
|
|
48
|
-
##
|
|
49
|
-
-
|
|
52
|
+
## Decisões
|
|
53
|
+
- ${date} — projeto criado
|
|
50
54
|
|
|
51
|
-
##
|
|
52
|
-
- Primeira interacao: {data}
|
|
55
|
+
## Histórico
|
|
53
56
|
`;
|
|
57
|
+
}
|
|
54
58
|
|
|
55
59
|
function ensureDir(dir) {
|
|
56
60
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function today() {
|
|
64
|
+
return new Date().toISOString().split('T')[0];
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
function ensureStructure() {
|
|
60
68
|
ensureDir(ROOT_DIR);
|
|
61
|
-
ensureDir(INTERNAL_DIR);
|
|
62
69
|
ensureDir(CONFIG_DIR);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fs.writeFileSync(youMdPath, YOU_MD_TEMPLATE, 'utf-8');
|
|
70
|
+
if (!fs.existsSync(GENERAL_MEMORY)) {
|
|
71
|
+
fs.writeFileSync(GENERAL_MEMORY, GENERAL_MEMORY_TEMPLATE, 'utf-8');
|
|
66
72
|
}
|
|
67
|
-
return { rootDir: ROOT_DIR
|
|
73
|
+
return { rootDir: ROOT_DIR };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// "Jogo de Carro!" -> "jogo-de-carro"
|
|
77
|
+
function slugify(name) {
|
|
78
|
+
return String(name)
|
|
79
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
82
|
+
.replace(/^-+|-+$/g, '')
|
|
83
|
+
.slice(0, 60);
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
function createProjectDir(projectName) {
|
|
71
87
|
ensureStructure();
|
|
72
|
-
const
|
|
88
|
+
const slug = slugify(projectName);
|
|
89
|
+
const projectDir = path.join(ROOT_DIR, slug);
|
|
73
90
|
ensureDir(projectDir);
|
|
74
91
|
const memoryPath = path.join(projectDir, 'memory.md');
|
|
75
92
|
if (!fs.existsSync(memoryPath)) {
|
|
76
|
-
|
|
77
|
-
.replace(/{nome_do_projeto}/g, projectName)
|
|
78
|
-
.replace(/{tecnologia}/g, 'a definir')
|
|
79
|
-
.replace(/{estrutura}/g, 'a definir')
|
|
80
|
-
.replace(/{data}/g, new Date().toISOString().split('T')[0]);
|
|
81
|
-
fs.writeFileSync(memoryPath, content, 'utf-8');
|
|
93
|
+
fs.writeFileSync(memoryPath, memoryTemplate(projectName, today()), 'utf-8');
|
|
82
94
|
}
|
|
83
95
|
return projectDir;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
function getProjectDir(projectName) {
|
|
87
99
|
if (!projectName) return ROOT_DIR;
|
|
88
|
-
return path.join(ROOT_DIR, projectName);
|
|
100
|
+
return path.join(ROOT_DIR, slugify(projectName));
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
function listProjects() {
|
|
92
104
|
ensureStructure();
|
|
93
105
|
try {
|
|
94
|
-
return fs.readdirSync(ROOT_DIR)
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
106
|
+
return fs.readdirSync(ROOT_DIR).filter((name) => {
|
|
107
|
+
if (name.startsWith('.')) return false;
|
|
108
|
+
return fs.statSync(path.join(ROOT_DIR, name)).isDirectory();
|
|
109
|
+
});
|
|
99
110
|
} catch { return []; }
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
// Projeto parecido com o que o usuário pediu? Compara os pedaços do slug.
|
|
114
|
+
// "jogo-de-carro" vs "jogo-carro-2d" -> compartilham jogo+carro -> 0.5
|
|
115
|
+
function findSimilarProject(projectName, minScore = 0.4) {
|
|
116
|
+
const wanted = new Set(slugify(projectName).split('-').filter((t) => t.length > 2));
|
|
117
|
+
if (!wanted.size) return null;
|
|
118
|
+
|
|
119
|
+
let best = null;
|
|
120
|
+
for (const existing of listProjects()) {
|
|
121
|
+
const tokens = new Set(existing.split('-').filter((t) => t.length > 2));
|
|
122
|
+
if (!tokens.size) continue;
|
|
123
|
+
let shared = 0;
|
|
124
|
+
for (const t of wanted) if (tokens.has(t)) shared++;
|
|
125
|
+
const score = shared / Math.max(wanted.size, tokens.size);
|
|
126
|
+
if (score >= minScore && (!best || score > best.score)) {
|
|
127
|
+
best = { name: existing, dir: path.join(ROOT_DIR, existing), score };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readGeneralMemory(limit = 2000) {
|
|
134
|
+
try { return fs.readFileSync(GENERAL_MEMORY, 'utf-8').slice(0, limit); }
|
|
135
|
+
catch { return ''; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Recebem o DIRETÓRIO de trabalho, não o nome do projeto: quando o PrimoCode
|
|
139
|
+
// roda dentro de uma pasta sua, a memória fica lá, junto do código.
|
|
140
|
+
function readProjectMemory(dir, limit = 2000) {
|
|
141
|
+
if (!dir) return '';
|
|
142
|
+
try { return fs.readFileSync(path.join(dir, 'memory.md'), 'utf-8').slice(0, limit); }
|
|
143
|
+
catch { return ''; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function appendProjectMemory(dir, entry) {
|
|
147
|
+
if (!dir || !entry) return;
|
|
148
|
+
try {
|
|
149
|
+
ensureDir(dir);
|
|
150
|
+
fs.appendFileSync(path.join(dir, 'memory.md'), `\n### ${today()}\n${entry}\n`, 'utf-8');
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
|
|
102
154
|
function loadConfig() {
|
|
103
|
-
ensureDir(CONFIG_DIR);
|
|
104
155
|
ensureStructure();
|
|
105
156
|
let file = {};
|
|
106
157
|
try {
|
|
@@ -109,6 +160,7 @@ function loadConfig() {
|
|
|
109
160
|
return {
|
|
110
161
|
server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
|
|
111
162
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
163
|
+
token: process.env.PRIMOCODE_TOKEN || file.token || null,
|
|
112
164
|
autoApply: file.autoApply === true,
|
|
113
165
|
currentProject: file.currentProject || null,
|
|
114
166
|
rootDir: ROOT_DIR,
|
|
@@ -119,7 +171,9 @@ function loadConfig() {
|
|
|
119
171
|
function saveConfig(partial) {
|
|
120
172
|
ensureDir(CONFIG_DIR);
|
|
121
173
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
122
|
-
|
|
174
|
+
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
175
|
+
const { server, model, token, autoApply, currentProject } = { ...cur, ...partial };
|
|
176
|
+
const next = { server, model, token, autoApply, currentProject };
|
|
123
177
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
124
178
|
return next;
|
|
125
179
|
}
|
|
@@ -133,8 +187,8 @@ function appendHistory(entry) {
|
|
|
133
187
|
|
|
134
188
|
module.exports = {
|
|
135
189
|
loadConfig, saveConfig, appendHistory,
|
|
136
|
-
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
190
|
+
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
191
|
+
ensureStructure, slugify,
|
|
192
|
+
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
|
193
|
+
readGeneralMemory, readProjectMemory, appendProjectMemory,
|
|
140
194
|
};
|
package/lib/desktop.js
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* desktop.js — Controle nativo do mouse/teclado do sistema operacional.
|
|
3
|
-
* Zero dependências npm. Usa ferramentas nativas do SO:
|
|
4
|
-
* Linux: xdotool
|
|
5
|
-
* macOS: cliclick (brew install cliclick) ou osascript
|
|
6
|
-
* Windows: powershell + System.Windows.Forms
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
const { execSync } = require('child_process');
|
|
12
|
-
const os = require('os');
|
|
13
|
-
|
|
14
|
-
const PLATFORM = os.platform();
|
|
15
|
-
|
|
16
|
-
function hasCmd(cmd) {
|
|
17
|
-
try { execSync(`which ${cmd} || where ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Detectar ferramenta disponível
|
|
21
|
-
const TOOLS = {
|
|
22
|
-
xdotool: PLATFORM === 'linux' && hasCmd('xdotool'),
|
|
23
|
-
cliclick: PLATFORM === 'darwin' && hasCmd('cliclick'),
|
|
24
|
-
nircmd: PLATFORM === 'win32' && (() => { try { execSync('where nircmd', {stdio:'ignore'}); return true; } catch { return false; } })(),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function run(cmd) {
|
|
28
|
-
try { execSync(cmd, { stdio: 'ignore', timeout: 10000 }); return { ok: true }; }
|
|
29
|
-
catch (e) { return { ok: false, error: e.message }; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ── Mouse ─────────────────────────────────────────────
|
|
33
|
-
function click(x, y, button = 'left') {
|
|
34
|
-
if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y} click ${button === 'right' ? 3 : 1}`);
|
|
35
|
-
if (TOOLS.cliclick) return run(`cliclick c:${x},${y}`);
|
|
36
|
-
if (PLATFORM === 'win32') {
|
|
37
|
-
const b = button === 'right' ? 'Right' : 'Left';
|
|
38
|
-
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y}); [System.Windows.Forms.MouseButtons]::${b} -bor 0`;
|
|
39
|
-
return run(`powershell -Command "${ps}"`);
|
|
40
|
-
}
|
|
41
|
-
return { ok: false, error: 'Nenhuma ferramenta de automação encontrada. Instale xdotool (Linux), cliclick (Mac) ou configure o modo browser.' };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function move(x, y) {
|
|
45
|
-
if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y}`);
|
|
46
|
-
if (TOOLS.cliclick) return run(`cliclick m:${x},${y}`);
|
|
47
|
-
if (PLATFORM === 'win32') {
|
|
48
|
-
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`;
|
|
49
|
-
return run(`powershell -Command "${ps}"`);
|
|
50
|
-
}
|
|
51
|
-
return { ok: false, error: 'Automação não disponível' };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function drag(fromX, fromY, toX, toY) {
|
|
55
|
-
if (TOOLS.xdotool) {
|
|
56
|
-
return run(`xdotool mousemove ${fromX} ${fromY} mousedown 1 mousemove ${toX} ${toY} mouseup 1`);
|
|
57
|
-
}
|
|
58
|
-
if (TOOLS.cliclick) {
|
|
59
|
-
return run(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
|
|
60
|
-
}
|
|
61
|
-
return { ok: false, error: 'Drag não suportado nesta plataforma sem xdotool/cliclick' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function scroll(dy) {
|
|
65
|
-
if (TOOLS.xdotool) return run(`xdotool click --repeat ${Math.abs(Math.round(dy / 50))} ${dy > 0 ? 5 : 4}`);
|
|
66
|
-
if (TOOLS.cliclick) return run(`cliclick scroll:${Math.round(dy / 50)}`);
|
|
67
|
-
return { ok: false, error: 'Scroll não suportado' };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── Teclado ───────────────────────────────────────────
|
|
71
|
-
function type(text) {
|
|
72
|
-
if (TOOLS.xdotool) return run(`xdotool type --delay 10 '${text.replace(/'/g, "'\''")}'`);
|
|
73
|
-
if (TOOLS.cliclick) {
|
|
74
|
-
const safe = text.replace(/"/g, '\\"');
|
|
75
|
-
return run(`osascript -e 'tell application "System Events" to keystroke "${safe}"'`);
|
|
76
|
-
}
|
|
77
|
-
if (PLATFORM === 'win32') {
|
|
78
|
-
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${text.replace(/'/g, "''")}')`;
|
|
79
|
-
return run(`powershell -Command "${ps}"`);
|
|
80
|
-
}
|
|
81
|
-
return { ok: false, error: 'Automação de teclado não disponível' };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function key(name) {
|
|
85
|
-
const map = {
|
|
86
|
-
Enter: 'Return', Tab: 'Tab', Escape: 'Escape', Backspace: 'Backspace',
|
|
87
|
-
ArrowUp: 'Up', ArrowDown: 'Down', ArrowLeft: 'Left', ArrowRight: 'Right',
|
|
88
|
-
Space: 'space', Delete: 'Delete',
|
|
89
|
-
};
|
|
90
|
-
const k = map[name] || name;
|
|
91
|
-
if (TOOLS.xdotool) return run(`xdotool key ${k}`);
|
|
92
|
-
if (PLATFORM === 'darwin') return run(`osascript -e 'tell application "System Events" to key code ${k}'`);
|
|
93
|
-
if (PLATFORM === 'win32') {
|
|
94
|
-
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{${name.toUpperCase()}}')`;
|
|
95
|
-
return run(`powershell -Command "${ps}"`);
|
|
96
|
-
}
|
|
97
|
-
return { ok: false, error: 'Key não suportado' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── Screenshot ────────────────────────────────────────
|
|
101
|
-
function screenshot(saveTo) {
|
|
102
|
-
const filename = saveTo || `desktop-${Date.now()}.png`;
|
|
103
|
-
if (PLATFORM === 'darwin') {
|
|
104
|
-
return run(`screencapture -x ${filename}`) ? { ok: true, path: filename } : { ok: false, error: 'screencapture falhou' };
|
|
105
|
-
}
|
|
106
|
-
if (PLATFORM === 'win32') {
|
|
107
|
-
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{PRTSC}'); Add-Type -AssemblyName System.Drawing; \$bmp = New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); \$graphics = [System.Drawing.Graphics]::FromImage(\$bmp); \$graphics.CopyFromScreen([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Location, [System.Drawing.Point]::Empty, \$bmp.Size); \$bmp.Save('${filename}'); \$graphics.Dispose(); \$bmp.Dispose()`;
|
|
108
|
-
return run(`powershell -Command "${ps}"`) ? { ok: true, path: filename } : { ok: false, error: 'Screenshot falhou' };
|
|
109
|
-
}
|
|
110
|
-
return { ok: false, error: 'Screenshot desktop não suportado nesta plataforma. Instale scrot ou gnome-screenshot.' };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function getScreenSize() {
|
|
114
|
-
if (PLATFORM === 'win32') {
|
|
115
|
-
try {
|
|
116
|
-
const out = execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Width.ToString() + ',' + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Height.ToString()"`).toString().trim();
|
|
117
|
-
const [w, h] = out.split(',').map(Number);
|
|
118
|
-
return { ok: true, width: w, height: h };
|
|
119
|
-
} catch { }
|
|
120
|
-
}
|
|
121
|
-
if (PLATFORM === 'darwin') {
|
|
122
|
-
try {
|
|
123
|
-
const out = execSync(`system_profiler SPDisplaysDataType | grep Resolution`).toString();
|
|
124
|
-
const m = out.match(/(\d+)\s*x\s*(\d+)/);
|
|
125
|
-
if (m) return { ok: true, width: parseInt(m[1]), height: parseInt(m[2]) };
|
|
126
|
-
} catch { }
|
|
127
|
-
}
|
|
128
|
-
if (TOOLS.xdotool) {
|
|
129
|
-
try {
|
|
130
|
-
const wh = execSync(`xdotool getdisplaygeometry`).toString().trim().split(' ');
|
|
131
|
-
return { ok: true, width: parseInt(wh[0]), height: parseInt(wh[1]) };
|
|
132
|
-
} catch { }
|
|
133
|
-
}
|
|
134
|
-
return { ok: false, error: 'Não foi possível detectar resolução', width: 1920, height: 1080 };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = {
|
|
138
|
-
click, move, drag, scroll,
|
|
139
|
-
type, key,
|
|
140
|
-
screenshot, getScreenSize,
|
|
141
|
-
TOOLS,
|
|
142
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* desktop.js — Controle nativo do mouse/teclado do sistema operacional.
|
|
3
|
+
* Zero dependências npm. Usa ferramentas nativas do SO:
|
|
4
|
+
* Linux: xdotool
|
|
5
|
+
* macOS: cliclick (brew install cliclick) ou osascript
|
|
6
|
+
* Windows: powershell + System.Windows.Forms
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const PLATFORM = os.platform();
|
|
15
|
+
|
|
16
|
+
function hasCmd(cmd) {
|
|
17
|
+
try { execSync(`which ${cmd} || where ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Detectar ferramenta disponível
|
|
21
|
+
const TOOLS = {
|
|
22
|
+
xdotool: PLATFORM === 'linux' && hasCmd('xdotool'),
|
|
23
|
+
cliclick: PLATFORM === 'darwin' && hasCmd('cliclick'),
|
|
24
|
+
nircmd: PLATFORM === 'win32' && (() => { try { execSync('where nircmd', {stdio:'ignore'}); return true; } catch { return false; } })(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function run(cmd) {
|
|
28
|
+
try { execSync(cmd, { stdio: 'ignore', timeout: 10000 }); return { ok: true }; }
|
|
29
|
+
catch (e) { return { ok: false, error: e.message }; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Mouse ─────────────────────────────────────────────
|
|
33
|
+
function click(x, y, button = 'left') {
|
|
34
|
+
if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y} click ${button === 'right' ? 3 : 1}`);
|
|
35
|
+
if (TOOLS.cliclick) return run(`cliclick c:${x},${y}`);
|
|
36
|
+
if (PLATFORM === 'win32') {
|
|
37
|
+
const b = button === 'right' ? 'Right' : 'Left';
|
|
38
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y}); [System.Windows.Forms.MouseButtons]::${b} -bor 0`;
|
|
39
|
+
return run(`powershell -Command "${ps}"`);
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, error: 'Nenhuma ferramenta de automação encontrada. Instale xdotool (Linux), cliclick (Mac) ou configure o modo browser.' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function move(x, y) {
|
|
45
|
+
if (TOOLS.xdotool) return run(`xdotool mousemove ${x} ${y}`);
|
|
46
|
+
if (TOOLS.cliclick) return run(`cliclick m:${x},${y}`);
|
|
47
|
+
if (PLATFORM === 'win32') {
|
|
48
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`;
|
|
49
|
+
return run(`powershell -Command "${ps}"`);
|
|
50
|
+
}
|
|
51
|
+
return { ok: false, error: 'Automação não disponível' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function drag(fromX, fromY, toX, toY) {
|
|
55
|
+
if (TOOLS.xdotool) {
|
|
56
|
+
return run(`xdotool mousemove ${fromX} ${fromY} mousedown 1 mousemove ${toX} ${toY} mouseup 1`);
|
|
57
|
+
}
|
|
58
|
+
if (TOOLS.cliclick) {
|
|
59
|
+
return run(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
|
|
60
|
+
}
|
|
61
|
+
return { ok: false, error: 'Drag não suportado nesta plataforma sem xdotool/cliclick' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scroll(dy) {
|
|
65
|
+
if (TOOLS.xdotool) return run(`xdotool click --repeat ${Math.abs(Math.round(dy / 50))} ${dy > 0 ? 5 : 4}`);
|
|
66
|
+
if (TOOLS.cliclick) return run(`cliclick scroll:${Math.round(dy / 50)}`);
|
|
67
|
+
return { ok: false, error: 'Scroll não suportado' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Teclado ───────────────────────────────────────────
|
|
71
|
+
function type(text) {
|
|
72
|
+
if (TOOLS.xdotool) return run(`xdotool type --delay 10 '${text.replace(/'/g, "'\''")}'`);
|
|
73
|
+
if (TOOLS.cliclick) {
|
|
74
|
+
const safe = text.replace(/"/g, '\\"');
|
|
75
|
+
return run(`osascript -e 'tell application "System Events" to keystroke "${safe}"'`);
|
|
76
|
+
}
|
|
77
|
+
if (PLATFORM === 'win32') {
|
|
78
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${text.replace(/'/g, "''")}')`;
|
|
79
|
+
return run(`powershell -Command "${ps}"`);
|
|
80
|
+
}
|
|
81
|
+
return { ok: false, error: 'Automação de teclado não disponível' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function key(name) {
|
|
85
|
+
const map = {
|
|
86
|
+
Enter: 'Return', Tab: 'Tab', Escape: 'Escape', Backspace: 'Backspace',
|
|
87
|
+
ArrowUp: 'Up', ArrowDown: 'Down', ArrowLeft: 'Left', ArrowRight: 'Right',
|
|
88
|
+
Space: 'space', Delete: 'Delete',
|
|
89
|
+
};
|
|
90
|
+
const k = map[name] || name;
|
|
91
|
+
if (TOOLS.xdotool) return run(`xdotool key ${k}`);
|
|
92
|
+
if (PLATFORM === 'darwin') return run(`osascript -e 'tell application "System Events" to key code ${k}'`);
|
|
93
|
+
if (PLATFORM === 'win32') {
|
|
94
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{${name.toUpperCase()}}')`;
|
|
95
|
+
return run(`powershell -Command "${ps}"`);
|
|
96
|
+
}
|
|
97
|
+
return { ok: false, error: 'Key não suportado' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Screenshot ────────────────────────────────────────
|
|
101
|
+
function screenshot(saveTo) {
|
|
102
|
+
const filename = saveTo || `desktop-${Date.now()}.png`;
|
|
103
|
+
if (PLATFORM === 'darwin') {
|
|
104
|
+
return run(`screencapture -x ${filename}`) ? { ok: true, path: filename } : { ok: false, error: 'screencapture falhou' };
|
|
105
|
+
}
|
|
106
|
+
if (PLATFORM === 'win32') {
|
|
107
|
+
const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{PRTSC}'); Add-Type -AssemblyName System.Drawing; \$bmp = New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); \$graphics = [System.Drawing.Graphics]::FromImage(\$bmp); \$graphics.CopyFromScreen([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Location, [System.Drawing.Point]::Empty, \$bmp.Size); \$bmp.Save('${filename}'); \$graphics.Dispose(); \$bmp.Dispose()`;
|
|
108
|
+
return run(`powershell -Command "${ps}"`) ? { ok: true, path: filename } : { ok: false, error: 'Screenshot falhou' };
|
|
109
|
+
}
|
|
110
|
+
return { ok: false, error: 'Screenshot desktop não suportado nesta plataforma. Instale scrot ou gnome-screenshot.' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getScreenSize() {
|
|
114
|
+
if (PLATFORM === 'win32') {
|
|
115
|
+
try {
|
|
116
|
+
const out = execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Width.ToString() + ',' + [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size.Height.ToString()"`).toString().trim();
|
|
117
|
+
const [w, h] = out.split(',').map(Number);
|
|
118
|
+
return { ok: true, width: w, height: h };
|
|
119
|
+
} catch { }
|
|
120
|
+
}
|
|
121
|
+
if (PLATFORM === 'darwin') {
|
|
122
|
+
try {
|
|
123
|
+
const out = execSync(`system_profiler SPDisplaysDataType | grep Resolution`).toString();
|
|
124
|
+
const m = out.match(/(\d+)\s*x\s*(\d+)/);
|
|
125
|
+
if (m) return { ok: true, width: parseInt(m[1]), height: parseInt(m[2]) };
|
|
126
|
+
} catch { }
|
|
127
|
+
}
|
|
128
|
+
if (TOOLS.xdotool) {
|
|
129
|
+
try {
|
|
130
|
+
const wh = execSync(`xdotool getdisplaygeometry`).toString().trim().split(' ');
|
|
131
|
+
return { ok: true, width: parseInt(wh[0]), height: parseInt(wh[1]) };
|
|
132
|
+
} catch { }
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, error: 'Não foi possível detectar resolução', width: 1920, height: 1080 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
click, move, drag, scroll,
|
|
139
|
+
type, key,
|
|
140
|
+
screenshot, getScreenSize,
|
|
141
|
+
TOOLS,
|
|
142
|
+
};
|