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.
Files changed (2) hide show
  1. package/desktop.js +83 -107
  2. package/package.json +1 -1
package/desktop.js CHANGED
@@ -1,85 +1,81 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // fabulist — launches saeeol server + Electron desktop with React UI
4
- const { existsSync, realpathSync, createReadStream, statSync, readdirSync } = require("fs")
5
- const { join, dirname, extname, mimeLookup } = require("path")
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
- ".js": "text/javascript",
12
- ".css": "text/css",
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 archMap = { x64: "x64", arm64: "arm64" }
23
- const platformMap = { win32: "windows", darwin: "darwin", linux: "linux" }
24
- const p = platformMap[process.platform] || process.platform
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 binaryName = getBinaryName()
31
- const binaryRel = process.platform === "win32" ? "bin/saeeol.exe" : "bin/saeeol"
32
- const scriptDir = dirname(realpathSync(__filename))
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 current = scriptDir
44
+ // walk up
45
+ let dir = SCRIPT_DIR
38
46
  for (let i = 0; i < 10; i++) {
39
- const candidate = join(current, "node_modules", binaryName, binaryRel)
40
- if (existsSync(candidate)) return candidate
41
- const parent = dirname(current)
42
- if (parent === current) break
43
- current = parent
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(scriptDir, "download", process.platform === "win32" ? `${binaryName}.exe` : "saeeol")
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(scriptDir, "node_modules", "electron", "dist", exe)
56
- if (existsSync(p)) return p
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 urlPath = req.url.split("?")[0]
64
- if (urlPath === "/") urlPath = "/index.html"
65
-
66
- const filePath = join(webDir, urlPath)
67
- if (!existsSync(filePath)) {
68
- res.writeHead(404)
69
- res.end("not found")
70
- return
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(`Run: npm install saeeol`)
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(dirname(realpathSync(__filename)), "web")
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
- // Fallback: terminal mode
100
- if (!electronPath) {
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. Start static file server for React UI
116
+ // 2. Serve React UI
129
117
  const staticServer = await serveStatic(webDir, webPort)
130
- console.log(`UI server: http://127.0.0.1:${webPort}`)
118
+ console.log(`UI: http://127.0.0.1:${webPort}`)
131
119
 
132
- // 3. Wait for API to be ready
120
+ // 3. Wait for API
133
121
  await new Promise((r) => setTimeout(r, 3000))
134
122
 
135
- // 4. Launch Electron with inline app
123
+ // 4. Write Electron app inline
136
124
  console.log("Opening FABULIST desktop...")
137
-
138
- const electronAppDir = join(dirname(realpathSync(__filename__)), "desktop-app")
139
- const { mkdirSync, writeFileSync } = require("fs")
140
- mkdirSync(electronAppDir, { recursive: true })
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
- height: 900,
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
- const electron = spawn(electronPath, [join(electronAppDir, "main.js")], {
164
- stdio: "inherit",
165
- env: { ...process.env },
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
- electron.on("exit", (code) => {
170
- api.kill()
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
- process.on("SIGINT", () => {
176
- api.kill()
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) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saeeol",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "AI agent engine for SAEEOL — CLI + Desktop in one package",
5
5
  "author": "byfabulist",
6
6
  "license": "Apache-2.0",