@jo.cs98/opencode-plus 1.3.0-plus.2
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/README.md +15 -0
- package/bin/install.cjs +171 -0
- package/bin/opencodeplus.cjs +179 -0
- package/package.json +20 -0
package/README.md
ADDED
package/bin/install.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// postinstall: downloads the pre-built opencode-plus binary from GitHub Releases
|
|
3
|
+
|
|
4
|
+
const https = require("https")
|
|
5
|
+
const http = require("http")
|
|
6
|
+
const fs = require("fs")
|
|
7
|
+
const path = require("path")
|
|
8
|
+
const os = require("os")
|
|
9
|
+
const { execFileSync } = require("child_process")
|
|
10
|
+
|
|
11
|
+
const REPO = "joaquincabrerasimoes/opencode-plus"
|
|
12
|
+
const NPM_PKG = "@jo.cs98/opencode-plus"
|
|
13
|
+
|
|
14
|
+
// Resolve the directory containing this script (bin/)
|
|
15
|
+
const binDir = path.dirname(fs.realpathSync(__filename))
|
|
16
|
+
const dest = path.join(binDir, ".opencode" + (os.platform() === "win32" ? ".exe" : ""))
|
|
17
|
+
|
|
18
|
+
// Skip if already present (e.g. re-running postinstall after an install)
|
|
19
|
+
if (fs.existsSync(dest)) {
|
|
20
|
+
process.exit(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }
|
|
24
|
+
const archMap = { x64: "x64", arm64: "arm64" }
|
|
25
|
+
|
|
26
|
+
const platform = platformMap[os.platform()]
|
|
27
|
+
const arch = archMap[os.arch()]
|
|
28
|
+
|
|
29
|
+
if (!platform || !arch) {
|
|
30
|
+
console.warn(`[opencode-plus] Unsupported platform: ${os.platform()} ${os.arch()} — skipping binary download`)
|
|
31
|
+
process.exit(0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function needsBaseline() {
|
|
35
|
+
if (arch !== "x64") return false
|
|
36
|
+
if (platform === "linux") {
|
|
37
|
+
try {
|
|
38
|
+
return !/(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const base = `opencode-plus-${platform}-${arch}`
|
|
47
|
+
const candidates = needsBaseline() ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
|
|
48
|
+
|
|
49
|
+
// Asset name on GitHub Releases: opencode-plus-windows-x64.zip / opencode-plus-linux-x64.tar.gz
|
|
50
|
+
const ext = platform === "linux" ? ".tar.gz" : ".zip"
|
|
51
|
+
const binName = platform === "windows" ? "opencode.exe" : "opencode"
|
|
52
|
+
|
|
53
|
+
function fetch(url, redirect = 0) {
|
|
54
|
+
if (redirect > 5) throw new Error("Too many redirects")
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const mod = url.startsWith("https") ? https : http
|
|
57
|
+
mod
|
|
58
|
+
.get(url, { headers: { "User-Agent": "opencode-plus-installer" } }, (res) => {
|
|
59
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
60
|
+
resolve(fetch(res.headers.location, redirect + 1))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (res.statusCode !== 200) {
|
|
64
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const chunks = []
|
|
68
|
+
res.on("data", (c) => chunks.push(c))
|
|
69
|
+
res.on("end", () => resolve(Buffer.concat(chunks)))
|
|
70
|
+
res.on("error", reject)
|
|
71
|
+
})
|
|
72
|
+
.on("error", reject)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getTag() {
|
|
77
|
+
// First try: find a release whose tag matches our package version
|
|
78
|
+
const ver = require(path.join(binDir, "..", "package.json")).version
|
|
79
|
+
const data = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=20`)
|
|
80
|
+
const releases = JSON.parse(data.toString())
|
|
81
|
+
const match = releases.find((r) => r.tag_name.includes(ver))
|
|
82
|
+
if (match) return match.tag_name
|
|
83
|
+
// Fallback: find any release that has our platform asset
|
|
84
|
+
const asset = `opencode-plus-${platform}-${arch}${ext}`
|
|
85
|
+
const withAsset = releases.find((r) => r.assets && r.assets.some((a) => a.name === asset))
|
|
86
|
+
if (withAsset) return withAsset.tag_name
|
|
87
|
+
// Last resort: latest
|
|
88
|
+
return releases[0].tag_name
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function tryDownload(tag, assetName) {
|
|
92
|
+
const url = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`
|
|
93
|
+
try {
|
|
94
|
+
const buf = await fetch(url)
|
|
95
|
+
return buf
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function extractBinary(buf, assetName) {
|
|
102
|
+
const tmp = path.join(os.tmpdir(), `opencode-plus-${Date.now()}`)
|
|
103
|
+
fs.mkdirSync(tmp, { recursive: true })
|
|
104
|
+
const archive = path.join(tmp, assetName)
|
|
105
|
+
fs.writeFileSync(archive, buf)
|
|
106
|
+
|
|
107
|
+
if (assetName.endsWith(".zip")) {
|
|
108
|
+
// Use PowerShell on Windows, unzip on others
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
execFileSync("powershell.exe", [
|
|
111
|
+
"-NoProfile",
|
|
112
|
+
"-NonInteractive",
|
|
113
|
+
"-Command",
|
|
114
|
+
`Expand-Archive -Path '${archive}' -DestinationPath '${tmp}' -Force`,
|
|
115
|
+
])
|
|
116
|
+
} else {
|
|
117
|
+
execFileSync("unzip", ["-q", "-o", archive, "-d", tmp])
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
execFileSync("tar", ["-xzf", archive, "-C", tmp])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const extracted = path.join(tmp, binName)
|
|
124
|
+
if (!fs.existsSync(extracted)) {
|
|
125
|
+
throw new Error(`Binary not found in archive at ${extracted}`)
|
|
126
|
+
}
|
|
127
|
+
return extracted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
let tag
|
|
132
|
+
try {
|
|
133
|
+
tag = await getTag()
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.warn(`[opencode-plus] Could not fetch release tag: ${e.message} — skipping`)
|
|
136
|
+
process.exit(0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`[opencode-plus] Downloading binary for ${platform}-${arch} from release ${tag}`)
|
|
140
|
+
|
|
141
|
+
for (const name of candidates) {
|
|
142
|
+
const assetName = name + ext
|
|
143
|
+
const buf = await tryDownload(tag, assetName)
|
|
144
|
+
if (!buf) continue
|
|
145
|
+
|
|
146
|
+
let extracted
|
|
147
|
+
try {
|
|
148
|
+
extracted = await extractBinary(buf, assetName)
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.warn(`[opencode-plus] Failed to extract ${assetName}: ${e.message}`)
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.copyFileSync(extracted, dest)
|
|
155
|
+
if (platform !== "windows") fs.chmodSync(dest, 0o755)
|
|
156
|
+
console.log(`[opencode-plus] Installed binary to ${dest}`)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.warn(
|
|
161
|
+
`[opencode-plus] No pre-built binary found for ${platform}-${arch} in release ${tag}.\n` +
|
|
162
|
+
` Tried: ${candidates.map((n) => n + ext).join(", ")}\n` +
|
|
163
|
+
` You may need to build from source.`,
|
|
164
|
+
)
|
|
165
|
+
// non-fatal: bin/opencode will give a helpful error at runtime
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((e) => {
|
|
169
|
+
console.warn(`[opencode-plus] Install script failed: ${e.message}`)
|
|
170
|
+
// non-fatal
|
|
171
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const childProcess = require("child_process")
|
|
4
|
+
const fs = require("fs")
|
|
5
|
+
const path = require("path")
|
|
6
|
+
const os = require("os")
|
|
7
|
+
|
|
8
|
+
function run(target) {
|
|
9
|
+
const result = childProcess.spawnSync(target, process.argv.slice(2), {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
})
|
|
12
|
+
if (result.error) {
|
|
13
|
+
console.error(result.error.message)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
const code = typeof result.status === "number" ? result.status : 0
|
|
17
|
+
process.exit(code)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const envPath = process.env.OPENCODE_BIN_PATH
|
|
21
|
+
if (envPath) {
|
|
22
|
+
run(envPath)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const scriptPath = fs.realpathSync(__filename)
|
|
26
|
+
const scriptDir = path.dirname(scriptPath)
|
|
27
|
+
|
|
28
|
+
//
|
|
29
|
+
const cached = path.join(scriptDir, ".opencode" + (os.platform() === "win32" ? ".exe" : ""))
|
|
30
|
+
if (fs.existsSync(cached)) {
|
|
31
|
+
run(cached)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const platformMap = {
|
|
35
|
+
darwin: "darwin",
|
|
36
|
+
linux: "linux",
|
|
37
|
+
win32: "windows",
|
|
38
|
+
}
|
|
39
|
+
const archMap = {
|
|
40
|
+
x64: "x64",
|
|
41
|
+
arm64: "arm64",
|
|
42
|
+
arm: "arm",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let platform = platformMap[os.platform()]
|
|
46
|
+
if (!platform) {
|
|
47
|
+
platform = os.platform()
|
|
48
|
+
}
|
|
49
|
+
let arch = archMap[os.arch()]
|
|
50
|
+
if (!arch) {
|
|
51
|
+
arch = os.arch()
|
|
52
|
+
}
|
|
53
|
+
const base = "opencode-plus-" + platform + "-" + arch
|
|
54
|
+
const binary = platform === "windows" ? "opencode.exe" : "opencode"
|
|
55
|
+
|
|
56
|
+
function supportsAvx2() {
|
|
57
|
+
if (arch !== "x64") return false
|
|
58
|
+
|
|
59
|
+
if (platform === "linux") {
|
|
60
|
+
try {
|
|
61
|
+
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (platform === "darwin") {
|
|
68
|
+
try {
|
|
69
|
+
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
timeout: 1500,
|
|
72
|
+
})
|
|
73
|
+
if (result.status !== 0) return false
|
|
74
|
+
return (result.stdout || "").trim() === "1"
|
|
75
|
+
} catch {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (platform === "windows") {
|
|
81
|
+
const cmd =
|
|
82
|
+
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
|
|
83
|
+
|
|
84
|
+
for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
|
|
85
|
+
try {
|
|
86
|
+
const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
timeout: 3000,
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
})
|
|
91
|
+
if (result.status !== 0) continue
|
|
92
|
+
const out = (result.stdout || "").trim().toLowerCase()
|
|
93
|
+
if (out === "true" || out === "1") return true
|
|
94
|
+
if (out === "false" || out === "0") return false
|
|
95
|
+
} catch {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const names = (() => {
|
|
107
|
+
const avx2 = supportsAvx2()
|
|
108
|
+
const baseline = arch === "x64" && !avx2
|
|
109
|
+
|
|
110
|
+
if (platform === "linux") {
|
|
111
|
+
const musl = (() => {
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync("/etc/alpine-release")) return true
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
|
|
120
|
+
const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
|
|
121
|
+
if (text.includes("musl")) return true
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false
|
|
127
|
+
})()
|
|
128
|
+
|
|
129
|
+
if (musl) {
|
|
130
|
+
if (arch === "x64") {
|
|
131
|
+
if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
|
|
132
|
+
return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
|
|
133
|
+
}
|
|
134
|
+
return [`${base}-musl`, base]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (arch === "x64") {
|
|
138
|
+
if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
|
|
139
|
+
return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
|
|
140
|
+
}
|
|
141
|
+
return [base, `${base}-musl`]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arch === "x64") {
|
|
145
|
+
if (baseline) return [`${base}-baseline`, base]
|
|
146
|
+
return [base, `${base}-baseline`]
|
|
147
|
+
}
|
|
148
|
+
return [base]
|
|
149
|
+
})()
|
|
150
|
+
|
|
151
|
+
function findBinary(startDir) {
|
|
152
|
+
let current = startDir
|
|
153
|
+
for (;;) {
|
|
154
|
+
const modules = path.join(current, "node_modules")
|
|
155
|
+
if (fs.existsSync(modules)) {
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const candidate = path.join(modules, name, "bin", binary)
|
|
158
|
+
if (fs.existsSync(candidate)) return candidate
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const parent = path.dirname(current)
|
|
162
|
+
if (parent === current) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
current = parent
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const resolved = findBinary(scriptDir)
|
|
170
|
+
if (!resolved) {
|
|
171
|
+
console.error(
|
|
172
|
+
"It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing " +
|
|
173
|
+
names.map((n) => `\"${n}\"`).join(" or ") +
|
|
174
|
+
" package",
|
|
175
|
+
)
|
|
176
|
+
process.exit(1)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
run(resolved)
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.3.0-plus.2",
|
|
3
|
+
"name": "@jo.cs98/opencode-plus",
|
|
4
|
+
"description": "OpenCode Plus - AI coding agent CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node ./bin/install.cjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"opencodeplus": "./bin/opencodeplus.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|