primocode 9.9.0 → 10.0.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.
Files changed (63) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/ende.js +2 -0
  12. package/lib/estudio/estilo.js +51 -0
  13. package/lib/estudio/glifos.js +215 -0
  14. package/lib/estudio/index.js +63 -11
  15. package/lib/estudio/kit/base.jsx +285 -0
  16. package/lib/estudio/kit/index.jsx +11 -0
  17. package/lib/estudio/kit/marca.jsx +741 -0
  18. package/lib/estudio/kit/vox.jsx +357 -0
  19. package/lib/estudio/kit.js +72 -0
  20. package/lib/estudio/lancamento.js +413 -0
  21. package/lib/estudio/morfar.js +191 -0
  22. package/lib/estudio/motion.js +333 -0
  23. package/lib/estudio/plano.js +54 -7
  24. package/lib/estudio/remotion.js +167 -52
  25. package/lib/estudio/ritmo.js +77 -0
  26. package/lib/estudio/sons-leves.js +62 -0
  27. package/lib/estudio/vox.js +385 -0
  28. package/lib/fala.js +1 -94
  29. package/lib/pipeline.js +55 -56
  30. package/lib/repeticao.js +3 -1
  31. package/lib/skills.js +8 -2
  32. package/lib/snapshot.js +111 -0
  33. package/lib/tools.js +79 -20
  34. package/lib/ui.js +13 -6
  35. package/lib/video/analise.js +89 -0
  36. package/lib/video/cli.js +74 -0
  37. package/lib/video/delta.js +90 -0
  38. package/lib/video/edits.js +379 -0
  39. package/lib/video/filters.js +247 -0
  40. package/lib/video/ops.js +322 -0
  41. package/lib/video/projeto.js +133 -0
  42. package/lib/video/render.js +221 -0
  43. package/lib/video/texto.js +142 -0
  44. package/lib/video/timeline.js +310 -0
  45. package/lib/video/transcript.js +124 -0
  46. package/lib/voz.js +75 -223
  47. package/package.json +2 -3
  48. package/skills/motion-de-primeira/SKILL.md +21 -1
  49. package/studio/texto.py +208 -0
  50. package/voz/primo_voz/__main__.py +96 -236
  51. package/voz/primo_voz/transcrever.py +31 -1
  52. package/app/ansi.js +0 -55
  53. package/app/app.css +0 -500
  54. package/app/app.js +0 -1017
  55. package/app/icone.svg +0 -6
  56. package/app/index.html +0 -220
  57. package/app/primo-marca.js +0 -192
  58. package/lib/app.js +0 -615
  59. package/lib/icone.js +0 -263
  60. package/lib/instalar-app.js +0 -231
  61. package/lib/painel.js +0 -291
  62. package/voz/primo_voz/desperta.py +0 -195
  63. package/voz/primo_voz/orbe.py +0 -406
