@nemoobc/opencode-termux 1.18.21 → 1.18.23

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/opencode.js CHANGED
@@ -5,13 +5,42 @@ const path = require("path")
5
5
  const vendor = path.join(__dirname, "..", "vendor")
6
6
  const loader = path.join(vendor, "ld-musl.so")
7
7
  const bin = path.join(vendor, "opencode")
8
- if (!fs.existsSync(loader) || !fs.existsSync(bin)) {
9
- console.error("[opencode-termux] vendor belum terpasang. Jalankan ulang:")
10
- console.error(" npm rebuild -g @nemoobc/opencode-termux")
11
- process.exit(1)
8
+
9
+ function ready() {
10
+ return fs.existsSync(loader) && fs.existsSync(bin)
12
11
  }
12
+
13
+ // Auto-heal: kalau postinstall terlewat (mis. --ignore-scripts), pasang sekarang.
14
+ if (!ready()) {
15
+ console.log("[opencode-termux] vendor belum ada — menjalankan installer…")
16
+ const r = spawnSync(process.execPath, [path.join(__dirname, "..", "install.mjs")], {
17
+ stdio: "inherit",
18
+ env: process.env,
19
+ })
20
+ if (r.status !== 0 || !ready()) {
21
+ console.error("[opencode-termux] instalasi bundle gagal. Coba manual:")
22
+ console.error(" npm rebuild @nemoobc/opencode-termux")
23
+ process.exit(1)
24
+ }
25
+ }
26
+
27
+ if (!ready()) process.exit(1)
28
+
29
+ // Diagnosa awal: Android sering tidak punya /etc/resolv.conf yang dibaca musl.
30
+ try {
31
+ if (!fs.existsSync("/etc/resolv.conf")) {
32
+ console.error("[opencode-termux] ⚠️ /etc/resolv.conf tidak ada di device ini;")
33
+ console.error(" kalau perintah jaringan (models/login) gagal/gantung, itu sebabnya.")
34
+ console.error(" Solusi sementara (butuh akses root):")
35
+ console.error(' su -c \'mkdir -p /etc && echo "nameserver 8.8.8.8" > /etc/resolv.conf\'')
36
+ }
37
+ } catch {}
38
+
39
+ // LD_PRELOAD bawaan Termux (libtermux-exec) dibuat untuk Bionic dan akan
40
+ // gagal relokasi jika ikut dimuat ke proses musl — jadi selalu dibersihkan.
41
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
13
42
  const r = spawnSync(loader, [bin, ...process.argv.slice(2)], {
14
43
  stdio: "inherit",
15
- env: { ...process.env, LD_LIBRARY_PATH: vendor },
44
+ env: { ...cleanEnv, LD_LIBRARY_PATH: vendor },
16
45
  })
17
46
  process.exit(r.status ?? 1)
package/install.mjs CHANGED
@@ -1,40 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * postinstall untuk @nemoobc/opencode-termux
4
- * Menyusun bundle native: loader musl + libgcc/libstdc++ + binary opencode
4
+ * Menyusun bundle native: loader musl + libgcc/libstdc++ + binary opencode.
5
+ * Tanpa dependensi curl — unduhan memakai fetch bawaan Node >=18.
5
6
  * Target: Termux (android/arm64). Override uji: OCX_ARCH=x64 OCX_FORCE=1
6
7
  */
7
8
  import fs from "fs"
8
9
  import path from "path"
9
10
  import { execFileSync } from "child_process"
11
+ import { Readable } from "stream"
12
+ import { pipeline } from "stream/promises"
10
13
  import { fileURLToPath } from "url"
11
14
 
12
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
- const V = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version
16
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
14
17
  const ARCH = process.env.OCX_ARCH || "arm64"
15
18
  const FORCE = !!process.env.OCX_FORCE
16
19
  const IS_ANDROID = process.platform === "android"
17
- const IS_ARM64 = process.arch === "arm64"
20
+
21
+ // Versi upstream: env > package.json > otomatis ambil terbaru dari registry
22
+ let V = process.env.OCX_UPSTREAM || pkgJson.opencodeUpstream
23
+ if (!V) {
24
+ const latest = await (await fetch("https://registry.npmjs.org/opencode-ai/latest")).json()
25
+ V = latest.version
26
+ console.log(`[opencode-termux] upstream opencode-ai terbaru: ${V}`)
27
+ }
28
+ const GLOBAL_HINT =
29
+ process.platform === "win32" ? "" : ""
18
30
 
19
31
  if (!IS_ANDROID && !FORCE) {
20
32
  console.log("[opencode-termux] Bukan Termux/Android — instalasi dilewati (pakai opencode-ai resmi).")
21
33
  process.exit(0)
22
34
  }
23
- if (!FORCE && !IS_ARM64 && ARCH !== "x64") {
24
- console.log(`[opencode-termux] arsitektur ${process.arch} belum didukung.`)
25
- process.exit(0)
26
- }
35
+
27
36
  const A = ARCH === "x64" ? "x86_64" : "aarch64"
28
37
 
29
- const dl = (url, dest) => {
38
+ async function dl(url, dest) {
30
39
  console.log(`[opencode-termux] download ${url.split("/").pop()}`)
31
- execFileSync("curl", ["-sL", "--retry", "3", "-o", dest, url], { stdio: "inherit" })
40
+ const res = await fetch(url)
41
+ if (!res.ok) throw new Error(`HTTP ${res.status} — ${url}`)
42
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest))
32
43
  }
