saeeol 1.4.9 → 1.5.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/bin/saeeol ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // Proxy script — delegates to platform-specific binary
3
+ const path = require("path")
4
+ const { execFileSync } = require("child_process")
5
+ const os = require("os")
6
+ const fs = require("fs")
7
+
8
+ const platform = os.platform()
9
+ const arch = os.arch()
10
+
11
+ function findBinary() {
12
+ const dir = path.resolve(__dirname, "..")
13
+ // Try optionalDependency packages first
14
+ const pkgNames = platform === "win32"
15
+ ? [`saeeol-windows-${arch}`]
16
+ : platform === "darwin"
17
+ ? [`saeeol-darwin-${arch}`, `saeeol-darwin-${arch}-baseline`]
18
+ : [`saeeol-linux-${arch}`, `saeeol-linux-${arch}-baseline`, `saeeol-linux-${arch}-musl`]
19
+
20
+ for (const pkg of pkgNames) {
21
+ try {
22
+ const pkgDir = path.resolve(__dirname, "..", "..", pkg)
23
+ const exe = platform === "win32" ? "saeeol.exe" : "saeeol"
24
+ const binPath = path.join(pkgDir, "bin", exe)
25
+ if (fs.existsSync(binPath)) return binPath
26
+ } catch {}
27
+ }
28
+
29
+ // Try bundled bin
30
+ const localExe = platform === "win32" ? "saeeol.exe" : "saeeol"
31
+ const localPath = path.join(__dirname, localExe)
32
+ if (fs.existsSync(localPath)) return localPath
33
+
34
+ console.error("Could not find saeeol binary for " + platform + "-" + arch)
35
+ process.exit(1)
36
+ }
37
+
38
+ const binary = findBinary()
39
+ const result = execFileSync(binary, process.argv.slice(2), {
40
+ stdio: "inherit",
41
+ env: { ...process.env },
42
+ windowsHide: true,
43
+ })
44
+ if (result != null) process.exit(result)
package/package.json CHANGED
@@ -1,56 +1,11 @@
1
1
  {
2
2
  "name": "saeeol",
3
- "version": "1.4.9",
3
+ "version": "1.5.1",
4
4
  "description": "AI agent engine for SAEEOL — CLI + Desktop in one package",
5
- "author": "byfabulist",
5
+ "bin": { "saeeol": "./bin/saeeol", "fabulist": "./bin/saeeol" },
6
+ "scripts": { "postinstall": "node ./postinstall.mjs" },
6
7
  "license": "Apache-2.0",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/byfabulist/fabulist"
10
- },
11
- "bin": {
12
- "saeeol": "cli.js",
13
- "fabulist": "desktop.js"
14
- },
15
- "files": [
16
- "cli.js",
17
- "desktop.js",
18
- "postinstall.js",
19
- "web/**/*"
20
- ],
21
- "scripts": {
22
- "postinstall": "node postinstall.js"
23
- },
24
- "optionalDependencies": {
25
- "saeeol-windows-x64": "1.4.8",
26
- "saeeol-windows-x64-baseline": "1.4.5",
27
- "saeeol-darwin-x64": "1.4.5",
28
- "saeeol-darwin-x64-baseline": "1.4.5",
29
- "saeeol-darwin-arm64": "1.4.5",
30
- "saeeol-linux-x64": "1.4.5",
31
- "saeeol-linux-x64-baseline": "1.4.5",
32
- "saeeol-linux-arm64": "1.4.5",
33
- "saeeol-linux-arm64-musl": "1.4.5",
34
- "saeeol-linux-x64-musl": "1.4.5",
35
- "saeeol-linux-x64-musl-baseline": "1.4.5",
36
- "saeeol-win32-arm64": "1.4.5",
37
- "electron": "^35.2.1"
38
- },
39
- "publishConfig": {
40
- "access": "public",
41
- "registry": "https://registry.npmjs.org"
42
- },
43
- "engines": {
44
- "node": ">=18"
45
- },
46
- "keywords": [
47
- "ai",
48
- "agent",
49
- "cli",
50
- "coding",
51
- "assistant",
52
- "saeeol",
53
- "fabulist",
54
- "desktop"
55
- ]
8
+ "optionalDependencies": { "saeeol-windows-x64": "1.5.0" },
9
+ "repository": { "type": "git", "url": "https://github.com/byfabulist/fabulist" },
10
+ "keywords": ["ai", "agent", "cli", "coding", "assistant", "saeeol", "fabulist", "desktop"]
56
11
  }
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs"
4
+ import path from "path"
5
+ import os from "os"
6
+ import childProcess from "child_process"
7
+ import { fileURLToPath } from "url"
8
+ import { createRequire } from "module"
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const require = createRequire(import.meta.url)
12
+
13
+ // saeeolcode_change start - variant detection matching bin/saeeolcode logic
14
+ const platformMap = {
15
+ darwin: "darwin",
16
+ linux: "linux",
17
+ win32: "windows",
18
+ }
19
+ const archMap = {
20
+ x64: "x64",
21
+ arm64: "arm64",
22
+ arm: "arm",
23
+ }
24
+
25
+ function detectPlatformAndArch() {
26
+ const platform = platformMap[os.platform()] || os.platform()
27
+ const arch = archMap[os.arch()] || os.arch()
28
+ return { platform, arch }
29
+ }
30
+
31
+ function supportsAvx2() {
32
+ const { platform, arch } = detectPlatformAndArch()
33
+ if (arch !== "x64") return false
34
+
35
+ if (platform === "linux") {
36
+ try {
37
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ if (platform === "darwin") {
44
+ try {
45
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
46
+ encoding: "utf8",
47
+ timeout: 1500,
48
+ })
49
+ if (result.status !== 0) return false
50
+ return (result.stdout || "").trim() === "1"
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ return false
57
+ }
58
+
59
+ function isMusl() {
60
+ try {
61
+ if (fs.existsSync("/etc/alpine-release")) return true
62
+ } catch {
63
+ // ignore
64
+ }
65
+
66
+ try {
67
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
68
+ const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
69
+ if (text.includes("musl")) return true
70
+ } catch {
71
+ // ignore
72
+ }
73
+
74
+ return false
75
+ }
76
+
77
+ function getPackageNames() {
78
+ const { platform, arch } = detectPlatformAndArch()
79
+ const base = `@saeeolcode/cli-${platform}-${arch}`
80
+ const avx2 = supportsAvx2()
81
+ const baseline = arch === "x64" && !avx2
82
+
83
+ if (platform === "linux") {
84
+ const musl = isMusl()
85
+ if (musl) {
86
+ if (arch === "x64") {
87
+ if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
88
+ return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
89
+ }
90
+ return [`${base}-musl`, base]
91
+ }
92
+ if (arch === "x64") {
93
+ if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
94
+ return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
95
+ }
96
+ return [base, `${base}-musl`]
97
+ }
98
+
99
+ if (arch === "x64") {
100
+ if (baseline) return [`${base}-baseline`, base]
101
+ return [base, `${base}-baseline`]
102
+ }
103
+ return [base]
104
+ }
105
+
106
+ function findBinary() {
107
+ const { platform } = detectPlatformAndArch()
108
+ const binaryName = platform === "windows" ? "saeeolcode.exe" : "saeeolcode"
109
+ const names = getPackageNames()
110
+
111
+ for (const packageName of names) {
112
+ try {
113
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
114
+ const packageDir = path.dirname(packageJsonPath)
115
+ const binaryPath = path.join(packageDir, "bin", binaryName)
116
+
117
+ if (fs.existsSync(binaryPath)) {
118
+ return { binaryPath, binaryName }
119
+ }
120
+ } catch {
121
+ // package not installed, try next variant
122
+ }
123
+ }
124
+
125
+ throw new Error(`Could not find any binary package. Tried: ${names.map((n) => `"${n}"`).join(", ")}`)
126
+ }
127
+ // saeeolcode_change end
128
+
129
+ function registerWindowsTerminalFragment() {
130
+ const localAppData = process.env.LOCALAPPDATA
131
+ if (!localAppData) return
132
+
133
+ const fragmentDir = path.join(
134
+ localAppData,
135
+ "Microsoft",
136
+ "Windows Terminal",
137
+ "Fragments",
138
+ "saeeolcode",
139
+ )
140
+ const iconPath = path.join(__dirname, "..", "assets", "saeeol.ico")
141
+ if (!fs.existsSync(iconPath)) return
142
+
143
+ const profile = {
144
+ profiles: [
145
+ {
146
+ name: "SAEEOL Code",
147
+ commandline: "powershell.exe -NoExit -Command saeeol",
148
+ icon: iconPath,
149
+ startingDirectory: "%USERPROFILE%",
150
+ tabTitle: "SAEEOL Code",
151
+ },
152
+ ],
153
+ }
154
+
155
+ fs.mkdirSync(fragmentDir, { recursive: true })
156
+ fs.writeFileSync(
157
+ path.join(fragmentDir, "profile.json"),
158
+ JSON.stringify(profile, null, 2),
159
+ )
160
+ }
161
+
162
+ function main() {
163
+ if (os.platform() === "win32") {
164
+ console.log("Windows detected: binary setup not needed (using packaged .exe)")
165
+ try {
166
+ registerWindowsTerminalFragment()
167
+ } catch (err) {
168
+ console.error("Failed to register Windows Terminal fragment:", err.message)
169
+ }
170
+ return
171
+ }
172
+
173
+ const { binaryPath } = findBinary()
174
+ const target = path.join(__dirname, "bin", ".saeeolcode") // saeeolcode_change
175
+ if (fs.existsSync(target)) fs.unlinkSync(target)
176
+ try {
177
+ fs.linkSync(binaryPath, target)
178
+ } catch {
179
+ fs.copyFileSync(binaryPath, target)
180
+ }
181
+ fs.chmodSync(target, 0o755)
182
+ }
183
+
184
+ try {
185
+ void main()
186
+ } catch (error) {
187
+ console.error("Failed to setup saeeolcode binary:", error.message)
188
+ process.exit(1)
189
+ }
package/cli.js DELETED
@@ -1,65 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // saeeol bin wrapper — delegates to the platform-specific binary
4
- const { existsSync, chmodSync, realpathSync } = require("fs")
5
- const { join, resolve, dirname } = require("path")
6
- const { spawn } = require("child_process")
7
-
8
- function getBinaryName() {
9
- const archMap = { x64: "x64", arm64: "arm64" }
10
- const platformMap = { win32: "windows", darwin: "darwin", linux: "linux" }
11
- const p = platformMap[process.platform] || process.platform
12
- const a = archMap[process.arch] || process.arch
13
- return `saeeol-${p}-${a}`
14
- }
15
-
16
- function findBinary() {
17
- const binaryName = getBinaryName()
18
- const binaryRel = process.platform === "win32" ? "bin/saeeol.exe" : "bin/saeeol"
19
-
20
- // 1. Look in node_modules of this package
21
- const scriptDir = dirname(realpathSync(__filename))
22
- const localModules = join(scriptDir, "node_modules", binaryName, binaryRel)
23
- if (existsSync(localModules)) return localModules
24
-
25
- // 2. Walk up to find node_modules
26
- let current = scriptDir
27
- for (let i = 0; i < 10; i++) {
28
- const candidate = join(current, "node_modules", binaryName, binaryRel)
29
- if (existsSync(candidate)) return candidate
30
- const parent = dirname(current)
31
- if (parent === current) break
32
- current = parent
33
- }
34
-
35
- // 3. Download directory (postinstall fallback)
36
- const downloadPath = join(scriptDir, "download", process.platform === "win32" ? `${binaryName}.exe` : "saeeol")
37
- if (existsSync(downloadPath)) return downloadPath
38
-
39
- return null
40
- }
41
-
42
- const binaryPath = findBinary()
43
-
44
- if (!binaryPath) {
45
- console.error(`saeeol binary not found for your platform (${getBinaryName()})`)
46
- console.error(`Run: npm install saeeol`)
47
- console.error(`Or: https://github.com/byfabulist/fabulist/releases`)
48
- process.exit(1)
49
- }
50
-
51
- if (process.platform !== "win32") {
52
- try { chmodSync(binaryPath, 0o755) } catch {}
53
- }
54
-
55
- const child = spawn(binaryPath, process.argv.slice(2), {
56
- stdio: "inherit",
57
- env: { ...process.env },
58
- windowsHide: true,
59
- })
60
-
61
- child.on("exit", (code) => process.exit(code || 0))
62
- child.on("error", (err) => {
63
- console.error(`Failed to start saeeol: ${err.message}`)
64
- process.exit(1)
65
- })
package/desktop.js DELETED
@@ -1,167 +0,0 @@
1
- #!/usr/bin/env node
2
-
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")
7
- const { spawn } = require("child_process")
8
- const http = require("http")
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
-
26
- const MIME = {
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",
30
- }
31
-
32
- function getBinaryName() {
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}`
36
- }
37
-
38
- function findBinary() {
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)
43
- if (existsSync(local)) return local
44
- // walk up
45
- let dir = SCRIPT_DIR
46
- for (let i = 0; i < 10; i++) {
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
52
- }
53
- // download fallback
54
- const dl = join(SCRIPT_DIR, "download", process.platform === "win32" ? `${name}.exe` : "saeeol")
55
- if (existsSync(dl)) return dl
56
- return null
57
- }
58
-
59
- function findElectron() {
60
- const exe = process.platform === "win32" ? "electron.exe" : "electron"
61
- const p = join(SCRIPT_DIR, "node_modules", "electron", "dist", exe)
62
- return existsSync(p) ? p : null
63
- }
64
-
65
- function serveStatic(webDir, port) {
66
- return new Promise((resolve, reject) => {
67
- const server = http.createServer((req, res) => {
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)
76
- })
77
- server.listen(port, "127.0.0.1", () => resolve(server))
78
- server.on("error", reject)
79
- })
80
- }
81
-
82
- async function main() {
83
- const binaryPath = findBinary()
84
- if (!binaryPath) {
85
- console.error(`saeeol binary not found (${getBinaryName()})`)
86
- console.error("Run: npm install saeeol")
87
- process.exit(1)
88
- }
89
-
90
- const electronPath = findElectron()
91
- const webDir = join(SCRIPT_DIR, "web")
92
- const hasWebUI = existsSync(join(webDir, "index.html"))
93
-
94
- // Fallback to terminal mode if no electron or no web UI
95
- if (!electronPath || !hasWebUI) {
96
- console.log(!electronPath ? "Electron not found." : "Web UI not found.")
97
- console.log("Starting in terminal mode.\n")
98
- const child = spawn(binaryPath, process.argv.slice(2), {
99
- stdio: "inherit", env: { ...process.env }, windowsHide: false,
100
- })
101
- child.on("exit", (code) => process.exit(code || 0))
102
- return
103
- }
104
-
105
- const webPort = 31000 + Math.floor(Math.random() * 5000)
106
- const apiPort = 32000 + Math.floor(Math.random() * 5000)
107
-
108
- // 1. Start saeeol API server
109
- console.log("Starting saeeol API server...")
110
- const api = spawn(binaryPath, ["web", "--port", String(apiPort), "--hostname", "127.0.0.1"], {
111
- stdio: "pipe", env: { ...process.env }, windowsHide: true,
112
- })
113
- api.stdout.on("data", (d) => { const m = d.toString().trim(); if (m) console.log(`[api] ${m}`) })
114
- api.stderr.on("data", (d) => { const m = d.toString().trim(); if (m) console.error(`[api] ${m}`) })
115
-
116
- // 2. Serve React UI
117
- const staticServer = await serveStatic(webDir, webPort)
118
- console.log(`UI: http://127.0.0.1:${webPort}`)
119
-
120
- // 3. Wait for API
121
- await new Promise((r) => setTimeout(r, 3000))
122
-
123
- // 4. Write Electron app inline
124
- console.log("Opening FABULIST desktop...")
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"), `
129
- const { app, BrowserWindow } = require("electron")
130
- let mainWindow = null
131
- function createWindow() {
132
- mainWindow = new BrowserWindow({
133
- width: 1400, height: 900, minWidth: 800, minHeight: 600,
134
- title: "\uD30C\uBD88\uB9AC\uC2A4\uD2B8 \u2014 FABULIST",
135
- autoHideMenuBar: true,
136
- show: false,
137
- webPreferences: { nodeIntegration: false, contextIsolation: true },
138
- })
139
- mainWindow.once("ready-to-show", () => mainWindow.show())
140
- mainWindow.loadURL("http://127.0.0.1:${webPort}")
141
- mainWindow.on("closed", () => { mainWindow = null })
142
- }
143
- app.on("ready", createWindow)
144
- app.on("window-all-closed", () => { /* keep running */ })
145
- app.on("activate", () => { if (!mainWindow) createWindow() })
146
- `)
147
-
148
- // Remove ELECTRON_RUN_AS_NODE if set (prevents Electron from running as Node)
149
- const env = { ...process.env }
150
- delete env.ELECTRON_RUN_AS_NODE
151
-
152
- const electron = spawn(electronPath, [join(appDir, "main.js")], {
153
- stdio: "ignore",
154
- env,
155
- detached: true,
156
- windowsHide: false,
157
- })
158
- electron.unref()
159
-
160
- console.log("FABULIST desktop opened! Close the window to exit.")
161
- console.log("Press Ctrl+C to stop the server.")
162
-
163
- const cleanup = () => { api.kill(); staticServer.close(); process.exit(0) }
164
- process.on("SIGINT", cleanup)
165
- }
166
-
167
- main().catch((err) => { console.error(err.message); process.exit(1) })
package/postinstall.js DELETED
@@ -1,162 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // saeeol postinstall — downloads platform binary from GitHub Releases
4
- const https = require("https")
5
- const http = require("http")
6
- const fs = require("fs")
7
- const path = require("path")
8
- const zlib = require("zlib")
9
- const { execSync } = require("child_process")
10
-
11
- const REPO = "byfabulist/fabulist"
12
- const VERSION = require("./package.json").version
13
-
14
- function getPlatformInfo() {
15
- const archMap = { x64: "x64", arm64: "arm64" }
16
- const platformMap = { win32: "windows", darwin: "darwin", linux: "linux" }
17
- const p = platformMap[process.platform] || process.platform
18
- const a = archMap[process.arch] || process.arch
19
- const ext = process.platform === "win32" ? ".zip" : ".tar.gz"
20
- const archiveName = `saeeol-${p}-${a}${ext}`
21
- const binaryName = process.platform === "win32" ? "saeeol.exe" : "saeeol"
22
- return { archiveName, binaryName, platform: p, arch: a }
23
- }
24
-
25
- function fetch(url, redirects = 5) {
26
- return new Promise((resolve, reject) => {
27
- const mod = url.startsWith("https") ? https : http
28
- mod.get(url, { timeout: 60000 }, (res) => {
29
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
30
- if (redirects <= 0) return reject(new Error("Too many redirects"))
31
- return fetch(res.headers.location, redirects - 1).then(resolve, reject)
32
- }
33
- if (res.statusCode !== 200) {
34
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`))
35
- }
36
- const chunks = []
37
- res.on("data", (chunk) => chunks.push(chunk))
38
- res.on("end", () => resolve(Buffer.concat(chunks)))
39
- res.on("error", reject)
40
- }).on("error", reject)
41
- })
42
- }
43
-
44
- async function extractTarGz(buffer, destDir, binaryName) {
45
- // Use tar on non-windows, or platform tar on windows (git bash)
46
- const tmpFile = path.join(destDir, "download.tar.gz")
47
- fs.writeFileSync(tmpFile, buffer)
48
- try {
49
- if (process.platform === "win32") {
50
- // Try PowerShell Expand-Archive for .tar.gz or just use tar if available
51
- try {
52
- execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe", windowsHide: true })
53
- } catch {
54
- // Fallback: use zlib + tar parsing manually not practical, try powershell
55
- throw new Error("tar command not available on Windows. Install Git Bash or add tar to PATH.")
56
- }
57
- } else {
58
- execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" })
59
- }
60
- } finally {
61
- try { fs.unlinkSync(tmpFile) } catch {}
62
- }
63
- }
64
-
65
- async function extractZip(buffer, destDir, binaryName) {
66
- if (process.platform === "win32") {
67
- const tmpFile = path.join(destDir, "download.zip")
68
- fs.writeFileSync(tmpFile, buffer)
69
- try {
70
- execSync(`powershell -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${destDir}' -Force"`, {
71
- stdio: "pipe",
72
- windowsHide: true,
73
- })
74
- } finally {
75
- try { fs.unlinkSync(tmpFile) } catch {}
76
- }
77
- } else {
78
- // On non-windows, try unzip
79
- const tmpFile = path.join(destDir, "download.zip")
80
- fs.writeFileSync(tmpFile, buffer)
81
- try {
82
- execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" })
83
- } finally {
84
- try { fs.unlinkSync(tmpFile) } catch {}
85
- }
86
- }
87
- }
88
-
89
- async function main() {
90
- const { archiveName, binaryName, platform: p, arch: a } = getPlatformInfo()
91
- const binDir = path.join(__dirname, "download")
92
-
93
- // Check if binary already exists
94
- const binaryPath = path.join(binDir, binaryName)
95
- if (fs.existsSync(binaryPath)) {
96
- console.log(`saeeol binary already exists at ${binaryPath}`)
97
- return
98
- }
99
-
100
- fs.mkdirSync(binDir, { recursive: true })
101
-
102
- const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`
103
- console.log(`Downloading saeeol v${VERSION} for ${p}-${a}...`)
104
- console.log(` ${url}`)
105
-
106
- let buffer
107
- try {
108
- buffer = await fetch(url)
109
- } catch (err) {
110
- // Fallback: try without version tag (latest)
111
- const fallbackUrl = `https://github.com/${REPO}/releases/latest/download/${archiveName}`
112
- console.log(`Version-specific download failed: ${err.message}`)
113
- console.log(`Trying latest: ${fallbackUrl}`)
114
- try {
115
- buffer = await fetch(fallbackUrl)
116
- } catch (err2) {
117
- console.error(`Failed to download saeeol binary: ${err2.message}`)
118
- console.error(`Please download manually from: https://github.com/${REPO}/releases`)
119
- process.exit(0) // Don't fail npm install
120
- }
121
- }
122
-
123
- console.log(`Downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
124
-
125
- // Extract
126
- if (archiveName.endsWith(".tar.gz")) {
127
- await extractTarGz(buffer, binDir, binaryName)
128
- } else {
129
- await extractZip(buffer, binDir, binaryName)
130
- }
131
-
132
- // Verify
133
- if (!fs.existsSync(binaryPath)) {
134
- // The archive might extract to a subdirectory, search for the binary
135
- const files = fs.readdirSync(binDir, { recursive: true })
136
- const found = files.find((f) => {
137
- const basename = path.basename(f)
138
- return basename === "saeeol" || basename === "saeeol.exe"
139
- })
140
- if (found) {
141
- const fullPath = path.join(binDir, found)
142
- fs.copyFileSync(fullPath, binaryPath)
143
- }
144
- }
145
-
146
- if (fs.existsSync(binaryPath)) {
147
- if (process.platform !== "win32") {
148
- fs.chmodSync(binaryPath, 0o755)
149
- }
150
- console.log(`saeeol v${VERSION} installed successfully!`)
151
- console.log(` Binary: ${binaryPath}`)
152
- } else {
153
- console.warn(`Warning: binary not found after extraction at ${binaryPath}`)
154
- console.warn(`You may need to install manually from https://github.com/${REPO}/releases`)
155
- }
156
- }
157
-
158
- main().catch((err) => {
159
- console.error(`saeeol postinstall failed: ${err.message}`)
160
- console.error(`Install manually: https://github.com/${REPO}/releases`)
161
- process.exit(0) // Don't fail npm install
162
- })