primocode 8.27.0 → 8.31.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 +232 -2
- package/lib/catalogo.js +4 -0
- package/lib/conta.js +197 -0
- package/lib/icone.js +247 -0
- package/lib/instalar-app.js +231 -0
- package/lib/nuvem.js +407 -0
- package/lib/remotion.js +463 -0
- package/lib/tools.js +30 -0
- package/package.json +2 -2
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/ferramentas/corte.md +31 -0
package/lib/icone.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
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 Primo, o mesmo do terminal: cabeça de topo arredondado, ombros largos,
|
|
86
|
+
* dois pezinhos. Lá ele é feito de glifos de bloco:
|
|
87
|
+
*
|
|
88
|
+
* ▐▛███▜▌ a cabeça
|
|
89
|
+
* ▝▜█████▛▘ o corpo, mais largo
|
|
90
|
+
* ▘▘ ▝▝ os pés
|
|
91
|
+
*
|
|
92
|
+
* Aqui são as mesmas três peças em proporção, e não uma transliteração dos
|
|
93
|
+
* glifos: meio bloco de terminal vira escada num ícone de 512px. O que faz
|
|
94
|
+
* reconhecer é a SILHUETA — topo redondo, ombro saliente, dois pés —, e ela é
|
|
95
|
+
* a mesma.
|
|
96
|
+
*/
|
|
97
|
+
function desenharPrimo(n) {
|
|
98
|
+
const t = tela(n);
|
|
99
|
+
const u = n / 32; // a grade do desenho, em pixels
|
|
100
|
+
|
|
101
|
+
// A base: o quadrado escuro do app, com o mesmo arredondado do ícone de
|
|
102
|
+
// sistema (22% do lado é o raio que o macOS e o Windows usam hoje).
|
|
103
|
+
caixa(t, 0, 0, n, n, n * 0.22, FUNDO);
|
|
104
|
+
|
|
105
|
+
// Um brilho fraco atrás do bicho — o mesmo halo do fundo do app.
|
|
106
|
+
for (let y = 0; y < n; y++) {
|
|
107
|
+
for (let x = 0; x < n; x++) {
|
|
108
|
+
const dx = (x - n / 2) / (n * 0.6);
|
|
109
|
+
const dy = (y - n * 0.34) / (n * 0.6);
|
|
110
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
111
|
+
if (d < 1) ponto(t, x, y, AZUL, 0.11 * (1 - d) * (1 - d));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cabeça: topo arredondado, ombros retos embaixo (raio só em cima é o que
|
|
116
|
+
// dá a silhueta do ▐▛███▜▌ — redondo por cima, quadrado por baixo).
|
|
117
|
+
caixa(t, 9 * u, 7 * u, 14 * u, 11 * u, 3.4 * u, AZUL);
|
|
118
|
+
caixa(t, 9 * u, 14 * u, 14 * u, 4 * u, 0.6 * u, AZUL);
|
|
119
|
+
|
|
120
|
+
// Corpo: mais largo que a cabeça, que é o ombro marcado do desenho.
|
|
121
|
+
caixa(t, 6 * u, 17.6 * u, 20 * u, 7.6 * u, 2.2 * u, AZUL);
|
|
122
|
+
|
|
123
|
+
// Os dois pés.
|
|
124
|
+
caixa(t, 10 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
|
|
125
|
+
caixa(t, 17.6 * u, 24.6 * u, 4.4 * u, 2.6 * u, 1 * u, AZUL_FUNDO);
|
|
126
|
+
|
|
127
|
+
// Os olhos, no negativo — é o que faz o bloco virar bicho.
|
|
128
|
+
caixa(t, 12.4 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
|
|
129
|
+
caixa(t, 17 * u, 10.6 * u, 2.6 * u, 3.2 * u, 1.1 * u, FUNDO);
|
|
130
|
+
|
|
131
|
+
// A luz de cima: uma faixa clara na testa e no ombro. Sem ela o bicho é
|
|
132
|
+
// uma silhueta chapada — é o mesmo bisel do "vidro" do estúdio.
|
|
133
|
+
caixa(t, 10.4 * u, 8.2 * u, 11.2 * u, 1.4 * u, 0.7 * u, BRILHO);
|
|
134
|
+
caixa(t, 7.6 * u, 18.6 * u, 16.8 * u, 1.2 * u, 0.6 * u, BRILHO);
|
|
135
|
+
|
|
136
|
+
return t;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── PNG ──────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const TABELA_CRC = (() => {
|
|
142
|
+
const t = new Int32Array(256);
|
|
143
|
+
for (let n = 0; n < 256; n++) {
|
|
144
|
+
let c = n;
|
|
145
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
146
|
+
t[n] = c;
|
|
147
|
+
}
|
|
148
|
+
return t;
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
function crc32(buf) {
|
|
152
|
+
let c = -1;
|
|
153
|
+
for (let i = 0; i < buf.length; i++) c = TABELA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
154
|
+
return (c ^ -1) >>> 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pedaco(tipo, dados) {
|
|
158
|
+
const cabeca = Buffer.alloc(8);
|
|
159
|
+
cabeca.writeUInt32BE(dados.length, 0);
|
|
160
|
+
cabeca.write(tipo, 4, 'latin1');
|
|
161
|
+
const crc = Buffer.alloc(4);
|
|
162
|
+
crc.writeUInt32BE(crc32(Buffer.concat([Buffer.from(tipo, 'latin1'), dados])), 0);
|
|
163
|
+
return Buffer.concat([cabeca, dados, crc]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function paraPng(t) {
|
|
167
|
+
const linha = t.n * 4;
|
|
168
|
+
// Cada linha do PNG começa com o byte do filtro. Zero — "sem filtro" — é o
|
|
169
|
+
// certo aqui: o desenho é de blocos chapados, o zlib já o comprime bem, e
|
|
170
|
+
// filtro melhoraria bytes num arquivo que já sai com poucos KB.
|
|
171
|
+
const cru = Buffer.alloc((linha + 1) * t.n);
|
|
172
|
+
for (let y = 0; y < t.n; y++) {
|
|
173
|
+
cru[y * (linha + 1)] = 0;
|
|
174
|
+
t.px.copy(cru, y * (linha + 1) + 1, y * linha, (y + 1) * linha);
|
|
175
|
+
}
|
|
176
|
+
const ihdr = Buffer.alloc(13);
|
|
177
|
+
ihdr.writeUInt32BE(t.n, 0);
|
|
178
|
+
ihdr.writeUInt32BE(t.n, 4);
|
|
179
|
+
ihdr[8] = 8; // 8 bits por canal
|
|
180
|
+
ihdr[9] = 6; // cor 6 = RGBA
|
|
181
|
+
return Buffer.concat([
|
|
182
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
183
|
+
pedaco('IHDR', ihdr),
|
|
184
|
+
pedaco('IDAT', zlib.deflateSync(cru, { level: 9 })),
|
|
185
|
+
pedaco('IEND', Buffer.alloc(0)),
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** O mascote em PNG, no tamanho pedido. */
|
|
190
|
+
function png(tamanho) {
|
|
191
|
+
return paraPng(desenharPrimo(tamanho));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── .icns (macOS) e .ico (Windows) ───────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/* Os dois são containers de PNGs, e é por isso que cabem em vinte linhas cada.
|
|
197
|
+
* Os tamanhos não são gosto: são os que cada sistema procura. Faltando um, ele
|
|
198
|
+
* escala o vizinho e o ícone sai borrado justamente na barra de tarefas. */
|
|
199
|
+
const TIPOS_ICNS = [
|
|
200
|
+
['icp4', 16], ['icp5', 32], ['icp6', 64],
|
|
201
|
+
['ic07', 128], ['ic08', 256], ['ic09', 512], ['ic10', 1024],
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
function icns() {
|
|
205
|
+
const partes = [];
|
|
206
|
+
for (const [tipo, tam] of TIPOS_ICNS) {
|
|
207
|
+
const dados = png(tam);
|
|
208
|
+
const cab = Buffer.alloc(8);
|
|
209
|
+
cab.write(tipo, 0, 'latin1');
|
|
210
|
+
cab.writeUInt32BE(dados.length + 8, 4);
|
|
211
|
+
partes.push(cab, dados);
|
|
212
|
+
}
|
|
213
|
+
const corpo = Buffer.concat(partes);
|
|
214
|
+
const cabeca = Buffer.alloc(8);
|
|
215
|
+
cabeca.write('icns', 0, 'latin1');
|
|
216
|
+
cabeca.writeUInt32BE(corpo.length + 8, 4);
|
|
217
|
+
return Buffer.concat([cabeca, corpo]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const TAMANHOS_ICO = [16, 32, 48, 64, 128, 256];
|
|
221
|
+
|
|
222
|
+
function ico() {
|
|
223
|
+
const imagens = TAMANHOS_ICO.map((t) => ({ t, dados: png(t) }));
|
|
224
|
+
const cabeca = Buffer.alloc(6);
|
|
225
|
+
cabeca.writeUInt16LE(0, 0);
|
|
226
|
+
cabeca.writeUInt16LE(1, 2); // 1 = ícone
|
|
227
|
+
cabeca.writeUInt16LE(imagens.length, 4);
|
|
228
|
+
|
|
229
|
+
let deslocamento = 6 + imagens.length * 16;
|
|
230
|
+
const entradas = [];
|
|
231
|
+
for (const im of imagens) {
|
|
232
|
+
const e = Buffer.alloc(16);
|
|
233
|
+
// 256 se escreve como 0 no .ico — o campo tem um byte só.
|
|
234
|
+
e[0] = im.t >= 256 ? 0 : im.t;
|
|
235
|
+
e[1] = im.t >= 256 ? 0 : im.t;
|
|
236
|
+
e[2] = 0; // paleta: nenhuma
|
|
237
|
+
e.writeUInt16LE(1, 4); // planos
|
|
238
|
+
e.writeUInt16LE(32, 6); // bits por pixel
|
|
239
|
+
e.writeUInt32LE(im.dados.length, 8);
|
|
240
|
+
e.writeUInt32LE(deslocamento, 12);
|
|
241
|
+
deslocamento += im.dados.length;
|
|
242
|
+
entradas.push(e);
|
|
243
|
+
}
|
|
244
|
+
return Buffer.concat([cabeca, ...entradas, ...imagens.map((i) => i.dados)]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { png, icns, ico, AZUL, FUNDO, TAMANHOS_ICO, TIPOS_ICNS };
|
|
@@ -0,0 +1,231 @@
|
|
|
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 };
|