primocode 8.26.0 → 8.30.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.
@@ -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 };