fantasta 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -13,12 +13,17 @@ pacchetto è firmato da GitHub, quindi non c'è Gatekeeper da aggirare.
13
13
 
14
14
  ## Cosa succede
15
15
 
16
- - Si apre la **finestra dell'app desktop** (regia);
16
+ - Al **primo avvio** su macOS viene preparata una vera **Fantasta.app** (nome,
17
+ icona e identificatore corretti) dentro `~/.fantasta/` — pochi secondi, una
18
+ volta sola;
19
+ - si apre la **finestra dell'app desktop** (regia) con l'icona e il nome di
20
+ Fantasta nel Dock;
17
21
  - si avvia il **server SQLite/LAN** locale (porta `47821`);
18
22
  - si avvia il **renderer** della regia (porta `47822`), raggiungibile anche dal
19
23
  browser dei partecipanti;
20
- - **il terminale resta attivo** mentre l'asta è in corso: chiudilo con `Ctrl+C`
21
- per fermare tutto e disconnettere i partecipanti (oppure chiudi la finestra).
24
+ - il **terminale torna subito libero**: l'app gira da sola, chiudi la **finestra**
25
+ per fermare tutto e disconnettere i partecipanti (i log finiscono in
26
+ `~/.fantasta/desktop.log`).
22
27
 
23
28
  I partecipanti si collegano con il **QR code** mostrato durante la creazione
24
29
  della lega (o col link) e seguono l'asta dal telefono. Serve una sola
@@ -39,8 +44,9 @@ npx fantasta --browser
39
44
 
40
45
  ## Dove vengono salvati i dati
41
46
 
42
- - Finestra desktop: `~/Library/Application Support/Fantasta/fantasta.db` (macOS),
43
- `%APPDATA%/Fantasta/fantasta.db` (Windows), `~/.config/Fantasta/fantasta.db` (Linux).
47
+ - Tutti i dati (database, sessione admin, log e l'app `Fantasta.app` su macOS)
48
+ stanno in `~/.fantasta/` (`FANTASTA_DATA_DIR` per cambiare cartella).
49
+ - Finestra desktop: `fantasta.db` in `~/.fantasta/` (o `FANTASTA_DATABASE_PATH`).
44
50
  - Modalità browser / sessioni: `~/.fantasta/` (`fantasta.db`, `admin-session.txt`).
45
51
 
46
52
  ## Opzioni (variabili d'ambiente)
@@ -67,9 +73,12 @@ L'"app desktop" di Fantasta è in realtà **due processi Node** (server LAN e
67
73
  renderer) avvolti da **Electron**. Il pacchetto npm include tutto e lancia il
68
74
  binario Electron ufficiale, firmato da GitHub: per questo non c'è alcun
69
75
  **avviso di sicurezza** all'apertura su macOS, Windows o Linux (niente
70
- Gatekeeper da aggirare). Se preferisci una finestra "installata" a sistema,
71
- la distribuzione installer (`.dmg`/`.exe`) resta scaricabile dalle **GitHub
72
- Releases** del progetto.
76
+ Gatekeeper da aggirare). Su macOS al primo avvio il pacchetto assembla una
77
+ vera `Fantasta.app` (copia del dist Electron con nome e icona corretti): nel
78
+ Dock compare **solo Fantasta**, senza l'icona "exec" né quella generica di
79
+ Electron. Se preferisci una finestra "installata" a sistema, la distribuzione
80
+ installer (`.dmg`/`.exe`) resta scaricabile dalle **GitHub Releases** del
81
+ progetto.
73
82
 
74
83
  ## Sviluppo in questo repo
75
84
 
package/bin/fantasta.js CHANGED
@@ -1,18 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  // Launcher "fantasta".
3
3
  //
4
- // Di default avvia la VERA app desktop: avvia Electron con le risorse incluse
5
- // (resources/app), che apre la finestra della regia e serve server SQLite/LAN
6
- // (porta 47821) e renderer (porta 47822). Il binario Electron è firmato da
7
- // GitHub, quindi NIENTE avviso Gatekeeper all'apertura.
4
+ // Su macOS assembla al primo avvio una vera Fantasta.app (copia del dist
5
+ // Electron con plist, nome e icona corretti, rifirmata adhoc) e la lancia
6
+ // in modalità detached: nel Dock compare solo Fantasta con la sua icona,
7
+ // senza il processo "exec" (che su macOS 26 appare quando un processo node
8
+ // resta vivo accanto a Electron) e senza l'icona atomo di Electron.
8
9
  //
