primocode 8.10.0 → 8.10.1

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/lib/studio.js CHANGED
@@ -84,12 +84,12 @@ function copiar(de, para) {
84
84
  for (const item of fs.readdirSync(de)) copiar(path.join(de, item), path.join(para, item));
85
85
  return;
86
86
  }
87
- let precisa = true;
88
- try {
89
- const alvo = fs.statSync(para);
90
- precisa = alvo.size !== st.size || alvo.mtimeMs < st.mtimeMs;
91
- } catch {}
92
- if (precisa) fs.copyFileSync(de, para);
87
+ // Copia SEMPRE. A comparação esperta (tamanho + data) deixava passar
88
+ // arquivo alterado de mesmo tamanho: o npm instala com data antiga
89
+ // (1985, de propósito, por reprodutibilidade) e o teste de mtime nunca
90
+ // disparava. O que o usuário criou não corre risco — arquivos/ e midia/
91
+ // não estão em COPIAVEIS.
92
+ fs.copyFileSync(de, para);
93
93
  }
94
94
 
95
95
  function prepararCasa() {
@@ -139,10 +139,64 @@ async function estaNoAr() {
139
139
  // Uma subida por vez: sem esta trava, duas tools chamadas no mesmo turno
140
140
  // disparariam dois servidores na mesma porta e o segundo morreria com
141
141
  // "address in use" — barulho por nada.
142
+ /** A assinatura do server.py que está NO DISCO — a versão que deveria rodar. */
143
+ function assinaturaDoDisco() {
144
+ try {
145
+ const corpo = fs.readFileSync(path.join(CASA, 'server.py'));
146
+ return require('crypto').createHash('sha1').update(corpo).digest('hex').slice(0, 12);
147
+ } catch { return null; }
148
+ }
149
+
150
+ /** A assinatura do processo que está NO AR — a versão que roda de fato. */
151
+ async function assinaturaNoAr() {
152
+ try { return (await pedir('GET', '/api/versao', null, 1500)).assinatura || null; }
153
+ catch { return null; } // servidor antigo não tem a rota — também é "desatualizado"
154
+ }
155
+
156
+ /**
157
+ * Derruba o Studio que está no ar. Primeiro pelo caminho limpo (/api/sair);
158
+ * se for um servidor antigo, que não tem a rota, pelo sistema.
159
+ */
160
+ async function derrubar() {
161
+ try { await pedir('POST', '/api/sair', {}, 1500); } catch {}
162
+ for (let i = 0; i < 10; i++) {
163
+ await new Promise((r) => setTimeout(r, 300));
164
+ if (!(await estaNoAr())) return true;
165
+ }
166
+ // Servidor antigo: sem /api/sair. Mata pelo padrão da linha de comando.
167
+ try {
168
+ const { execSync } = require('child_process');
169
+ if (os.platform() === 'win32') {
170
+ execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process | ' +
171
+ "Where-Object {$_.CommandLine -like '*primocode*studio*server.py*'} | " +
172
+ 'ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"', { stdio: 'ignore' });
173
+ } else {
174
+ execSync("pkill -f '[.]primocode/studio/server.py'", { stdio: 'ignore' });
175
+ }
176
+ } catch {}
177
+ for (let i = 0; i < 10; i++) {
178
+ await new Promise((r) => setTimeout(r, 300));
179
+ if (!(await estaNoAr())) return true;
180
+ }
181
+ return !(await estaNoAr());
182
+ }
183
+
142
184
  let subindo = null;
143
185
 
144
186
  async function garantirNoAr() {
145
- if (await estaNoAr()) return { ok: true, url: BASE, jaEstava: true };
187
+ if (await estaNoAr()) {
188
+ // O npm pode ter trocado o código no disco DEPOIS de este processo
189
+ // subir — e aí o Studio no ar é o antigo, servindo o bug que o
190
+ // update corrigiu. Atualiza o disco e compara as assinaturas; se
191
+ // divergirem, reinicia. Na véspera de uma apresentação, "atualizei
192
+ // mas continua igual" é o pior modo de falha que existe.
193
+ prepararCasa();
194
+ const doDisco = assinaturaDoDisco();
195
+ const noAr = await assinaturaNoAr();
196
+ if (!doDisco || (noAr && noAr === doDisco)) return { ok: true, url: BASE, jaEstava: true };
197
+ await derrubar();
198
+ // cai para o caminho de subida normal, já com o código novo
199
+ }
146
200
  if (subindo) return subindo;
147
201
 
148
202
  subindo = (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.10.0",
3
+ "version": "8.10.1",
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": {
package/studio/server.py CHANGED
@@ -9,6 +9,7 @@ Só biblioteca padrão do Python. Nada para instalar.
9
9
  """
10
10
 
11
11
  import base64
12
+ import hashlib
12
13
  import functools
13
14
  import json
14
15
  import mimetypes
@@ -30,6 +31,19 @@ FERRAMENTAS = os.path.join(RAIZ, "ferramentas")
30
31
  MIDIA = os.path.join(RAIZ, "midia")
31
32
  PORTA = int(sys.argv[1]) if len(sys.argv) > 1 else 7777
32
33
 
34
+ def _assinatura():
35
+ """SHA-1 do próprio arquivo, lido NA SUBIDA. Se o npm trocar o código no
36
+ disco depois, o processo no ar continua respondendo a assinatura antiga —
37
+ e é exatamente essa diferença que o CLI usa para saber que precisa
38
+ reiniciar o Studio em vez de servir a versão velha para sempre."""
39
+ try:
40
+ with open(os.path.abspath(__file__), "rb") as fh:
41
+ return hashlib.sha1(fh.read()).hexdigest()[:12]
42
+ except Exception:
43
+ return "?"
44
+
45
+ ASSINATURA = _assinatura()
46
+
33
47
  # 60 MB por arquivo: cobre faixa longa em mp3 e foto de câmera sem susto
34
48
  LIMITE_MIDIA = 60 * 1024 * 1024
35
49
 
@@ -506,6 +520,9 @@ class Handler(SimpleHTTPRequestHandler):
506
520
  texto = fh.read()
507
521
  return self.txt(texto or json.dumps(info, ensure_ascii=False, indent=2))
508
522
 
523
+ if p == "/api/versao":
524
+ return self.js({"assinatura": ASSINATURA})
525
+
509
526
  if p == "/api/voz":
510
527
  return self.voz(urlparse(self.path).query)
511
528
  if p == "/api/voz/status":
@@ -752,6 +769,16 @@ class Handler(SimpleHTTPRequestHandler):
752
769
  corpo, status = guardar_midia(self.corpo())
753
770
  return self.js(corpo, status)
754
771
 
772
+ if rota == "/api/sair":
773
+ # Só o CLI chama, e só quando o código no disco mudou (update do
774
+ # npm). Sem isto o processo antigo ficava no ar servindo o bug que
775
+ # o update tinha acabado de corrigir. Responde antes de morrer,
776
+ # para quem pediu não ver erro de conexão.
777
+ self.js({"ok": True, "tchau": ASSINATURA})
778
+ import threading
779
+ threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start()
780
+ return
781
+
755
782
  if rota == "/api/arquivos":
756
783
  try:
757
784
  a = criar(self.corpo())