33
44
  const untar = (tgz, dest, members = []) => {
34
45
  fs.mkdirSync(dest, { recursive: true })
35
- const args = ["xzf", tgz, "-C", dest, ...members]
36
- try { execFileSync("tar", args) }
37
- catch { execFileSync("tar", args.map(a => a.startsWith("./") ? a.slice(2) : a)) }
46
+ const run = m => execFileSync("tar", ["xzf", tgz, "-C", dest, ...m], { stdio: ["ignore", "ignore", "pipe"] })
47
+ try { run(members.map(x => "./" + x)) }
48
+ catch { try { run(members) } catch (e) {
49
+ console.error("[opencode-termux] 'tar' tidak ditemukan. Jalankan: pkg install tar")
50
+ throw e
51
+ } }
38
52
  }
39
53
 
40
54
  const work = path.join(__dirname, ".build")
@@ -42,37 +56,38 @@ fs.rmSync(work, { recursive: true, force: true })
42
56
  fs.mkdirSync(work, { recursive: true })
43
57
 
44
58
  // 1) binary opencode (musl) dari npm resmi
45
- dl(`https://registry.npmjs.org/opencode-linux-${ARCH}-musl/-/opencode-linux-${ARCH}-musl-${V}.tgz`, `${work}/oc.tgz`)
59
+ await dl(`https://registry.npmjs.org/opencode-linux-${ARCH}-musl/-/opencode-linux-${ARCH}-musl-${V}.tgz`, `${work}/oc.tgz`)
46
60
  untar(`${work}/oc.tgz`, `${work}/oc`)
47
61
 
48
62
  // 2) loader+libc musl dari Alpine minirootfs
49
63
  const AV = "v3.21", AL = "3.21.3"
50
- dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/releases/${A}/alpine-minirootfs-${AL}-${A}.tar.gz`, `${work}/ap.tgz`)
51
- untar(`${work}/ap.tgz`, `${work}/ap`, ["./lib"])
64
+ await dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/releases/${A}/alpine-minirootfs-${AL}-${A}.tar.gz`, `${work}/ap.tgz`)
65
+ untar(`${work}/ap.tgz`, `${work}/ap`, ["lib"])
52
66
 
53
67
  // 3) libgcc + libstdc++
