fantasta 0.3.1 → 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.
Files changed (3) hide show
  1. package/README.md +17 -8
  2. package/bin/fantasta.js +133 -10
  3. package/package.json +1 -1
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.1",
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",