saeeol 1.4.7 → 1.4.8
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/desktop.js +83 -107
- package/package.json +1 -1
package/desktop.js
CHANGED
|
@@ -1,85 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// fabulist —
|
|
4
|
-
|
|
5
|
-
const {
|
|
3
|
+
// fabulist — saeeol server + Electron desktop with React UI
|
|
4
|
+
"use strict"
|
|
5
|
+
const { existsSync, realpathSync, createReadStream, statSync, mkdirSync, writeFileSync } = require("fs")
|
|
6
|
+
const { join, dirname, extname } = require("path")
|
|
6
7
|
const { spawn } = require("child_process")
|
|
7
8
|
const http = require("http")
|
|
8
9
|
|
|
10
|
+
// Resolve script directory (works with symlinks and global installs)
|
|
11
|
+
function getScriptDir() {
|
|
12
|
+
// 1. process.argv[1] (how npm bin scripts invoke us)
|
|
13
|
+
if (process.argv[1]) {
|
|
14
|
+
try { const r = realpathSync(process.argv[1]); if (r) return dirname(r) } catch {}
|
|
15
|
+
}
|
|
16
|
+
// 2. __filename (CJS context)
|
|
17
|
+
try { if (typeof __filename === "string") return dirname(realpathSync(__filename)) } catch {}
|
|
18
|
+
// 3. require.main
|
|
19
|
+
try { if (require.main && require.main.filename) return dirname(realpathSync(require.main.filename)) } catch {}
|
|
20
|
+
// 4. find saeeol package in global node_modules
|
|
21
|
+
try { const p = require.resolve("saeeol/cli.js"); return dirname(p) } catch {}
|
|
22
|
+
return process.cwd()
|
|
23
|
+
}
|
|
24
|
+
const SCRIPT_DIR = getScriptDir()
|
|
25
|
+
|
|
9
26
|
const MIME = {
|
|
10
|
-
".html": "text/html",
|
|
11
|
-
".
|
|
12
|
-
".
|
|
13
|
-
".json": "application/json",
|
|
14
|
-
".png": "image/png",
|
|
15
|
-
".ico": "image/x-icon",
|
|
16
|
-
".svg": "image/svg+xml",
|
|
17
|
-
".woff2": "font/woff2",
|
|
18
|
-
".wasm": "application/wasm",
|
|
27
|
+
".html": "text/html", ".js": "text/javascript", ".css": "text/css",
|
|
28
|
+
".json": "application/json", ".png": "image/png", ".ico": "image/x-icon",
|
|
29
|
+
".svg": "image/svg+xml", ".woff2": "font/woff2", ".wasm": "application/wasm",
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
function getBinaryName() {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const a = archMap[process.arch] || process.arch
|
|
26
|
-
return `saeeol-${p}-${a}`
|
|
33
|
+
const a = { x64: "x64", arm64: "arm64" }
|
|
34
|
+
const p = { win32: "windows", darwin: "darwin", linux: "linux" }
|
|
35
|
+
return `saeeol-${p[process.platform] || process.platform}-${a[process.arch] || process.arch}`
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
function findBinary() {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const local = join(scriptDir, "node_modules", binaryName, binaryRel)
|
|
39
|
+
const name = getBinaryName()
|
|
40
|
+
const rel = process.platform === "win32" ? "bin/saeeol.exe" : "bin/saeeol"
|
|
41
|
+
// node_modules of this package
|
|
42
|
+
const local = join(SCRIPT_DIR, "node_modules", name, rel)
|
|
35
43
|
if (existsSync(local)) return local
|
|
36
|
-
|
|
37
|
-
let
|
|
44
|
+
// walk up
|
|
45
|
+
let dir = SCRIPT_DIR
|
|
38
46
|
for (let i = 0; i < 10; i++) {
|
|
39
|
-
const
|
|
40
|
-
if (existsSync(
|
|
41
|
-
const parent = dirname(
|
|
42
|
-
if (parent ===
|
|
43
|
-
|
|
47
|
+
const c = join(dir, "node_modules", name, rel)
|
|
48
|
+
if (existsSync(c)) return c
|
|
49
|
+
const parent = dirname(dir)
|
|
50
|
+
if (parent === dir) break
|
|
51
|
+
dir = parent
|
|
44
52
|
}
|
|
45
|
-
|
|
46
|
-
const dl = join(
|
|
53
|
+
// download fallback
|
|
54
|
+
const dl = join(SCRIPT_DIR, "download", process.platform === "win32" ? `${name}.exe` : "saeeol")
|
|
47
55
|
if (existsSync(dl)) return dl
|
|
48
|
-
|
|
49
56
|
return null
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
function findElectron() {
|
|
53
|
-
const scriptDir = dirname(realpathSync(__filename))
|
|
54
60
|
const exe = process.platform === "win32" ? "electron.exe" : "electron"
|
|
55
|
-
const p = join(
|
|
56
|
-
|
|
57
|
-
return null
|
|
61
|
+
const p = join(SCRIPT_DIR, "node_modules", "electron", "dist", exe)
|
|
62
|
+
return existsSync(p) ? p : null
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function serveStatic(webDir, port) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
62
67
|
const server = http.createServer((req, res) => {
|
|
63
|
-
let
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const ext = extname(filePath)
|
|
74
|
-
const ct = MIME[ext] || "application/octet-stream"
|
|
75
|
-
const stat = statSync(filePath)
|
|
76
|
-
res.writeHead(200, { "Content-Type": ct, "Content-Length": stat.size })
|
|
77
|
-
createReadStream(filePath).pipe(res)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
server.listen(port, "127.0.0.1", () => {
|
|
81
|
-
resolve(server)
|
|
68
|
+
let path = req.url.split("?")[0]
|
|
69
|
+
if (path === "/") path = "/index.html"
|
|
70
|
+
const fp = join(webDir, path)
|
|
71
|
+
if (!existsSync(fp)) { res.writeHead(404); res.end("not found"); return }
|
|
72
|
+
const ct = MIME[extname(fp)] || "application/octet-stream"
|
|
73
|
+
const s = statSync(fp)
|
|
74
|
+
res.writeHead(200, { "Content-Type": ct, "Content-Length": s.size })
|
|
75
|
+
createReadStream(fp).pipe(res)
|
|
82
76
|
})
|
|
77
|
+
server.listen(port, "127.0.0.1", () => resolve(server))
|
|
78
|
+
server.on("error", reject)
|
|
83
79
|
})
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -87,100 +83,80 @@ async function main() {
|
|
|
87
83
|
const binaryPath = findBinary()
|
|
88
84
|
if (!binaryPath) {
|
|
89
85
|
console.error(`saeeol binary not found (${getBinaryName()})`)
|
|
90
|
-
console.error(
|
|
86
|
+
console.error("Run: npm install saeeol")
|
|
91
87
|
process.exit(1)
|
|
92
88
|
}
|
|
93
89
|
|
|
94
90
|
const electronPath = findElectron()
|
|
95
|
-
const webDir = join(
|
|
91
|
+
const webDir = join(SCRIPT_DIR, "web")
|
|
96
92
|
const hasWebUI = existsSync(join(webDir, "index.html"))
|
|
97
93
|
|
|
94
|
+
// Fallback to terminal mode if no electron or no web UI
|
|
98
95
|
if (!electronPath || !hasWebUI) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log("Electron not found, starting in terminal mode.\n")
|
|
102
|
-
} else {
|
|
103
|
-
console.log("Web UI not found, starting in terminal mode.\n")
|
|
104
|
-
}
|
|
96
|
+
console.log(!electronPath ? "Electron not found." : "Web UI not found.")
|
|
97
|
+
console.log("Starting in terminal mode.\n")
|
|
105
98
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
106
|
-
stdio: "inherit",
|
|
107
|
-
env: { ...process.env },
|
|
108
|
-
windowsHide: false,
|
|
99
|
+
stdio: "inherit", env: { ...process.env }, windowsHide: false,
|
|
109
100
|
})
|
|
110
101
|
child.on("exit", (code) => process.exit(code || 0))
|
|
111
102
|
return
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
// Find ports
|
|
115
105
|
const webPort = 31000 + Math.floor(Math.random() * 5000)
|
|
116
106
|
const apiPort = 32000 + Math.floor(Math.random() * 5000)
|
|
117
107
|
|
|
118
108
|
// 1. Start saeeol API server
|
|
119
109
|
console.log("Starting saeeol API server...")
|
|
120
110
|
const api = spawn(binaryPath, ["web", "--port", String(apiPort), "--hostname", "127.0.0.1"], {
|
|
121
|
-
stdio: "pipe",
|
|
122
|
-
env: { ...process.env },
|
|
123
|
-
windowsHide: true,
|
|
111
|
+
stdio: "pipe", env: { ...process.env }, windowsHide: true,
|
|
124
112
|
})
|
|
125
113
|
api.stdout.on("data", (d) => { const m = d.toString().trim(); if (m) console.log(`[api] ${m}`) })
|
|
126
114
|
api.stderr.on("data", (d) => { const m = d.toString().trim(); if (m) console.error(`[api] ${m}`) })
|
|
127
115
|
|
|
128
|
-
// 2.
|
|
116
|
+
// 2. Serve React UI
|
|
129
117
|
const staticServer = await serveStatic(webDir, webPort)
|
|
130
|
-
console.log(`UI
|
|
118
|
+
console.log(`UI: http://127.0.0.1:${webPort}`)
|
|
131
119
|
|
|
132
|
-
// 3. Wait for API
|
|
120
|
+
// 3. Wait for API
|
|
133
121
|
await new Promise((r) => setTimeout(r, 3000))
|
|
134
122
|
|
|
135
|
-
// 4.
|
|
123
|
+
// 4. Write Electron app inline
|
|
136
124
|
console.log("Opening FABULIST desktop...")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
writeFileSync(join(electronAppDir, "package.json"), '{"name":"fabulist-desktop"}')
|
|
142
|
-
writeFileSync(join(electronAppDir, "main.js"), `
|
|
125
|
+
const appDir = join(SCRIPT_DIR, ".desktop-cache")
|
|
126
|
+
mkdirSync(appDir, { recursive: true })
|
|
127
|
+
writeFileSync(join(appDir, "package.json"), '{"name":"fabulist-desktop"}')
|
|
128
|
+
writeFileSync(join(appDir, "main.js"), `
|
|
143
129
|
const { app, BrowserWindow } = require("electron")
|
|
144
|
-
const path = require("path")
|
|
145
|
-
|
|
146
130
|
function createWindow() {
|
|
147
131
|
const win = new BrowserWindow({
|
|
148
|
-
width: 1400,
|
|
149
|
-
|
|
150
|
-
minWidth: 800,
|
|
151
|
-
minHeight: 600,
|
|
152
|
-
title: "파불리스트 — FABULIST",
|
|
132
|
+
width: 1400, height: 900, minWidth: 800, minHeight: 600,
|
|
133
|
+
title: "\uD30C\uBD88\uB9AC\uC2A4\uD2B8 \u2014 FABULIST",
|
|
153
134
|
autoHideMenuBar: true,
|
|
154
135
|
webPreferences: { nodeIntegration: false, contextIsolation: true },
|
|
155
136
|
})
|
|
156
137
|
win.loadURL("http://127.0.0.1:${webPort}")
|
|
157
138
|
}
|
|
158
|
-
|
|
159
139
|
app.whenReady().then(createWindow)
|
|
160
140
|
app.on("window-all-closed", () => app.quit())
|
|
161
141
|
`)
|
|
162
142
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
143
|
+
// Remove ELECTRON_RUN_AS_NODE if set (prevents Electron from running as Node)
|
|
144
|
+
const env = { ...process.env }
|
|
145
|
+
delete env.ELECTRON_RUN_AS_NODE
|
|
146
|
+
|
|
147
|
+
const electron = spawn(electronPath, [join(appDir, "main.js")], {
|
|
148
|
+
stdio: "ignore",
|
|
149
|
+
env,
|
|
150
|
+
detached: true,
|
|
166
151
|
windowsHide: false,
|
|
167
152
|
})
|
|
153
|
+
electron.unref()
|
|
168
154
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
staticServer.close()
|
|
172
|
-
process.exit(code || 0)
|
|
173
|
-
})
|
|
155
|
+
console.log("FABULIST desktop opened! Close the window to exit.")
|
|
156
|
+
console.log("Press Ctrl+C to stop the server.")
|
|
174
157
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
electron.kill()
|
|
178
|
-
staticServer.close()
|
|
179
|
-
process.exit(0)
|
|
180
|
-
})
|
|
158
|
+
const cleanup = () => { api.kill(); staticServer.close(); process.exit(0) }
|
|
159
|
+
process.on("SIGINT", cleanup)
|
|
181
160
|
}
|
|
182
161
|
|
|
183
|
-
main().catch((err) => {
|
|
184
|
-
console.error(err.message)
|
|
185
|
-
process.exit(1)
|
|
186
|
-
})
|
|
162
|
+
main().catch((err) => { console.error(err.message); process.exit(1) })
|