@nemoobc/opencode-termux 1.19.0 β 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 +21 -0
- package/README.md +74 -17
- package/bin/opencode-termux.js +182 -0
- package/install.mjs +98 -75
- package/lib/alpine.mjs +30 -0
- package/lib/integrity.mjs +24 -0
- package/lib/net.mjs +24 -0
- package/package.json +10 -6
- package/bin/opencode.js +0 -48
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
|
@@ -3,28 +3,23 @@
|
|
|
3
3
|
**[opencode](https://opencode.ai) CLI native di Termux/Android β tanpa proot, tanpa root, plus agent & tools otomatis.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@nemoobc/opencode-termux)
|
|
6
|
+
[](https://github.com/nemoobc/opencode-termux/releases)
|
|
7
|
+
[](.github/workflows/test.yml)
|
|
6
8
|
[](#)
|
|
7
9
|
[](#)
|
|
8
10
|
[](.github/workflows/sync-upstream.yml)
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pkg update && pkg install nodejs-lts
|
|
16
|
-
npm i -g @nemoobc/opencode-termux
|
|
17
|
-
```
|
|
14
|
+
## π€ Agent & tools otomatis
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## π€ Yang otomatis terpasang
|
|
16
|
+
Sekali install β agent, command, dan config **langsung terpasang** tanpa setting ulang:
|
|
22
17
|
|
|
23
18
|
| Agent | Tugas |
|
|
24
19
|
|---|---|
|
|
25
20
|
| **autodev** | Developer otonom universal: coding, tools, build, tes, fix, rilis β berurutan |
|
|
26
21
|
| **termux-coder** | Coding assistant yang paham Termux (PATH, pkg, tanpa root) |
|
|
27
|
-
| **apk-builder** | Build APK Android: payload, align, sign
|
|
22
|
+
| **apk-builder** | Build APK Android: payload, align, sign |
|
|
28
23
|
| **tester** | Jalankan seluruh test suite + laporan pass/fail |
|
|
29
24
|
| **fixer** | Loop perbaikan: uji β analisis β fix minimal β uji ulang (maks 5x) |
|
|
30
25
|
|
|
@@ -39,30 +34,92 @@ Config default ikut terpasang: **model gratis `opencode/x-preview-f-free`** β
|
|
|
39
34
|
|
|
40
35
|
## π Sync upstream otomatis
|
|
41
36
|
|
|
42
|
-
Workflow GitHub mengecek [opencode-ai](https://github.com/anomalyco/opencode) baru
|
|
43
|
-
|
|
37
|
+
Workflow GitHub mengecek [opencode-ai](https://github.com/anomalyco/opencode) baru setiap
|
|
38
|
+
6 jam β begitu ada versi baru, paket ini otomatis menyesuaikan (commit memakai identitas nemoobc).
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Kenapa paket ini ada?
|
|
43
|
+
|
|
44
|
+
Installer resmi `opencode-ai` gagal di Termux karena tidak mengenali
|
|
45
|
+
`process.platform === "android"` dan tidak menyediakan build untuk Bionic libc.
|
|
46
|
+
|
|
47
|
+
Paket ini menjembatani tanpa trik aneh:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
opencode-termux (shim Node)
|
|
51
|
+
ββ vendor/ld-musl.so β musl libc hasil build khusus Termux*
|
|
52
|
+
ββ vendor/opencode β binary resmi opencode-linux-arm64-musl
|
|
53
|
+
β€³ LD_LIBRARY_PATH β libstdc++ / libgcc_s (dari Alpine)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
\* path `/etc/resolv.conf` & `/etc/hosts` dipatch saat kompilasi menuju
|
|
57
|
+
`$PREFIX/etc/` sehingga DNS jalan tanpa root.
|
|
58
|
+
|
|
59
|
+
## π Install
|
|
46
60
|
|
|
47
61
|
```bash
|
|
48
|
-
|
|
62
|
+
pkg update && pkg install nodejs-lts tar
|
|
63
|
+
npm i -g @nemoobc/opencode-termux
|
|
49
64
|
```
|
|
50
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
|
+
|
|
51
73
|
> Jika muncul warning `install-scripts`, izinkan sekali:
|
|
52
74
|
> ```bash
|
|
53
75
|
> npm config set allow-scripts=@nemoobc/opencode-termux --location=user
|
|
54
76
|
> npm rebuild -g @nemoobc/opencode-termux
|
|
55
77
|
> ```
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
## β
Verifikasi
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
opencode-termux --version
|
|
83
|
+
```
|
|
84
|
+
|
|
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
|
|
58
98
|
|
|
59
99
|
```bash
|
|
60
|
-
|
|
100
|
+
npm test # struktur + unit (cepat, tanpa unduhan besar)
|
|
101
|
+
npm run test:e2e # e2e penuh: install bundle + smoke test + subcommand
|
|
61
102
|
```
|
|
62
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
|
+
|
|
63
112
|
## π¦ Versi
|
|
64
113
|
|
|
65
|
-
|
|
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)) |
|
|
66
123
|
|
|
67
124
|
## π Lisensi
|
|
68
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
const res = await fetch(
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
untar(`${work}/oc.tgz`, `${work}/oc`)
|
|
60
|
+
try {
|
|
61
|
+
const AV = "v3.21", AL = "3.21.3"
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
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
|
-
//
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "opencode CLI native untuk Termux/Android tanpa proot
|
|
3
|
+
"version": "1.20.1",
|
|
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,16 +24,18 @@
|
|
|
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.
|
|
35
|
+
"opencodeUpstream": "1.18.22",
|
|
32
36
|
"repository": {
|
|
33
37
|
"type": "git",
|
|
34
38
|
"url": "git+https://github.com/nemoobc/opencode-termux.git"
|
|
35
39
|
},
|
|
36
40
|
"homepage": "https://github.com/nemoobc/opencode-termux#readme"
|
|
37
|
-
}
|
|
41
|
+
}
|
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)
|