# Copyright 1999-2026 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=8 # opencodex is TypeScript executed DIRECTLY by the Bun runtime -- there is no # compile step for src/, so this package builds nothing and installs the tree # as upstream ships it. # # Two ways to launch it exist and this package deliberately uses the second: # # 1. bin/ocx.mjs, a Node shim that locates a Bun and spawns it. # 2. Bun straight at src/cli/index.ts. # # (2) buys two things at once. It drops net-libs/nodejs from the dependency # graph entirely, and it makes `ocx update` detect an "installed via source" # layout, at which point it REFUSES to self-update instead of writing an npm # tree into /usr. That refusal is upstream behaviour, not a patch: # # opencodex v2.48.0 (installed via source, tag latest) # Running from a source checkout -- update with: ... # # src_prepare only rewrites the ADVICE in that message to name Portage. # # bin/ is still installed even though it is not an entry point: src/update/ # stats /bin/ocx.mjs (job.ts, transactional-install.mjs), so deleting # the directory would change code paths for no gain. What is NOT installed is # assets/ -- 5 MiB of README screenshots with no runtime reader. # The @napi-rs/keyring native package version, which is NOT ${PV} and must not # be made to follow it on a bump. It comes from upstream's package.json # ("@napi-rs/keyring": "1.3.0") and moves on its own schedule. KEYRING_PV="1.3.0" # --- vendored runtime tree --------------------------------------------------- # # REGENERATING THE DISTFILES ON A BUMP. Neither obentoo tarball is produced by # upstream or by any CI, so a bump that skips this fails at fetch time. From a # directory holding upstream's package.json + bun.lock at tag v${PV}: # # bun install --frozen-lockfile --production --ignore-scripts # rm -rf node_modules/{bun,@oven} node_modules/.bin/{bun,bunx} # rm -rf node_modules/@napi-rs/keyring-linux-* # tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 --format=gnu \ # -cf - node_modules | xz -9e -T0 > ${PN}-node_modules-${PV}.tar.xz # npx --yes wrangler@latest r2 object put "obentoo-distfiles/" \ # --file= --content-type=application/x-xz --remote # # and the same put for each keyring-linux-*-gnu directory, tarred on its own. # # Three things in that recipe are load-bearing and none of them is obvious: # # * bun.lock -- NOT package-lock.json -- is the pin. `--frozen-lockfile` is # what makes the tree reproducible from the tag; without it bun re-resolves # the ranges in package.json and the result depends on the day it ran. # * @oven/bun-* is ~149 MiB of Bun runtime that gets dropped ON PURPOSE. The # runtime comes from net-libs/bun-bin, and vendoring a second copy would # ship an unmanaged, unpatched interpreter inside /usr/lib. # * `--remote` on wrangler decides whether the upload happens at all. # Without it wrangler writes to LOCAL dev storage, still prints "Upload # complete", and the object never reaches the bucket -- so the next fetch # 404s with nothing in the transcript to explain why. # # The npm tarball is used instead of the GitHub tag tarball because only the # npm one carries gui/dist, the Vite-built web UI. The tag would force a # networked `bun install` plus a Vite build inside the gui/ workspace. NODE_MODULES_TARBALL="${PN}-node_modules-${PV}.tar.xz" DESCRIPTION="Universal provider proxy: any LLM with Codex CLI/App/SDK and Claude Code" HOMEPAGE="https://lidge-jun.github.io/opencodex/ https://github.com/lidge-jun/opencodex" # No "-> ${P}.tgz" rename on the registry URL: its basename already IS # opencodex-.tgz, so the rename would be a no-op -- and a no-op rename # is a pkgcheck RedundantUriRename. The comment lives out here rather than # inside the string because SRC_URI is parsed as a depset, where a "#" is a # token and not a comment: `bash -n` would still pass while the depset broke. SRC_URI=" https://registry.npmjs.org/@bitkyc08/${PN}/-/${P}.tgz https://distfiles.obentoo.org/${NODE_MODULES_TARBALL} keyring? ( amd64? ( https://distfiles.obentoo.org/${PN}-keyring-${KEYRING_PV}-linux-x64-gnu.tar.xz ) arm64? ( https://distfiles.obentoo.org/${PN}-keyring-${KEYRING_PV}-linux-arm64-gnu.tar.xz ) ) " S="${WORKDIR}/package" # opencodex itself is MIT, and so is @napi-rs/keyring. # # VENDORED SURVEY -- redone across all 93 packages in the node_modules tarball # for 2.48.0: 83 MIT, 8 ISC, 2 BSD-3-Clause, 1 BSD-2-Clause, 1 declared # "(Apache-2.0 AND BSD-3-Clause)". The 5 packages carrying no `license` field # at all are zod sub-entrypoint stubs, and zod is MIT. gui/dist bundles only # React 19, react-dom and @tanstack/react-virtual -- all MIT. # # REDO THIS ON EVERY BUMP. The autoupdate machinery bumps ${PV} and the # vendored set changes underneath it with nothing here going red; this overlay # has already been bitten by a vendored-license list going quietly stale. LICENSE="MIT ISC BSD BSD-2 Apache-2.0" SLOT="0" KEYWORDS="~amd64 ~arm64" IUSE="+keyring" # No REQUIRED_USE on purpose. A `^^ ( ... )` group with no default kills # emerge outright on a headless machine, and nothing here needs one: with # USE=-keyring the CLI simply has no OS-keychain backend and stores keys in its # own config, which is a working configuration rather than an invalid one. # net-libs/bun-bin is the whole runtime dependency, and NOT net-libs/nodejs: # the wrapper execs Bun directly on the TypeScript, so Node never runs. RDEPEND="net-libs/bun-bin" # The only native object in the image is the keyring .node blob, and it exists # only under USE=keyring -- which is precisely the point of the flag: with # -keyring this package ships zero prebuilt binaries and both variables below # describe an empty set. That is why they are unconditional rather than gated # in pkg_setup: QA_PREBUILT is a path WHITELIST, so a glob matching nothing is # silently fine, and gating it would trade a harmless no-op for a whole extra # phase whose value has to survive Portage's inter-phase environment save. RESTRICT="strip" QA_PREBUILT="usr/lib/${PN}/node_modules/@napi-rs/keyring-linux-*-gnu/*.node" DOCS=( README.md AGENTS_INSTALL.md ) src_prepare() { default # `ocx update` already refuses to touch a source-layout install; only the # ADVICE it prints is wrong for us, naming a git checkout the user does # not have. Point it at Portage instead. # # Both seds are guarded with `grep -qF` first because sed exits 0 when its # pattern matches nothing: an unguarded rewrite would go on "succeeding" # silently the moment upstream reworded the line, and the wrong advice # would ship again with no signal anywhere. local old_msg="Running from a source checkout — update with: git pull && bun install" local new_msg="Installed by Portage — update with: emerge --ask --update dev-util/opencodex" grep -qF "${old_msg}" src/update/index.ts \ || die "src/update/index.ts no longer prints the source-checkout advice; recheck this sed" sed -i "s|${old_msg}|${new_msg}|" src/update/index.ts || die # The same advice reaches the GUI and `ocx update --check` through # manualSourceCommand() in src/update/job.ts. For a source install # latestVersion() returns null, so this string is only ever DISPLAYED, # never executed -- but it is displayed, so it has to be right too. grep -qF 'return "git pull && bun install && bun run build:gui";' src/update/job.ts \ || die "src/update/job.ts manualSourceCommand() changed; recheck this sed" sed -i \ 's|return "git pull && bun install && bun run build:gui";|return "emerge --ask --update dev-util/opencodex";|' \ src/update/job.ts || die } src_install() { local dest="/usr/lib/${PN}" dodir "${dest}" # cp -a rather than doins -r, and that is not a style preference: # node_modules/.bin holds relative SYMLINKS (node-which -> ../which/bin/ # node-which) which doins dereferences, and the tree carries executable # bits that doins would flatten to 0644. cp -a "${S}"/src "${S}"/bin "${S}"/gui "${S}"/package.json \ "${ED}${dest}"/ || die cp -a "${WORKDIR}"/node_modules "${ED}${dest}"/ || die if use keyring; then # @napi-rs/keyring's loader require()s the platform package as a # SIBLING under node_modules/@napi-rs/, so the tarball's directory # has to land there rather than anywhere else. local triple if use amd64; then triple="x64" elif use arm64; then triple="arm64" else die "USE=keyring on an arch with no prebuilt @napi-rs/keyring" fi cp -a "${WORKDIR}/keyring-linux-${triple}-gnu" \ "${ED}${dest}"/node_modules/@napi-rs/ || die fi # Upstream's package.json declares both `opencodex` and `ocx`, so both # names exist. The wrapper execs Bun on an absolute path and never reads # $0, so the second name is a plain symlink. newbin "${FILESDIR}"/${PN}-wrapper.sh ${PN} dosym ${PN} /usr/bin/ocx # User-scope OpenRC service. `ocx service install` writes a user-scope # systemd unit AT RUNTIME, so this ebuild installs no unit and the # overlay's "every daemon needs an OpenRC counterpart" rule is not # triggered by an installed file -- but someone without systemd still has # no supervised way to run the proxy, which is the situation that rule # exists to prevent. newinitd has no user-scope variant; installing it as # a plain executable follows sys-apps/xdg-desktop-portal and # sci-ml/lemonade-bin in this overlay. exeinto /etc/user/init.d newexe "${FILESDIR}"/${PN}-user.initd ${PN} einstalldocs } pkg_postinst() { elog "opencodex proxies Codex CLI/App/SDK and Claude Code onto any LLM" elog "backend. First run:" elog elog " ocx setup # pick providers and store credentials" elog " ocx start # proxy on 127.0.0.1:10100" elog elog "Supervised in your own session, without systemd:" elog elog " rc-service --user ${PN} start" elog if use keyring; then elog "USE=keyring is on, so credentials can go to the OS keyring." elog "That needs a running Secret Service provider on the session bus" elog "(gnome-keyring, KWallet or KeePassXC). There is no package" elog "dependency to express this: the native blob links only glibc and" elog "libgcc and talks org.freedesktop.secrets over D-Bus with the" elog "client embedded, so the requirement is a runtime one. Without a" elog "provider, keyring reads and writes fail while everything else in" elog "opencodex keeps working." elog fi elog "\`ocx update\` is disabled by design in this package -- it detects the" elog "source layout and declines to write into /usr. Update through" elog "Portage instead." }