package/lib/icone.js DELETED
@@ -1,263 +0,0 @@
1
- /**
2
- * icone.js — o mascote do PrimoCode, desenhado em pixel e empacotado.
3
- *
4
- * "Tenha um ícone, e o ícone tem que ser o mascote do PrimoCode, tem que ter
5
- * todas as cores do PrimoCode."
6
- *
7
- * ── POR QUE DESENHAR EM VEZ DE GUARDAR UM ARQUIVO ────────────────────────
8
- * Um .icns e um .ico são formatos binários com várias resoluções dentro. Podia
9
- * ter os dois no repositório, prontos — só que cada um seria uma cópia do
10
- * desenho, e mudar a cor da marca passaria a exigir abrir um editor de imagem e
11
- * lembrar dos dois. O mascote é blocado por natureza (ele nasceu de glifos de
12
- * bloco no terminal), então desenhá-lo em código é curto e a fonte é UMA.
13
- *
14
- * ── POR QUE SEM BIBLIOTECA ───────────────────────────────────────────────
15
- * O PrimoCode tem zero dependência, e isso não abre exceção para um ícone.
16
- * PNG é um cabeçalho, os pixels passados pelo `zlib` (que vem no Node) e um
17
- * CRC-32; .icns e .ico são containers que só empacotam PNGs prontos. São umas
18
- * cem linhas somando os três, contra um pacote de imagem inteiro na árvore.
19
- */
20
-
21
- 'use strict';
22
-
23
- const zlib = require('zlib');
24
-
25
- // As cores são as MESMAS do lib/ui.js e do app/app.css — a marca é uma só.
26
- const AZUL = [0x22, 0xd3, 0xee]; // cyan-400 — o azul do PrimoCode
27
- const AZUL_FUNDO = [0x08, 0x91, 0xb2]; // cyan-600 — a mesma cor recuando
28
- const FUNDO = [0x0a, 0x0f, 0x16]; // o quase-preto do app
29
- const BRILHO = [0x67, 0xe8, 0xf9]; // cyan-300 — a luz de cima
30
-
31
- // ── tela de pixels ───────────────────────────────────────────────────────
32
-
33
- function tela(n) {
34
- // RGBA, quatro bytes por pixel, começando transparente.
35
- return { n, px: Buffer.alloc(n * n * 4) };
36
- }
37
-
38
- function ponto(t, x, y, cor, alfa) {
39
- x = Math.round(x); y = Math.round(y);
40
- if (x < 0 || y < 0 || x >= t.n || y >= t.n) return;
41
- const i = (y * t.n + x) * 4;
42
- const a = alfa == null ? 1 : alfa;
43
- if (a >= 1) {
44
- t.px[i] = cor[0]; t.px[i + 1] = cor[1]; t.px[i + 2] = cor[2]; t.px[i + 3] = 255;
45
- return;
46
- }
47
- // Mistura sobre o que já está lá — é o que dá a borda macia do arredondado.
48
- const de = t.px[i + 3] / 255;
49
- const fora = a + de * (1 - a);
50
- for (let k = 0; k < 3; k++) {
51
- t.px[i + k] = Math.round((cor[k] * a + t.px[i + k] * de * (1 - a)) / (fora || 1));
52
- }
53
- t.px[i + 3] = Math.round(fora * 255);
54
- }
55
-
56
- /* Retângulo de cantos redondos, com a borda suavizada por amostragem.
57
- *
58
- * Sem a suavização o ícone fica com escada visível em 512px e, pior, some o
59
- * arredondado em 16px — que é o tamanho em que ele mais aparece (aba, barra de
60
- * tarefas, Dock recolhido). Três por três amostras bastam: nove níveis de
61
- * cobertura é mais do que o olho separa numa borda de um pixel. */
62
- function caixa(t, x0, y0, l, a, r, cor) {
63
- const x1 = x0 + l, y1 = y0 + a;
64
- for (let y = Math.floor(y0) - 1; y <= Math.ceil(y1) + 1; y++) {
65
- for (let x = Math.floor(x0) - 1; x <= Math.ceil(x1) + 1; x++) {
66
- let dentro = 0;
67
- for (let sy = 0; sy < 3; sy++) {
68
- for (let sx = 0; sx < 3; sx++) {
69
- const px = x + (sx + 0.5) / 3;
70
- const py = y + (sy + 0.5) / 3;
71
- if (px < x0 || px > x1 || py < y0 || py > y1) continue;
72
- // Só os quatro cantos têm curva; o miolo é reto.
73
- const cx = px < x0 + r ? x0 + r : (px > x1 - r ? x1 - r : px);
74
- const cy = py < y0 + r ? y0 + r : (py > y1 - r ? y1 - r : py);
75
- if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) dentro++;
76
- }
77
- }
78
- if (dentro) ponto(t, x, y, cor, dentro / 9);
79
- }
80
- }
81
- }
82
-
83
- // ── o mascote ────────────────────────────────────────────────────────────
84
-
85
- /* O DESENHO VEM DE UM LUGAR SÓ.
86
- *
87
- * Antes esta função desenhava um bicho à mão — cabeça arredondada, ombros,
88
- * dois pés, olhos no negativo — e o app e o site desenhavam OUTRO, a matriz de
89
- * pixels do logo que o dono mandou. Dois desenhos com o mesmo nome: o ícone no
90
- * Dock não era o bicho que aparecia dentro do programa.
91
- *
92
- * Agora os dois leem a mesma `MATRIZ` do `app/primo-marca.js`. Mudar o logo é
93
- * mudar onze linhas de texto, e o Dock, a janela e o site mudam juntos.
94
- *
95
- * (Aquele arquivo é escrito para o navegador, mas se pendura no `globalThis`
96
- * quando não há `window` — então dá para carregá-lo aqui sem uma segunda
97
- * versão dele.) */
98
- require('../app/primo-marca.js');
99
- const MARCA = (typeof globalThis !== 'undefined' && globalThis.PrimoMarca) || null;
100
-
101
- /** #5ec8ef → [94, 200, 239] */
102
- function doHex(h) {
103
- const n = parseInt(String(h).replace('#', ''), 16);
104
- return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
105
- }
106
-
107
- function desenharPrimo(n) {
108
- const t = tela(n);
109
-
110
- // A base: o quadrado escuro do app, com o mesmo arredondado que o macOS e
111
- // o Windows usam hoje (22% do lado). É o formato de azulejo do estilo
112
- // pedido — glifo chapado, centrado, sobre cor sólida.
113
- caixa(t, 0, 0, n, n, n * 0.22, FUNDO);
114
-
115
- // Um brilho fraco atrás do bicho, para o azulejo não ficar chapado.
116
- for (let y = 0; y < n; y++) {
117
- for (let x = 0; x < n; x++) {
118
- const dx = (x - n / 2) / (n * 0.62);
119
- const dy = (y - n * 0.42) / (n * 0.62);
120
- const d = Math.sqrt(dx * dx + dy * dy);
121
- if (d < 1) ponto(t, x, y, AZUL, 0.10 * (1 - d) * (1 - d));
122
- }
123
- }
124
-
125
- if (!MARCA) return t; // sem a matriz, fica o azulejo
126
-
127
- const linhas = MARCA.MATRIZ;
128
- const colunas = linhas[0].length;
129
- const corpo = doHex(MARCA.CORPO);
130
- const detalhe = doHex(MARCA.DETALHE);
131
-
132
- /* O bicho ocupa 62% do azulejo. Mais que isso ele encosta na borda
133
- * arredondada e o ícone fica apertado; menos, e some no tamanho de 16px da
134
- * barra de tarefas — que é onde ele mais aparece. */
135
- const lado = Math.floor((n * 0.62) / colunas);
136
- const largura = lado * colunas;
137
- const altura = lado * linhas.length;
138
- const x0 = Math.round((n - largura) / 2);
139
- const y0 = Math.round((n - altura) / 2);
140
-
141
- for (let f = 0; f < linhas.length; f++) {
142
- for (let cIdx = 0; cIdx < colunas; cIdx++) {
143
- const ch = linhas[f][cIdx];
144
- if (ch !== 'C' && ch !== 'D') continue;
145
- // Sem raio: pixel art com canto redondo deixa de ser pixel art, e
146
- // vira um monte de bolinhas quando o quadrado é pequeno.
147
- caixa(t, x0 + cIdx * lado, y0 + f * lado, lado, lado, 0,
148
- ch === 'C' ? corpo : detalhe);
149
- }
150
- }
151
-
152
- return t;
153
- }
154
-
155
- // ── PNG ──────────────────────────────────────────────────────────────────
156
-
157
- const TABELA_CRC = (() => {
158
- const t = new Int32Array(256);
159
- for (let n = 0; n < 256; n++) {
160
- let c = n;
161
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
162
- t[n] = c;
163
- }
164
- return t;
165
- })();
166
-
167
- function crc32(buf) {
168
- let c = -1;
169
- for (let i = 0; i < buf.length; i++) c = TABELA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
170
- return (c ^ -1) >>> 0;
171
- }
172
-
173
- function pedaco(tipo, dados) {
174
- const cabeca = Buffer.alloc(8);
175
- cabeca.writeUInt32BE(dados.length, 0);
176
- cabeca.write(tipo, 4, 'latin1');
177
- const crc = Buffer.alloc(4);
178
- crc.writeUInt32BE(crc32(Buffer.concat([Buffer.from(tipo, 'latin1'), dados])), 0);
179
- return Buffer.concat([cabeca, dados, crc]);
180
- }
181
-
182
- function paraPng(t) {
183
- const linha = t.n * 4;
184
- // Cada linha do PNG começa com o byte do filtro. Zero — "sem filtro" — é o
185
- // certo aqui: o desenho é de blocos chapados, o zlib já o comprime bem, e
186
- // filtro melhoraria bytes num arquivo que já sai com poucos KB.
187
- const cru = Buffer.alloc((linha + 1) * t.n);
188
- for (let y = 0; y < t.n; y++) {
189
- cru[y * (linha + 1)] = 0;
190
- t.px.copy(cru, y * (linha + 1) + 1, y * linha, (y + 1) * linha);
191
- }
192
- const ihdr = Buffer.alloc(13);
193
- ihdr.writeUInt32BE(t.n, 0);
194
- ihdr.writeUInt32BE(t.n, 4);
195
- ihdr[8] = 8; // 8 bits por canal
196
- ihdr[9] = 6; // cor 6 = RGBA
197
- return Buffer.concat([
198
- Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
199
- pedaco('IHDR', ihdr),
200
- pedaco('IDAT', zlib.deflateSync(cru, { level: 9 })),
201
- pedaco('IEND', Buffer.alloc(0)),
202
- ]);
203
- }
204
-
205
- /** O mascote em PNG, no tamanho pedido. */
206
- function png(tamanho) {
207
- return paraPng(desenharPrimo(tamanho));
208
- }
209
-
210
- // ── .icns (macOS) e .ico (Windows) ───────────────────────────────────────
211
-
212
- /* Os dois são containers de PNGs, e é por isso que cabem em vinte linhas cada.
213
- * Os tamanhos não são gosto: são os que cada sistema procura. Faltando um, ele
214
- * escala o vizinho e o ícone sai borrado justamente na barra de tarefas. */
215
- const TIPOS_ICNS = [
216
- ['icp4', 16], ['icp5', 32], ['icp6', 64],
217
- ['ic07', 128], ['ic08', 256], ['ic09', 512], ['ic10', 1024],
218
- ];
219
-
220
- function icns() {
221
- const partes = [];
222
- for (const [tipo, tam] of TIPOS_ICNS) {
223
- const dados = png(tam);
224
- const cab = Buffer.alloc(8);
225
- cab.write(tipo, 0, 'latin1');
226
- cab.writeUInt32BE(dados.length + 8, 4);
227
- partes.push(cab, dados);
228
- }
229
- const corpo = Buffer.concat(partes);
230
- const cabeca = Buffer.alloc(8);
231
- cabeca.write('icns', 0, 'latin1');
232
- cabeca.writeUInt32BE(corpo.length + 8, 4);
233
- return Buffer.concat([cabeca, corpo]);
234
- }
235
-
236
- const TAMANHOS_ICO = [16, 32, 48, 64, 128, 256];
237
-
238
- function ico() {
239
- const imagens = TAMANHOS_ICO.map((t) => ({ t, dados: png(t) }));
240
- const cabeca = Buffer.alloc(6);
241
- cabeca.writeUInt16LE(0, 0);
242
- cabeca.writeUInt16LE(1, 2); // 1 = ícone
243
- cabeca.writeUInt16LE(imagens.length, 4);
244
-
245
- let deslocamento = 6 + imagens.length * 16;
246
- const entradas = [];
247
- for (const im of imagens) {
248
- const e = Buffer.alloc(16);
249
- // 256 se escreve como 0 no .ico — o campo tem um byte só.
250
- e[0] = im.t >= 256 ? 0 : im.t;
251
- e[1] = im.t >= 256 ? 0 : im.t;
252
- e[2] = 0; // paleta: nenhuma
253
- e.writeUInt16LE(1, 4); // planos
254
- e.writeUInt16LE(32, 6); // bits por pixel
255
- e.writeUInt32LE(im.dados.length, 8);
256
- e.writeUInt32LE(deslocamento, 12);
257
- deslocamento += im.dados.length;
258
- entradas.push(e);
259
- }
260
- return Buffer.concat([cabeca, ...entradas, ...imagens.map((i) => i.dados)]);
261
- }
262
-
263
- module.exports = { png, icns, ico, AZUL, FUNDO, TAMANHOS_ICO, TIPOS_ICNS };
@@ -1,231 +0,0 @@
1
- /**
2
- * instalar-app.js — o PrimoCode como aplicativo do sistema.
3
- *
4
- * "Ele está abrindo no navegador, pode ser — só que eu gostaria que fosse um
5
- * aplicativo mesmo desktop, tenha um ícone, e o ícone tem que ser o mascote
6
- * do PrimoCode."
7
- *
8
- * ── O QUE ESTE ARQUIVO ENTREGA, E O QUE ELE NÃO ENTREGA ──────────────────
9
- * Ele entrega o que faz uma coisa SER um aplicativo para quem usa: entrada no
10
- * Launchpad e no Dock do Mac, no menu Iniciar e na barra de tarefas do Windows,
11
- * no menu de aplicativos do Linux. Ícone próprio, nome próprio, abre com dois
12
- * cliques, aparece no Spotlight e no Alt-Tab com a cara dele.
13
- *
14
- * O que ele NÃO faz é trocar o motor da janela. Dentro, continua sendo o
15
- * Chromium da máquina em modo `--app`, sem barra de endereço e sem abas — é o
16
- * mesmo desenho de sempre. Trocar isso por uma janela nativa exigiria Electron
17
- * ou Tauri: um download de centenas de MB, uma cadeia de compilação por
18
- * sistema e assinatura de código para o macOS e o Windows não recusarem o
19
- * binário. Isso derruba a promessa de "já instala junto com o npm", que foi
20
- * pedida na mesma frase.
21
- *
22
- * Está escrito aqui porque é a diferença que importa se um dia alguém quiser
23
- * mudar: o obstáculo não é técnico, é o tamanho da instalação.
24
- *
25
- * ── POR QUE NÃO NO `postinstall` ─────────────────────────────────────────
26
- * Um `npm i -g` que cria pasta em ~/Applications e escreve no menu Iniciar sem
27
- * ninguém pedir é um instalador se comportando mal. Quem quiser o aplicativo
28
- * digita `/app --instalar` uma vez, e o `/app` sozinho continua abrindo a
29
- * janela sem instalar nada.
30
- */
31
-
32
- 'use strict';
33
-
34
- const fs = require('fs');
35
- const os = require('os');
36
- const path = require('path');
37
- const { execFile, execFileSync } = require('child_process');
38
-
39
- const icone = require('./icone.js');
40
-
41
- const NOME = 'PrimoCode';
42
- const CASA = path.join(os.homedir(), '.primocode');
43
-
44
- function escrever(destino, dados) {
45
- fs.mkdirSync(path.dirname(destino), { recursive: true });
46
- fs.writeFileSync(destino, dados);
47
- }
48
-
49
- /* O caminho do Node e o do CLI, absolutos.
50
- *
51
- * Aplicativo aberto pelo Finder ou pelo menu Iniciar NÃO herda o PATH do
52
- * terminal: o `.zshrc` não roda, e num Mac com Node do Homebrew ou do nvm o
53
- * comando `primo` simplesmente não existe para quem clicou no ícone. O
54
- * aplicativo abria e fechava na hora, sem dizer nada. Com os dois caminhos
55
- * escritos no lançador, ele funciona vindo de qualquer lugar. */
56
- function caminhos() {
57
- return {
58
- node: process.execPath,
59
- cli: path.resolve(__dirname, '..', 'bin', 'primocode.js'),
60
- };
61
- }
62
-
63
- // ── macOS ────────────────────────────────────────────────────────────────
64
-
65
- const PLIST = (versao) => `<?xml version="1.0" encoding="UTF-8"?>
66
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
67
- <plist version="1.0">
68
- <dict>
69
- <key>CFBundleName</key><string>${NOME}</string>
70
- <key>CFBundleDisplayName</key><string>${NOME}</string>
71
- <key>CFBundleIdentifier</key><string>ai.conectaprimo.primocode</string>
72
- <key>CFBundleVersion</key><string>${versao}</string>
73
- <key>CFBundleShortVersionString</key><string>${versao}</string>
74
- <key>CFBundlePackageType</key><string>APPL</string>
75
- <key>CFBundleExecutable</key><string>${NOME}</string>
76
- <key>CFBundleIconFile</key><string>${NOME}</string>
77
- <key>NSHighResolutionCapable</key><true/>
78
- <!-- Sem isto o ícone do Dock vira um foguete genérico e o app aparece com
79
- o nome do processo. "LSUIElement" NÃO entra: com ele o aplicativo some
80
- do Dock, e o Dock é metade do que foi pedido aqui.
81
-
82
- (Aspas e não acento grave neste comentário de propósito: ele mora dentro
83
- de um template literal, e um acento grave ali FECHA a string — o erro
84
- sai vinte linhas abaixo, apontando para o lugar errado.) -->
85
- <key>LSMinimumSystemVersion</key><string>10.15</string>
86
- </dict>
87
- </plist>
88
- `;
89
-
90
- function instalarMac(versao) {
91
- const { node, cli } = caminhos();
92
- const raiz = path.join(os.homedir(), 'Applications', `${NOME}.app`);
93
- const conteudo = path.join(raiz, 'Contents');
94
-
95
- escrever(path.join(conteudo, 'Info.plist'), PLIST(versao));
96
- escrever(path.join(conteudo, 'Resources', `${NOME}.icns`), icone.icns());
97
-
98
- /* O lançador. `exec` no fim para o processo do Node HERDAR o lugar do
99
- script: sem ele o shell fica de pé só esperando, e o macOS mostra dois
100
- processos no monitor de atividade para um aplicativo só. */
101
- const bin = path.join(conteudo, 'MacOS', NOME);
102
- escrever(bin, `#!/bin/sh
103
- # Gerado pelo PrimoCode (/app --instalar). Não edite: rode o comando de novo.
104
- exec ${JSON.stringify(node)} ${JSON.stringify(cli)} --app "$@"
105
- `);
106
- fs.chmodSync(bin, 0o755);
107
-
108
- /* O macOS guarda em cache o ícone e o nome pelo caminho do bundle. Sem
109
- tocar na pasta, um app reinstalado continua mostrando o ícone velho até
110
- o sistema reiniciar — o que parece, de fora, o comando não ter
111
- funcionado. `touch` na pasta invalida esse cache. */
112
- try { fs.utimesSync(raiz, new Date(), new Date()); } catch {}
113
-
114
- return { ok: true, caminho: raiz, onde: 'Launchpad e pasta Aplicativos' };
115
- }
116
-
117
- // ── Windows ──────────────────────────────────────────────────────────────
118
-
119
- /* O atalho do Windows (.lnk) é um formato binário com estrutura própria. Em
120
- * vez de montá-lo byte a byte, quem o escreve é o próprio Windows, pelo
121
- * WScript.Shell — que existe em toda instalação desde sempre e é a API que os
122
- * instaladores usam. */
123
- function instalarWindows(versao) {
124
- const { node, cli } = caminhos();
125
- const recursos = path.join(CASA, 'app');
126
- const ico = path.join(recursos, 'primocode.ico');
127
- escrever(ico, icone.ico());
128
-
129
- /* Um .cmd abriria uma janela preta de console junto do aplicativo. O .vbs
130
- com `Run ..., 0` lança o Node com a janela escondida — é o que faz o
131
- aplicativo abrir só a janela dele, como qualquer programa. */
132
- const vbs = path.join(recursos, 'primocode.vbs');
133
- escrever(vbs, `' Gerado pelo PrimoCode (/app --instalar). Não edite: rode o comando de novo.\r
134
- Set sh = CreateObject("WScript.Shell")\r
135
- sh.Run """${node}"" ""${cli}"" --app", 0, False\r
136
- `);
137
-
138
- const menu = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
139
- 'Microsoft', 'Windows', 'Start Menu', 'Programs', `${NOME}.lnk`);
140
- const area = path.join(os.homedir(), 'Desktop', `${NOME}.lnk`);
141
-
142
- const ps = [
143
- '$w = New-Object -ComObject WScript.Shell',
144
- ...[menu, area].map((alvo) => [
145
- `$s = $w.CreateShortcut(${JSON.stringify(alvo)})`,
146
- '$s.TargetPath = "wscript.exe"',
147
- `$s.Arguments = '"${vbs}"'`,
148
- `$s.IconLocation = ${JSON.stringify(ico)}`,
149
- `$s.Description = "PrimoCode ${versao}"`,
150
- `$s.WorkingDirectory = ${JSON.stringify(path.dirname(cli))}`,
151
- '$s.Save()',
152
- ].join('; ')),
153
- ].join('; ');
154
-
155
- try {
156
- fs.mkdirSync(path.dirname(menu), { recursive: true });
157
- execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps],
158
- { stdio: 'ignore', timeout: 30000 });
159
- } catch (e) {
160
- return { ok: false, error: `não consegui criar o atalho: ${e.message}` };
161
- }
162
- return { ok: true, caminho: menu, onde: 'menu Iniciar e Área de Trabalho' };
163
- }
164
-
165
- // ── Linux ────────────────────────────────────────────────────────────────
166
-
167
- function instalarLinux(versao) {
168
- const { node, cli } = caminhos();
169
- const icones = path.join(os.homedir(), '.local', 'share', 'icons', 'hicolor');
170
- for (const t of [16, 32, 48, 64, 128, 256, 512]) {
171
- escrever(path.join(icones, `${t}x${t}`, 'apps', 'primocode.png'), icone.png(t));
172
- }
173
- const arquivo = path.join(os.homedir(), '.local', 'share', 'applications', 'primocode.desktop');
174
- escrever(arquivo, `[Desktop Entry]
175
- Type=Application
176
- Name=${NOME}
177
- Comment=Agente de engenharia em português
178
- Exec=${node} ${cli} --app
179
- Icon=primocode
180
- Terminal=false
181
- Categories=Development;Utility;
182
- StartupWMClass=primocode
183
- X-PrimoCode-Version=${versao}
184
- `);
185
- fs.chmodSync(arquivo, 0o755);
186
- // Sem atualizar o banco, o menu só mostra o aplicativo na sessão seguinte.
187
- try {
188
- execFileSync('update-desktop-database', [path.dirname(arquivo)],
189
- { stdio: 'ignore', timeout: 15000 });
190
- } catch { /* nem toda distribuição tem, e a entrada funciona sem */ }
191
- return { ok: true, caminho: arquivo, onde: 'menu de aplicativos' };
192
- }
193
-
194
- // ── porta única ──────────────────────────────────────────────────────────
195
-
196
- function instalar(versao) {
197
- try {
198
- const p = os.platform();
199
- if (p === 'darwin') return instalarMac(versao);
200
- if (p === 'win32') return instalarWindows(versao);
201
- return instalarLinux(versao);
202
- } catch (e) {
203
- return { ok: false, error: e.message };
204
- }
205
- }
206
-
207
- /** Já instalado? Serve para o /app oferecer a instalação uma vez só. */
208
- function instalado() {
209
- const p = os.platform();
210
- const alvo = p === 'darwin'
211
- ? path.join(os.homedir(), 'Applications', `${NOME}.app`)
212
- : p === 'win32'
213
- ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
214
- 'Microsoft', 'Windows', 'Start Menu', 'Programs', `${NOME}.lnk`)
215
- : path.join(os.homedir(), '.local', 'share', 'applications', 'primocode.desktop');
216
- try { return fs.existsSync(alvo) ? alvo : null; } catch { return null; }
217
- }
218
-
219
- function desinstalar() {
220
- const alvo = instalado();
221
- if (!alvo) return { ok: true, jaSaiu: true };
222
- try {
223
- fs.rmSync(alvo, { recursive: true, force: true });
224
- if (os.platform() === 'win32') {
225
- try { fs.rmSync(path.join(os.homedir(), 'Desktop', `${NOME}.lnk`), { force: true }); } catch {}
226
- }
227
- return { ok: true, caminho: alvo };
228
- } catch (e) { return { ok: false, error: e.message }; }
229
- }
230
-
231
- module.exports = { instalar, instalado, desinstalar, NOME };