primocode 7.0.3 → 7.1.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 +243 -225
- package/lib/act.js +120 -116
- package/lib/api.js +110 -17
- package/lib/browser.js +492 -492
- package/lib/config.js +105 -52
- package/lib/desktop.js +142 -142
- package/lib/tools.js +147 -104
- package/lib/ui.js +165 -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,141 @@ 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
|
+
function readProjectMemory(projectName, limit = 2000) {
|
|
139
|
+
if (!projectName) return '';
|
|
140
|
+
try { return fs.readFileSync(path.join(getProjectDir(projectName), 'memory.md'), 'utf-8').slice(0, limit); }
|
|
141
|
+
catch { return ''; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendProjectMemory(projectName, entry) {
|
|
145
|
+
if (!projectName || !entry) return;
|
|
146
|
+
const memoryPath = path.join(getProjectDir(projectName), 'memory.md');
|
|
147
|
+
try {
|
|
148
|
+
ensureDir(path.dirname(memoryPath));
|
|
149
|
+
fs.appendFileSync(memoryPath, `\n### ${today()}\n${entry}\n`, 'utf-8');
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
102
153
|
function loadConfig() {
|
|
103
|
-
ensureDir(CONFIG_DIR);
|
|
104
154
|
ensureStructure();
|
|
105
155
|
let file = {};
|
|
106
156
|
try {
|
|
@@ -109,6 +159,7 @@ function loadConfig() {
|
|
|
109
159
|
return {
|
|
110
160
|
server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
|
|
111
161
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
162
|
+
token: process.env.PRIMOCODE_TOKEN || file.token || null,
|
|
112
163
|
autoApply: file.autoApply === true,
|
|
113
164
|
currentProject: file.currentProject || null,
|
|
114
165
|
rootDir: ROOT_DIR,
|
|
@@ -119,7 +170,9 @@ function loadConfig() {
|
|
|
119
170
|
function saveConfig(partial) {
|
|
120
171
|
ensureDir(CONFIG_DIR);
|
|
121
172
|
const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
|
|
122
|
-
|
|
173
|
+
// Só persiste o que faz sentido guardar — nada de campos derivados.
|
|
174
|
+
const { server, model, token, autoApply, currentProject } = { ...cur, ...partial };
|
|
175
|
+
const next = { server, model, token, autoApply, currentProject };
|
|
123
176
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
124
177
|
return next;
|
|
125
178
|
}
|
|
@@ -133,8 +186,8 @@ function appendHistory(entry) {
|
|
|
133
186
|
|
|
134
187
|
module.exports = {
|
|
135
188
|
loadConfig, saveConfig, appendHistory,
|
|
136
|
-
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
190
|
+
ensureStructure, slugify,
|
|
191
|
+
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
|
192
|
+
readGeneralMemory, readProjectMemory, appendProjectMemory,
|
|
140
193
|
};
|
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
|
+
};
|