primocode 7.13.0 → 7.14.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/bin/primocode.js +68 -2
- package/lib/desktop.js +31 -16
- package/lib/streaming.js +213 -101
- package/lib/ui.js +3 -3
- package/package.json +1 -1
package/bin/primocode.js
CHANGED
|
@@ -78,6 +78,53 @@ ${c.bold('Onde ele trabalha')}
|
|
|
78
78
|
${c.dim('Pasta de projeto nova só nasce quando ele vai mexer em arquivo.')}
|
|
79
79
|
`;
|
|
80
80
|
|
|
81
|
+
// ── Autopreenchimento dos comandos (estilo Claude Code) ────────────────
|
|
82
|
+
// Metade do comando já serve: Tab completa, e um prefixo único executa
|
|
83
|
+
// direto — "/str" vira "/streaming" sozinho. A lista é só a dos nomes
|
|
84
|
+
// canônicos; os apelidos (/cd, /stream…) continuam valendo por extenso.
|
|
85
|
+
const SLASH_COMANDOS = [
|
|
86
|
+
['/help', 'todos os comandos'],
|
|
87
|
+
['/project', 'lista/cria projetos em ~/primocode'],
|
|
88
|
+
['/dir', 'trabalhar numa pasta sua'],
|
|
89
|
+
['/mode', 'primocode | chat | auto'],
|
|
90
|
+
['/model', 'top | main | fast'],
|
|
91
|
+
['/attach', 'anexa arquivo ao pedido'],
|
|
92
|
+
['/detach', 'remove os anexos'],
|
|
93
|
+
['/files', 'lista os anexos'],
|
|
94
|
+
['/tudo', 'tudo que ele sabe fazer'],
|
|
95
|
+
['/tree', 'árvore do projeto'],
|
|
96
|
+
['/read', 'mostra um arquivo'],
|
|
97
|
+
['/run', 'roda um comando'],
|
|
98
|
+
['/open', 'abre no navegador'],
|
|
99
|
+
['/streaming', 'painel ao vivo — vídeo dele trabalhando'],
|
|
100
|
+
['/status', 'checa o servidor'],
|
|
101
|
+
['/name', 'como ele te chama'],
|
|
102
|
+
['/mascot', 'primo | fox | cat'],
|
|
103
|
+
['/config', 'suas preferências'],
|
|
104
|
+
['/clear', 'limpa a conversa'],
|
|
105
|
+
['/cls', 'limpa a tela'],
|
|
106
|
+
['/uninstall', 'remove o PrimoCode'],
|
|
107
|
+
['/version', 'versão'],
|
|
108
|
+
['/exit', 'sair'],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// O completer do readline: Tab completa o que der. Só age na palavra do
|
|
112
|
+
// comando — depois do primeiro espaço o argumento é seu, não dele.
|
|
113
|
+
function completarSlash(line) {
|
|
114
|
+
if (!line.startsWith('/') || /\s/.test(line)) return [[], line];
|
|
115
|
+
const baixa = line.toLowerCase();
|
|
116
|
+
const acertos = SLASH_COMANDOS.map(([nome]) => nome).filter((nome) => nome.startsWith(baixa));
|
|
117
|
+
return [acertos, line];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mostrarMenuSlash() {
|
|
121
|
+
console.log('');
|
|
122
|
+
for (const [nome, desc] of SLASH_COMANDOS) {
|
|
123
|
+
console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(c.dim('\n Tab completa · metade do comando já serve: /str vira /streaming'));
|
|
126
|
+
}
|
|
127
|
+
|
|
81
128
|
let rl = null;
|
|
82
129
|
|
|
83
130
|
function makePrompt() {
|
|
@@ -605,7 +652,26 @@ async function handleSlash(text) {
|
|
|
605
652
|
// rl.close() em vez de process.exit(): sair na marra descarta o que
|
|
606
653
|
// ainda estava no buffer de saída.
|
|
607
654
|
case 'exit': case 'quit': if (rl) rl.close(); break;
|
|
608
|
-
default:
|
|
655
|
+
default: {
|
|
656
|
+
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
657
|
+
// ("/str" → /streaming); prefixo ambíguo mostra as opções.
|
|
658
|
+
if (!cmd) { mostrarMenuSlash(); break; }
|
|
659
|
+
const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
|
|
660
|
+
if (casam.length === 1) {
|
|
661
|
+
console.log(c.dim(` → /${casam[0]}`));
|
|
662
|
+
await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
if (casam.length > 1) {
|
|
666
|
+
console.log(c.muted(` /${cmd}… pode ser:`));
|
|
667
|
+
for (const n of casam) {
|
|
668
|
+
const [, desc] = SLASH_COMANDOS.find(([nome]) => nome === '/' + n);
|
|
669
|
+
console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
console.log(c.err(`Comando desconhecido: /${cmd}. Digite / para ver o menu.`));
|
|
674
|
+
}
|
|
609
675
|
}
|
|
610
676
|
}
|
|
611
677
|
|
|
@@ -682,7 +748,7 @@ async function main() {
|
|
|
682
748
|
userName: state.config.userName,
|
|
683
749
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
684
750
|
}));
|
|
685
|
-
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
751
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt(), completer: completarSlash });
|
|
686
752
|
rl.prompt();
|
|
687
753
|
|
|
688
754
|
// Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
|
package/lib/desktop.js
CHANGED
|
@@ -659,25 +659,40 @@ function clickElement(nome, opts = {}) {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
// ── Screenshot ────────────────────────────────────────
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
662
|
+
// A ferramenta do Linux é resolvida UMA vez: o vídeo do /streaming chama isto
|
|
663
|
+
// várias vezes por segundo, e um `which` por frame seria desperdício.
|
|
664
|
+
let LINUX_SHOT;
|
|
665
|
+
function linuxShot() {
|
|
666
|
+
if (LINUX_SHOT === undefined) LINUX_SHOT = firstCmd(['scrot', 'maim', 'gnome-screenshot', 'spectacle', 'import']);
|
|
667
|
+
return LINUX_SHOT;
|
|
668
|
+
}
|
|
665
669
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
670
|
+
/**
|
|
671
|
+
* O comando de shell que fotografa a tela para `filename` — ou null quando a
|
|
672
|
+
* plataforma não tem como. Existe separado do screenshot() para o painel do
|
|
673
|
+
* /streaming capturar em LOOP, assíncrono, sem travar o agente.
|
|
674
|
+
*/
|
|
675
|
+
function screenshotCommand(filename) {
|
|
676
|
+
if (PLATFORM === 'darwin') return `screencapture -x "${filename}"`;
|
|
669
677
|
if (PLATFORM === 'win32') {
|
|
670
678
|
const ps = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$bmp.Size); $bmp.Save('${filename.replace(/'/g, "''")}'); $g.Dispose(); $bmp.Dispose()`;
|
|
671
|
-
return
|
|
679
|
+
return pwshCmd(ps);
|
|
672
680
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (
|
|
676
|
-
if (
|
|
677
|
-
if (
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
+
const tool = linuxShot();
|
|
682
|
+
if (tool === 'scrot') return `scrot -o "${filename}"`;
|
|
683
|
+
if (tool === 'maim') return `maim "${filename}"`;
|
|
684
|
+
if (tool === 'gnome-screenshot') return `gnome-screenshot -f "${filename}"`;
|
|
685
|
+
if (tool === 'spectacle') return `spectacle -b -n -o "${filename}"`;
|
|
686
|
+
if (tool === 'import') return `import -window root "${filename}"`; // ImageMagick
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function screenshot(saveTo) {
|
|
691
|
+
const filename = saveTo || `desktop-${Date.now()}.png`;
|
|
692
|
+
const cmd = screenshotCommand(filename);
|
|
693
|
+
if (!cmd) return { ok: false, error: 'Screenshot indisponível no Linux. Instale scrot, maim, gnome-screenshot, spectacle ou ImageMagick (import).' };
|
|
694
|
+
const r = run(cmd);
|
|
695
|
+
return r.ok ? { ok: true, path: filename, bytes: sizeOf(filename) } : r;
|
|
681
696
|
}
|
|
682
697
|
|
|
683
698
|
function sizeOf(f) {
|
|
@@ -719,7 +734,7 @@ module.exports = {
|
|
|
719
734
|
click, doubleClick, move, drag, scroll,
|
|
720
735
|
type, key,
|
|
721
736
|
openApp, listWindows, focusWindow, uiElements, clickElement,
|
|
722
|
-
screenshot, getScreenSize,
|
|
737
|
+
screenshot, screenshotCommand, getScreenSize,
|
|
723
738
|
TOOLS,
|
|
724
739
|
// Puras, exportadas para o test/desktop.test.js provar a tradução de teclas
|
|
725
740
|
// e a escolha de elemento sem depender do SO da máquina de teste.
|
package/lib/streaming.js
CHANGED
|
@@ -19,6 +19,7 @@ const http = require('http');
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const os = require('os');
|
|
22
|
+
const { exec } = require('child_process');
|
|
22
23
|
const desktop = require('./desktop');
|
|
23
24
|
|
|
24
25
|
const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
@@ -44,8 +45,61 @@ const state = {
|
|
|
44
45
|
erroFoto: null,
|
|
45
46
|
ultimaFoto: 0,
|
|
46
47
|
passo: 0,
|
|
48
|
+
// ── vídeo ao vivo ──
|
|
49
|
+
liveSeq: 0, // sobe a cada frame novo; a página troca a imagem quando muda
|
|
50
|
+
liveAt: 0, // quando o último frame de vídeo foi capturado
|
|
51
|
+
viewerAt: 0, // último sinal de que alguém está com a página aberta
|
|
52
|
+
liveTimer: null,
|
|
47
53
|
};
|
|
48
54
|
|
|
55
|
+
const LIVE = () => path.join(DIR, 'live.png');
|
|
56
|
+
const LIVE_TMP = () => path.join(DIR, 'live-tmp.png');
|
|
57
|
+
|
|
58
|
+
// ── O vídeo: um loop de captura enquanto alguém assiste ──────────────────
|
|
59
|
+
// "Vídeo" aqui é o jeito honesto de fazer sem dependência: fotografar a tela
|
|
60
|
+
// em sequência (2 a 3 quadros por segundo, conforme a máquina aguenta) e a
|
|
61
|
+
// página trocar a imagem no ritmo — como um VNC simples. O loop é assíncrono
|
|
62
|
+
// (exec, nunca execSync) para não roubar tempo do agente, escreve num tmp e
|
|
63
|
+
// renomeia (troca atômica: o navegador nunca lê um PNG pela metade), e SÓ
|
|
64
|
+
// roda enquanto a página deu sinal de vida há menos de 8s — painel fechado
|
|
65
|
+
// não custa nada.
|
|
66
|
+
function viuAgora() {
|
|
67
|
+
state.viewerAt = Date.now();
|
|
68
|
+
agendarVideo(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function agendarVideo(ms) {
|
|
72
|
+
if (!state.on || state.liveTimer) return;
|
|
73
|
+
state.liveTimer = setTimeout(cicloVideo, ms);
|
|
74
|
+
state.liveTimer.unref();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cicloVideo() {
|
|
78
|
+
state.liveTimer = null;
|
|
79
|
+
if (!state.on) return;
|
|
80
|
+
if (Date.now() - state.viewerAt > 8000) return; // ninguém olhando — o próximo /state religa
|
|
81
|
+
|
|
82
|
+
const cmd = desktop.screenshotCommand(LIVE_TMP());
|
|
83
|
+
if (!cmd) { if (!state.erroFoto) state.erroFoto = 'screenshot indisponível nesta máquina'; return; }
|
|
84
|
+
|
|
85
|
+
const t0 = Date.now();
|
|
86
|
+
exec(cmd, { timeout: 8000 }, (err) => {
|
|
87
|
+
const dur = Date.now() - t0;
|
|
88
|
+
if (!err) {
|
|
89
|
+
try {
|
|
90
|
+
fs.renameSync(LIVE_TMP(), LIVE());
|
|
91
|
+
state.liveSeq++;
|
|
92
|
+
state.liveAt = Date.now();
|
|
93
|
+
state.erroFoto = null;
|
|
94
|
+
} catch {}
|
|
95
|
+
} else if (!state.erroFoto) {
|
|
96
|
+
state.erroFoto = corta(String(err.message || err), 120);
|
|
97
|
+
}
|
|
98
|
+
// Máquina rápida ≈ 3 quadros/s; máquina lenta desacelera sozinha.
|
|
99
|
+
agendarVideo(Math.max(280, Math.min(1200, dur)));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
// Ações cujo efeito aparece NA TELA — são as que merecem uma foto nova.
|
|
50
104
|
const ACOES_NA_TELA = new Set([
|
|
51
105
|
'desktop_click', 'desktop_double_click', 'desktop_click_element',
|
|
@@ -179,6 +233,20 @@ function capturar(entrada) {
|
|
|
179
233
|
|
|
180
234
|
const id = state.proximoFrame;
|
|
181
235
|
const arquivo = path.join(DIR, `frame-${id}.png`);
|
|
236
|
+
|
|
237
|
+
// Com o vídeo rodando, o quadro mais novo já está no disco — copiar é
|
|
238
|
+
// grátis e não fotografa duas vezes a mesma tela.
|
|
239
|
+
if (agora - state.liveAt < 900) {
|
|
240
|
+
try { fs.copyFileSync(LIVE(), arquivo); } catch { return null; }
|
|
241
|
+
state.proximoFrame++;
|
|
242
|
+
state.frames.push({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba });
|
|
243
|
+
while (state.frames.length > MAX_FRAMES) {
|
|
244
|
+
const velho = state.frames.shift();
|
|
245
|
+
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
246
|
+
}
|
|
247
|
+
return id;
|
|
248
|
+
}
|
|
249
|
+
|
|
182
250
|
const r = desktop.screenshot(arquivo);
|
|
183
251
|
if (!r.ok) { if (!state.erroFoto) state.erroFoto = r.error; return null; }
|
|
184
252
|
|
|
@@ -205,13 +273,13 @@ const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
|
205
273
|
<g fill="#0b1220"><rect x="3" y=".35" width=".8" height=".5" rx=".1"/><rect x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
206
274
|
</svg>`;
|
|
207
275
|
|
|
208
|
-
// A tela
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
276
|
+
// A tela do painel, na identidade do app — a MESMA paleta do design system do
|
|
277
|
+
// CLI (lib/ui.js): primária cyan #22d3ee, neutros slate, rose para falha. A
|
|
278
|
+
// estrutura é a do desenho do dono: mascote inclinado no canto, "Primocode"
|
|
279
|
+
// no alto, a janela com as bolinhas e as abas separadas por "|", a tela
|
|
280
|
+
// embaixo. Dentro da tela roda o VÍDEO ao vivo (o loop de captura acima); as
|
|
281
|
+
// abas em cima são tudo que o agente abriu — clicáveis, cada uma com seus
|
|
282
|
+
// quadros e passos; e a barra de comando autopreenche a instrução digitando.
|
|
215
283
|
const FAVICON = 'data:image/svg+xml,' + encodeURIComponent(
|
|
216
284
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 3.6">${MASCOTE_SVG.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</svg>`,
|
|
217
285
|
);
|
|
@@ -223,94 +291,97 @@ const PAGINA = `<!doctype html>
|
|
|
223
291
|
<link rel="icon" href="${FAVICON}">
|
|
224
292
|
<style>
|
|
225
293
|
* { box-sizing: border-box; margin: 0; }
|
|
226
|
-
::selection { background: rgba(
|
|
294
|
+
::selection { background: rgba(34,211,238,.28); }
|
|
227
295
|
body {
|
|
228
|
-
background: radial-gradient(
|
|
229
|
-
color: #
|
|
296
|
+
background: radial-gradient(1100px 480px at 50% -120px, #12263f 0%, #0d1729 46%, #0b1220 100%) fixed #0b1220;
|
|
297
|
+
color: #e2e8f0; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
230
298
|
min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 18px 34px;
|
|
231
299
|
}
|
|
232
300
|
|
|
233
|
-
/* ── Cabeçalho ── */
|
|
234
|
-
#topo { position: relative; width: min(1120px, 100%); padding:
|
|
235
|
-
#topo .mascote { position: absolute; left: 4px; top:
|
|
301
|
+
/* ── Cabeçalho: mascote inclinado + nome, como no desenho ── */
|
|
302
|
+
#topo { position: relative; width: min(1120px, 100%); padding: 44px 0 26px; text-align: center; }
|
|
303
|
+
#topo .mascote { position: absolute; left: 4px; top: 26px; width: 100px; height: 42px; transform: rotate(-32deg); filter: drop-shadow(0 8px 20px rgba(34,211,238,.4)); animation: flutua 3.2s ease-in-out infinite alternate; }
|
|
236
304
|
@keyframes flutua { from { translate: 0 0 } to { translate: 0 5px } }
|
|
237
305
|
#topo .mascote svg { width: 100%; height: 100%; display: block; }
|
|
238
|
-
#topo h1 { font-size: clamp(
|
|
239
|
-
#topo
|
|
240
|
-
.
|
|
306
|
+
#topo h1 { font-size: clamp(34px, 5.6vw, 54px); font-weight: 650; letter-spacing: .4px; color: #f1f5f9; text-shadow: 0 4px 28px rgba(34,211,238,.22); }
|
|
307
|
+
#topo h1 span { color: #22d3ee; }
|
|
308
|
+
#topo .vivo { position: absolute; right: 2px; top: 48px; display: inline-flex; align-items: center; gap: 8px; color: #94a3b8; font-size: 12.5px; background: rgba(15,23,42,.85); border: 1px solid #1e293b; border-radius: 999px; padding: 6px 14px; }
|
|
309
|
+
.pulso { width: 8px; height: 8px; border-radius: 50%; background: #22d3ee; box-shadow: 0 0 10px #22d3ee99; animation: pulsa 1.4s infinite; }
|
|
241
310
|
@keyframes pulsa { 50% { opacity: .3 } }
|
|
242
311
|
|
|
243
312
|
/* ── A janela ── */
|
|
244
313
|
#janela {
|
|
245
|
-
width: min(1120px, 100%); background: #000; border:
|
|
314
|
+
width: min(1120px, 100%); background: #000; border: 1px solid #24344d; border-radius: 22px;
|
|
246
315
|
overflow: hidden; display: flex; flex-direction: column;
|
|
247
|
-
box-shadow: 0
|
|
316
|
+
box-shadow: 0 30px 80px rgba(2,6,23,.75), 0 0 60px rgba(34,211,238,.07), inset 0 1px 0 rgba(226,232,240,.07);
|
|
248
317
|
}
|
|
249
|
-
#barra { position: relative; display: flex; align-items: center; background: linear-gradient(#
|
|
250
|
-
#barra .b { flex: none; margin-right:
|
|
251
|
-
#barra .b.r {
|
|
252
|
-
#barra .b.y { width: 30px; height: 17px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #ffe95c, #f5c400); }
|
|
253
|
-
#barra .b.g { width: 24px; height: 14px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #6fe589, #24b53e); }
|
|
318
|
+
#barra { position: relative; display: flex; align-items: center; background: linear-gradient(#152238, #101a2c); padding: 13px 22px; min-height: 54px; border-bottom: 1px solid #0a1120; }
|
|
319
|
+
#barra .b { flex: none; width: 13px; height: 13px; border-radius: 50%; margin-right: 9px; box-shadow: inset 0 -1.5px 3px rgba(0,0,0,.35); }
|
|
320
|
+
#barra .b.r { background: #ff5f57; } #barra .b.y { background: #febc2e; } #barra .b.g { background: #28c840; }
|
|
254
321
|
|
|
255
|
-
|
|
322
|
+
/* Abas: tudo que ele abriu, clicável, separado por | como no desenho */
|
|
323
|
+
#abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 62%; display: flex; align-items: center; gap: 11px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(14px, 1.9vw, 18px); font-weight: 550; }
|
|
256
324
|
#abas::-webkit-scrollbar { display: none; }
|
|
257
|
-
.aba { border: 0; background: none; cursor: pointer; font: inherit; color:
|
|
258
|
-
.aba:hover { color:
|
|
259
|
-
.aba.ativa { color: #
|
|
260
|
-
.aba
|
|
261
|
-
.
|
|
325
|
+
.aba { position: relative; border: 0; background: none; cursor: pointer; font: inherit; color: #64748b; padding: 4px 3px 7px; transition: color .18s ease; }
|
|
326
|
+
.aba:hover { color: #cbd5e1; }
|
|
327
|
+
.aba.ativa { color: #22d3ee; text-shadow: 0 0 18px rgba(34,211,238,.5); }
|
|
328
|
+
.aba.ativa::after { content: ''; position: absolute; left: 15%; right: 15%; bottom: 0; height: 2px; border-radius: 2px; background: #22d3ee; box-shadow: 0 0 10px #22d3ee; }
|
|
329
|
+
.aba .qtd { color: #475569; font-size: .68em; vertical-align: super; margin-left: 4px; font-weight: 600; }
|
|
330
|
+
.sep { color: #334155; font-weight: 300; }
|
|
262
331
|
|
|
263
332
|
/* ── Barra de comando: a instrução autopreenchendo ── */
|
|
264
|
-
#comando { display: flex; align-items: center; gap:
|
|
265
|
-
#comando .chevron { color: #
|
|
266
|
-
#comando .texto { color: #
|
|
267
|
-
#comando .texto.vazio { color: #
|
|
268
|
-
#comando .cursor { flex: none; width: 8px; height:
|
|
333
|
+
#comando { display: flex; align-items: center; gap: 11px; background: #0a1120; border-bottom: 1px solid #060b16; padding: 11px 22px; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; font-size: 14px; min-height: 44px; }
|
|
334
|
+
#comando .chevron { color: #22d3ee; font-weight: 700; flex: none; text-shadow: 0 0 10px rgba(34,211,238,.6); }
|
|
335
|
+
#comando .texto { color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
336
|
+
#comando .texto.vazio { color: #475569; font-style: italic; }
|
|
337
|
+
#comando .cursor { flex: none; width: 8px; height: 16px; background: #22d3ee; margin-left: 2px; animation: pisca 1s steps(1) infinite; box-shadow: 0 0 8px rgba(34,211,238,.7); }
|
|
269
338
|
@keyframes pisca { 50% { opacity: 0 } }
|
|
270
|
-
#comando .hora { margin-left: auto; color: #
|
|
271
|
-
|
|
272
|
-
/* ── A tela ── */
|
|
273
|
-
#tela { position: relative; min-height:
|
|
274
|
-
#tela img { width: 100%; max-height:
|
|
275
|
-
#aviso {
|
|
276
|
-
#aviso .titulo { font-size: clamp(
|
|
277
|
-
#aviso .sub { color: #
|
|
278
|
-
#
|
|
279
|
-
#
|
|
339
|
+
#comando .hora { margin-left: auto; color: #475569; font-size: 11.5px; flex: none; }
|
|
340
|
+
|
|
341
|
+
/* ── A tela: o vídeo ao vivo ── */
|
|
342
|
+
#tela { position: relative; min-height: 440px; display: flex; align-items: center; justify-content: center; background: #000; }
|
|
343
|
+
#tela img { width: 100%; max-height: 60vh; object-fit: contain; display: none; }
|
|
344
|
+
#aviso { text-align: center; padding: 84px 24px; }
|
|
345
|
+
#aviso .titulo { font-size: clamp(19px, 3vw, 28px); font-weight: 550; color: #f1f5f9; }
|
|
346
|
+
#aviso .sub { color: #64748b; font-size: 14px; margin-top: 13px; line-height: 1.7; }
|
|
347
|
+
#badge { position: absolute; top: 14px; right: 16px; display: none; align-items: center; gap: 7px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #f1f5f9; font-size: 11.5px; font-weight: 700; letter-spacing: 1.2px; padding: 6px 12px; border-radius: 999px; }
|
|
348
|
+
#badge .rec { width: 8px; height: 8px; border-radius: 50%; background: #fb7185; box-shadow: 0 0 9px #fb7185; animation: pulsa 1.1s infinite; }
|
|
349
|
+
#carimbo { position: absolute; left: 16px; bottom: 13px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #94a3b8; font-size: 12px; padding: 5px 13px; border-radius: 999px; display: none; max-width: 82%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
350
|
+
#carimbo b { color: #22d3ee; font-weight: 600; }
|
|
280
351
|
|
|
281
352
|
/* ── Linha do tempo ── */
|
|
282
|
-
#historico { width: min(1120px, 100%); padding-top:
|
|
283
|
-
#filme { display: flex; gap:
|
|
284
|
-
.foto { flex: none; border: 1.5px solid #
|
|
285
|
-
.foto:hover { opacity: 1; transform: translateY(-2px); border-color: #
|
|
353
|
+
#historico { width: min(1120px, 100%); padding-top: 18px; }
|
|
354
|
+
#filme { display: flex; gap: 9px; padding: 4px 2px 13px; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
|
|
355
|
+
.foto { flex: none; border: 1.5px solid #1e293b; border-radius: 9px; background: #000; cursor: pointer; padding: 0; width: 100px; height: 60px; overflow: hidden; opacity: .6; transition: transform .16s ease, opacity .16s ease, border-color .16s ease, box-shadow .16s ease; }
|
|
356
|
+
.foto:hover { opacity: 1; transform: translateY(-2px); border-color: #475569; }
|
|
286
357
|
.foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
287
|
-
.foto.ativa { border-color: #
|
|
288
|
-
|
|
289
|
-
#cartao { background: #
|
|
290
|
-
#cartao header { display: flex; align-items: center; justify-content: space-between; padding:
|
|
291
|
-
#cartao header span b { color: #
|
|
292
|
-
#passos { max-height:
|
|
293
|
-
#passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #
|
|
294
|
-
.item { padding: 5px
|
|
295
|
-
.item:hover { background: #
|
|
296
|
-
.item .n { color: #
|
|
297
|
-
.item .ok { color: #
|
|
298
|
-
.item .txt { flex: 1; word-break: break-word; color: #
|
|
299
|
-
.item .txt .erro { color: #
|
|
300
|
-
.item .hora { color: #
|
|
301
|
-
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #
|
|
302
|
-
.item.pedido { background: #
|
|
303
|
-
.item.pedido .txt { color: #
|
|
304
|
-
.item.pedido .chev { color: #
|
|
305
|
-
|
|
306
|
-
footer { color: #
|
|
307
|
-
footer b { color: #
|
|
358
|
+
.foto.ativa { border-color: #22d3ee; opacity: 1; box-shadow: 0 0 0 2px rgba(34,211,238,.3), 0 6px 18px rgba(2,6,23,.6); }
|
|
359
|
+
|
|
360
|
+
#cartao { background: #0f172a; border: 1px solid #1e293b; border-radius: 16px; overflow: hidden; }
|
|
361
|
+
#cartao header { display: flex; align-items: center; justify-content: space-between; padding: 11px 17px; border-bottom: 1px solid #16223a; color: #94a3b8; font-size: 13px; font-weight: 600; letter-spacing: .3px; }
|
|
362
|
+
#cartao header span b { color: #22d3ee; }
|
|
363
|
+
#passos { max-height: 236px; overflow-y: auto; padding: 5px 0 9px; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
|
|
364
|
+
#passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 4px; }
|
|
365
|
+
.item { padding: 5px 17px; display: flex; gap: 10px; font-size: 13.5px; align-items: baseline; transition: background .12s ease; }
|
|
366
|
+
.item:hover { background: #131f35; }
|
|
367
|
+
.item .n { color: #475569; min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }
|
|
368
|
+
.item .ok { color: #22d3ee; } .item .falha { color: #fb7185; }
|
|
369
|
+
.item .txt { flex: 1; word-break: break-word; color: #cbd5e1; }
|
|
370
|
+
.item .txt .erro { color: #fb7185; font-size: 12px; display: block; margin-top: 1px; }
|
|
371
|
+
.item .hora { color: #475569; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
372
|
+
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #f1f5f9; }
|
|
373
|
+
.item.pedido { background: #0c1526; border-top: 1px solid #16223a; border-bottom: 1px solid #16223a; margin: 6px 0; padding: 8px 17px; }
|
|
374
|
+
.item.pedido .txt { color: #f1f5f9; font-weight: 600; }
|
|
375
|
+
.item.pedido .chev { color: #22d3ee; font-weight: 800; }
|
|
376
|
+
|
|
377
|
+
footer { color: #475569; font-size: 12px; padding-top: 17px; }
|
|
378
|
+
footer b { color: #64748b; font-weight: 600; }
|
|
308
379
|
</style></head>
|
|
309
380
|
<body>
|
|
310
381
|
<div id="topo">
|
|
311
382
|
<div class="mascote">${MASCOTE_SVG}</div>
|
|
312
|
-
<h1>
|
|
313
|
-
<span class="vivo"><span class="
|
|
383
|
+
<h1>Primo<span>code</span></h1>
|
|
384
|
+
<span class="vivo"><span class="pulso"></span><span id="st">conectando…</span></span>
|
|
314
385
|
</div>
|
|
315
386
|
|
|
316
387
|
<div id="janela">
|
|
@@ -325,11 +396,12 @@ const PAGINA = `<!doctype html>
|
|
|
325
396
|
<span class="hora" id="cmdhora"></span>
|
|
326
397
|
</div>
|
|
327
398
|
<section id="tela">
|
|
328
|
-
<img id="frame" alt="tela
|
|
399
|
+
<img id="frame" alt="tela do computador, ao vivo">
|
|
329
400
|
<div id="aviso">
|
|
330
|
-
<div class="titulo">
|
|
331
|
-
<div class="sub">Dê uma instrução no terminal do PrimoCode e
|
|
401
|
+
<div class="titulo">A tela do computador aparece aqui, em vídeo</div>
|
|
402
|
+
<div class="sub">Dê uma instrução no terminal do PrimoCode e assista ele trabalhando, ao vivo.<br>Cada app que ele abrir vira uma aba lá em cima — clique nelas para rever o que já foi feito.</div>
|
|
332
403
|
</div>
|
|
404
|
+
<div id="badge"><span class="rec"></span>AO VIVO</div>
|
|
333
405
|
<div id="carimbo"></div>
|
|
334
406
|
</section>
|
|
335
407
|
</div>
|
|
@@ -345,12 +417,35 @@ const PAGINA = `<!doctype html>
|
|
|
345
417
|
|
|
346
418
|
<script>
|
|
347
419
|
const VIVO = 0; // id da aba "Ao vivo"
|
|
348
|
-
let dados = { abas: [], frames: [], log: [], instrucoes: [] };
|
|
420
|
+
let dados = { abas: [], frames: [], log: [], instrucoes: [], liveSeq: 0 };
|
|
349
421
|
let abaSel = VIVO; // aba escolhida pelo usuário
|
|
350
|
-
let fotoSel = null; // frame fixado
|
|
422
|
+
let fotoSel = null; // frame fixado (null = vídeo/mais recente)
|
|
351
423
|
let ultimoEstado = '';
|
|
352
424
|
|
|
353
|
-
|
|
425
|
+
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
426
|
+
|
|
427
|
+
// ── o vídeo: troca o quadro assim que um novo existe, sem piscar ──
|
|
428
|
+
let quadroNaTela = -1;
|
|
429
|
+
function modoVideo() { return abaSel === VIVO && fotoSel == null; }
|
|
430
|
+
function videoTick() {
|
|
431
|
+
if (modoVideo() && dados.liveSeq > 0 && dados.liveSeq !== quadroNaTela) {
|
|
432
|
+
const seq = dados.liveSeq;
|
|
433
|
+
const pre = new Image(); // pré-carrega fora da tela:
|
|
434
|
+
pre.onload = () => { // o quadro só entra inteiro
|
|
435
|
+
if (!modoVideo()) return;
|
|
436
|
+
const img = document.getElementById('frame');
|
|
437
|
+
img.src = pre.src; img.style.display = 'block';
|
|
438
|
+
document.getElementById('aviso').style.display = 'none';
|
|
439
|
+
document.getElementById('badge').style.display = 'flex';
|
|
440
|
+
document.getElementById('carimbo').style.display = 'none';
|
|
441
|
+
quadroNaTela = seq;
|
|
442
|
+
};
|
|
443
|
+
pre.src = '/live?' + seq;
|
|
444
|
+
}
|
|
445
|
+
setTimeout(videoTick, 280);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── a instrução autopreenchendo, letra por letra ──
|
|
354
449
|
let digitando = null, instrucaoNaTela = 0;
|
|
355
450
|
function autopreencher(inst) {
|
|
356
451
|
if (!inst || inst.n === instrucaoNaTela) return;
|
|
@@ -362,14 +457,12 @@ function autopreencher(inst) {
|
|
|
362
457
|
document.getElementById('cmdhora').textContent = inst.hora;
|
|
363
458
|
let i = 0;
|
|
364
459
|
digitando = setInterval(() => {
|
|
365
|
-
i += 1 + Math.floor(Math.random() * 2);
|
|
460
|
+
i += 1 + Math.floor(Math.random() * 2); // cadência de gente digitando
|
|
366
461
|
el.textContent = inst.texto.slice(0, i);
|
|
367
462
|
if (i >= inst.texto.length) clearInterval(digitando);
|
|
368
|
-
},
|
|
463
|
+
}, 32);
|
|
369
464
|
}
|
|
370
465
|
|
|
371
|
-
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
372
|
-
|
|
373
466
|
function framesDaAba(id) {
|
|
374
467
|
return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
|
|
375
468
|
}
|
|
@@ -378,46 +471,52 @@ function logDaAba(id) {
|
|
|
378
471
|
}
|
|
379
472
|
|
|
380
473
|
function render() {
|
|
381
|
-
// Abas: "Ao vivo" +
|
|
474
|
+
// Abas: "Ao vivo" + tudo que ele abriu, separadas por |
|
|
382
475
|
const nav = document.getElementById('abas');
|
|
383
476
|
const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
|
|
384
477
|
nav.innerHTML = abas.map((a) =>
|
|
385
478
|
'<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome)
|
|
386
479
|
+ (a.passos ? '<span class="qtd">' + a.passos + '</span>' : '') + '</button>'
|
|
387
480
|
).join('<span class="sep">|</span>');
|
|
388
|
-
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => {
|
|
481
|
+
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => {
|
|
482
|
+
abaSel = Number(b.dataset.id); fotoSel = null; quadroNaTela = -1; render();
|
|
483
|
+
});
|
|
389
484
|
const abaObj = abas.find((a) => a.id === abaSel) || abas[0];
|
|
390
485
|
document.getElementById('abanome').textContent = abaObj.nome;
|
|
391
486
|
|
|
392
|
-
//
|
|
487
|
+
// Fora do modo vídeo (aba antiga ou foto fixada): mostra o quadro daquela aba
|
|
393
488
|
const fs = framesDaAba(abaSel);
|
|
394
|
-
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
395
489
|
const img = document.getElementById('frame');
|
|
396
490
|
const aviso = document.getElementById('aviso');
|
|
491
|
+
const badge = document.getElementById('badge');
|
|
397
492
|
const carimbo = document.getElementById('carimbo');
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
img.src = '/frame?n=' + atual.id;
|
|
403
|
-
img.
|
|
493
|
+
if (!modoVideo()) {
|
|
494
|
+
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
495
|
+
badge.style.display = 'none';
|
|
496
|
+
if (atual) {
|
|
497
|
+
if (img.dataset.n !== 'f' + atual.id) { img.src = '/frame?n=' + atual.id; img.dataset.n = 'f' + atual.id; }
|
|
498
|
+
img.style.display = 'block'; aviso.style.display = 'none';
|
|
499
|
+
carimbo.style.display = 'block';
|
|
500
|
+
carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao)
|
|
501
|
+
+ (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar</b>' : '');
|
|
502
|
+
} else {
|
|
503
|
+
img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
|
|
404
504
|
}
|
|
405
|
-
img.style.display = 'block'; aviso.style.display = 'none';
|
|
406
|
-
carimbo.style.display = 'block';
|
|
407
|
-
carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao) + (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar ao ao-vivo</b>' : '');
|
|
408
505
|
} else {
|
|
409
|
-
|
|
410
|
-
if (dados.erroFoto) aviso.querySelector('.sub').textContent = 'Sem
|
|
506
|
+
carimbo.style.display = 'none';
|
|
507
|
+
if (dados.erroFoto) { aviso.style.display = 'block'; aviso.querySelector('.sub').textContent = 'Sem imagem da tela: ' + dados.erroFoto; }
|
|
411
508
|
}
|
|
412
509
|
|
|
413
510
|
// Filmstrip da aba (as últimas 16)
|
|
414
511
|
const filme = document.getElementById('filme');
|
|
415
512
|
const tira = fs.slice(-16);
|
|
513
|
+
const marcada = fotoSel != null ? fotoSel : -1;
|
|
416
514
|
filme.innerHTML = tira.map((f) =>
|
|
417
|
-
'<button class="foto' + (
|
|
515
|
+
'<button class="foto' + (f.id === marcada ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
|
|
418
516
|
filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
|
|
419
517
|
const id = Number(b.dataset.id);
|
|
420
|
-
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao
|
|
518
|
+
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao vídeo
|
|
519
|
+
quadroNaTela = -1;
|
|
421
520
|
render();
|
|
422
521
|
});
|
|
423
522
|
if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
|
|
@@ -436,7 +535,7 @@ function render() {
|
|
|
436
535
|
+ '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
|
|
437
536
|
+ '<span class="hora">' + e.hora + '</span></div>';
|
|
438
537
|
}).join('');
|
|
439
|
-
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
|
|
538
|
+
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); quadroNaTela = -1; render(); });
|
|
440
539
|
}
|
|
441
540
|
|
|
442
541
|
async function tick() {
|
|
@@ -450,9 +549,10 @@ async function tick() {
|
|
|
450
549
|
} catch (e) {
|
|
451
550
|
document.getElementById('st').textContent = 'reconectando…';
|
|
452
551
|
}
|
|
453
|
-
setTimeout(tick,
|
|
552
|
+
setTimeout(tick, 600);
|
|
454
553
|
}
|
|
455
554
|
tick();
|
|
555
|
+
videoTick();
|
|
456
556
|
</script>
|
|
457
557
|
</body></html>`;
|
|
458
558
|
|
|
@@ -461,12 +561,13 @@ function start() {
|
|
|
461
561
|
if (state.on) return Promise.resolve(state.url);
|
|
462
562
|
try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
|
|
463
563
|
// Fotos de sessões passadas não pertencem a esta linha do tempo.
|
|
464
|
-
try { for (const f of fs.readdirSync(DIR)) if (/^frame-\d
|
|
564
|
+
try { for (const f of fs.readdirSync(DIR)) if (/^(frame-\d+|live|live-tmp)\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
|
|
465
565
|
|
|
466
566
|
return new Promise((resolve, reject) => {
|
|
467
567
|
const server = http.createServer((req, res) => {
|
|
468
568
|
const u = new URL(req.url || '/', 'http://x');
|
|
469
569
|
if (u.pathname === '/state') {
|
|
570
|
+
viuAgora(); // página aberta = vídeo ligado
|
|
470
571
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
471
572
|
res.end(JSON.stringify({
|
|
472
573
|
erroFoto: state.erroFoto,
|
|
@@ -474,9 +575,19 @@ function start() {
|
|
|
474
575
|
frames: state.frames,
|
|
475
576
|
log: state.log.slice(-120),
|
|
476
577
|
instrucoes: state.instrucoes.slice(-20),
|
|
578
|
+
liveSeq: state.liveSeq,
|
|
477
579
|
}));
|
|
478
580
|
return;
|
|
479
581
|
}
|
|
582
|
+
if (u.pathname === '/live') {
|
|
583
|
+
viuAgora();
|
|
584
|
+
fs.readFile(LIVE(), (err, buf) => {
|
|
585
|
+
if (err) { res.writeHead(404); res.end(); return; }
|
|
586
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' });
|
|
587
|
+
res.end(buf);
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
480
591
|
if (u.pathname === '/frame') {
|
|
481
592
|
// O id vem de fora: só serve o arquivo se for número — nada de
|
|
482
593
|
// path traversal por aqui.
|
|
@@ -512,6 +623,7 @@ function start() {
|
|
|
512
623
|
|
|
513
624
|
function stop() {
|
|
514
625
|
if (state.server) { try { state.server.close(); } catch {} }
|
|
626
|
+
if (state.liveTimer) { clearTimeout(state.liveTimer); state.liveTimer = null; }
|
|
515
627
|
state.server = null;
|
|
516
628
|
state.on = false;
|
|
517
629
|
state.url = '';
|
package/lib/ui.js
CHANGED
|
@@ -161,9 +161,9 @@ const TIPS = [
|
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
164
|
+
'/streaming agora é VÍDEO ao vivo — assista ele trabalhando, aba por aba',
|
|
165
|
+
'Comandos se autopreenchem: /str + Tab (ou Enter) vira /streaming',
|
|
166
|
+
'Ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.14.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|