@nemoobc/opencode-termux 1.19.1 β†’ 1.20.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nemoobc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@nemoobc/opencode-termux?color=cb3837&logo=npm)](https://www.npmjs.com/package/@nemoobc/opencode-termux)
6
6
  [![release](https://img.shields.io/github/v/release/nemoobc/opencode-termux?color=3B82F6)](https://github.com/nemoobc/opencode-termux/releases)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/nemo-base-eth/opencode-termux/test.yml?label=test&color=22C55E)](.github/workflows/test.yml)
7
8
  [![platform](https://img.shields.io/badge/platform-Android%20%7C%20Termux-3DDC84)](#)
8
9
  [![arch](https://img.shields.io/badge/arch-arm64-blue)](#)
9
10
  [![sync upstream](https://img.shields.io/badge/sync-upstream%20otomatis-C9A227)](.github/workflows/sync-upstream.yml)
@@ -58,10 +59,17 @@ opencode-termux (shim Node)
58
59
  ## πŸš€ Install
59
60
 
60
61
  ```bash
61
- pkg update && pkg install nodejs-lts
62
+ pkg update && pkg install nodejs-lts tar
62
63
  npm i -g @nemoobc/opencode-termux
63
64
  ```
64
65
 
66
+ > πŸ“– **Panduan lengkap** β€” persyaratan, verifikasi, update/uninstall,
67
+ > troubleshooting, FAQ: [docs/INSTALASI.md](docs/INSTALASI.md)
68
+
69
+ > πŸ“¦ **Tanpa Node / offline?** Ambil `opencode-termux-installer.sh` +
70
+ > bundle `.tar.gz` dari [Releases](https://github.com/nemo-base-eth/opencode-termux/releases) β€”
71
+ > installer murni POSIX sh, auto-detect arm64/x64, sha256 diverifikasi.
72
+
65
73
  > Jika muncul warning `install-scripts`, izinkan sekali:
66
74
  > ```bash
67
75
  > npm config set allow-scripts=@nemoobc/opencode-termux --location=user
@@ -74,15 +82,44 @@ npm i -g @nemoobc/opencode-termux
74
82
  opencode-termux --version
75
83
  ```
76
84
 
77
- Alias biar terasa asli:
85
+ ### πŸ›  Perintah bawaan CLI
86
+
87
+ | Perintah | Fungsi |
88
+ |---|---|
89
+ | `opencode-termux` | Jalankan CLI opencode (argumen diteruskan) |
90
+ | `opencode-termux update` | Perbarui binary ke upstream terbaru |
91
+ | `opencode-termux doctor` | Diagnosis lingkungan & bundle (exit code jujur) |
92
+ | `opencode-termux version` | Info versi paket + binary |
93
+
94
+ > Identitas perintah konsisten satu nama: **`opencode-termux`** β€” tanpa alias,
95
+ > tanpa nama lain, di semua dokumentasi, log, dan script internal.
96
+
97
+ ## πŸ§ͺ Tes
78
98
 
79
99
  ```bash
80
- ln -sf $PREFIX/bin/opencode-termux $PREFIX/bin/opencode
100
+ npm test # struktur + unit (cepat, tanpa unduhan besar)
101
+ npm run test:e2e # e2e penuh: install bundle + smoke test + subcommand
81
102
  ```
82
103
 
104
+ CI GitHub menjalankan tes struktur di setiap push, plus **e2e arm64 sungguhan**
105
+ (emulasi QEMU β€” loader musl prebuilt Termux benar-benar dieksekusi).
106
+
107
+ ## πŸ”’ Keamanan
108
+
109
+ - Tarball binary diverifikasi **sha512** terhadap metadata resmi registry npm.
110
+ - Unduhan memakai retry + backoff eksponensial (tahan jaringan gemetar).
111
+
83
112
  ## πŸ“¦ Versi
84
113
 
85
- Tiga versi terakhir selalu tersedia β€” [lihat semua](https://www.npmjs.com/package/@nemoobc/opencode-termux?activeTab=versions).
114
+ Semua versi npm: [npmjs.com/@nemoobc/opencode-termux](https://www.npmjs.com/package/@nemoobc/opencode-termux?activeTab=versions) Β·
115
+ Rilis & changelog: [GitHub Releases](https://github.com/nemo-base-eth/opencode-termux/releases)
116
+
117
+ ## 🌐 Ekosistem
118
+
119
+ | Proyek | Fungsi |
120
+ |---|---|
121
+ | **[opencode-termux](https://github.com/nemo-base-eth/opencode-termux)** | CLI opencode native di Termux β€” repo ini |
122
+ | **[opencode-android](https://github.com/nemo-base-eth/opencode-android)** | opencode sebagai aplikasi Android native ([unduh APK v1.5.0](https://github.com/nemoobc/opencode-android/releases/download/v1.5.0/OpenCode-v1.5.0.apk)) |
86
123
 
87
124
  ## πŸ“„ Lisensi
88
125
 
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require("child_process")
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const root = path.join(__dirname, "..")
6
+ const vendor = path.join(root, "vendor")
7
+ const loader = path.join(vendor, "ld-musl.so")
8
+ const bin = path.join(vendor, "opencode")
9
+ const PKG = require(path.join(root, "package.json"))
10
+ const PREFIX = process.env.TERMUX_PREFIX || "/data/data/com.termux/files/usr"
11
+
12
+ function ready() {
13
+ return fs.existsSync(loader) && fs.existsSync(bin)
14
+ }
15
+
16
+ // Auto-heal: kalau postinstall terlewat (mis. --ignore-scripts), pasang sekarang.
17
+ function heal() {
18
+ if (ready()) return true
19
+ console.log("[opencode-termux] vendor belum ada β€” menjalankan installer…")
20
+ const r = spawnSync(process.execPath, [path.join(root, "install.mjs")], {
21
+ stdio: "inherit",
22
+ env: process.env,
23
+ })
24
+ if (r.status !== 0 || !ready()) {
25
+ console.error("[opencode-termux] instalasi bundle gagal. Coba manual:")
26
+ console.error(" npm rebuild @nemoobc/opencode-termux")
27
+ return false
28
+ }
29
+ return true
30
+ }
31
+
32
+ // DNS fix: musl hasil build kita membaca config dari prefix Termux β€”
33
+ // pastikan filenya ada (bisa ditulis tanpa root).
34
+ function ensureDns() {
35
+ try {
36
+ const etc = path.join(PREFIX, "etc")
37
+ fs.mkdirSync(etc, { recursive: true })
38
+ const rc = path.join(etc, "resolv.conf")
39
+ if (!fs.existsSync(rc)) fs.writeFileSync(rc, "nameserver 1.1.1.1\nnameserver 8.8.8.8\n")
40
+ const hh = path.join(etc, "hosts")
41
+ if (!fs.existsSync(hh)) fs.writeFileSync(hh, "127.0.0.1 localhost\n")
42
+ } catch {}
43
+ }
44
+
45
+ function runBinary(args) {
46
+ // LD_PRELOAD bawaan Termux (libtermux-exec) dibuat untuk Bionic dan akan
47
+ // gagal relokasi jika ikut dimuat ke proses musl β€” jadi selalu dibersihkan.
48
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
49
+ ensureDns()
50
+ const r = spawnSync(loader, [bin, ...args], {
51
+ stdio: "inherit",
52
+ env: { ...cleanEnv, LD_LIBRARY_PATH: vendor },
53
+ })
54
+ if (r.error) {
55
+ console.error("[opencode-termux] gagal menjalankan binary:", r.error.message)
56
+ return 1
57
+ }
58
+ const sigExit = { SIGINT: 130, SIGQUIT: 131, SIGTERM: 143 }
59
+ return r.status ?? sigExit[r.signal] ?? 1
60
+ }
61
+
62
+ async function cmdUpdate() {
63
+ console.log(`[opencode-termux] memperbarui bundle (paket v${PKG.version})…`)
64
+ const env = { ...process.env }
65
+ delete env.LD_PRELOAD
66
+ delete env.LD_PRELOAD_32BIT
67
+ const r = spawnSync(process.execPath, [path.join(root, "install.mjs")], {
68
+ stdio: "inherit",
69
+ env: env,
70
+ })
71
+ if (r.status !== 0) {
72
+ console.error("[opencode-termux] ❌ update gagal.")
73
+ return 1
74
+ }
75
+ // sinkronkan pin di package.json modul agar auto-heal berikutnya konsisten
76
+ try {
77
+ const latest = await (await fetch("https://registry.npmjs.org/opencode-ai/latest")).json()
78
+ if (latest.version && latest.version !== PKG.opencodeUpstream) {
79
+ PKG.opencodeUpstream = latest.version
80
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(PKG, null, 2) + "\n")
81
+ }
82
+ } catch {}
83
+ console.log("[opencode-termux] βœ… update selesai.")
84
+ return 0
85
+ }
86
+
87
+ function cmdDoctor() {
88
+ let critical = 0
89
+ const cek = (name, fn, { crit = true } = {}) => {
90
+ try {
91
+ const info = fn()
92
+ console.log(`βœ… ${name}${info ? ` β€” ${info}` : ""}`)
93
+ } catch (e) {
94
+ if (crit) critical++
95
+ console.log(`${crit ? "❌" : "⚠️ "} ${name} β€” ${e.message}`)
96
+ }
97
+ }
98
+
99
+ console.log(`[opencode-termux] doctor v${PKG.version} (upstream ${PKG.opencodeUpstream})`)
100
+ cek("platform", () => {
101
+ if (process.platform !== "android") throw new Error(`process.platform=${process.platform} (bukan android)`)
102
+ return "android"
103
+ }, { crit: false })
104
+ cek("arsitektur", () => {
105
+ if (process.arch !== "arm64" && process.arch !== "x64") throw new Error(`${process.arch} tidak didukung`)
106
+ return process.arch
107
+ })
108
+ cek("node >= 18", () => {
109
+ const [M] = process.versions.node.split(".").map(Number)
110
+ if (M < 18) throw new Error(`node ${process.versions.node}`)
111
+ return process.versions.node
112
+ })
113
+ cek("tar tersedia", () => {
114
+ const r = spawnSync("tar", ["--version"], { stdio: "ignore" })
115
+ if (r.error || r.status !== 0) throw new Error("tidak ditemukan β€” pkg install tar")
116
+ })
117
+ cek("vendor lengkap", () => {
118
+ if (!ready()) throw new Error("vendor/ tidak lengkap β€” jalankan 'opencode-termux update'")
119
+ return `${fs.readdirSync(vendor).length} file`
120
+ })
121
+ cek("DNS resolv.conf", () => {
122
+ const rc = path.join(PREFIX, "etc", "resolv.conf")
123
+ if (!fs.existsSync(rc)) throw new Error(`${rc} hilang`)
124
+ return "ada"
125
+ }, { crit: false })
126
+ cek("jaringan registry npm", () => {
127
+ const r = spawnSync(process.execPath, ["-e", "fetch('https://registry.npmjs.org/-/ping').then(r=>{if(!r.ok)process.exit(1)})"], {
128
+ timeout: 10000,
129
+ })
130
+ if (r.status !== 0) throw new Error("registry tak terjangkau")
131
+ })
132
+ cek("binary opencode", () => {
133
+ if (!ready()) throw new Error("binary belum terpasang")
134
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
135
+ const out = spawnSync(loader, [bin, "--version"], {
136
+ encoding: "utf8",
137
+ env: { ...cleanEnv, LD_LIBRARY_PATH: vendor },
138
+ })
139
+ if (out.status !== 0) throw new Error("gagal dieksekusi")
140
+ return `v${out.stdout.trim()}`
141
+ })
142
+
143
+ console.log(critical === 0 ? "[opencode-termux] βœ… semua komponen kritis sehat" : `[opencode-termux] ❌ ${critical} masalah kritis`)
144
+ return critical === 0 ? 0 : 1
145
+ }
146
+
147
+ function cmdVersion() {
148
+ let binVer = "(belum terpasang)"
149
+ if (ready()) {
150
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
151
+ const out = spawnSync(loader, [bin, "--version"], {
152
+ encoding: "utf8",
153
+ env: { ...cleanEnv, LD_LIBRARY_PATH: vendor },
154
+ })
155
+ if (out.status === 0 && out.stdout.trim()) binVer = out.stdout.trim()
156
+ }
157
+ console.log(`opencode-termux v${PKG.version} (upstream opencode ${PKG.opencodeUpstream}, binary ${binVer})`)
158
+ return 0
159
+ }
160
+
161
+ async function main() {
162
+ const arg = process.argv[2]
163
+ if (arg === "update") return cmdUpdate()
164
+ if (arg === "doctor") return cmdDoctor()
165
+ if (arg === "version") return cmdVersion()
166
+ if (arg === "help" || arg === "--help" || arg === "-h") {
167
+ console.log(`opencode-termux v${PKG.version}
168
+ pakai:
169
+ opencode-termux jalankan CLI opencode (argumen diteruskan)
170
+ opencode-termux update perbarui binary ke upstream terbaru
171
+ opencode-termux doctor diagnosis lingkungan & bundle
172
+ opencode-termux version info versi paket + binary`)
173
+ return 0
174
+ }
175
+ if (!heal()) return 1
176
+ return runBinary(process.argv.slice(2))
177
+ }
178
+
179
+ main().then(code => process.exit(code)).catch(e => {
180
+ console.error("[opencode-termux]", e.message)
181
+ process.exit(1)
182
+ })
package/install.mjs CHANGED
@@ -11,33 +11,35 @@ import { execFileSync } from "child_process"
11
11
  import { Readable } from "stream"
12
12
  import { pipeline } from "stream/promises"
13
13
  import { fileURLToPath } from "url"
14
+ import { alpinePkg } from "./lib/alpine.mjs"
15
+ import { fetchWithRetry } from "./lib/net.mjs"
16
+ import { expectedFromRegistry, verifySha512 } from "./lib/integrity.mjs"
14
17
 
15
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
19
  const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
17
20
  const ARCH = process.env.OCX_ARCH || "arm64"
18
21
  const FORCE = !!process.env.OCX_FORCE
19
22
  const IS_ANDROID = process.platform === "android"
23
+ const T0 = Date.now()
24
+ const log = m => console.log(`[opencode-termux] ${m}`)
20
25
 
21
26
  // Versi upstream: env > package.json > otomatis ambil terbaru dari registry
22
27
  let V = process.env.OCX_UPSTREAM || pkgJson.opencodeUpstream
23
28
  if (!V) {
24
29
  const latest = await (await fetch("https://registry.npmjs.org/opencode-ai/latest")).json()
25
30
  V = latest.version
26
- console.log(`[opencode-termux] upstream opencode-ai terbaru: ${V}`)
31
+ log(`upstream opencode-ai terbaru: ${V}`)
27
32
  }
28
- const GLOBAL_HINT =
29
- process.platform === "win32" ? "" : ""
30
-
31
33
  if (!IS_ANDROID && !FORCE) {
32
- console.log("[opencode-termux] Bukan Termux/Android β€” instalasi dilewati (pakai opencode-ai resmi).")
34
+ log("Bukan Termux/Android β€” instalasi dilewati (pakai opencode-ai resmi).")
33
35
  process.exit(0)
34
36
  }
35
37
 
36
38
  const A = ARCH === "x64" ? "x86_64" : "aarch64"
37
39
 
38
40
  async function dl(url, dest) {
39
- console.log(`[opencode-termux] download ${url.split("/").pop()}`)
40
- const res = await fetch(url)
41
+ log(`download ${url.split("/").pop()}`)
42
+ const res = await fetchWithRetry(fetch, url, {}, 3, m => log(m))
41
43
  if (!res.ok) throw new Error(`HTTP ${res.status} β€” ${url}`)
42
44
  await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest))
43
45
  }
@@ -54,87 +56,108 @@ const untar = (tgz, dest, members = []) => {
54
56
  const work = path.join(__dirname, ".build")
55
57
  fs.rmSync(work, { recursive: true, force: true })
56
58
  fs.mkdirSync(work, { recursive: true })
57
- const AV = "v3.21", AL = "3.21.3"
58
59
 
59
- // 1) binary opencode (musl) dari npm resmi
60
- await dl(`https://registry.npmjs.org/opencode-linux-${ARCH}-musl/-/opencode-linux-${ARCH}-musl-${V}.tgz`, `${work}/oc.tgz`)
61
- untar(`${work}/oc.tgz`, `${work}/oc`)
60
+ try {
61
+ const AV = "v3.21", AL = "3.21.3"
62
62
 
63
- // 2) libgcc + libstdc++
64
- const apkDir = `${work}/apk`; fs.mkdirSync(apkDir, { recursive: true })
65
- for (const f of [`libgcc-14.2.0-r4.apk`, `libstdc%2B%2B-14.2.0-r4.apk`]) {
66
- await dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/main/${A}/${f}`, `${apkDir}/${f}`)
67
- untar(`${apkDir}/${f}`, apkDir)
68
- }
63
+ // Resolusi dinamis paket Alpine dari CDN (lihat lib/alpine.mjs)
64
+ const pkg = name => alpinePkg(fetch, `https://dl-cdn.alpinelinux.org/alpine/${AV}/main/${A}`, name)
69
65
 
70
- // 3) rakit vendor/
71
- const vendor = path.join(__dirname, "vendor")
72
- fs.rmSync(vendor, { recursive: true, force: true }); fs.mkdirSync(vendor)
73
- const cp = (dir, name) => fs.copyFileSync(path.join(dir, name), path.join(vendor, name))
74
- if (A === "aarch64") {
75
- // loader hasil build khusus: resolv.conf & hosts menunjuk ke prefix Termux
76
- cp(path.join(__dirname, "prebuilt"), "ld-musl-aarch64-termux.so")
77
- fs.renameSync(path.join(vendor, "ld-musl-aarch64-termux.so"), path.join(vendor, "ld-musl.so"))
78
- } else {
79
- const mini = `${work}/ap`; await dl(
80
- `https://dl-cdn.alpinelinux.org/alpine/${AV}/releases/${A}/alpine-minirootfs-${AL}-${A}.tar.gz`, `${work}/ap.tgz`)
81
- untar(`${work}/ap.tgz`, mini, ["lib"])
82
- cp(`${mini}/lib`, `ld-musl-${A}.so.1`); fs.renameSync(path.join(vendor, `ld-musl-${A}.so.1`), path.join(vendor, "ld-musl.so"))
83
- }
84
- cp(`${work}/oc/package/bin`, "opencode")
85
- cp(`${apkDir}/usr/lib`, "libstdc++.so.6"); cp(`${apkDir}/usr/lib`, "libstdc++.so.6.0.33"); cp(`${apkDir}/usr/lib`, "libgcc_s.so.1")
86
- for (const f of fs.readdirSync(vendor)) fs.chmodSync(path.join(vendor, f), 0o755)
66
+ // 1) binary opencode (musl) dari npm resmi β€” diverifikasi sha512 registry
67
+ const ocTgz = `opencode-linux-${ARCH}-musl-${V}.tgz`
68
+ await dl(`https://registry.npmjs.org/opencode-linux-${ARCH}-musl/-/${ocTgz}`, `${work}/oc.tgz`)
69
+ log("verifikasi integritas sha512…")
70
+ const pk = await (await fetchWithRetry(fetch, `https://registry.npmjs.org/opencode-linux-${ARCH}-musl`, {}, 3)).json()
71
+ verifySha512(`${work}/oc.tgz`, expectedFromRegistry(pk, V))
72
+ untar(`${work}/oc.tgz`, `${work}/oc`)
87
73
 
88
- // 4) siapkan DNS config di prefix Termux (bisa ditulis TANPA root)
89
- function ensureEtc() {
90
- try {
91
- const PREFIX = process.env.TERMUX_PREFIX || "/data/data/com.termux/files/usr"
92
- const etc = path.join(PREFIX, "etc")
93
- fs.mkdirSync(etc, { recursive: true })
94
- const rc = path.join(etc, "resolv.conf")
95
- if (!fs.existsSync(rc)) fs.writeFileSync(rc, "nameserver 1.1.1.1\nnameserver 8.8.8.8\n")
96
- const hh = path.join(etc, "hosts")
97
- if (!fs.existsSync(hh)) fs.writeFileSync(hh, "127.0.0.1 localhost\n")
98
- } catch (e) {
99
- if (!FORCE && IS_ANDROID) throw e
74
+ // 2) libgcc + libstdc++ (versi terbaru yang tersedia di CDN)
75
+ const apkDir = `${work}/apk`; fs.mkdirSync(apkDir, { recursive: true })
76
+ for (const name of ["libgcc", "libstdc%2B%2B"]) {
77
+ const f = await pkg(name)
78
+ await dl(`https://dl-cdn.alpinelinux.org/alpine/${AV}/main/${A}/${f}`, `${apkDir}/${f}`)
79
+ untar(`${apkDir}/${f}`, apkDir)
100
80
  }
101
- }
102
- ensureEtc()
103
81
 
104
- // 5) smoke test (LD_PRELOAD termux-exec dibuang: tidak kompatibel dengan musl)
105
- console.log("[opencode-termux] smoke test…")
106
- const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
107
- execFileSync(path.join(vendor, "ld-musl.so"),
108
- [path.join(vendor, "opencode"), "--version"],
109
- { stdio: "inherit", env: { ...cleanEnv, LD_LIBRARY_PATH: vendor } })
82
+ // 3) rakit vendor/
83
+ const vendor = path.join(__dirname, "vendor")
84
+ fs.rmSync(vendor, { recursive: true, force: true }); fs.mkdirSync(vendor)
85
+ const cp = (dir, name) => fs.copyFileSync(path.join(dir, name), path.join(vendor, name))
86
+ if (A === "aarch64") {
87
+ // loader hasil build khusus: resolv.conf & hosts menunjuk ke prefix Termux
88
+ cp(path.join(__dirname, "prebuilt"), "ld-musl-aarch64-termux.so")
89
+ fs.renameSync(path.join(vendor, "ld-musl-aarch64-termux.so"), path.join(vendor, "ld-musl.so"))
90
+ } else {
91
+ const mini = `${work}/ap`; await dl(
92
+ `https://dl-cdn.alpinelinux.org/alpine/${AV}/releases/${A}/alpine-minirootfs-${AL}-${A}.tar.gz`, `${work}/ap.tgz`)
93
+ untar(`${work}/ap.tgz`, mini, ["lib"])
94
+ cp(`${mini}/lib`, `ld-musl-${A}.so.1`); fs.renameSync(path.join(vendor, `ld-musl-${A}.so.1`), path.join(vendor, "ld-musl.so"))
95
+ }
96
+ cp(`${work}/oc/package/bin`, "opencode")
97
+ cp(`${apkDir}/usr/lib`, "libstdc++.so.6"); cp(`${apkDir}/usr/lib`, "libstdc++.so.6.0.33"); cp(`${apkDir}/usr/lib`, "libgcc_s.so.1")
98
+ for (const f of fs.readdirSync(vendor)) fs.chmodSync(path.join(vendor, f), 0o755)
110
99
 
111
- // 6) auto-install agents, commands & config opencode (tanpa menimpa milik user)
112
- try {
113
- const HOME = process.env.HOME || "/data/data/com.termux/files/home"
114
- const OC = path.join(HOME, ".config", "opencode")
115
- for (const [srcDir, dstName] of [["agents", "agent"], ["commands", "command"]]) {
116
- const src = path.join(__dirname, srcDir)
117
- if (!fs.existsSync(src)) continue
118
- const dst = path.join(OC, dstName)
119
- fs.mkdirSync(dst, { recursive: true })
120
- for (const f of fs.readdirSync(src)) {
121
- fs.copyFileSync(path.join(src, f), path.join(dst, f))
122
- console.log(`[opencode-termux] βœ… terpasang: ${dstName}/${f}`)
100
+ // 4) siapkan DNS config di prefix Termux (bisa ditulis TANPA root)
101
+ function ensureEtc() {
102
+ try {
103
+ const PREFIX = process.env.TERMUX_PREFIX || "/data/data/com.termux/files/usr"
104
+ const etc = path.join(PREFIX, "etc")
105
+ fs.mkdirSync(etc, { recursive: true })
106
+ const rc = path.join(etc, "resolv.conf")
107
+ if (!fs.existsSync(rc)) fs.writeFileSync(rc, "nameserver 1.1.1.1\nnameserver 8.8.8.8\n")
108
+ const hh = path.join(etc, "hosts")
109
+ if (!fs.existsSync(hh)) fs.writeFileSync(hh, "127.0.0.1 localhost\n")
110
+ } catch (e) {
111
+ if (!FORCE && IS_ANDROID) throw e
112
+ log("peringatan: setup resolv.conf dilewati (" + e.message.split("\n")[0] + ")")
123
113
  }
124
114
  }
125
- const cfgSrc = path.join(__dirname, "config", "opencode.json")
126
- const cfgDst = path.join(OC, "opencode.json")
127
- if (!fs.existsSync(cfgDst)) {
128
- fs.copyFileSync(cfgSrc, cfgDst)
129
- console.log("[opencode-termux] βœ… config default terpasang (model gratis)")
115
+ ensureEtc()
116
+
117
+ // 5) smoke test (LD_PRELOAD termux-exec dibuang: tidak kompatibel dengan musl)
118
+ if (process.env.OCX_SKIP_SMOKE === "1") {
119
+ log("smoke test dilewati (OCX_SKIP_SMOKE=1 β€” mode cross-build)")
130
120
  } else {
131
- console.log("[opencode-termux] config user sudah ada β€” tidak disentuh")
121
+ log("smoke test…")
122
+ const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
123
+ execFileSync(path.join(vendor, "ld-musl.so"),
124
+ [path.join(vendor, "opencode"), "--version"],
125
+ { stdio: "inherit", env: { ...cleanEnv, LD_LIBRARY_PATH: vendor } })
126
+ }
127
+
128
+ // 6) auto-install agents, commands & config opencode (tanpa menimpa milik user)
129
+ try {
130
+ const HOME = process.env.HOME || "/data/data/com.termux/files/home"
131
+ const OC = path.join(HOME, ".config", "opencode")
132
+ for (const [srcDir, dstName] of [["agents", "agent"], ["commands", "command"]]) {
133
+ const src = path.join(__dirname, srcDir)
134
+ if (!fs.existsSync(src)) continue
135
+ const dst = path.join(OC, dstName)
136
+ fs.mkdirSync(dst, { recursive: true })
137
+ for (const f of fs.readdirSync(src)) {
138
+ fs.copyFileSync(path.join(src, f), path.join(dst, f))
139
+ log(`βœ… terpasang: ${dstName}/${f}`)
140
+ }
141
+ }
142
+ const cfgSrc = path.join(__dirname, "config", "opencode.json")
143
+ const cfgDst = path.join(OC, "opencode.json")
144
+ if (!fs.existsSync(cfgDst)) {
145
+ fs.copyFileSync(cfgSrc, cfgDst)
146
+ log("βœ… config default terpasang (model gratis)")
147
+ } else {
148
+ log("config user sudah ada β€” tidak disentuh")
149
+ }
150
+ } catch (e) {
151
+ log("auto-install agent dilewati:", e.message)
132
152
  }
133
153
  } catch (e) {
134
- console.log("[opencode-termux] auto-install agent dilewati:", e.message)
154
+ console.error("[opencode-termux] ❌ instalasi gagal:", e.message)
155
+ process.exitCode = 1
156
+ throw e
157
+ } finally {
158
+ fs.rmSync(work, { recursive: true, force: true })
135
159
  }
136
160
 
137
- fs.rmSync(work, { recursive: true, force: true })
138
- console.log(`[opencode-termux] βœ… siap!
161
+ console.log(`[opencode-termux] βœ… siap dalam ${((Date.now() - T0) / 1000).toFixed(1)}s
139
162
  β€’ global : jalankan 'opencode-termux'
140
163
  β€’ lokal : 'npx opencode-termux' dari folder project ini`)
package/lib/alpine.mjs ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Resolver paket Alpine dari CDN β€” anti-404 saat Alpine memperbarui paket
3
+ * dalam satu branch. Fallback ke versi terakhir yang diketahui hidup.
4
+ */
5
+ export const cmpVer = (a, b) => {
6
+ const pa = a.split(/[.\-r]/).filter(Boolean).map(Number)
7
+ const pb = b.split(/[.\-r]/).filter(Boolean).map(Number)
8
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
9
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0)
10
+ }
11
+ return 0
12
+ }
13
+
14
+ export async function alpinePkg(fetchFn, cdnBase, nameEncoded, fallback = "14.2.0-r4") {
15
+ let latest = null
16
+ try {
17
+ const res = await fetchFn(`${cdnBase}/`)
18
+ const idx = await res.text()
19
+ const re = new RegExp(`${nameEncoded}-([0-9][0-9a-zA-Z.+]*)-r([0-9]+)\\.apk`, "g")
20
+ for (const m of idx.matchAll(re)) {
21
+ const v = `${m[1]}-r${m[2]}`
22
+ if (!latest || cmpVer(v, latest.v) > 0) latest = { file: m[0], v }
23
+ }
24
+ } catch {}
25
+ if (!latest) {
26
+ console.warn(`[opencode-termux] listing CDN gagal β€” fallback ${nameEncoded}-${fallback}.apk`)
27
+ latest = { file: `${nameEncoded}-${fallback}.apk`, v: fallback }
28
+ }
29
+ return latest.file
30
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Verifikasi integritas file terhadap hash sha512 format registry npm
3
+ * ("sha512-<base64>"). Mencegah tarball korup/termanipulasi saat transit.
4
+ */
5
+ import crypto from "crypto"
6
+ import fs from "fs"
7
+
8
+ export const expectedFromRegistry = (packument, version) => {
9
+ const dist = packument?.versions?.[version]?.dist
10
+ return dist?.integrity?.startsWith("sha512-") ? dist.integrity.slice(7) : null
11
+ }
12
+
13
+ export function verifySha512(filePath, expectedB64) {
14
+ if (!expectedB64) throw new Error("tidak ada hash acuan (integrity) dari registry")
15
+ const h = crypto.createHash("sha512")
16
+ h.update(fs.readFileSync(filePath))
17
+ const got = h.digest("base64")
18
+ const a = Buffer.from(got)
19
+ const b = Buffer.from(expectedB64)
20
+ if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
21
+ throw new Error(`integritas gagal: file β‰  sha512 registry (${filePath})`)
22
+ }
23
+ return true
24
+ }
package/lib/net.mjs ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Utilitas jaringan: fetch dengan retry + backoff eksponensial.
3
+ * fetchFn dapat disuntik untuk unit test.
4
+ */
5
+ export async function fetchWithRetry(fetchFn, url, opts = {}, retries = 3, log = () => {}) {
6
+ let lastErr
7
+ for (let attempt = 1; attempt <= retries; attempt++) {
8
+ try {
9
+ const res = await fetchFn(url, opts)
10
+ // 429/5xx β†’ layak dicoba ulang; 4xx lain β†’ gagal permanen
11
+ if (!res.ok && res.status < 500 && res.status !== 429) return res
12
+ if (res.ok) return res
13
+ lastErr = new Error(`HTTP ${res.status} β€” ${url}`)
14
+ } catch (e) {
15
+ lastErr = e
16
+ }
17
+ if (attempt < retries) {
18
+ const wait = Math.min(1000 * 2 ** (attempt - 1), 8000)
19
+ log(`gagal (${attempt}/${retries}) β€” ulang dalam ${wait}ms`)
20
+ await new Promise(r => setTimeout(r, wait))
21
+ }
22
+ }
23
+ throw lastErr
24
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@nemoobc/opencode-termux",
3
- "version": "1.19.1",
3
+ "version": "1.20.1",
4
4
  "description": "opencode CLI native untuk Termux/Android tanpa proot β€” membundel loader musl + binary opencode resmi (upstream opencode-ai)",
5
5
  "bin": {
6
- "opencode-termux": "./bin/opencode.js"
6
+ "opencode-termux": "./bin/opencode-termux.js"
7
7
  },
8
8
  "scripts": {
9
- "postinstall": "node install.mjs"
9
+ "postinstall": "node install.mjs",
10
+ "test": "node test/run.mjs",
11
+ "test:e2e": "node test/run.mjs --e2e"
10
12
  },
11
13
  "keywords": [
12
14
  "opencode",
@@ -22,13 +24,15 @@
22
24
  "files": [
23
25
  "bin",
24
26
  "install.mjs",
27
+ "lib",
25
28
  "prebuilt",
29
+ "LICENSE",
26
30
  "README.md",
27
31
  "agents",
28
32
  "commands",
29
33
  "config"
30
34
  ],
31
- "opencodeUpstream": "1.18.21",
35
+ "opencodeUpstream": "1.18.22",
32
36
  "repository": {
33
37
  "type": "git",
34
38
  "url": "git+https://github.com/nemoobc/opencode-termux.git"
package/bin/opencode.js DELETED
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env node
2
- const { spawnSync } = require("child_process")
3
- const fs = require("fs")
4
- const path = require("path")
5
- const vendor = path.join(__dirname, "..", "vendor")
6
- const loader = path.join(vendor, "ld-musl.so")
7
- const bin = path.join(vendor, "opencode")
8
-
9
- function ready() {
10
- return fs.existsSync(loader) && fs.existsSync(bin)
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
- // DNS fix: musl hasil build kita membaca config dari prefix Termux β€”
30
- // pastikan filenya ada (bisa ditulis tanpa root).
31
- try {
32
- const PREFIX = process.env.TERMUX_PREFIX || "/data/data/com.termux/files/usr"
33
- const etc = path.join(PREFIX, "etc")
34
- fs.mkdirSync(etc, { recursive: true })
35
- const rc = path.join(etc, "resolv.conf")
36
- if (!fs.existsSync(rc)) fs.writeFileSync(rc, "nameserver 1.1.1.1\nnameserver 8.8.8.8\n")
37
- const hh = path.join(etc, "hosts")
38
- if (!fs.existsSync(hh)) fs.writeFileSync(hh, "127.0.0.1 localhost\n")
39
- } catch {}
40
-
41
- // LD_PRELOAD bawaan Termux (libtermux-exec) dibuat untuk Bionic dan akan
42
- // gagal relokasi jika ikut dimuat ke proses musl β€” jadi selalu dibersihkan.
43
- const { LD_PRELOAD, LD_PRELOAD_32BIT, ...cleanEnv } = process.env
44
- const r = spawnSync(loader, [bin, ...process.argv.slice(2)], {
45
- stdio: "inherit",
46
- env: { ...cleanEnv, LD_LIBRARY_PATH: vendor },
47
- })
48
- process.exit(r.status ?? 1)