diff --git a/.github/workflows/publish-codev.yml b/.github/workflows/publish-codev.yml new file mode 100644 index 0000000000..243f8a5923 --- /dev/null +++ b/.github/workflows/publish-codev.yml @@ -0,0 +1,70 @@ +name: publish-codev + +# Publishes @quickbeard/codev-code (+ platform binary packages) to the private +# npm registry. Manual-only: versioning is explicit, no auto-bumps. +# +# Required repo secrets: +# CODEV_NPM_TOKEN auth token (npm automation token for npmjs, or the +# self-hosted registry's token) +# Optional: +# CODEV_NPM_REGISTRY defaults to https://registry.npmjs.org (packages are +# published with restricted access) +on: + workflow_dispatch: + inputs: + version: + description: "Version to publish (e.g. 1.0.0)" + required: true + channel: + description: "npm dist-tag" + default: "latest" + required: false + +concurrency: publish-codev + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Configure npm for the private registry + env: + REGISTRY: ${{ secrets.CODEV_NPM_REGISTRY }} + TOKEN: ${{ secrets.CODEV_NPM_TOKEN }} + run: | + if [ -z "$TOKEN" ]; then + echo "::error::The CODEV_NPM_TOKEN secret must be set." + exit 1 + fi + REGISTRY="${REGISTRY:-https://registry.npmjs.org}" + # Route the @quickbeard scope to the private registry and attach the + # auth token to that host (npmrc's //host/path:_authToken form). + hostpath="${REGISTRY#*://}" + hostpath="${hostpath%/}" + { + echo "@quickbeard:registry=${REGISTRY%/}/" + echo "//${hostpath}/:_authToken=${TOKEN}" + } >> ~/.npmrc + + - name: Build all platform binaries + working-directory: packages/opencode + env: + OPENCODE_VERSION: ${{ inputs.version }} + OPENCODE_CHANNEL: ${{ inputs.channel }} + run: bun run script/build.ts + + - name: Publish to private registry + working-directory: packages/opencode + env: + OPENCODE_VERSION: ${{ inputs.version }} + OPENCODE_CHANNEL: ${{ inputs.channel }} + run: bun run script/publish.ts diff --git a/DIVERGENCES.md b/DIVERGENCES.md index d5c3528712..66faf61c4d 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -15,19 +15,25 @@ future `packages/codev-*`) never conflict and are not tracked here. | `packages/opencode/src/index.ts` | `scriptName("opencode")` → `"codev code"`; `show()` prefix check `"opencode "` → `"codev code "` | Help/usage text shows the real user-facing command. The `show()` check must always match the scriptName. | Keep our two strings; take upstream's surrounding changes. | | `packages/opencode/package.json` | bin key `opencode` → `codev-code` (shim path `./bin/opencode` unchanged) | Installed binary is named `codev-code`. | Keep our bin key; take upstream's other fields. | | `bun.lock` | Mirrors the bin key rename above | Lockfile records workspace bin mappings. | Regenerate with `bun install`; never hand-merge. | +| `packages/opencode/script/build.ts` | Platform packages' `package.json` name scoped to `@quickbeard/codev-code-` (dist dir names unchanged — the bun compile target derives from them) | Private-registry publishing. | Keep our `name:` line; take upstream's surrounding changes. | +| `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "@quickbeard/codev-code"` const; scoped-name→dir map; main pkg name + bin key; `process.exit(0)` before the Docker/AUR/Homebrew section | npm-only publishing to the private registry; upstream's other channels target anomalyco infra. | Keep our names + early exit; take upstream's npm-section changes above the exit. | +| `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `@quickbeard/codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | +| `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `@quickbeard/codev-code-` | Install-time platform-binary fetch must match the published names. | Keep our prefix. | | `README.md` | Replaced with fork README | Fork identity, sync + divergence policy. | Keep ours entirely. | | `README.*.md` (21 translations) | Deleted | Upstream marketing translations, irrelevant to the fork. | Keep deleted (`git rm` the upstream side of modify/delete conflicts). | | `screenshot-uk.png` | Deleted | Orphaned README-translation asset; referenced by nothing. | Keep deleted. | ## Known rebrand gaps (deliberate, revisit before external distribution) -- **Auto-update**: upstream's updater installs from upstream's release channel. A distributed - `codev-code` binary must not update itself into stock opencode. Ship with - `OPENCODE_DISABLE_AUTOUPDATE=1` (e.g. set by the `codev` wrapper) or patch the installation - module before building release binaries. -- **Build/publish pipeline** (`packages/opencode/script/build.ts`, publish workflows): still - produces `opencode`-named artifacts and publishes to upstream's npm names. Rework when we - set up internal distribution. +- **Auto-update**: upstream's updater (`packages/core/src/installation/`) still hardcodes + `opencode-ai` and upstream's release channels. The codev hub launches the agent with + `OPENCODE_DISABLE_AUTOUPDATE=1`, so the notify/auto path is off in normal use — but a + direct `codev-code upgrade` would still install stock opencode. Patch or disable the + upgrade command before distributing outside the team. +- **Publishing**: npm packages go to the private registry via `.github/workflows/publish-codev.yml` + (manual dispatch; requires `CODEV_NPM_REGISTRY` + `CODEV_NPM_TOKEN` secrets and an explicit + version). Docker/AUR/Homebrew/GitHub-release channels are upstream-only and disabled. + The binary inside platform packages is still named `opencode` (cosmetic, keeps diffs small). - **UI branding**: TUI logo (`UI.logo()`), desktop/web app names and assets — all still upstream-branded. Cosmetic; not worth the merge-conflict surface until the product ships beyond the team. diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/opencode index a7101f42b0..1311a41e01 100755 --- a/packages/opencode/bin/opencode +++ b/packages/opencode/bin/opencode @@ -70,7 +70,10 @@ let arch = archMap[os.arch()] if (!arch) { arch = os.arch() } -const base = "opencode-" + platform + "-" + arch +// Fork: platform binary packages are published under the private @quickbeard +// scope (see script/publish.ts). The scope becomes a directory segment in the +// node_modules lookups below. +const base = "@quickbeard/codev-code-" + platform + "-" + arch const binary = platform === "windows" ? "opencode.exe" : "opencode" function supportsAvx2() { diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 236838dbde..ec53886b5c 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -215,7 +215,10 @@ for (const item of targets) { await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( { - name, + // Published under the private @quickbeard scope. The dist DIRECTORY + // keeps the pkg.name-derived name — the bun compile target string is + // derived from it above (name.replace(pkg.name, "bun")). + name: name.replace(pkg.name, "@quickbeard/codev-code"), version: Script.version, preferUnplugged: true, os: [item.os], diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index fa303b746b..fb0f459a8f 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -24,7 +24,9 @@ const archMap = { const platform = platformMap[os.platform()] ?? os.platform() const arch = archMap[os.arch()] ?? os.arch() -const base = `opencode-${platform}-${arch}` +// Fork: platform binary packages live under the private @quickbeard scope +// (must match bin/opencode and script/publish.ts). +const base = `@quickbeard/codev-code-${platform}-${arch}` const sourceBinary = platform === "windows" ? "opencode.exe" : "opencode" const targetBinary = path.join(__dirname, "bin", "opencode.exe") diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index ac1cafff56..d27321dcb0 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -7,6 +7,13 @@ import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) +// Fork: published names live under the private @quickbeard scope so a single +// npmrc scope mapping (`@quickbeard:registry=`) routes install AND the +// platform-binary optionalDependencies to the private registry. dist/ +// directory names stay pkg.name-derived (build.ts derives the bun compile +// target from them); only package.json `name` fields are scoped. +const PUBLISH_NAME = "@quickbeard/codev-code" + async function published(name: string, version: string) { return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 } @@ -20,13 +27,18 @@ async function publish(dir: string, name: string, version: string) { return } await $`bun pm pack`.cwd(dir) - await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) + // restricted: the fork is commercial — scoped packages must never land + // public on npmjs. (Self-hosted registries ignore the flag.) + await $`npm publish *.tgz --access restricted --tag ${Script.channel}`.cwd(dir) } const binaries: Record = {} +// Scoped published name -> unscoped dist directory (they differ in this fork). +const dirs: Record = {} for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { const pkg = await Bun.file(`./dist/${filepath}`).json() binaries[pkg.name] = pkg.version + dirs[pkg.name] = filepath.replace("/package.json", "") } console.log("binaries", binaries) const version = Object.values(binaries)[0] @@ -37,15 +49,15 @@ await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs` await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text()) await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write( [ - `echo "Error: ${pkg.name}-ai's postinstall script was not run." >&2`, + `echo "Error: ${PUBLISH_NAME}'s postinstall script was not run." >&2`, 'echo "" >&2', 'echo "This occurs when using --ignore-scripts during installation, or when using a" >&2', 'echo "package manager like pnpm that does not run postinstall scripts by default." >&2', 'echo "" >&2', 'echo "To fix this, run the postinstall script manually:" >&2', - `echo " cd node_modules/${pkg.name}-ai && node postinstall.mjs" >&2`, + `echo " cd node_modules/${PUBLISH_NAME} && node postinstall.mjs" >&2`, 'echo "" >&2', - `echo "Or reinstall ${pkg.name}-ai without the --ignore-scripts flag." >&2`, + `echo "Or reinstall ${PUBLISH_NAME} without the --ignore-scripts flag." >&2`, "exit 1", "", ].join("\n"), @@ -54,9 +66,9 @@ await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write( await Bun.file(`./dist/${pkg.name}/package.json`).write( JSON.stringify( { - name: pkg.name + "-ai", + name: PUBLISH_NAME, bin: { - [pkg.name]: `./bin/${pkg.name}.exe`, + "codev-code": `./bin/${pkg.name}.exe`, }, scripts: { postinstall: "node ./postinstall.mjs", @@ -73,10 +85,15 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( ) const tasks = Object.entries(binaries).map(async ([name]) => { - await publish(`./dist/${name}`, name, binaries[name]) + await publish(`./dist/${dirs[name]}`, name, binaries[name]) }) await Promise.all(tasks) -await publish(`./dist/${pkg.name}`, `${pkg.name}-ai`, version) +await publish(`./dist/${pkg.name}`, PUBLISH_NAME, version) + +// Fork: npm packages only. Upstream's Docker/AUR/Homebrew distribution below +// targets anomalyco-owned registries and taps; keep the code for cheap merges +// but don't run it. (Env-guarded so tsc doesn't mark the rest unreachable.) +if (!process.env.CODEV_PUBLISH_UPSTREAM_CHANNELS) process.exit(0) const image = "ghcr.io/anomalyco/opencode" const platforms = "linux/amd64,linux/arm64"