54
68
  const apkDir = `${work}/apk`; fs.mkdirSync(apkDir, { recursive: true })
55
69
  for (const f of [`libgcc-14.2.0-r4.apk`, `libstdc%2B%2B-14.2.0-r4.apk`]) {
56
- dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/main/${A}/${f}`, `${apkDir}/${f}`)
70
+ await dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/main/${A}/${f}`, `${apkDir}/${f}`)
57
71
  untar(`${apkDir}/${f}`, apkDir)
58
72
  }
59
73
 
60
74
  // 4) rakit vendor/
61
75
  const vendor = path.join(__dirname, "vendor")
62
76
  fs.rmSync(vendor, { recursive: true, force: true }); fs.mkdirSync(vendor)
63
- const cp = (from, name) => fs.copyFileSync(from, path.join(vendor, name))
64
- cp(`${work}/ap/lib/ld-musl-${A}.so.1`, "ld-musl.so")
65
- cp(`${work}/oc/package/bin/opencode`, "opencode")
66
- for (const f of fs.readdirSync(`${apkDir}/usr/lib`)) {
67
- if (/^libstdc\+\+\.so|^libgcc_s\.so/.test(f)) cp(`${apkDir}/usr/lib/${f}`, f)
68
- }
77
+ const cp = (from, name) => fs.copyFileSync(path.join(from, name), path.join(vendor, name))
78
+ cp(`${work}/ap/lib`, `ld-musl-${A}.so.1`); fs.renameSync(path.join(vendor, `ld-musl-${A}.so.1`), path.join(vendor, "ld-musl.so"))
79
+ cp(`${work}/oc/package/bin`, "opencode")
80
+ cp(`${apkDir}/usr/lib`, "libstdc++.so.6"); cp(`${apkDir}/usr/lib`, "libstdc++.so.6.0.33"); cp(`${apkDir}/usr/lib`, "libgcc_s.so.1")
69
81
  for (const f of fs.readdirSync(vendor)) fs.chmodSync(path.join(vendor, f), 0o755)
70
82
 
71
- // 5) smoke test
83
+ // 5) smoke test (LD_PRELOAD termux-exec dibuang: tidak kompatibel dengan musl)
72
84
  console.log("[opencode-termux] smoke test…")
85
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
73
86
  execFileSync(path.join(vendor, "ld-musl.so"),
74
87
  [path.join(vendor, "opencode"), "--version"],
75
- { stdio: "inherit", env: { ...process.env, LD_LIBRARY_PATH: vendor } })
88
+ { stdio: "inherit", env: { ...cleanEnv, LD_LIBRARY_PATH: vendor } })
76
89
 
77
90
  fs.rmSync(work, { recursive: true, force: true })
78
- console.log(`[opencode-termux] ✅ siap! Jalankan dengan: opencode-termux`)
91
+ console.log(`[opencode-termux] ✅ siap!
92
+ • global : jalankan 'opencode-termux'
93
+ • lokal : 'npx opencode-termux' dari folder project ini`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nemoobc/opencode-termux",
3
- "version": "1.18.21",
3
+ "version": "1.18.23",
4
4
  "description": "opencode CLI native untuk Termux/Android tanpa proot — membundel loader musl + binary opencode resmi (upstream opencode-ai)",
5
5
  "bin": {
6
6
  "opencode-termux": "./bin/opencode.js"
@@ -8,8 +8,20 @@
8
8
  "scripts": {
9
9
  "postinstall": "node install.mjs"
10
10
  },
11
- "keywords": ["opencode", "termux", "android", "cli", "ai"],
11
+ "keywords": [
12
+ "opencode",
13
+ "termux",
14
+ "android",
15
+ "cli",
16
+ "ai"
17
+ ],
12
18
  "license": "MIT",
13
- "engines": { "node": ">=18" },
14
- "files": ["bin", "install.mjs", "README.md"]
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "install.mjs",
25
+ "README.md"
26
+ ]
15
27
  }