9
- // Con `--browser` (o FANTASTA_BROWSER=1) usa la modalità legacy senza Electron:
10
- // avvia i due processi Node (local-server.cjs + renderer standalone) e apre la
11
- // regia nel browser di default. Utile su macchine senza Electron installato.
10
+ // Con `--browser` (o FANTASTA_BROWSER=1) usa la modalità legacy senza
11
+ // Electron: avvia i due processi Node (local-server.cjs + renderer standalone)
12
+ // e apre la regia nel browser di default.
12
13
  "use strict";
13
14
 
14
- const { spawn } = require("node:child_process");
15
- const { mkdir, readFile, writeFile } = require("node:fs/promises");
15
+ const { spawn, execFileSync } = require("node:child_process");
16
+ const {
17
+ access,
18
+ copyFile,
19
+ mkdir,
20
+ open,
21
+ readFile,
22
+ rm,
23
+ writeFile,
24
+ } = require("node:fs/promises");
16
25
  const { homedir } = require("node:os");
17
26
  const path = require("node:path");
18
27
 
@@ -43,6 +52,15 @@ function error(message) {
43
52
  console.error(`[fantasta] Errore: ${message}`);
44
53
  }
45
54
 
55
+ async function fileExists(filePath) {
56
+ try {
57
+ await access(filePath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
46
64
  async function waitForHealth(url, label, healthPath = "/api/health", timeoutMs = 30_000) {
47
65
  const deadline = Date.now() + timeoutMs;
48
66
  while (Date.now() < deadline) {
@@ -127,6 +145,73 @@ function electronBinary() {
127
145
  }
128
146
  }
129
147
 
148
+ function cliVersion() {
149
+ try {
150
+ return require(path.join(__dirname, "..", "package.json")).version;
151
+ } catch {
152
+ return "0.0.0";
153
+ }
154
+ }
155
+
156
+ // Assemblea una vera Fantasta.app (macOS) dentro DATA_DIR copiando il dist
157
+ // Electron e patchando plist, icona e identificatore, poi rifirma adhoc.
158
+ // Ritorna il percorso del bundle. Se il bundle esiste già e la versione di
159
+ // Electron/risorse non è cambiata, lo riusa.
160
+ async function ensureMacAppBundle(binary) {
161
+ const distApp = path.resolve(path.dirname(binary), "../.."); // .../dist/Electron.app
162
+ const electronVersion = await readFile(path.join(path.dirname(distApp), "version"), "utf8").then((v) => v.trim()).catch(() => "unknown");
163
+ const marker = `${electronVersion}|${cliVersion()}`;
164
+ const bundlePath = path.join(DATA_DIR, "Fantasta.app");
165
+ const markerPath = path.join(DATA_DIR, ".bundle-version");
166
+
167
+ try {
168
+ const current = (await readFile(markerPath, "utf8")).trim();
169
+ if (current === marker && (await fileExists(bundlePath))) return bundlePath;
170
+ } catch {
171
+ // nessun marker: si assembla
172
+ }
173
+
174
+ log("Preparazione app Fantasta (solo la prima volta, pochi secondi)...");
175
+ await rm(bundlePath, { recursive: true, force: true });
176
+
177
+ // 1. Copia il dist Electron come base del bundle. Su macOS si usa `ditto`:
178
+ // fs.cp risolverebbe i symlink relativi dei framework in percorsi
179
+ // assoluti, rendendo il bundle non autonomo (icudtl.dat non trovato).
180
+ execFileSync("ditto", [distApp, bundlePath], { stdio: "ignore" });
181
+
182
+ // 2. Patch dell'Info.plist: nome, icona, identificatore e versione.
183
+ const plist = path.join(bundlePath, "Contents", "Info.plist");
184
+ const replacements = [
185
+ ["CFBundleName", "Fantasta"],
186
+ ["CFBundleDisplayName", "Fantasta"],
187
+ ["CFBundleIdentifier", "com.fantasta.desktop"],
188
+ ["CFBundleIconFile", "icon.icns"],
189
+ ["CFBundleShortVersionString", cliVersion()],
190
+ ["CFBundleVersion", cliVersion()],
191
+ ];
192
+ for (const [key, value] of replacements) {
193
+ execFileSync("plutil", ["-replace", key, "-string", value, plist]);
194
+ }
195
+
196
+ // 3. Sostituisci l'icona di default (electron.icns) con quella di Fantasta.
197
+ await rm(path.join(bundlePath, "Contents", "Resources", "electron.icns"), { force: true });
198
+ await copyFile(path.join(appDir, "build", "icon.icns"), path.join(bundlePath, "Contents", "Resources", "icon.icns"));
199
+
200
+ // 4. Copia le risorse dell'app dentro il bundle: main.cjs/preload/icone in
201
+ // Contents/Resources/app, e server + renderer in Contents/Resources.
202
+ await rm(path.join(bundlePath, "Contents", "Resources", "app"), { recursive: true, force: true });
203
+ execFileSync("ditto", [appDir, path.join(bundlePath, "Contents", "Resources", "app")], { stdio: "ignore" });
204
+ await copyFile(serverEntry, path.join(bundlePath, "Contents", "Resources", "local-server.cjs"));
205
+ await rm(path.join(bundlePath, "Contents", "Resources", "web"), { recursive: true, force: true });
206
+ execFileSync("ditto", [path.join(resourcesRoot, "web"), path.join(bundlePath, "Contents", "Resources", "web")], { stdio: "ignore" });
207
+
208
+ // 5. Rifirma adhoc (la modifica del plist invalida la firma originale).
209
+ execFileSync("codesign", ["--force", "--deep", "--sign", "-", bundlePath], { stdio: "ignore" });
210
+
211
+ await writeFile(markerPath, marker);
212
+ return bundlePath;
213
+ }
214
+
130
215
  async function runBrowserMode() {
131
216
  log("Modalità browser (senza finestra).");
132
217
  startServer();
@@ -188,6 +273,38 @@ async function runBrowserMode() {
188
273
  }
189
274
  }
190
275
 
276
+ async function runMacDesktopMode(binary) {
277
+ try {
278
+ const bundlePath = await ensureMacAppBundle(binary);
279
+ const executable = path.join(bundlePath, "Contents", "MacOS", "Electron");
280
+ const logPath = path.join(DATA_DIR, "desktop.log");
281
+ const logFd = await open(logPath, "a");
282
+ log(`Avvio app desktop da ${bundlePath}`);
283
+ log("App desktop avviata: chiudi la finestra per fermarla.");
284
+ log(`Log: ${logPath}`);
285
+ const child = spawn(executable, [`--user-data-dir=${DATA_DIR}`], {
286
+ env: {
287
+ ...process.env,
288
+ FANTASTA_NPM_MODE: "1",
289
+ FANTASTA_HOST: HOST,
290
+ FANTASTA_PORT: LOCAL_PORT,
291
+ FANTASTA_RENDERER_PORT: RENDERER_PORT,
292
+ FANTASTA_DATABASE_PATH: DB_PATH,
293
+ FANTASTA_LOCAL_SERVER_URL: LOCAL_URL,
294
+ },
295
+ stdio: ["ignore", logFd, logFd],
296
+ detached: true,
297
+ });
298
+ // Il launcher esce subito: se restasse vivo, macOS mostrerebbe un'icona
299
+ // "exec" nel Dock accanto all'app.
300
+ child.unref();
301
+ } catch (cause) {
302
+ error(cause instanceof Error ? cause.message : String(cause));
303
+ log("Ripiego sulla modalità browser.");
304
+ return runBrowserMode();
305
+ }
306
+ }
307
+
191
308
  async function runDesktopMode() {
192
309
  const binary = electronBinary();
193
310
  if (!binary) {
@@ -196,6 +313,12 @@ async function runDesktopMode() {
196
313
  }
197
314
  // Crea la directory dei dati prima che Electron apra il database.
198
315
  await mkdir(DATA_DIR, { recursive: true });
316
+ if (process.platform === "darwin") {
317
+ await runMacDesktopMode(binary);
318
+ return;
319
+ }
320
+ // Windows/Linux: lancio del binario Electron con le risorse del pacchetto.
321
+ // (Su queste piattaforme non esiste il problema dell'icona "exec" nel Dock.)
199
322
  log(`Avvio app desktop (Electron) da ${appDir}`);
200
323
  const child = spawn(binary, [appDir], {
201
324
  env: {
@@ -233,4 +356,4 @@ async function main() {
233
356
  }
234
357
  }
235
358
 
236
- void main();
359
+ void main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fantasta",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Asta di Fantacalcio in LAN senza server: avvia la regia come vera app desktop e i partecipanti si collegano dal telefono. Nessuna installazione richiesta.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
Binary file
Binary file
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, dialog, ipcMain, safeStorage, session, shell, utilityProcess } = require("electron");
1
+ const { app, BrowserWindow, dialog, ipcMain, nativeImage, safeStorage, session, shell, utilityProcess } = require("electron");
2
2
  const { spawn } = require("node:child_process");
3
3
  const { readFile, writeFile, mkdir, copyFile, rename, unlink, access } = require("node:fs/promises");
4
4
  const { randomUUID } = require("node:crypto");
@@ -249,8 +249,12 @@ async function startRendererService() {
249
249
  // Il renderer deve essere raggiungibile dalla LAN: il QR del wizard punta a
250
250
  // http://<IP-locale>:47822, dove i telefoni caricano la vista partecipante.
251
251
  // (In sviluppo il renderer è esposto con `next dev --hostname 0.0.0.0`.)
252
- rendererService = spawn(process.execPath, [entrypoint], {
253
- env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", HOSTNAME: "0.0.0.0", PORT: port },
252
+ // utilityProcess.fork, come per il server locale, evita la seconda icona
253
+ // "exec" nel Dock di macOS (spawn del binario Electron con RUN_AS_NODE la
254
+ // creerebbe).
255
+ rendererService = utilityProcess.fork(entrypoint, [], {
256
+ cwd: path.join(resourcesRoot, "web"),
257
+ env: { ...process.env, HOSTNAME: "0.0.0.0", PORT: port },
254
258
  stdio: "inherit",
255
259
  });
256
260
  await waitForRenderer(rendererOrigin);
@@ -267,6 +271,22 @@ async function resolveAdminSession() {
267
271
  return payload.sessionId;
268
272
  }
269
273
 
274
+ // Icona dell'app quando si gira dal bundle Electron generico (modalità npm).
275
+ // Nel pacchetto .dmg/.exe l'icona arriva già dal bundle, qui va impostata a mano.
276
+ function applyAppIcon(window) {
277
+ if (process.platform === "darwin") {
278
+ try {
279
+ const image = nativeImage.createFromPath(path.join(__dirname, "..", "build", "icon.icns"));
280
+ if (!image.isEmpty() && app.dock) app.dock.setIcon(image);
281
+ } catch { /* icona opzionale */ }
282
+ } else if (window) {
283
+ const icon = process.platform === "win32"
284
+ ? path.join(__dirname, "..", "build", "icon.ico")
285
+ : path.join(__dirname, "..", "build", "icon.icns");
286
+ try { window.setIcon(nativeImage.createFromPath(icon)); } catch { /* icona opzionale */ }
287
+ }
288
+ }
289
+
270
290
  function createWindow(adminSession) {
271
291
  const rendererUrl = assertLoopback(rendererOrigin, "FANTASTA_RENDERER_URL");
272
292
  const serverUrl = assertLoopback(localServerUrl, "FANTASTA_LOCAL_SERVER_URL");
@@ -288,6 +308,7 @@ function createWindow(adminSession) {
288
308
  spellcheck: true,
289
309
  },
290
310
  });
311
+ if (npmMode) applyAppIcon(window);
291
312
  // L'app si apre sempre dalla home: da lì si crea una nuova asta oppure si
292
313
  // rientra in regia con la card "Entra nella tua asta" (se c'è una lega salvata).
293
314
  const target = new URL("/local/home", rendererUrl);
@@ -314,12 +335,12 @@ function createWindow(adminSession) {
314
335
 
315
336
  if (!app.requestSingleInstanceLock()) app.quit();
316
337
  else {
338
+ // Da npm l'app gira come binario "Electron": rinominare subito fa sì che
339
+ // userData (e quindi il database) finisca in ~/Library/Application Support/
340
+ // Fantasta, come per l'app installata, non in ".../Electron".
341
+ if (npmMode) app.setName("Fantasta");
317
342
  app.on("second-instance", () => BrowserWindow.getAllWindows()[0]?.focus());
318
343
  app.whenReady().then(async () => {
319
- // Da npm l'app gira come binario "Electron": rinominare subito fa sì che
320
- // userData (e quindi il database) finisca in ~/Library/Application Support/
321
- // Fantasta, come per l'app installata, non in ".../Electron".
322
- if (npmMode) app.setName("Fantasta");
323
344
  app.setAboutPanelOptions({
324
345
  applicationName: "Fantasta",
325
346
  applicationVersion: app.getVersion(),