mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
dev is ours: the Hanzo Cloud by default, a binary named dev, our namespaces
hanzoai/dev went public defaulting to someone else's cloud. Four things were
wrong, and all four now have a test that fails if they come back.
DEFAULT PROVIDER AND MODEL. built_in_model_providers() gains `hanzo` -- the
Hanzo Cloud at api.hanzo.ai/v1, Responses wire, bearer from HANZO_USER_KEY --
and it is the default. The default model is zen5-coder. This was not cosmetic:
the old default is not in the gateway catalog, so it 400s against our own
cloud. Measured, not assumed:
POST api.hanzo.ai/v1/responses model=gpt-5-codex -> 400 "model is not
available. Use GET /v1/models to list available models"
POST api.hanzo.ai/v1/responses model=zen5-coder -> 200, clean output
Our own CLI already documented the bug from the other side -- dev.rs says
"Name the model too -- codex's built-in default is not in the gateway catalog
and would 400" and passes `-c model=` on every launch to work around it. It
also asserts that on the native gateway NO model_provider override is needed,
which is only true once `hanzo` is the built-in default. It is now.
zen5-coder over enso because dev is a coding agent and because enso returns
system-prompt text on the Responses wire that dev actually speaks; zen5-coder
returns the answer. Model ids were read from GET /v1/models with a bearer, not
guessed. Other providers stay fully configurable -- this is the default, not
the only choice.
THE LOGIN COMMAND. Messages said `code login` / `codex login`. The real command
is `hanzo auth login` (`hanzo login` is not a command -- it is swallowed as an
AI task). It now lives once, as auth::LOGIN_COMMAND, so no message can drift
into naming a command that does not exist. References to this binary's own
subcommand became `dev login`.
A BINARY NAMED dev. HEAD built `code` and `codex` and nothing named `dev`, so a
release cut today would ship no `dev` -- and `hanzo code` launches a bare `dev`
off PATH, so every new install would break. `dev` is now the canonical bin,
declared first; `code` remains as an alias to the same program, never the
reverse. The CLI identifies as `dev` / "Hanzo Dev", not "OpenAI Codex".
OUR NAMESPACES. The pipeline published to @just-every/* on npm and pushed
formulae to just-every/homebrew-tap. Both of ours already exist and are already
populated at v0.6.91 -- @hanzo/dev plus all five @hanzo/dev-<target> packages,
and hanzoai/homebrew-tap's Formula/hanzo-dev.rb, which installs `dev` and
expects assets named dev-<triple>. Nothing here is squatted; this is our own
distribution, reclaimed. Assets are renamed to match the formula.
Two live supply-chain bugs fell out of that, both worse than a naming problem:
postinstall.js (runs on every npm install) and the tui auto-updater fetched
from a third party's releases -- the updater also told our users to
`npm install -g @just-every/code@latest`. Worse, a shadowed `isWSL` const put
the two fast paths in the temporal dead zone, so their catch blocks swallowed
the error and EVERY install fell through to that download: the five published
platform packages were never used, and the third-party fetch was not an edge
case but the only path that ran. Fixed and exercised in a sandbox across all
three paths. codex-cli/.pack/ -- a committed `npm pack` artifact CI
regenerates, and the reason the bad URL had two homes -- is now untracked.
DURABILITY, because upstream-merge.yml merges openai/codex every 30 minutes.
A one-time edit to a default is a suggestion that survives until the next
merge. Two mechanisms:
- .github/merge-policy.json prefer_ours_globs listed only codex-rs/** while
the workspace that SHIPS is code-rs/**, so it protected nothing that
ships. The identity-bearing code-rs files are now listed.
- code-rs/core/tests/hanzo_identity.rs asserts all four properties -- the
provider, a Zen default, a bin named dev, and no third-party publish
target. Upstream has no such file, so a merge cannot conflict with it or
carry the guard away with the thing it guards. Policy can be edited;
a red build cannot be ignored.
The gate is not decorative: it failed on the un-retargeted pipeline
("release.yml still references `@just-every/`") and went green only when the
pipeline was actually fixed.
Verified: cargo build --bin dev clean; hanzo_identity 4/4; code-core lib
332 passed, 1 failed -- git_info::test_collect_git_info_with_remote, which
fails identically on a clean origin/main checkout here because this machine
has a global [email protected]:.insteadof rewrite. Pre-existing and
environmental, not from this change.
$ dev exec "say OK" # fresh profile, no config
Hanzo Dev v0.0.0
model: zen5-coder
provider: hanzo
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -9,9 +9,9 @@ body:
|
||||
value: |
|
||||
Thank you for submitting a bug report! It helps make Code better for everyone.
|
||||
|
||||
If you need help or support using Code, and are not reporting a bug, please post on [code/discussions](https://github.com/just-every/code/discussions), where you can ask questions or engage with others on ideas for how to improve Code.
|
||||
If you need help or support using Code, and are not reporting a bug, please post on [code/discussions](https://github.com/hanzoai/dev/discussions), where you can ask questions or engage with others on ideas for how to improve Code.
|
||||
|
||||
Make sure you are running the [latest](https://npmjs.com/package/@just-every/code) version of the Code CLI. The bug you are experiencing may already have been fixed.
|
||||
Make sure you are running the [latest](https://npmjs.com/package/@hanzo/dev) version of the Code CLI. The bug you are experiencing may already have been fixed.
|
||||
|
||||
Please try to include as much information as possible.
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"_comment": "code-rs/** is the workspace that SHIPS. The codex-rs/** entries below guard the vendored upstream copy; the code-rs/** entries guard the product. Files carrying Hanzo identity (default provider, default model, binary name, login command) are listed explicitly so an upstream merge cannot take them back. code-rs/core/tests/hanzo_identity.rs asserts the same invariants and fails the build if it does.",
|
||||
"prefer_ours_globs": [
|
||||
"code-rs/core/src/model_provider_info.rs",
|
||||
"code-rs/core/src/config.rs",
|
||||
"code-rs/core/src/auth.rs",
|
||||
"code-rs/core/tests/hanzo_identity.rs",
|
||||
"code-rs/cli/Cargo.toml",
|
||||
"code-rs/cli/src/main.rs",
|
||||
"code-rs/exec/src/event_processor_with_human_output.rs",
|
||||
"code-rs/mcp-server/src/message_processor.rs",
|
||||
"code-rs/tui/**",
|
||||
"codex-rs/tui/**",
|
||||
"codex-cli/**",
|
||||
"codex-rs/core/src/openai_tools.rs",
|
||||
|
||||
@@ -40,21 +40,21 @@ jobs:
|
||||
# Linux MUSL (static-ish) builds
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
artifact: code-x86_64-unknown-linux-musl
|
||||
artifact: dev-x86_64-unknown-linux-musl
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
artifact: code-aarch64-unknown-linux-musl
|
||||
artifact: dev-aarch64-unknown-linux-musl
|
||||
# macOS builds (both architectures)
|
||||
- os: macos-14
|
||||
target: x86_64-apple-darwin
|
||||
artifact: code-x86_64-apple-darwin
|
||||
artifact: dev-x86_64-apple-darwin
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
artifact: code-aarch64-apple-darwin
|
||||
artifact: dev-aarch64-apple-darwin
|
||||
# Windows build
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: code-x86_64-pc-windows-msvc.exe
|
||||
artifact: dev-x86_64-pc-windows-msvc.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -135,20 +135,20 @@ jobs:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Build code (release)
|
||||
- name: Build dev (release)
|
||||
shell: bash
|
||||
run: |
|
||||
cd "${RUST_WORKSPACE_DIR}"
|
||||
cargo build --release --locked --target "${{ matrix.target }}" --bin code
|
||||
cargo build --release --locked --target "${{ matrix.target }}" --bin dev
|
||||
|
||||
- name: Prepare artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||||
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/code.exe" "artifacts/${{ matrix.artifact }}"
|
||||
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/dev.exe" "artifacts/${{ matrix.artifact }}"
|
||||
else
|
||||
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/code" "artifacts/${{ matrix.artifact }}"
|
||||
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/dev" "artifacts/${{ matrix.artifact }}"
|
||||
fi
|
||||
|
||||
- name: Compress artifacts (Windows)
|
||||
@@ -406,20 +406,20 @@ jobs:
|
||||
'',
|
||||
'You can run this right now! In your terminal just use:',
|
||||
'```bash',
|
||||
`code preview ${slug}`,
|
||||
`dev preview ${slug}`,
|
||||
'```',
|
||||
'This will run the version of Code we\'ve created for this issue.',
|
||||
'',
|
||||
'Direct downloads:',
|
||||
`- Linux x86_64 (.zst): ${releaseBase}/code-x86_64-unknown-linux-musl.zst`,
|
||||
`- Linux x86_64 (.tar.gz): ${releaseBase}/code-x86_64-unknown-linux-musl.tar.gz`,
|
||||
`- Linux aarch64 (.zst): ${releaseBase}/code-aarch64-unknown-linux-musl.zst`,
|
||||
`- Linux aarch64 (.tar.gz): ${releaseBase}/code-aarch64-unknown-linux-musl.tar.gz`,
|
||||
`- macOS x86_64 (.zst): ${releaseBase}/code-x86_64-apple-darwin.zst`,
|
||||
`- macOS x86_64 (.tar.gz): ${releaseBase}/code-x86_64-apple-darwin.tar.gz`,
|
||||
`- macOS arm64 (.zst): ${releaseBase}/code-aarch64-apple-darwin.zst`,
|
||||
`- macOS arm64 (.tar.gz): ${releaseBase}/code-aarch64-apple-darwin.tar.gz`,
|
||||
`- Windows x86_64 (.zip): ${releaseBase}/code-x86_64-pc-windows-msvc.exe.zip`,
|
||||
`- Linux x86_64 (.zst): ${releaseBase}/dev-x86_64-unknown-linux-musl.zst`,
|
||||
`- Linux x86_64 (.tar.gz): ${releaseBase}/dev-x86_64-unknown-linux-musl.tar.gz`,
|
||||
`- Linux aarch64 (.zst): ${releaseBase}/dev-aarch64-unknown-linux-musl.zst`,
|
||||
`- Linux aarch64 (.tar.gz): ${releaseBase}/dev-aarch64-unknown-linux-musl.tar.gz`,
|
||||
`- macOS x86_64 (.zst): ${releaseBase}/dev-x86_64-apple-darwin.zst`,
|
||||
`- macOS x86_64 (.tar.gz): ${releaseBase}/dev-x86_64-apple-darwin.tar.gz`,
|
||||
`- macOS arm64 (.zst): ${releaseBase}/dev-aarch64-apple-darwin.zst`,
|
||||
`- macOS arm64 (.tar.gz): ${releaseBase}/dev-aarch64-apple-darwin.tar.gz`,
|
||||
`- Windows x86_64 (.zip): ${releaseBase}/dev-x86_64-pc-windows-msvc.exe.zip`,
|
||||
'',
|
||||
'After you run it, please comment here and let me know if it does what you need.',
|
||||
marker
|
||||
|
||||
@@ -57,13 +57,13 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd code-rs
|
||||
cargo build --locked --profile dev-fast --bin code
|
||||
cargo build --locked --profile dev-fast --bin dev
|
||||
|
||||
- name: CLI smokes (skip duplicated cargo tests)
|
||||
shell: bash
|
||||
env:
|
||||
SKIP_CARGO_TESTS: "1"
|
||||
CI_CLI_BIN: ${{ env.CARGO_TARGET_DIR }}/dev-fast/code
|
||||
CI_CLI_BIN: ${{ env.CARGO_TARGET_DIR }}/dev-fast/dev
|
||||
run: bash scripts/ci-tests.sh
|
||||
|
||||
- name: Install cargo-nextest
|
||||
@@ -112,12 +112,12 @@ jobs:
|
||||
set -euo pipefail
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PKGS=(
|
||||
"@just-every/code"
|
||||
"@just-every/code-darwin-arm64"
|
||||
"@just-every/code-darwin-x64"
|
||||
"@just-every/code-linux-x64-musl"
|
||||
"@just-every/code-linux-arm64-musl"
|
||||
"@just-every/code-win32-x64"
|
||||
"@hanzo/dev"
|
||||
"@hanzo/dev-darwin-arm64"
|
||||
"@hanzo/dev-darwin-x64"
|
||||
"@hanzo/dev-linux-x64-musl"
|
||||
"@hanzo/dev-linux-arm64-musl"
|
||||
"@hanzo/dev-win32-x64"
|
||||
)
|
||||
# Find the highest published version across all packages
|
||||
MAX_PUBLISHED="0.0.0"
|
||||
@@ -157,22 +157,22 @@ jobs:
|
||||
# Linux builds
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
artifact: code-x86_64-unknown-linux-musl
|
||||
artifact: dev-x86_64-unknown-linux-musl
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
artifact: code-aarch64-unknown-linux-musl
|
||||
artifact: dev-aarch64-unknown-linux-musl
|
||||
# (GNU variants omitted to reduce asset duplication; npm defaults to musl)
|
||||
# macOS builds
|
||||
- os: macos-14
|
||||
target: x86_64-apple-darwin
|
||||
artifact: code-x86_64-apple-darwin
|
||||
artifact: dev-x86_64-apple-darwin
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
artifact: code-aarch64-apple-darwin
|
||||
artifact: dev-aarch64-apple-darwin
|
||||
# Windows build
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: code-x86_64-pc-windows-msvc.exe
|
||||
artifact: dev-x86_64-pc-windows-msvc.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -296,7 +296,7 @@ jobs:
|
||||
RUST_BACKTRACE: "1"
|
||||
run: |
|
||||
cd code-rs
|
||||
cargo build --release --frozen --locked --timings --target ${{ matrix.target }} --bin code
|
||||
cargo build --release --frozen --locked --timings --target ${{ matrix.target }} --bin dev
|
||||
|
||||
- name: Post-build smoke (run binary) [Unix]
|
||||
if: |
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
exe="code-rs/target/${{ matrix.target }}/release/code"
|
||||
exe="code-rs/target/${{ matrix.target }}/release/dev"
|
||||
"$exe" --version
|
||||
"$exe" completion bash > /dev/null
|
||||
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
if: matrix.os == 'windows-latest' && matrix.target == 'x86_64-pc-windows-msvc'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$exe = "code-rs/target/${{ matrix.target }}/release/code.exe"
|
||||
$exe = "code-rs/target/${{ matrix.target }}/release/dev.exe"
|
||||
& $exe --version | Out-Null
|
||||
& $exe completion bash | Out-Null
|
||||
|
||||
@@ -327,9 +327,9 @@ jobs:
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||||
cp code-rs/target/${{ matrix.target }}/release/code.exe artifacts/${{ matrix.artifact }}
|
||||
cp code-rs/target/${{ matrix.target }}/release/dev.exe artifacts/${{ matrix.artifact }}
|
||||
else
|
||||
cp code-rs/target/${{ matrix.target }}/release/code artifacts/${{ matrix.artifact }}
|
||||
cp code-rs/target/${{ matrix.target }}/release/dev artifacts/${{ matrix.artifact }}
|
||||
fi
|
||||
|
||||
- name: Compress artifacts (Windows)
|
||||
@@ -449,10 +449,10 @@ jobs:
|
||||
set -euo pipefail
|
||||
mkdir -p release-assets
|
||||
shopt -s nullglob
|
||||
# Gather all built "code-*" files (zst/tar.gz/zip or raw) recursively under artifacts/
|
||||
# Gather all built "dev-*" files (zst/tar.gz/zip or raw) recursively under artifacts/
|
||||
while IFS= read -r -d '' f; do
|
||||
cp "$f" release-assets/
|
||||
done < <(find artifacts -type f -name 'code-*' -print0)
|
||||
done < <(find artifacts -type f -name 'dev-*' -print0)
|
||||
# Show what we collected
|
||||
ls -la release-assets/ || true
|
||||
|
||||
@@ -467,11 +467,11 @@ jobs:
|
||||
|
||||
map_target() {
|
||||
case "$1" in
|
||||
aarch64-apple-darwin) echo "os=darwin cpu=arm64 pkg=@just-every/code-darwin-arm64" ;;
|
||||
x86_64-apple-darwin) echo "os=darwin cpu=x64 pkg=@just-every/code-darwin-x64" ;;
|
||||
x86_64-unknown-linux-musl) echo "os=linux cpu=x64 pkg=@just-every/code-linux-x64-musl" ;;
|
||||
aarch64-unknown-linux-musl) echo "os=linux cpu=arm64 pkg=@just-every/code-linux-arm64-musl" ;;
|
||||
x86_64-pc-windows-msvc.exe) echo "os=win32 cpu=x64 pkg=@just-every/code-win32-x64" ;;
|
||||
aarch64-apple-darwin) echo "os=darwin cpu=arm64 pkg=@hanzo/dev-darwin-arm64" ;;
|
||||
x86_64-apple-darwin) echo "os=darwin cpu=x64 pkg=@hanzo/dev-darwin-x64" ;;
|
||||
x86_64-unknown-linux-musl) echo "os=linux cpu=x64 pkg=@hanzo/dev-linux-x64-musl" ;;
|
||||
aarch64-unknown-linux-musl) echo "os=linux cpu=arm64 pkg=@hanzo/dev-linux-arm64-musl" ;;
|
||||
x86_64-pc-windows-msvc.exe) echo "os=win32 cpu=x64 pkg=@hanzo/dev-win32-x64" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
@@ -506,14 +506,14 @@ jobs:
|
||||
for f in release-assets/*; do
|
||||
b="$(basename "$f")"
|
||||
# Derive target triple from the filename while preserving optional .exe (Windows):
|
||||
# code-<triple>[.exe].(zst|tar.gz|zip) -> <triple>[.exe]
|
||||
# dev-<triple>[.exe].(zst|tar.gz|zip) -> <triple>[.exe]
|
||||
case "$b" in
|
||||
*.tar.gz) base="${b%.tar.gz}" ;;
|
||||
*.zst) base="${b%.zst}" ;;
|
||||
*.zip) base="${b%.zip}" ;;
|
||||
*) base="$b" ;;
|
||||
esac
|
||||
t="${base#code-}"
|
||||
t="${base#dev-}"
|
||||
meta="$(map_target "$t")" || true
|
||||
if [ -z "$meta" ]; then
|
||||
echo "Skipping unrecognized artifact: $b"
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
rawbin="$(decompress_to "$f" "$workdir/bin")"
|
||||
chmod +x "$rawbin" || true
|
||||
# package.json
|
||||
printf '{\n "name": "%s",\n "version": "%s",\n "license": "Apache-2.0",\n "description": "Platform binary for @just-every/code (%s)",\n "os": ["%s"],\n "cpu": ["%s"],\n "files": ["bin/"],\n "private": false\n}\n' \
|
||||
printf '{\n "name": "%s",\n "version": "%s",\n "license": "Apache-2.0",\n "description": "Platform binary for @hanzo/dev (%s)",\n "os": ["%s"],\n "cpu": ["%s"],\n "files": ["bin/"],\n "private": false\n}\n' \
|
||||
"$pkg" "$NEW_VERSION" "$t" "$os" "$cpu" > "$workdir/package.json"
|
||||
done
|
||||
|
||||
@@ -561,11 +561,11 @@ jobs:
|
||||
# Note: These are platform-filtered in the target packages, so npm will fetch only one.
|
||||
tmp=$(mktemp)
|
||||
jq '.optionalDependencies = {
|
||||
"@just-every/code-darwin-arm64": "'"$NEW_VERSION"'",
|
||||
"@just-every/code-darwin-x64": "'"$NEW_VERSION"'",
|
||||
"@just-every/code-linux-x64-musl": "'"$NEW_VERSION"'",
|
||||
"@just-every/code-linux-arm64-musl": "'"$NEW_VERSION"'",
|
||||
"@just-every/code-win32-x64": "'"$NEW_VERSION"'"
|
||||
"@hanzo/dev-darwin-arm64": "'"$NEW_VERSION"'",
|
||||
"@hanzo/dev-darwin-x64": "'"$NEW_VERSION"'",
|
||||
"@hanzo/dev-linux-x64-musl": "'"$NEW_VERSION"'",
|
||||
"@hanzo/dev-linux-arm64-musl": "'"$NEW_VERSION"'",
|
||||
"@hanzo/dev-win32-x64": "'"$NEW_VERSION"'"
|
||||
}' package.json > "$tmp" && mv "$tmp" package.json
|
||||
git add package.json
|
||||
if git diff --staged --quiet; then
|
||||
@@ -632,22 +632,22 @@ jobs:
|
||||
mkdir -p .code-bin release-notes
|
||||
# Always start from a blank file so stale content never leaks through
|
||||
: > release-notes/RELEASE_NOTES.md
|
||||
LNX_ZST=$(ls -1 release-assets/code-x86_64-unknown-linux-musl.* 2>/dev/null | head -n1 || true)
|
||||
LNX_ZST=$(ls -1 release-assets/dev-x86_64-unknown-linux-musl.* 2>/dev/null | head -n1 || true)
|
||||
if [ -z "$LNX_ZST" ]; then
|
||||
echo "Could not find linux x86_64 Code artifact in release-assets/" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$LNX_ZST" in
|
||||
*.zst)
|
||||
zstd -d -q --force "$LNX_ZST" -o .code-bin/code ;;
|
||||
zstd -d -q --force "$LNX_ZST" -o .code-bin/dev ;;
|
||||
*.tar.gz)
|
||||
tar -xzf "$LNX_ZST" -C .code-bin && mv .code-bin/code-x86_64-unknown-linux-musl .code-bin/code ;;
|
||||
tar -xzf "$LNX_ZST" -C .code-bin && mv .code-bin/dev-x86_64-unknown-linux-musl .code-bin/dev ;;
|
||||
*.zip)
|
||||
unzip -q -o "$LNX_ZST" -d .code-bin && mv .code-bin/code-x86_64-unknown-linux-musl .code-bin/code ;;
|
||||
unzip -q -o "$LNX_ZST" -d .code-bin && mv .code-bin/dev-x86_64-unknown-linux-musl .code-bin/dev ;;
|
||||
*)
|
||||
cp "$LNX_ZST" .code-bin/code ;;
|
||||
cp "$LNX_ZST" .code-bin/dev ;;
|
||||
esac
|
||||
chmod +x .code-bin/code
|
||||
chmod +x .code-bin/dev
|
||||
|
||||
# Prepare context for the model.
|
||||
DATE=$(date -u +%Y-%m-%d)
|
||||
@@ -684,14 +684,14 @@ jobs:
|
||||
|
||||
Release Notes (release-notes/RELEASE_NOTES.md)
|
||||
- Write exactly these sections in order; include the optional Thanks section only when applicable:
|
||||
1) Title: ## @just-every/code v${NEW_VERSION}
|
||||
1) Title: ## @hanzo/dev v${NEW_VERSION}
|
||||
2) One brief intro sentence (1–2 lines max).
|
||||
3) Section header: ### Changes
|
||||
- The same 2–5 bullets as in the changelog (you may omit SHAs).
|
||||
4) Section header: ### Install
|
||||
Code block with exactly:
|
||||
npm install -g @just-every/code@latest
|
||||
code
|
||||
npm install -g @hanzo/dev@latest
|
||||
dev
|
||||
5) Optional section header: ### Thanks
|
||||
- Include ONE line like: "Thanks to @alice and @bob for contributions!"
|
||||
- Only include if at least one merged PR in ${RANGE} is authored by an external contributor.
|
||||
@@ -716,7 +716,7 @@ jobs:
|
||||
# Run Code in fully automated exec mode against the repo root.
|
||||
# Note: the working-directory flag is `--cd` on the `exec` subcommand.
|
||||
OPENAI_BASE_URL="http://127.0.0.1:5058/v1" \
|
||||
./.code-bin/code exec --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt | tee .github/auto/RELEASE_AGENT_OUT.txt || {
|
||||
./.code-bin/dev exec --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt | tee .github/auto/RELEASE_AGENT_OUT.txt || {
|
||||
echo "Code exec returned non-zero; continuing to check for outputs..." >&2
|
||||
}
|
||||
|
||||
@@ -773,7 +773,7 @@ jobs:
|
||||
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > release-notes/RELEASE_NOTES.md || true
|
||||
fi
|
||||
if [ ! -s release-notes/RELEASE_NOTES.md ]; then
|
||||
printf "## @just-every/code v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > release-notes/RELEASE_NOTES.md
|
||||
printf "## @hanzo/dev v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > release-notes/RELEASE_NOTES.md
|
||||
fi
|
||||
|
||||
# Commit CHANGELOG changes if any.
|
||||
@@ -816,7 +816,7 @@ jobs:
|
||||
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > release-notes/RELEASE_NOTES.md || true
|
||||
fi
|
||||
if [ ! -s release-notes/RELEASE_NOTES.md ]; then
|
||||
printf "## @just-every/code v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > release-notes/RELEASE_NOTES.md
|
||||
printf "## @hanzo/dev v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > release-notes/RELEASE_NOTES.md
|
||||
fi
|
||||
|
||||
- name: Assert release notes exist
|
||||
@@ -947,14 +947,14 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
|
||||
# --- Homebrew Tap Update -------------------------------------------------
|
||||
- name: Generate Homebrew formula (Code.rb)
|
||||
- name: Generate Homebrew formula (hanzo-dev.rb)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash scripts/generate-homebrew-formula.sh
|
||||
test -s Formula/Code.rb || { echo "Formula/Code.rb missing" >&2; exit 1; }
|
||||
test -s Formula/hanzo-dev.rb || { echo "Formula/hanzo-dev.rb missing" >&2; exit 1; }
|
||||
|
||||
- name: Publish Homebrew formula to just-every/homebrew-tap
|
||||
- name: Publish Homebrew formula to hanzoai/homebrew-tap
|
||||
shell: bash
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
|
||||
@@ -965,9 +965,9 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
tmpdir=$(mktemp -d)
|
||||
git clone "https://x-access-token:${GH_PAT}@github.com/just-every/homebrew-tap.git" "$tmpdir"
|
||||
git clone "https://x-access-token:${GH_PAT}@github.com/hanzoai/homebrew-tap.git" "$tmpdir"
|
||||
mkdir -p "$tmpdir/Formula"
|
||||
cp -f Formula/Code.rb "$tmpdir/Formula/Code.rb"
|
||||
cp -f Formula/hanzo-dev.rb "$tmpdir/Formula/hanzo-dev.rb"
|
||||
cd "$tmpdir"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -977,7 +977,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
ver="${{ steps.version.outputs.version }}"
|
||||
git commit -m "chore(homebrew): bump Code formula to v${ver}"
|
||||
git commit -m "chore(homebrew): bump hanzo-dev formula to v${ver}"
|
||||
# Ensure default branch exists
|
||||
if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
||||
git branch -M main || true
|
||||
@@ -1013,7 +1013,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
name="@just-every/code"
|
||||
name="@hanzo/dev"
|
||||
version=$(jq -r '.version' package.json)
|
||||
echo "Preparing to publish $name@$version"
|
||||
existing=$(npm view "$name@$version" version 2>/dev/null || true)
|
||||
|
||||
@@ -96,6 +96,8 @@ yarn-error.log*
|
||||
|
||||
# package
|
||||
*.tgz
|
||||
# `npm pack` output, regenerated by release.yml; never a source of truth
|
||||
codex-cli/.pack/
|
||||
|
||||
# ci
|
||||
.vercel/
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
class Code < Formula
|
||||
desc "Terminal coding agent"
|
||||
homepage "https://github.com/just-every/code"
|
||||
version "v0.2.67"
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/just-every/code/releases/download/v0.2.67/code-aarch64-apple-darwin.tar.gz"
|
||||
sha256 "ed8fbb68d8cff0f76d28c9c9b69445dab66f8e645613e9145061e950e8cf7507"
|
||||
else
|
||||
url "https://github.com/just-every/code/releases/download/v0.2.67/code-x86_64-apple-darwin.tar.gz"
|
||||
sha256 "642f656b1d45fe305738f519b5d44c8329bd9963f3e3781ee542a4313d2102f7"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["code-*"].first => "code"
|
||||
# Provide a compatibility shim
|
||||
(bin/"coder").write <<~EOS
|
||||
#!/bin/bash
|
||||
exec "#{bin}/code" "$@"
|
||||
EOS
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/code", "--help"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
class HanzoDev < Formula
|
||||
desc "Local AI coding agent for your terminal, powered by Hanzo AI"
|
||||
homepage "https://github.com/hanzoai/dev"
|
||||
version "0.6.91"
|
||||
license "Apache-2.0"
|
||||
|
||||
on_macos do
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-apple-darwin.tar.gz"
|
||||
sha256 "2a4f5eb12bba0aac65f7d9cc7d577ea962913bea9c4ef2a21b2a4825fc814913"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-apple-darwin.tar.gz"
|
||||
sha256 "264992535a1c7f027fb67769baaa7183ca24155f45bb7519302dc3ebfd30739d"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-unknown-linux-musl.tar.gz"
|
||||
sha256 "1bb091624b6458087a79929c653dc8239d34eead9a7d012bb3b10207e817eeca"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-unknown-linux-musl.tar.gz"
|
||||
sha256 "69730db8e3261b6a3bd2ca5bad27b4c04480e28d0e341a7c7dcc663f84a3c8f7"
|
||||
end
|
||||
end
|
||||
|
||||
livecheck do
|
||||
url :stable
|
||||
strategy :github_latest
|
||||
end
|
||||
|
||||
def install
|
||||
# Installs `dev` ONLY. It must not also write a `hanzo` shim: `hanzo` is the
|
||||
# Hanzo CLI (Formula/hanzo.rb), a different program. A shim here would make
|
||||
# `hanzo` mean "the dev coding assistant" for anyone who installed this
|
||||
# formula first, and would collide with hanzo.rb on the same filename.
|
||||
bin.install Dir["dev-*"].first => "dev"
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/dev", "--help"
|
||||
end
|
||||
end
|
||||
Generated
+1
@@ -1293,6 +1293,7 @@ dependencies = [
|
||||
"code-core",
|
||||
"code-ollama",
|
||||
"code-protocol",
|
||||
"code-version",
|
||||
"opentelemetry-appender-tracing",
|
||||
"owo-colors",
|
||||
"serde",
|
||||
|
||||
@@ -22,7 +22,7 @@ pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
|
||||
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
|
||||
|
||||
let account_id = token.account_id.ok_or_else(|| {
|
||||
anyhow::anyhow!("ChatGPT account ID not available, please re-run `code login`")
|
||||
anyhow::anyhow!("ChatGPT account ID not available, please re-run `dev login`")
|
||||
});
|
||||
|
||||
let response = client
|
||||
|
||||
@@ -3,6 +3,14 @@ edition = "2024"
|
||||
name = "code-cli"
|
||||
version = { workspace = true }
|
||||
|
||||
# `dev` is the product. It is what `hanzo code` launches (by bare name, off
|
||||
# PATH) and what the installer contract promises.
|
||||
[[bin]]
|
||||
name = "dev"
|
||||
path = "src/main.rs"
|
||||
|
||||
# `code` stays as an alias so existing installs keep working. It is the same
|
||||
# program — an alias, never the canonical name.
|
||||
[[bin]]
|
||||
name = "code"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -70,7 +70,7 @@ pub fn read_api_key_from_stdin() -> String {
|
||||
|
||||
if stdin.is_terminal() {
|
||||
eprintln!(
|
||||
"--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
|
||||
"--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | dev login --with-api-key`."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
+18
-18
@@ -35,7 +35,7 @@ mod mcp_cmd;
|
||||
|
||||
use crate::mcp_cmd::McpCli;
|
||||
|
||||
const CLI_COMMAND_NAME: &str = "code";
|
||||
const CLI_COMMAND_NAME: &str = "dev";
|
||||
pub(crate) const CODEX_SECURE_MODE_ENV_VAR: &str = "CODEX_SECURE_MODE";
|
||||
|
||||
/// As early as possible in the process lifecycle, apply hardening measures
|
||||
@@ -57,20 +57,20 @@ fn pre_main_hardening() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Codex CLI
|
||||
/// Hanzo Dev — the Hanzo coding agent.
|
||||
///
|
||||
/// If no subcommand is specified, options will be forwarded to the interactive CLI.
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(
|
||||
author,
|
||||
name = "code",
|
||||
name = "dev",
|
||||
version = code_version::version(),
|
||||
// If a sub‑command is given, ignore requirements of the default args.
|
||||
subcommand_negates_reqs = true,
|
||||
// The executable is sometimes invoked via a platform‑specific name like
|
||||
// `codex-x86_64-unknown-linux-musl`, but the help output should always use
|
||||
// the generic `code` command name that users run.
|
||||
bin_name = "code"
|
||||
// `dev-x86_64-unknown-linux-musl`, or via the `code` alias, but the help
|
||||
// output should always name the product: `dev`.
|
||||
bin_name = "dev"
|
||||
)]
|
||||
struct MultitoolCli {
|
||||
#[clap(flatten)]
|
||||
@@ -85,7 +85,7 @@ struct MultitoolCli {
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum Subcommand {
|
||||
/// Run Codex non-interactively.
|
||||
/// Run Hanzo Dev non-interactively.
|
||||
#[clap(visible_alias = "e")]
|
||||
Exec(ExecCli),
|
||||
|
||||
@@ -95,11 +95,11 @@ enum Subcommand {
|
||||
/// Remove stored authentication credentials.
|
||||
Logout(LogoutCommand),
|
||||
|
||||
/// [experimental] Run Codex as an MCP server and manage MCP servers.
|
||||
/// [experimental] Run Hanzo Dev as an MCP server and manage MCP servers.
|
||||
#[clap(visible_alias = "acp")]
|
||||
Mcp(McpCli),
|
||||
|
||||
/// [experimental] Run the Codex MCP server (stdio transport).
|
||||
/// [experimental] Run the Hanzo Dev MCP server (stdio transport).
|
||||
McpServer,
|
||||
|
||||
/// [experimental] Run the app server.
|
||||
@@ -115,7 +115,7 @@ enum Subcommand {
|
||||
#[clap(hide = false)]
|
||||
OrderReplay(OrderReplayArgs),
|
||||
|
||||
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
|
||||
/// Apply the latest diff produced by the agent as a `git apply` to your local working tree.
|
||||
#[clap(visible_alias = "a")]
|
||||
Apply(ApplyCommand),
|
||||
|
||||
@@ -187,7 +187,7 @@ struct LoginCommand {
|
||||
|
||||
#[arg(
|
||||
long = "with-api-key",
|
||||
help = "Read the API key from stdin (e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`)"
|
||||
help = "Read the API key from stdin (e.g. `printenv OPENAI_API_KEY | dev login --with-api-key`)"
|
||||
)]
|
||||
with_api_key: bool,
|
||||
|
||||
@@ -254,7 +254,7 @@ struct OrderReplayArgs {
|
||||
struct PreviewArgs {
|
||||
/// Slug identifier (e.g., faster-downloads)
|
||||
slug: String,
|
||||
/// Optional owner/repo to override (defaults to just-every/code or $GITHUB_REPOSITORY)
|
||||
/// Optional owner/repo to override (defaults to hanzoai/dev or $GITHUB_REPOSITORY)
|
||||
#[arg(long = "repo", value_name = "OWNER/REPO")]
|
||||
repo: Option<String>,
|
||||
/// Output directory where the binary will be extracted
|
||||
@@ -373,7 +373,7 @@ async fn cli_main(code_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()>
|
||||
.await;
|
||||
} else if login_cli.api_key.is_some() {
|
||||
eprintln!(
|
||||
"The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
|
||||
"The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv OPENAI_API_KEY | dev login --with-api-key`."
|
||||
);
|
||||
std::process::exit(1);
|
||||
} else if login_cli.with_api_key {
|
||||
@@ -583,7 +583,7 @@ fn resolve_resume_path(session_id: Option<&str>, last: bool) -> anyhow::Result<O
|
||||
}
|
||||
|
||||
let code_home = code_core::config::find_code_home()
|
||||
.context("failed to locate Codex home directory")?;
|
||||
.context("failed to locate the Hanzo Dev home directory")?;
|
||||
|
||||
// Build the async work once, then execute it either on the existing
|
||||
// runtime (from a helper thread) or a fresh current-thread runtime.
|
||||
@@ -728,7 +728,7 @@ async fn preview_main(args: PreviewArgs) -> anyhow::Result<()> {
|
||||
let repo = args
|
||||
.repo
|
||||
.or_else(|| env::var("GITHUB_REPOSITORY").ok())
|
||||
.unwrap_or_else(|| "just-every/code".to_string());
|
||||
.unwrap_or_else(|| "hanzoai/dev".to_string());
|
||||
let (owner, name) = repo
|
||||
.split_once('/')
|
||||
.map(|(o, n)| (o.to_string(), n.to_string()))
|
||||
@@ -996,7 +996,7 @@ async fn doctor_main() -> anyhow::Result<()> {
|
||||
let bun_coder = format!("{}/coder", bun_bin);
|
||||
if coder_paths.iter().any(|p| p == &bun_coder) {
|
||||
println!("\nBun shim detected for 'coder': {}", bun_coder);
|
||||
println!("Suggestion: remove old Bun global with: bun remove -g @just-every/code");
|
||||
println!("Suggestion: remove old Bun global with: bun remove -g @hanzo/dev");
|
||||
}
|
||||
let bun_code = format!("{}/code", bun_bin);
|
||||
if code_paths.iter().any(|p| p == &bun_code) {
|
||||
@@ -1027,8 +1027,8 @@ async fn doctor_main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
println!("\nIf versions differ, remove older installs and keep one package manager:");
|
||||
println!(" - Bun: bun remove -g @just-every/code");
|
||||
println!(" - npm/pnpm: npm uninstall -g @just-every/code");
|
||||
println!(" - Bun: bun remove -g @hanzo/dev");
|
||||
println!(" - npm/pnpm: npm uninstall -g @hanzo/dev");
|
||||
println!(" - Homebrew: brew uninstall code");
|
||||
println!(" - Prefer using 'coder' to avoid conflicts with VS Code's 'code'.");
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
"Not signed in. Please run 'dev login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -233,7 +233,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
}
|
||||
None => {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
"Not signed in. Please run 'dev login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -1540,12 +1540,12 @@ async fn run_submit(args: crate::cli::SubmitArgs) -> anyhow::Result<()> {
|
||||
t
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Not signed in. Run 'codex login' and retry.");
|
||||
eprintln!("Not signed in. Run 'dev login' and retry.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
eprintln!("Not signed in. Run 'codex login' and retry.");
|
||||
eprintln!("Not signed in. Run 'dev login' and retry.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,11 @@ use crate::token_data::{parse_id_token, PlanType};
|
||||
use crate::token_data::KnownPlan;
|
||||
use crate::config::resolve_code_path_for_read;
|
||||
|
||||
/// The command a human runs to authenticate. It lives here once so no message
|
||||
/// can drift into naming a command that does not exist — `hanzo login` is not
|
||||
/// one, and gets swallowed as an AI task.
|
||||
pub const LOGIN_COMMAND: &str = "hanzo auth login";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CodexAuth {
|
||||
pub mode: AuthMode,
|
||||
@@ -349,7 +354,7 @@ fn load_auth(
|
||||
}
|
||||
None => {
|
||||
// We have an API key but no tokens in the auth.json file.
|
||||
// Perhaps the user ran `codex login --api-key <KEY>` or updated
|
||||
// Perhaps the user ran `dev login --api-key <KEY>` or updated
|
||||
// auth.json by hand. Either way, let's assume they are trying
|
||||
// to use their API key.
|
||||
return Ok(Some(CodexAuth::from_api_key_with_client(api_key, client)));
|
||||
|
||||
+28
-10
@@ -32,6 +32,7 @@ use crate::git_info::resolve_root_git_project_for_trust;
|
||||
use crate::model_family::ModelFamily;
|
||||
use crate::model_family::derive_default_model_family;
|
||||
use crate::model_family::find_family_for_model;
|
||||
use crate::model_provider_info::HANZO_PROVIDER_ID;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
use crate::model_provider_info::built_in_model_providers;
|
||||
use crate::openai_model_info::get_model_info;
|
||||
@@ -63,6 +64,16 @@ use toml_edit::Item as TomlItem;
|
||||
use toml_edit::Table as TomlTable;
|
||||
use which::which;
|
||||
|
||||
/// `dev`'s default model: Zen 5 Coder on the Hanzo Cloud.
|
||||
///
|
||||
/// It is a coding agent, so the default is the coding member of the Zen family.
|
||||
/// Anything else stays selectable — this is the one you get without asking.
|
||||
pub const HANZO_DEFAULT_MODEL: &str = "zen5-coder";
|
||||
/// Reviews run on the same model that wrote the code.
|
||||
pub const HANZO_DEFAULT_REVIEW_MODEL: &str = "zen5-coder";
|
||||
|
||||
/// Defaults used only when the caller has explicitly selected the `openai`
|
||||
/// provider. They are not `dev`'s defaults.
|
||||
const OPENAI_DEFAULT_MODEL: &str = "gpt-5-codex";
|
||||
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5-codex";
|
||||
pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5-codex";
|
||||
@@ -1954,7 +1965,7 @@ impl Config {
|
||||
let model_provider_id = model_provider
|
||||
.or(config_profile.model_provider)
|
||||
.or(cfg.model_provider)
|
||||
.unwrap_or_else(|| "openai".to_string());
|
||||
.unwrap_or_else(|| HANZO_PROVIDER_ID.to_string());
|
||||
let model_provider = model_providers
|
||||
.get(&model_provider_id)
|
||||
.ok_or_else(|| {
|
||||
@@ -2082,7 +2093,12 @@ impl Config {
|
||||
// Determine auth mode early so defaults like model selection can depend on it.
|
||||
let using_chatgpt_auth = Self::is_using_chatgpt_auth(&code_home);
|
||||
|
||||
let default_model_slug = if using_chatgpt_auth {
|
||||
// The default model follows the selected provider: a model id is only
|
||||
// meaningful to the endpoint that serves it. Asking the Hanzo Cloud for
|
||||
// an OpenAI id is a 400, and vice versa.
|
||||
let default_model_slug = if model_provider_id == HANZO_PROVIDER_ID {
|
||||
HANZO_DEFAULT_MODEL
|
||||
} else if using_chatgpt_auth {
|
||||
GPT_5_CODEX_MEDIUM_MODEL
|
||||
} else {
|
||||
OPENAI_DEFAULT_MODEL
|
||||
@@ -2149,10 +2165,16 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Default review model when not set in config; allow CLI override to take precedence.
|
||||
let review_model = override_review_model
|
||||
.or(cfg.review_model)
|
||||
.unwrap_or_else(default_review_model);
|
||||
// Default review model when not set in config; allow CLI override to take
|
||||
// precedence. Like `model`, it follows the provider — a review model id is
|
||||
// only meaningful to the endpoint that serves it.
|
||||
let review_model = override_review_model.or(cfg.review_model).unwrap_or_else(|| {
|
||||
if model_provider_id == HANZO_PROVIDER_ID {
|
||||
HANZO_DEFAULT_REVIEW_MODEL.to_string()
|
||||
} else {
|
||||
OPENAI_DEFAULT_REVIEW_MODEL.to_string()
|
||||
}
|
||||
});
|
||||
|
||||
let config = Self {
|
||||
model,
|
||||
@@ -2343,10 +2365,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_review_model() -> String {
|
||||
OPENAI_DEFAULT_REVIEW_MODEL.to_string()
|
||||
}
|
||||
|
||||
fn env_path(var: &str) -> std::io::Result<Option<PathBuf>> {
|
||||
match std::env::var(var) {
|
||||
Ok(val) if !val.trim().is_empty() => {
|
||||
|
||||
@@ -51,6 +51,8 @@ mod truncate;
|
||||
mod unified_exec;
|
||||
mod user_instructions;
|
||||
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||
pub use model_provider_info::HANZO_PROVIDER_ID;
|
||||
pub use model_provider_info::create_hanzo_provider;
|
||||
pub use model_provider_info::ModelProviderInfo;
|
||||
pub use model_provider_info::OpenRouterConfig;
|
||||
pub use model_provider_info::OpenRouterProviderConfig;
|
||||
|
||||
@@ -365,15 +365,27 @@ const DEFAULT_OLLAMA_PORT: u32 = 11434;
|
||||
|
||||
pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss";
|
||||
|
||||
/// The Hanzo Cloud — `dev`'s home and its default. Every other provider stays
|
||||
/// configurable; this is the one you get without asking.
|
||||
pub const HANZO_PROVIDER_ID: &str = "hanzo";
|
||||
|
||||
/// Where the Hanzo Cloud answers. Override with `HANZO_BASE_URL` for a private
|
||||
/// deployment; the `hanzo` CLI does exactly that when its active network points
|
||||
/// somewhere other than the public gateway.
|
||||
const HANZO_BASE_URL: &str = "https://api.hanzo.ai/v1";
|
||||
|
||||
/// The bearer `dev` reads. `hanzo code` sets it on the child process, and
|
||||
/// `hanzo auth login` is how a human fills it in.
|
||||
const HANZO_ENV_KEY: &str = "HANZO_USER_KEY";
|
||||
|
||||
/// Built-in default provider list.
|
||||
pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
|
||||
use ModelProviderInfo as P;
|
||||
|
||||
// We do not want to be in the business of adjucating which third-party
|
||||
// providers are bundled with Codex CLI, so we only include the OpenAI and
|
||||
// open source ("oss") providers by default. Users are encouraged to add to
|
||||
// `model_providers` in config.toml to add their own providers.
|
||||
// The Hanzo Cloud is the default. The rest are bundled because people ask
|
||||
// for them; add any other via `model_providers` in config.toml.
|
||||
[
|
||||
(HANZO_PROVIDER_ID, create_hanzo_provider()),
|
||||
(
|
||||
"openai",
|
||||
P {
|
||||
@@ -426,6 +438,42 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The Hanzo Cloud provider — `dev`'s default.
|
||||
///
|
||||
/// Speaks the Responses wire at `api.hanzo.ai/v1` and reads its bearer from
|
||||
/// `HANZO_USER_KEY`. `requires_openai_auth` is false: this account is ours, so
|
||||
/// the ChatGPT login dance never applies to it.
|
||||
pub fn create_hanzo_provider() -> ModelProviderInfo {
|
||||
let base_url = std::env::var("HANZO_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.unwrap_or_else(|| HANZO_BASE_URL.to_string());
|
||||
|
||||
ModelProviderInfo {
|
||||
name: "Hanzo".into(),
|
||||
base_url: Some(base_url),
|
||||
env_key: Some(HANZO_ENV_KEY.into()),
|
||||
env_key_instructions: Some(
|
||||
"Sign in with `hanzo auth login`, or set HANZO_USER_KEY to a key from \
|
||||
https://hanzo.ai/settings/keys."
|
||||
.into(),
|
||||
),
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: Some(
|
||||
[("version".to_string(), code_version::version().to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
stream_max_retries: None,
|
||||
stream_idle_timeout_ms: None,
|
||||
requires_openai_auth: false,
|
||||
openrouter: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_oss_provider() -> ModelProviderInfo {
|
||||
// These CODEX_OSS_ environment variables are experimental: we may
|
||||
// switch to reading values from config.toml instead.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! The product identity of `dev`, expressed as assertions.
|
||||
//!
|
||||
//! This repo tracks an upstream (`openai/codex`) and `upstream-merge.yml` merges
|
||||
//! from it every 30 minutes. A one-time edit to a default is therefore not a
|
||||
//! decision — it is a suggestion that survives until the next merge. These tests
|
||||
//! are how the decision is made durable: a merge that reverts any of them turns
|
||||
//! the build red instead of quietly shipping someone else's defaults.
|
||||
//!
|
||||
//! This file has no counterpart upstream, so a merge cannot conflict with it or
|
||||
//! carry away the guard along with the thing it guards.
|
||||
//!
|
||||
//! If you are here because this test failed after an upstream merge: the merge
|
||||
//! reverted our identity. Re-apply it — do not delete the test.
|
||||
|
||||
use code_core::HANZO_PROVIDER_ID;
|
||||
use code_core::WireApi;
|
||||
use code_core::built_in_model_providers;
|
||||
use code_core::config::HANZO_DEFAULT_MODEL;
|
||||
use code_core::config::HANZO_DEFAULT_REVIEW_MODEL;
|
||||
|
||||
/// Every model `dev` may pick by default belongs to the Zen family. Naming an
|
||||
/// upstream model in our own copy is the thing this guards against.
|
||||
fn is_zen_model(slug: &str) -> bool {
|
||||
slug.starts_with("zen") || slug.starts_with("enso")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hanzo_cloud_is_a_built_in_provider() {
|
||||
let providers = built_in_model_providers();
|
||||
let hanzo = providers
|
||||
.get(HANZO_PROVIDER_ID)
|
||||
.expect("the Hanzo Cloud must be a built-in provider");
|
||||
|
||||
assert_eq!(hanzo.name, "Hanzo");
|
||||
assert_eq!(
|
||||
hanzo.base_url.as_deref(),
|
||||
Some("https://api.hanzo.ai/v1"),
|
||||
"the Hanzo Cloud answers at api.hanzo.ai/v1"
|
||||
);
|
||||
assert_eq!(
|
||||
hanzo.env_key.as_deref(),
|
||||
Some("HANZO_USER_KEY"),
|
||||
"`hanzo code` sets HANZO_USER_KEY on the child; the provider must read it"
|
||||
);
|
||||
assert!(
|
||||
matches!(hanzo.wire_api, WireApi::Responses),
|
||||
"verified against the live gateway: /v1/responses serves our models"
|
||||
);
|
||||
assert!(
|
||||
!hanzo.requires_openai_auth,
|
||||
"this account is ours; the ChatGPT login flow never applies to it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_model_is_ours() {
|
||||
assert!(
|
||||
is_zen_model(HANZO_DEFAULT_MODEL),
|
||||
"default model must be a Zen model, got `{HANZO_DEFAULT_MODEL}`"
|
||||
);
|
||||
assert!(
|
||||
is_zen_model(HANZO_DEFAULT_REVIEW_MODEL),
|
||||
"default review model must be a Zen model, got `{HANZO_DEFAULT_REVIEW_MODEL}`"
|
||||
);
|
||||
}
|
||||
|
||||
/// The installer contract, `hanzo code`, and the Homebrew formula all resolve a
|
||||
/// binary named `dev` off PATH. A workspace that builds no such binary means a
|
||||
/// release ships nothing anyone can run.
|
||||
#[test]
|
||||
fn the_cli_builds_a_binary_named_dev() {
|
||||
let manifest = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../cli/Cargo.toml"
|
||||
))
|
||||
.expect("read code-rs/cli/Cargo.toml");
|
||||
|
||||
let bins: Vec<&str> = manifest
|
||||
.split("[[bin]]")
|
||||
.skip(1)
|
||||
.filter_map(|section| {
|
||||
section
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("name = "))
|
||||
.map(|n| n.trim().trim_matches('"'))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
bins.contains(&"dev"),
|
||||
"code-rs/cli must build a binary named `dev`; it builds {bins:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
bins.first(),
|
||||
Some(&"dev"),
|
||||
"`dev` is the product name and must be declared first; the rest are aliases"
|
||||
);
|
||||
}
|
||||
|
||||
/// We publish our releases to namespaces we own. Publishing into a third party's
|
||||
/// namespace is not a style question — it hands them our distribution.
|
||||
#[test]
|
||||
fn the_release_pipeline_publishes_to_our_namespaces() {
|
||||
let release_yml = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../.github/workflows/release.yml"
|
||||
))
|
||||
.expect("read .github/workflows/release.yml");
|
||||
|
||||
for (needle, why) in [
|
||||
("@just-every/", "npm packages must publish under @hanzo/"),
|
||||
(
|
||||
"just-every/homebrew-tap",
|
||||
"the Homebrew formula belongs in hanzoai/homebrew-tap",
|
||||
),
|
||||
(
|
||||
"just-every/code/releases",
|
||||
"release assets must be served from hanzoai/dev",
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
!release_yml.contains(needle),
|
||||
"release.yml still references `{needle}` — {why}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
release_yml.contains("--bin dev"),
|
||||
"the release must build the `dev` binary"
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ code-common = { workspace = true, features = [
|
||||
code-core = { workspace = true }
|
||||
code-ollama = { workspace = true }
|
||||
code-protocol = { workspace = true }
|
||||
code-version = { path = "../code-version" }
|
||||
code-app-server-protocol = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
opentelemetry-appender-tracing = { workspace = true }
|
||||
|
||||
@@ -141,12 +141,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
/// for the session. This mirrors the information shown in the TUI welcome
|
||||
/// screen.
|
||||
fn print_config_summary(&mut self, config: &Config, prompt: &str) {
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
ts_println!(
|
||||
self,
|
||||
"OpenAI Codex v{} (research preview)\n--------",
|
||||
VERSION
|
||||
);
|
||||
ts_println!(self, "Hanzo Dev v{}\n--------", code_version::version());
|
||||
|
||||
let entries = create_config_summary_entries(config);
|
||||
|
||||
|
||||
@@ -418,9 +418,12 @@ impl MessageProcessor {
|
||||
});
|
||||
|
||||
let auth_methods = serde_json::json!([{
|
||||
"id": "code-login",
|
||||
"name": "Use Code login",
|
||||
"description": "Run `code login` (ChatGPT or API key) before connecting."
|
||||
"id": "hanzo-login",
|
||||
"name": "Use Hanzo login",
|
||||
"description": format!(
|
||||
"Run `{}` before connecting.",
|
||||
code_core::auth::LOGIN_COMMAND
|
||||
),
|
||||
}]);
|
||||
|
||||
let result = serde_json::json!({
|
||||
|
||||
@@ -1710,8 +1710,9 @@ impl ChatWidget<'_> {
|
||||
app_event_tx_clone.send_background_event_with_ticket(
|
||||
&ticket,
|
||||
format!(
|
||||
"❌ Failed to initialize model session: {}.\n• Ensure an OpenAI API key is set (CODE_OPENAI_API_KEY / OPENAI_API_KEY) or run `code login`.\n• Also verify config.cwd is an absolute path.",
|
||||
e
|
||||
"❌ Failed to initialize model session: {}.\n• Sign in with `{}`, or set HANZO_USER_KEY.\n• Also verify config.cwd is an absolute path.",
|
||||
e,
|
||||
code_core::auth::LOGIN_COMMAND
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -225,7 +225,7 @@ async fn load_config() -> Result<CloudTasksConfig> {
|
||||
);
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.ok_or_else(|| anyhow!("Not signed in. Run `codex login` to authenticate with ChatGPT."))?;
|
||||
.ok_or_else(|| anyhow!("Not signed in. Run `dev login` to authenticate with ChatGPT."))?;
|
||||
let token = auth
|
||||
.get_token()
|
||||
.await
|
||||
|
||||
@@ -266,7 +266,7 @@ impl AuthModeWidget {
|
||||
Line::from(vec![
|
||||
Span::raw(" For more details see the "),
|
||||
Span::styled(
|
||||
"\u{1b}]8;;https://github.com/just-every/code\u{7}Code docs\u{1b}]8;;\u{7}",
|
||||
"\u{1b}]8;;https://github.com/hanzoai/dev\u{7}Code docs\u{1b}]8;;\u{7}",
|
||||
Style::default().add_modifier(Modifier::UNDERLINED),
|
||||
),
|
||||
])
|
||||
|
||||
@@ -117,8 +117,8 @@ struct ReleaseInfo {
|
||||
}
|
||||
|
||||
const VERSION_FILENAME: &str = "version.json";
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/just-every/code/releases/latest";
|
||||
pub const CODE_RELEASE_URL: &str = "https://github.com/just-every/code/releases/latest";
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/hanzoai/dev/releases/latest";
|
||||
pub const CODE_RELEASE_URL: &str = "https://github.com/hanzoai/dev/releases/latest";
|
||||
|
||||
const AUTO_UPGRADE_LOCK_FILE: &str = "auto-upgrade.lock";
|
||||
const AUTO_UPGRADE_LOCK_TTL: Duration = Duration::from_secs(900); // 15 minutes
|
||||
@@ -140,9 +140,9 @@ pub fn resolve_upgrade_resolution() -> UpgradeResolution {
|
||||
"npm".to_string(),
|
||||
"install".to_string(),
|
||||
"-g".to_string(),
|
||||
"@just-every/code@latest".to_string(),
|
||||
"@hanzo/dev@latest".to_string(),
|
||||
],
|
||||
display: "npm install -g @just-every/code@latest".to_string(),
|
||||
display: "npm install -g @hanzo/dev@latest".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
# CODE
|
||||
|
||||
 
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/logo.png" alt="Code Logo" width="400">
|
||||
</p>
|
||||
|
||||
 
|
||||
|
||||
**Code** is a fast, local coding agent for your terminal. It's a community-driven fork of `openai/codex` focused on real developer ergonomics: Browser integration, multi-agents, theming, and reasoning control — all while staying compatible with upstream.
|
||||
|
||||
 
|
||||
## Why Code
|
||||
|
||||
- 🌐 **Browser Integration** - CDP support, headless browsing, screenshots
|
||||
- 📝 **Diff Viewer** - Side-by-side diffs with syntax highlighting
|
||||
- 🤖 **Multi-Agent Commands** - /plan, /solve, /code with agent panels
|
||||
- 🎨 **Theme System** - /themes with live preview and accessibility
|
||||
- 🧠 **Reasoning Control** - /reasoning for dynamic effort adjustment
|
||||
- 🔌 **MCP support** – Extend with filesystem, DBs, APIs, or your own tools.
|
||||
- 🔒 **Safety modes** – Read-only, approvals, and workspace sandboxing.
|
||||
- 🔁 **Backwards compatible** – Reads both `~/.code/*` (primary) and legacy `~/.codex/*`; writes only to `~/.code/*`
|
||||
|
||||
 
|
||||
| <img src="docs/screenshots/simple.png" alt="Simple interface" width="100%"><br>Simple interface | <img src="docs/screenshots/diff.png" alt="Unified diff viewer" width="100%"><br>Unified diffs |
|
||||
|:--:|:--:|
|
||||
|
||||
| <br><img src="docs/screenshots/browser.png" alt="Browser control" width="100%"><br>Browser control | <br><img src="docs/screenshots/agents.png" alt="Assist with Claude & Gemini" width="100%"><br>Assist with Claude & Gemini |
|
||||
|:--:|:--:|
|
||||
|
||||
|
||||
 
|
||||
## Quickstart
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
npx -y @just-every/code
|
||||
```
|
||||
|
||||
### Install & Run
|
||||
|
||||
```bash
|
||||
npm install -g @just-every/code
|
||||
code // or `coder` if you're using VS Code
|
||||
```
|
||||
|
||||
Note: If another tool already provides a `code` command (e.g. VS Code), our CLI is also installed as `coder`. Use `coder` to avoid conflicts.
|
||||
|
||||
**Authenticate** (one of the following):
|
||||
- **Sign in with ChatGPT** (Plus/Pro/Team; uses models available to your plan)
|
||||
- Run `code` and pick "Sign in with ChatGPT"
|
||||
- Stores creds locally at `~/.code/auth.json` (still reads legacy `~/.codex/auth.json` if present)
|
||||
- **API key** (usage-based)
|
||||
- Set `export OPENAI_API_KEY=xyz` and run `code`
|
||||
|
||||
### Install Claude & Gemini (optional)
|
||||
|
||||
Code supports orchestrating other AI CLI tools. Install these and config to use alongside Code.
|
||||
|
||||
```bash
|
||||
|
||||
npm install -g @anthropic-ai/claude-code @google/gemini-cli && claude "Just checking you're working! Let me know how I can exit." && gemini -i "Just checking you're working! Let me know how I can exit."
|
||||
```
|
||||
|
||||
 
|
||||
## Commands
|
||||
|
||||
### Browser
|
||||
```bash
|
||||
# Connect code to external Chrome browser (running CDP)
|
||||
/chrome # Connect with auto-detect port
|
||||
/chrome 9222 # Connect to specific port
|
||||
|
||||
# Switch to internal browser mode
|
||||
/browser # Use internal headless browser
|
||||
/browser https://example.com # Open URL in internal browser
|
||||
```
|
||||
|
||||
### Agents
|
||||
```bash
|
||||
# Plan code changes (Claude, Gemini and GPT-5 consensus)
|
||||
# All agents review task and create a consolidated plan
|
||||
/plan "Stop the AI from ordering pizza at 3AM"
|
||||
|
||||
# Solve complex problems (Claude, Gemini and GPT-5 race)
|
||||
# Fastest preferred (see https://arxiv.org/abs/2505.17813)
|
||||
/solve "Why does deleting one user drop the whole database?"
|
||||
|
||||
# Write code! (Claude, Gemini and GPT-5 consensus)
|
||||
# Creates multiple worktrees then implements the optimal solution
|
||||
/code "Show dark mode when I feel cranky"
|
||||
```
|
||||
|
||||
### General
|
||||
```bash
|
||||
# Try a new theme!
|
||||
/themes
|
||||
|
||||
# Change reasoning level
|
||||
/reasoning low|medium|high
|
||||
|
||||
# Switch models or effort presets
|
||||
/model
|
||||
|
||||
# Start new conversation
|
||||
/new
|
||||
```
|
||||
|
||||
## CLI reference
|
||||
|
||||
```shell
|
||||
code [options] [prompt]
|
||||
|
||||
Options:
|
||||
--model <name> Override the model (gpt-5, claude-opus, etc.)
|
||||
--read-only Prevent file modifications
|
||||
--no-approval Skip approval prompts (use with caution)
|
||||
--config <key=val> Override config values
|
||||
--oss Use local open source models
|
||||
--sandbox <mode> Set sandbox level (read-only, workspace-write, etc.)
|
||||
--help Show help information
|
||||
--debug Log API requests and responses to file
|
||||
--version Show version number
|
||||
```
|
||||
|
||||
 
|
||||
## Memory & project docs
|
||||
|
||||
Code can remember context across sessions:
|
||||
|
||||
1. **Create an `AGENTS.md` or `CLAUDE.md` file** in your project root:
|
||||
```markdown
|
||||
# Project Context
|
||||
This is a React TypeScript application with:
|
||||
- Authentication via JWT
|
||||
- PostgreSQL database
|
||||
- Express.js backend
|
||||
|
||||
## Key files:
|
||||
- `/src/auth/` - Authentication logic
|
||||
- `/src/api/` - API client code
|
||||
- `/server/` - Backend services
|
||||
```
|
||||
|
||||
2. **Session memory**: Code maintains conversation history
|
||||
3. **Codebase analysis**: Automatically understands project structure
|
||||
|
||||
 
|
||||
## Non-interactive / CI mode
|
||||
|
||||
For automation and CI/CD:
|
||||
|
||||
```shell
|
||||
# Run a specific task
|
||||
code --no-approval "run tests and fix any failures"
|
||||
|
||||
# Generate reports
|
||||
code --read-only "analyze code quality and generate report"
|
||||
|
||||
# Batch processing
|
||||
code --config output_format=json "list all TODO comments"
|
||||
```
|
||||
|
||||
 
|
||||
## Model Context Protocol (MCP)
|
||||
|
||||
Code supports MCP for extended capabilities:
|
||||
|
||||
- **File operations**: Advanced file system access
|
||||
- **Database connections**: Query and modify databases
|
||||
- **API integrations**: Connect to external services
|
||||
- **Custom tools**: Build your own extensions
|
||||
|
||||
Configure MCP in `~/.code/config.toml` (legacy `~/.codex/config.toml` is still read if present). Define each server under a named table like `[mcp_servers.<name>]` (this maps to the JSON `mcpServers` object used by other clients):
|
||||
|
||||
```toml
|
||||
[mcp_servers.filesystem]
|
||||
command = "npx"
|
||||
args = ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
|
||||
```
|
||||
|
||||
 
|
||||
## Configuration
|
||||
|
||||
Main config file: `~/.code/config.toml`
|
||||
|
||||
> [!NOTE]
|
||||
> Code reads from both `~/.code/` and `~/.codex/` for backwards compatibility, but it only writes updates to `~/.code/`. If you switch back to Codex and it fails to start, remove `~/.codex/config.toml`. If Code appears to miss settings after upgrading, copy your legacy `~/.codex/config.toml` into `~/.code/`.
|
||||
|
||||
```toml
|
||||
# Model settings
|
||||
model = "gpt-5"
|
||||
model_provider = "openai"
|
||||
|
||||
# Behavior
|
||||
approval_policy = "on-request" # untrusted | on-failure | on-request | never
|
||||
model_reasoning_effort = "medium" # low | medium | high
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
# UI preferences see THEME_CONFIG.md
|
||||
[tui.theme]
|
||||
name = "light-photon"
|
||||
|
||||
# Add config for specific models
|
||||
[profiles.gpt-5]
|
||||
model = "gpt-5"
|
||||
model_provider = "openai"
|
||||
approval_policy = "never"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
- `CODEX_HOME`: Override config directory location
|
||||
- `OPENAI_API_KEY`: Use API key instead of ChatGPT auth
|
||||
- `OPENAI_BASE_URL`: Use alternative API endpoints
|
||||
- `OPENAI_WIRE_API`: Force the built-in OpenAI provider to use `chat` or `responses` wiring
|
||||
|
||||
 
|
||||
## FAQ
|
||||
|
||||
**How is this different from the original?**
|
||||
> This fork adds browser integration, multi-agent commands (`/plan`, `/solve`, `/code`), theme system, and enhanced reasoning controls while maintaining full compatibility.
|
||||
|
||||
**Can I use my existing Codex configuration?**
|
||||
> Yes. Code reads from both `~/.code/` (primary) and legacy `~/.codex/` directories. We only write to `~/.code/`, so Codex will keep running if you switch back; copy or remove legacy files if you notice conflicts.
|
||||
|
||||
**Does this work with ChatGPT Plus?**
|
||||
> Absolutely. Use the same "Sign in with ChatGPT" flow as the original.
|
||||
|
||||
**Is my data secure?**
|
||||
> Yes. Authentication stays on your machine, and we don't proxy your credentials or conversations.
|
||||
|
||||
 
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! This fork maintains compatibility with upstream while adding community-requested features.
|
||||
|
||||
### Development workflow
|
||||
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone https://github.com/just-every/code.git
|
||||
cd code
|
||||
npm install
|
||||
|
||||
# Build (use fast build for development)
|
||||
./build-fast.sh
|
||||
|
||||
# Run locally
|
||||
./codex-rs/target/dev-fast/code
|
||||
```
|
||||
|
||||
### Opening a pull request
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Make your changes
|
||||
4. Run tests: `cargo test`
|
||||
5. Build successfully: `./build-fast.sh`
|
||||
6. Submit a pull request
|
||||
|
||||
|
||||
 
|
||||
## Legal & Use
|
||||
|
||||
### License & attribution
|
||||
- This project is a community fork of `openai/codex` under **Apache-2.0**. We preserve upstream LICENSE and NOTICE files.
|
||||
- **Code** is **not** affiliated with, sponsored by, or endorsed by OpenAI.
|
||||
|
||||
### Your responsibilities
|
||||
Using OpenAI, Anthropic or Google services through Code means you agree to **their Terms and policies**. In particular:
|
||||
- **Don't** programmatically scrape/extract content outside intended flows.
|
||||
- **Don't** bypass or interfere with rate limits, quotas, or safety mitigations.
|
||||
- Use your **own** account; don't share or rotate accounts to evade limits.
|
||||
- If you configure other model providers, you're responsible for their terms.
|
||||
|
||||
### Privacy
|
||||
- Your auth file lives at `~/.code/auth.json` (legacy `~/.codex/auth.json` is still read).
|
||||
- Inputs/outputs you send to AI providers are handled under their Terms and Privacy Policy; consult those documents (and any org-level data-sharing settings).
|
||||
|
||||
### Subject to change
|
||||
AI providers can change eligibility, limits, models, or authentication flows. Code supports **both** ChatGPT sign-in and API-key modes so you can pick what fits (local/hobby vs CI/automation).
|
||||
|
||||
 
|
||||
## License
|
||||
|
||||
Apache 2.0 - See [LICENSE](LICENSE) file for details.
|
||||
|
||||
This project is a community fork of the original Codex CLI. We maintain compatibility while adding enhanced features requested by the developer community.
|
||||
|
||||
 
|
||||
---
|
||||
**Need help?** Open an issue on [GitHub](https://github.com/just-every/code/issues) or check our documentation.
|
||||
@@ -1,452 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Unified entry point for the Code CLI (fork of OpenAI Codex).
|
||||
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { platform as nodePlatform, arch as nodeArch } from "os";
|
||||
import { execSync } from "child_process";
|
||||
import { get as httpsGet } from "https";
|
||||
|
||||
// __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const { platform, arch } = process;
|
||||
|
||||
// Important: Never delegate to another system's `code` binary (e.g., VS Code).
|
||||
// When users run via `npx @just-every/code`, we must always execute our
|
||||
// packaged native binary by absolute path to avoid PATH collisions.
|
||||
|
||||
const isWSL = () => {
|
||||
if (platform !== "linux") return false;
|
||||
try {
|
||||
const txt = readFileSync("/proc/version", "utf8").toLowerCase();
|
||||
return txt.includes("microsoft");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let targetTriple = null;
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
case "android":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-unknown-linux-musl";
|
||||
break;
|
||||
case "arm64":
|
||||
targetTriple = "aarch64-unknown-linux-musl";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "darwin":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-apple-darwin";
|
||||
break;
|
||||
case "arm64":
|
||||
targetTriple = "aarch64-apple-darwin";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "win32":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-pc-windows-msvc.exe";
|
||||
break;
|
||||
case "arm64":
|
||||
// We do not build this today, fall through...
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!targetTriple) {
|
||||
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
||||
}
|
||||
|
||||
// Prefer new 'code-*' binary names; fall back to legacy 'coder-*' if missing.
|
||||
let binaryPath = path.join(__dirname, "..", "bin", `code-${targetTriple}`);
|
||||
let legacyBinaryPath = path.join(__dirname, "..", "bin", `coder-${targetTriple}`);
|
||||
|
||||
// --- Bootstrap helper (runs if the binary is missing, e.g. Bun blocked postinstall) ---
|
||||
import { existsSync, chmodSync, statSync, openSync, readSync, closeSync, mkdirSync, copyFileSync, readFileSync, unlinkSync, createWriteStream } from "fs";
|
||||
|
||||
const validateBinary = (p) => {
|
||||
try {
|
||||
const st = statSync(p);
|
||||
if (!st.isFile() || st.size === 0) {
|
||||
return { ok: false, reason: "empty or not a regular file" };
|
||||
}
|
||||
const fd = openSync(p, "r");
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
const n = readSync(fd, buf, 0, 4, 0);
|
||||
if (n < 2) return { ok: false, reason: "too short" };
|
||||
if (platform === "win32") {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: "invalid PE header (missing MZ)" };
|
||||
} else if (platform === "linux" || platform === "android") {
|
||||
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: "invalid ELF header" };
|
||||
} else if (platform === "darwin") {
|
||||
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: "invalid Mach-O header" };
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, reason: e.message };
|
||||
}
|
||||
};
|
||||
|
||||
const getCacheDir = (version) => {
|
||||
const plt = nodePlatform();
|
||||
const home = process.env.HOME || process.env.USERPROFILE || "";
|
||||
let base = "";
|
||||
if (plt === "win32") {
|
||||
base = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
||||
} else if (plt === "darwin") {
|
||||
base = path.join(home, "Library", "Caches");
|
||||
} else {
|
||||
base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
|
||||
}
|
||||
const dir = path.join(base, "just-every", "code", version);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
};
|
||||
|
||||
const getCachedBinaryPath = (version) => {
|
||||
// targetTriple already includes the proper extension on Windows ("...msvc.exe").
|
||||
// Do not append another suffix; just use the exact targetTriple-derived name.
|
||||
const cacheDir = getCacheDir(version);
|
||||
return path.join(cacheDir, `code-${targetTriple}`);
|
||||
};
|
||||
|
||||
let lastBootstrapError = null;
|
||||
|
||||
const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
|
||||
const req = httpsGet(url, (res) => {
|
||||
const status = res.statusCode || 0;
|
||||
if (status >= 300 && status < 400 && res.headers.location) {
|
||||
// follow one redirect recursively
|
||||
return resolve(httpsDownload(res.headers.location, dest));
|
||||
}
|
||||
if (status !== 200) {
|
||||
return reject(new Error(`HTTP ${status}`));
|
||||
}
|
||||
const out = createWriteStream(dest);
|
||||
res.pipe(out);
|
||||
out.on("finish", () => out.close(resolve));
|
||||
out.on("error", (e) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
req.on("error", (e) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(e);
|
||||
});
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error("download timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
const tryBootstrapBinary = async () => {
|
||||
try {
|
||||
// 1) Read our published version
|
||||
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
||||
const version = pkg.version;
|
||||
|
||||
const binDir = path.join(__dirname, "..", "bin");
|
||||
if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// 2) Fast path: user cache
|
||||
const cachePath = getCachedBinaryPath(version);
|
||||
if (existsSync(cachePath)) {
|
||||
const v = validateBinary(cachePath);
|
||||
if (v.ok) {
|
||||
// Prefer running directly from cache; mirror into node_modules on Unix
|
||||
if (platform !== "win32") {
|
||||
copyFileSync(cachePath, binaryPath);
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Try platform package (if present)
|
||||
try {
|
||||
const req = (await import("module")).createRequire(import.meta.url);
|
||||
const name = (() => {
|
||||
if (platform === "win32") return "@just-every/code-win32-x64"; // may be unpublished; falls through
|
||||
const plt = nodePlatform();
|
||||
const cpu = nodeArch();
|
||||
if (plt === "darwin" && cpu === "arm64") return "@just-every/code-darwin-arm64";
|
||||
if (plt === "darwin" && cpu === "x64") return "@just-every/code-darwin-x64";
|
||||
if (plt === "linux" && cpu === "x64") return "@just-every/code-linux-x64-musl";
|
||||
if (plt === "linux" && cpu === "arm64") return "@just-every/code-linux-arm64-musl";
|
||||
return null;
|
||||
})();
|
||||
if (name) {
|
||||
try {
|
||||
const pkgJson = req.resolve(`${name}/package.json`);
|
||||
const pkgDir = path.dirname(pkgJson);
|
||||
const src = path.join(pkgDir, "bin", `code-${targetTriple}`);
|
||||
if (existsSync(src)) {
|
||||
// Always ensure cache has the binary; on Unix mirror into node_modules
|
||||
copyFileSync(src, cachePath);
|
||||
if (platform !== "win32") {
|
||||
copyFileSync(cachePath, binaryPath);
|
||||
try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch { /* ignore and fall back */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// 4) Download from GitHub release
|
||||
const isWin = platform === "win32";
|
||||
const archiveName = isWin
|
||||
? `code-${targetTriple}.zip`
|
||||
: (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `code-${targetTriple}.zst`; } catch { return `code-${targetTriple}.tar.gz`; } })();
|
||||
const url = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
|
||||
const tmp = path.join(binDir, `.${archiveName}.part`);
|
||||
return httpsDownload(url, tmp)
|
||||
.then(() => {
|
||||
if (isWin) {
|
||||
// Extract zip with robust fallbacks and use a safe temp dir, then move to cache
|
||||
try {
|
||||
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\\Windows';
|
||||
const psFull = path.join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
||||
const unzipDest = getCacheDir(version); // extract directly to cache location
|
||||
const psCmd = `Expand-Archive -Path '${tmp}' -DestinationPath '${unzipDest}' -Force`;
|
||||
let ok = false;
|
||||
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
||||
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
if (!ok) { execSync(`tar -xf "${tmp}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
|
||||
} catch (e) {
|
||||
throw new Error(`failed to unzip: ${e.message}`);
|
||||
} finally { try { unlinkSync(tmp); } catch {} }
|
||||
} else {
|
||||
if (archiveName.endsWith(".zst")) {
|
||||
try { execSync(`zstd -d '${tmp}' -o '${binaryPath}'`, { stdio: 'ignore', shell: true }); }
|
||||
catch (e) { try { unlinkSync(tmp); } catch {}; throw new Error(`failed to decompress zst: ${e.message}`); }
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
} else {
|
||||
try { execSync(`tar -xzf '${tmp}' -C '${binDir}'`, { stdio: 'ignore', shell: true }); }
|
||||
catch (e) { try { unlinkSync(tmp); } catch {}; throw new Error(`failed to extract tar.gz: ${e.message}`); }
|
||||
try { unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
// On Windows, the file was extracted directly into the cache dir
|
||||
if (platform === "win32") {
|
||||
// Ensure the expected filename exists in cache; Expand-Archive extracts exact name
|
||||
// No action required here; validation occurs below against cachePath
|
||||
} else {
|
||||
try { copyFileSync(binaryPath, cachePath); } catch {}
|
||||
}
|
||||
|
||||
const v = validateBinary(platform === "win32" ? cachePath : binaryPath);
|
||||
if (!v.ok) throw new Error(`invalid binary (${v.reason})`);
|
||||
if (platform !== "win32") try { chmodSync(binaryPath, 0o755); } catch {}
|
||||
return true;
|
||||
})
|
||||
.catch((e) => { lastBootstrapError = e; return false; });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// If missing, attempt to bootstrap into place (helps when Bun blocks postinstall)
|
||||
if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) {
|
||||
const ok = await tryBootstrapBinary();
|
||||
if (!ok) {
|
||||
// retry legacy name in case archive provided coder-*
|
||||
if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) {
|
||||
binaryPath = legacyBinaryPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer cached binary when available
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
||||
const version = pkg.version;
|
||||
const cached = getCachedBinaryPath(version);
|
||||
const v = existsSync(cached) ? validateBinary(cached) : { ok: false };
|
||||
if (v.ok) {
|
||||
binaryPath = cached;
|
||||
} else if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
|
||||
binaryPath = legacyBinaryPath;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Check if binary exists and try to fix permissions if needed
|
||||
// fs imports are above; keep for readability if tree-shaken by bundlers
|
||||
import { spawnSync } from "child_process";
|
||||
if (existsSync(binaryPath)) {
|
||||
try {
|
||||
// Ensure binary is executable on Unix-like systems
|
||||
if (platform !== "win32") {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore permission errors, will be caught below if it's a real problem
|
||||
}
|
||||
} else {
|
||||
console.error(`Binary not found: ${binaryPath}`);
|
||||
if (lastBootstrapError) {
|
||||
const msg = (lastBootstrapError && (lastBootstrapError.message || String(lastBootstrapError))) || 'unknown bootstrap error';
|
||||
console.error(`Bootstrap error: ${msg}`);
|
||||
}
|
||||
console.error(`Please try reinstalling the package:`);
|
||||
console.error(` npm uninstall -g @just-every/code`);
|
||||
console.error(` npm install -g @just-every/code`);
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Install inside WSL (Ubuntu) separately:");
|
||||
console.error(" npx -y @just-every/code@latest (run inside WSL)");
|
||||
console.error("If installed globally on Windows, those binaries are not usable from WSL.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Lightweight header validation to provide clearer errors before spawn
|
||||
// Reuse the validateBinary helper defined above in the bootstrap section.
|
||||
|
||||
const validation = validateBinary(binaryPath);
|
||||
if (!validation.ok) {
|
||||
console.error(`The native binary at ${binaryPath} appears invalid: ${validation.reason}`);
|
||||
console.error("This can happen if the download failed or was modified by antivirus/proxy.");
|
||||
console.error("Please try reinstalling:");
|
||||
console.error(" npm uninstall -g @just-every/code");
|
||||
console.error(" npm install -g @just-every/code");
|
||||
if (platform === "win32") {
|
||||
console.error("If the issue persists, clear npm cache and disable antivirus temporarily:");
|
||||
console.error(" npm cache clean --force");
|
||||
}
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Ensure you install/run inside WSL, not Windows:");
|
||||
console.error(" npx -y @just-every/code@latest (inside WSL)");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If running under npx/npm, emit a concise notice about which binary path is used
|
||||
try {
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
const isNpx = ua.includes("npx");
|
||||
if (isNpx && process.stderr && process.stderr.isTTY) {
|
||||
// Best-effort discovery of another 'code' on PATH for user clarity
|
||||
let otherCode = "";
|
||||
try {
|
||||
const cmd = process.platform === "win32" ? "where code" : "command -v code || which code || true";
|
||||
const out = spawnSync(process.platform === "win32" ? "cmd" : "bash", [
|
||||
process.platform === "win32" ? "/c" : "-lc",
|
||||
cmd,
|
||||
], { encoding: "utf8" });
|
||||
const line = (out.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
||||
if (line && !line.includes("@just-every/code")) {
|
||||
otherCode = line;
|
||||
}
|
||||
} catch {}
|
||||
if (otherCode) {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
console.error(`Note: a different 'code' exists at ${otherCode}; not delegating.`);
|
||||
} else {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Use an asynchronous spawn instead of spawnSync so that Node is able to
|
||||
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
|
||||
// executing. This allows us to forward those signals to the child process
|
||||
// and guarantees that when either the child terminates or the parent
|
||||
// receives a fatal signal, both processes exit in a predictable manner.
|
||||
const { spawn } = await import("child_process");
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, CODER_MANAGED_BY_NPM: "1", CODEX_MANAGED_BY_NPM: "1" },
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
// Typically triggered when the binary is missing or not executable.
|
||||
const code = err && err.code;
|
||||
if (code === 'EACCES') {
|
||||
console.error(`Permission denied: ${binaryPath}`);
|
||||
console.error(`Try running: chmod +x "${binaryPath}"`);
|
||||
console.error(`Or reinstall the package with: npm install -g @just-every/code`);
|
||||
} else if (code === 'EFTYPE' || code === 'ENOEXEC') {
|
||||
console.error(`Failed to execute native binary: ${binaryPath}`);
|
||||
console.error("The file may be corrupt or of the wrong type. Reinstall usually fixes this:");
|
||||
console.error(" npm uninstall -g @just-every/code && npm install -g @just-every/code");
|
||||
if (platform === 'win32') {
|
||||
console.error("On Windows, ensure the .exe downloaded correctly (proxy/AV can interfere).");
|
||||
console.error("Try clearing cache: npm cache clean --force");
|
||||
}
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Windows binaries cannot be executed from WSL.");
|
||||
console.error("Install inside WSL and run there: npx -y @just-every/code@latest");
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Forward common termination signals to the child so that it shuts down
|
||||
// gracefully. In the handler we temporarily disable the default behavior of
|
||||
// exiting immediately; once the child has been signaled we simply wait for
|
||||
// its exit event which will in turn terminate the parent (see below).
|
||||
const forwardSignal = (signal) => {
|
||||
if (child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
|
||||
process.on(sig, () => forwardSignal(sig));
|
||||
});
|
||||
|
||||
// When the child exits, mirror its termination reason in the parent so that
|
||||
// shell scripts and other tooling observe the correct exit status.
|
||||
// Wrap the lifetime of the child process in a Promise so that we can await
|
||||
// its termination in a structured way. The Promise resolves with an object
|
||||
// describing how the child exited: either via exit code or due to a signal.
|
||||
const childResult = await new Promise((resolve) => {
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
resolve({ type: "signal", signal });
|
||||
} else {
|
||||
resolve({ type: "code", exitCode: code ?? 1 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (childResult.type === "signal") {
|
||||
// Re-emit the same signal so that the parent terminates with the expected
|
||||
// semantics (this also sets the correct exit code of 128 + n).
|
||||
process.kill(process.pid, childResult.signal);
|
||||
} else {
|
||||
process.exit(childResult.exitCode);
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Unified entry point for the Codex CLI.
|
||||
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const { platform, arch } = process;
|
||||
|
||||
let targetTriple = null;
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
case "android":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-unknown-linux-musl";
|
||||
break;
|
||||
case "arm64":
|
||||
targetTriple = "aarch64-unknown-linux-musl";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "darwin":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-apple-darwin";
|
||||
break;
|
||||
case "arm64":
|
||||
targetTriple = "aarch64-apple-darwin";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "win32":
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
targetTriple = "x86_64-pc-windows-msvc";
|
||||
break;
|
||||
case "arm64":
|
||||
targetTriple = "aarch64-pc-windows-msvc";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!targetTriple) {
|
||||
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
||||
}
|
||||
|
||||
const vendorRoot = path.join(__dirname, "..", "vendor");
|
||||
const archRoot = path.join(vendorRoot, targetTriple);
|
||||
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
|
||||
const binaryPath = path.join(archRoot, "codex", codexBinaryName);
|
||||
|
||||
// Use an asynchronous spawn instead of spawnSync so that Node is able to
|
||||
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
|
||||
// executing. This allows us to forward those signals to the child process
|
||||
// and guarantees that when either the child terminates or the parent
|
||||
// receives a fatal signal, both processes exit in a predictable manner.
|
||||
const { spawn } = await import("child_process");
|
||||
|
||||
function getUpdatedPath(newDirs) {
|
||||
const pathSep = process.platform === "win32" ? ";" : ":";
|
||||
const existingPath = process.env.PATH || "";
|
||||
const updatedPath = [
|
||||
...newDirs,
|
||||
...existingPath.split(pathSep).filter(Boolean),
|
||||
].join(pathSep);
|
||||
return updatedPath;
|
||||
}
|
||||
|
||||
const additionalDirs = [];
|
||||
const pathDir = path.join(archRoot, "path");
|
||||
if (existsSync(pathDir)) {
|
||||
additionalDirs.push(pathDir);
|
||||
}
|
||||
const updatedPath = getUpdatedPath(additionalDirs);
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, PATH: updatedPath, CODEX_MANAGED_BY_NPM: "1" },
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
// Typically triggered when the binary is missing or not executable.
|
||||
// Re-throwing here will terminate the parent with a non-zero exit code
|
||||
// while still printing a helpful stack trace.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Forward common termination signals to the child so that it shuts down
|
||||
// gracefully. In the handler we temporarily disable the default behavior of
|
||||
// exiting immediately; once the child has been signaled we simply wait for
|
||||
// its exit event which will in turn terminate the parent (see below).
|
||||
const forwardSignal = (signal) => {
|
||||
if (child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
|
||||
process.on(sig, () => forwardSignal(sig));
|
||||
});
|
||||
|
||||
// When the child exits, mirror its termination reason in the parent so that
|
||||
// shell scripts and other tooling observe the correct exit status.
|
||||
// Wrap the lifetime of the child process in a Promise so that we can await
|
||||
// its termination in a structured way. The Promise resolves with an object
|
||||
// describing how the child exited: either via exit code or due to a signal.
|
||||
const childResult = await new Promise((resolve) => {
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
resolve({ type: "signal", signal });
|
||||
} else {
|
||||
resolve({ type: "code", exitCode: code ?? 1 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (childResult.type === "signal") {
|
||||
// Re-emit the same signal so that the parent terminates with the expected
|
||||
// semantics (this also sets the correct exit code of 128 + n).
|
||||
process.kill(process.pid, childResult.signal);
|
||||
} else {
|
||||
process.exit(childResult.exitCode);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "@just-every/code",
|
||||
"version": "0.2.175",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex",
|
||||
"bin": {
|
||||
"coder": "bin/coder.js"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"files": [
|
||||
"bin/coder.js",
|
||||
"bin/codex.js",
|
||||
"postinstall.js",
|
||||
"scripts/preinstall.js",
|
||||
"scripts/windows-cleanup.ps1",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/preinstall.js",
|
||||
"postinstall": "node postinstall.js",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs'),path=require('path'); const repoGit=path.join(__dirname,'..','.git'); const inCi=process.env.GITHUB_ACTIONS==='true'||process.env.CI==='true'; if(fs.existsSync(repoGit) && !inCi){ console.error('Refusing to publish from codex-cli. Publishing happens via release.yml.'); process.exit(1);} else { console.log(inCi ? 'CI publish detected.' : 'Publishing staged package...'); }\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/just-every/code.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@just-every/code-darwin-arm64": "0.2.175",
|
||||
"@just-every/code-darwin-x64": "0.2.175",
|
||||
"@just-every/code-linux-x64-musl": "0.2.175",
|
||||
"@just-every/code-linux-arm64-musl": "0.2.175",
|
||||
"@just-every/code-win32-x64": "0.2.175"
|
||||
}
|
||||
}
|
||||
@@ -1,766 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Non-functional change to trigger release workflow
|
||||
|
||||
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync, fsyncSync, renameSync, realpathSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { get } from 'https';
|
||||
import { platform, arch, tmpdir } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Map Node.js platform/arch to Rust target triples
|
||||
function getTargetTriple() {
|
||||
const platformMap = {
|
||||
'darwin': 'apple-darwin',
|
||||
'linux': 'unknown-linux-musl', // Default to musl for better compatibility
|
||||
'win32': 'pc-windows-msvc'
|
||||
};
|
||||
|
||||
const archMap = {
|
||||
'x64': 'x86_64',
|
||||
'arm64': 'aarch64'
|
||||
};
|
||||
|
||||
const rustArch = archMap[arch()] || arch();
|
||||
const rustPlatform = platformMap[platform()] || platform();
|
||||
|
||||
return `${rustArch}-${rustPlatform}`;
|
||||
}
|
||||
|
||||
// Resolve a persistent user cache directory for binaries so that repeated
|
||||
// npx installs can reuse a previously downloaded artifact and skip work.
|
||||
function getCacheDir(version) {
|
||||
const plt = platform();
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
let base = '';
|
||||
if (plt === 'win32') {
|
||||
base = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
||||
} else if (plt === 'darwin') {
|
||||
base = join(home, 'Library', 'Caches');
|
||||
} else {
|
||||
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
||||
}
|
||||
const dir = join(base, 'just-every', 'code', version);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
||||
const ext = isWindows ? '.exe' : '';
|
||||
const cacheDir = getCacheDir(version);
|
||||
return join(cacheDir, `code-${targetTriple}${ext}`);
|
||||
}
|
||||
|
||||
const CODE_SHIM_SIGNATURES = [
|
||||
'@just-every/code',
|
||||
'bin/coder.js',
|
||||
'$(dirname "$0")/coder',
|
||||
'%~dp0coder'
|
||||
];
|
||||
|
||||
function shimContentsLookOurs(contents) {
|
||||
return CODE_SHIM_SIGNATURES.some(sig => contents.includes(sig));
|
||||
}
|
||||
|
||||
function looksLikeOurCodeShim(path) {
|
||||
try {
|
||||
const contents = readFileSync(path, 'utf8');
|
||||
return shimContentsLookOurs(contents);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWSL() {
|
||||
if (platform() !== 'linux') return false;
|
||||
try {
|
||||
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
||||
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function isPathOnWindowsFs(p) {
|
||||
try {
|
||||
const mounts = readFileSync('/proc/mounts', 'utf8').split(/\n/).filter(Boolean);
|
||||
let best = { mount: '/', type: 'unknown', len: 1 };
|
||||
for (const line of mounts) {
|
||||
const parts = line.split(' ');
|
||||
if (parts.length < 3) continue;
|
||||
const mnt = parts[1];
|
||||
const typ = parts[2];
|
||||
if (p.startsWith(mnt) && mnt.length > best.len) best = { mount: mnt, type: typ, len: mnt.length };
|
||||
}
|
||||
return best.type === 'drvfs' || best.type === 'cifs';
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
async function writeCacheAtomic(srcPath, cachePath) {
|
||||
try {
|
||||
if (existsSync(cachePath)) {
|
||||
const ok = validateDownloadedBinary(cachePath).ok;
|
||||
if (ok) return;
|
||||
}
|
||||
} catch {}
|
||||
const dir = dirname(cachePath);
|
||||
if (!existsSync(dir)) { try { mkdirSync(dir, { recursive: true }); } catch {} }
|
||||
const tmp = cachePath + '.tmp-' + Math.random().toString(36).slice(2, 8);
|
||||
copyFileSync(srcPath, tmp);
|
||||
try { const fd = openSync(tmp, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {}
|
||||
// Retry with exponential backoff up to ~1.6s total
|
||||
const delays = [100, 200, 400, 800, 1200, 1600];
|
||||
for (let i = 0; i < delays.length; i++) {
|
||||
try {
|
||||
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
||||
renameSync(tmp, cachePath);
|
||||
return;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, delays[i]));
|
||||
}
|
||||
}
|
||||
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
||||
renameSync(tmp, cachePath);
|
||||
}
|
||||
|
||||
function resolveGlobalBinDir() {
|
||||
const plt = platform();
|
||||
const userAgent = process.env.npm_config_user_agent || '';
|
||||
|
||||
const fromPrefix = (prefixPath) => {
|
||||
if (!prefixPath) return '';
|
||||
return plt === 'win32' ? prefixPath : join(prefixPath, 'bin');
|
||||
};
|
||||
|
||||
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || '';
|
||||
const direct = fromPrefix(prefixEnv);
|
||||
if (direct) return direct;
|
||||
|
||||
const tryExec = (command) => {
|
||||
try {
|
||||
return execSync(command, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
shell: true,
|
||||
}).toString().trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
|
||||
if (prefixFromNpm) return prefixFromNpm;
|
||||
|
||||
const binFromNpm = tryExec('npm bin -g');
|
||||
if (binFromNpm) return binFromNpm;
|
||||
|
||||
if (userAgent.includes('pnpm')) {
|
||||
const pnpmBin = tryExec('pnpm bin --global');
|
||||
if (pnpmBin) return pnpmBin;
|
||||
const pnpmPrefix = fromPrefix(tryExec('pnpm env get prefix'));
|
||||
if (pnpmPrefix) return pnpmPrefix;
|
||||
}
|
||||
|
||||
if (userAgent.includes('yarn')) {
|
||||
const yarnBin = tryExec('yarn global bin');
|
||||
if (yarnBin) return yarnBin;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
const doAttempt = () => new Promise((resolve, reject) => {
|
||||
const attempt = (currentUrl, redirectsLeft) => {
|
||||
const req = get(currentUrl, (response) => {
|
||||
const status = response.statusCode || 0;
|
||||
const location = response.headers.location;
|
||||
|
||||
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
||||
if (redirectsLeft <= 0) {
|
||||
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
||||
return;
|
||||
}
|
||||
attempt(location, redirectsLeft - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
|
||||
let bytes = 0;
|
||||
let timer;
|
||||
const timeoutMs = 30000; // 30s inactivity timeout
|
||||
|
||||
const resetTimer = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
req.destroy(new Error('download stalled'));
|
||||
}, timeoutMs);
|
||||
};
|
||||
|
||||
resetTimer();
|
||||
response.on('data', (chunk) => {
|
||||
bytes += chunk.length;
|
||||
resetTimer();
|
||||
});
|
||||
|
||||
const file = createWriteStream(dest);
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
file.close();
|
||||
if (expected && bytes !== expected) {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(new Error(`incomplete download: got ${bytes} of ${expected} bytes`));
|
||||
} else if (bytes === 0) {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(new Error('empty download'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Failed to download: HTTP ${status}`));
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
try { unlinkSync(dest); } catch {}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Absolute request timeout to avoid hanging forever
|
||||
req.setTimeout(120000, () => {
|
||||
req.destroy(new Error('download timed out'));
|
||||
});
|
||||
};
|
||||
|
||||
attempt(url, maxRedirects);
|
||||
});
|
||||
|
||||
let attemptNum = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return await doAttempt();
|
||||
} catch (e) {
|
||||
attemptNum += 1;
|
||||
if (attemptNum > maxRetries) throw e;
|
||||
const backoff = Math.min(2000, 200 * attemptNum);
|
||||
await sleep(backoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateDownloadedBinary(p) {
|
||||
try {
|
||||
const st = statSync(p);
|
||||
if (!st.isFile() || st.size === 0) {
|
||||
return { ok: false, reason: 'empty or not a regular file' };
|
||||
}
|
||||
const fd = openSync(p, 'r');
|
||||
try {
|
||||
const buf = Buffer.alloc(4);
|
||||
const n = readSync(fd, buf, 0, 4, 0);
|
||||
if (n < 2) return { ok: false, reason: 'too short' };
|
||||
const plt = platform();
|
||||
if (plt === 'win32') {
|
||||
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
||||
} else if (plt === 'linux' || plt === 'android') {
|
||||
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
||||
} else if (plt === 'darwin') {
|
||||
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
||||
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
||||
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
||||
}
|
||||
return { ok: true };
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
} catch (e) {
|
||||
return { ok: false, reason: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
|
||||
// Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
|
||||
const ua = process.env.npm_config_user_agent || '';
|
||||
const isNpx = ua.includes('npx');
|
||||
const isGlobal = process.env.npm_config_global === 'true';
|
||||
if (isGlobal && !isNpx) {
|
||||
try {
|
||||
const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
|
||||
const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
|
||||
if (resolved) {
|
||||
let contents = '';
|
||||
try {
|
||||
contents = readFileSync(resolved, 'utf8');
|
||||
} catch {
|
||||
contents = '';
|
||||
}
|
||||
const looksLikeOurs = shimContentsLookOurs(contents);
|
||||
if (!looksLikeOurs) {
|
||||
console.warn('[notice] Found an existing `code` on PATH at:');
|
||||
console.warn(` ${resolved}`);
|
||||
console.warn('[notice] We will still install our CLI, also available as `coder`.');
|
||||
console.warn(' If `code` runs another tool, prefer using: coder');
|
||||
console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore detection failures; proceed with install.
|
||||
}
|
||||
}
|
||||
|
||||
const targetTriple = getTargetTriple();
|
||||
const isWindows = platform() === 'win32';
|
||||
const binaryExt = isWindows ? '.exe' : '';
|
||||
|
||||
const binDir = join(__dirname, 'bin');
|
||||
if (!existsSync(binDir)) {
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get package version - use readFileSync for compatibility
|
||||
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
||||
const version = packageJson.version;
|
||||
|
||||
// Download only the primary binary; we'll create wrappers for legacy names.
|
||||
const binaries = ['code'];
|
||||
|
||||
console.log(`Installing @just-every/code v${version} for ${targetTriple}...`);
|
||||
|
||||
for (const binary of binaries) {
|
||||
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
||||
const localPath = join(binDir, binaryName);
|
||||
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
||||
|
||||
// On Windows we avoid placing the executable inside node_modules to prevent
|
||||
// EBUSY/EPERM during global upgrades when the binary is in use.
|
||||
// We treat the user cache path as the canonical home of the native binary.
|
||||
// For macOS/Linux we keep previous behavior and also place a copy in binDir
|
||||
// for convenience.
|
||||
|
||||
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
|
||||
try {
|
||||
if (existsSync(cachePath)) {
|
||||
const valid = validateDownloadedBinary(cachePath);
|
||||
if (valid.ok) {
|
||||
// Avoid mirroring into node_modules on Windows or WSL-on-NTFS.
|
||||
const wsl = isWSL();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
||||
if (mirrorToLocal) {
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
}
|
||||
console.log(`✓ ${binaryName} ready from user cache`);
|
||||
continue; // next binary
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore cache errors and fall through to normal paths
|
||||
}
|
||||
|
||||
// First try platform package via npm optionalDependencies (fast path on npm CDN).
|
||||
const require = createRequire(import.meta.url);
|
||||
const platformPkg = (() => {
|
||||
const name = (() => {
|
||||
if (isWindows) return '@just-every/code-win32-x64';
|
||||
const plt = platform();
|
||||
const cpu = arch();
|
||||
if (plt === 'darwin' && cpu === 'arm64') return '@just-every/code-darwin-arm64';
|
||||
if (plt === 'darwin' && cpu === 'x64') return '@just-every/code-darwin-x64';
|
||||
if (plt === 'linux' && cpu === 'x64') return '@just-every/code-linux-x64-musl';
|
||||
if (plt === 'linux' && cpu === 'arm64') return '@just-every/code-linux-arm64-musl';
|
||||
return null;
|
||||
})();
|
||||
if (!name) return null;
|
||||
try {
|
||||
const pkgJsonPath = require.resolve(`${name}/package.json`);
|
||||
const pkgDir = dirname(pkgJsonPath);
|
||||
return { name, dir: pkgDir };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (platformPkg) {
|
||||
try {
|
||||
// Expect binary inside platform package bin directory
|
||||
const src = join(platformPkg.dir, 'bin', binaryName);
|
||||
if (!existsSync(src)) {
|
||||
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
||||
}
|
||||
// Populate cache first (canonical location) atomically
|
||||
await writeCacheAtomic(src, cachePath);
|
||||
// Mirror into local bin only on Unix-like filesystems (not Windows/WSL-on-NTFS)
|
||||
const wsl = isWSL();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
||||
if (mirrorToLocal) {
|
||||
copyFileSync(cachePath, localPath);
|
||||
try { chmodSync(localPath, 0o755); } catch {}
|
||||
}
|
||||
console.log(`✓ Installed ${binaryName} from ${platformPkg.name} (cached)`);
|
||||
continue; // next binary
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
||||
}
|
||||
}
|
||||
|
||||
// Decide archive format per OS with fallback on macOS/Linux:
|
||||
// - Windows: .zip
|
||||
// - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
|
||||
const isWin = isWindows;
|
||||
const isWSL = (() => {
|
||||
if (platform() !== 'linux') return false;
|
||||
try {
|
||||
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
||||
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch { return false; }
|
||||
})();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWin || (isWSL && isPathOnWindowsFs(binDirReal)));
|
||||
let useZst = false;
|
||||
if (!isWin) {
|
||||
try {
|
||||
execSync('zstd --version', { stdio: 'ignore', shell: true });
|
||||
useZst = true;
|
||||
} catch {
|
||||
useZst = false;
|
||||
}
|
||||
}
|
||||
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
||||
const downloadUrl = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
|
||||
|
||||
console.log(`Downloading ${archiveName}...`);
|
||||
try {
|
||||
const needsIsolation = isWin || (!isWin && !mirrorToLocal); // Windows or WSL-on-NTFS
|
||||
let safeTempDir = needsIsolation ? join(tmpdir(), 'just-every', 'code', version) : binDir;
|
||||
// Ensure staging dir exists; if tmp fails (permissions/space), fall back to user cache.
|
||||
if (needsIsolation) {
|
||||
try {
|
||||
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
||||
} catch {
|
||||
try {
|
||||
safeTempDir = getCacheDir(version);
|
||||
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
|
||||
await downloadBinary(downloadUrl, tmpPath);
|
||||
|
||||
if (isWin) {
|
||||
// Unzip to a temp directory, then move into the per-user cache.
|
||||
const unzipDest = safeTempDir;
|
||||
try {
|
||||
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
||||
const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
||||
const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
|
||||
let ok = false;
|
||||
// Attempt full-path powershell.exe
|
||||
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
||||
// Fallback to powershell in PATH
|
||||
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
// Fallback to pwsh (PowerShell 7)
|
||||
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
||||
// Final fallback: bsdtar can extract .zip
|
||||
if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
|
||||
} catch (e) {
|
||||
throw new Error(`failed to unzip archive: ${e.message}`);
|
||||
} finally {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
}
|
||||
// Move the extracted file from temp to cache; do not leave a copy in node_modules
|
||||
try {
|
||||
const extractedPath = join(unzipDest, binaryName);
|
||||
await writeCacheAtomic(extractedPath, cachePath);
|
||||
try { unlinkSync(extractedPath); } catch {}
|
||||
} catch (e) {
|
||||
throw new Error(`failed to move binary to cache: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
if (useZst) {
|
||||
// Decompress .zst via system zstd
|
||||
try {
|
||||
const outPath = mirrorToLocal ? localPath : join(safeTempDir, binaryName);
|
||||
execSync(`zstd -d '${tmpPath}' -o '${outPath}'`, { stdio: 'ignore', shell: true });
|
||||
} catch (e) {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
|
||||
}
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
} else {
|
||||
// Extract .tar.gz using system tar
|
||||
try {
|
||||
const dest = mirrorToLocal ? binDir : safeTempDir;
|
||||
execSync(`tar -xzf '${tmpPath}' -C '${dest}'`, { stdio: 'ignore', shell: true });
|
||||
} catch (e) {
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
||||
}
|
||||
try { unlinkSync(tmpPath); } catch {}
|
||||
}
|
||||
if (!mirrorToLocal) {
|
||||
try {
|
||||
const extractedPath = join(safeTempDir, binaryName);
|
||||
await writeCacheAtomic(extractedPath, cachePath);
|
||||
try { unlinkSync(extractedPath); } catch {}
|
||||
} catch (e) {
|
||||
throw new Error(`failed to move binary to cache: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
||||
|
||||
const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
|
||||
if (!valid.ok) {
|
||||
try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
|
||||
throw new Error(`invalid binary (${valid.reason})`);
|
||||
}
|
||||
|
||||
// Make executable on Unix-like systems
|
||||
if (!isWin && mirrorToLocal) {
|
||||
chmodSync(localPath, 0o755);
|
||||
}
|
||||
|
||||
console.log(`✓ Installed ${binaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
|
||||
// Ensure persistent cache holds the binary (already true for Windows path)
|
||||
if (!isWin && mirrorToLocal) {
|
||||
try { await writeCacheAtomic(localPath, cachePath); } catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
|
||||
console.error(` Downloaded from: ${downloadUrl}`);
|
||||
// Continue with other binaries even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Create platform-specific symlink/copy for main binary
|
||||
const mainBinary = `code-${targetTriple}${binaryExt}`;
|
||||
const mainBinaryPath = join(binDir, mainBinary);
|
||||
|
||||
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
||||
try {
|
||||
const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
|
||||
const stats = statSync(probePath);
|
||||
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
||||
const valid = validateDownloadedBinary(probePath);
|
||||
if (!valid.ok) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${valid.reason}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${e.message}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
console.log('Setting up main code binary...');
|
||||
|
||||
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
||||
// On Unix, the JS wrapper will find the correct binary
|
||||
console.log('✓ Installation complete!');
|
||||
} else {
|
||||
console.warn('⚠ Main code binary not found. You may need to build from source.');
|
||||
}
|
||||
|
||||
// Handle collisions (e.g., VS Code) and add wrappers. We no longer publish a
|
||||
// `code` bin in package.json. Instead, for global installs we create a `code`
|
||||
// wrapper only when there is no conflicting `code` earlier on PATH. This avoids
|
||||
// hijacking the VS Code CLI while still giving users a friendly name when safe.
|
||||
// For upgrades from older versions that published a `code` bin, we also remove
|
||||
// our old shim if a conflict is detected.
|
||||
if (isGlobal && !isNpx) try {
|
||||
const isTTY = process.stdout && process.stdout.isTTY;
|
||||
const isWindows = platform() === 'win32';
|
||||
const ua = process.env.npm_config_user_agent || '';
|
||||
const isBun = ua.includes('bun') || !!process.env.BUN_INSTALL;
|
||||
|
||||
const installedCmds = new Set(['coder']); // global install always exposes coder via package manager
|
||||
const skippedCmds = [];
|
||||
|
||||
// Helper to resolve all 'code' on PATH
|
||||
const resolveAllOnPath = () => {
|
||||
try {
|
||||
if (isWindows) {
|
||||
const out = execSync('where code', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
let out = '';
|
||||
try {
|
||||
out = execSync('bash -lc "which -a code 2>/dev/null"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
} catch {
|
||||
try {
|
||||
out = execSync('command -v code || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
||||
} catch { out = ''; }
|
||||
}
|
||||
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
if (isBun) {
|
||||
// Bun creates shims for every bin; if another 'code' exists elsewhere on PATH, remove Bun's shim
|
||||
let bunBin = '';
|
||||
try {
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const bunBase = process.env.BUN_INSTALL || join(home, '.bun');
|
||||
bunBin = join(bunBase, 'bin');
|
||||
} catch {}
|
||||
|
||||
const bunShim = join(bunBin || '', isWindows ? 'code.cmd' : 'code');
|
||||
const candidates = resolveAllOnPath();
|
||||
const other = candidates.find(p => p && (!bunBin || !p.startsWith(bunBin)));
|
||||
if (other && existsSync(bunShim)) {
|
||||
try {
|
||||
unlinkSync(bunShim);
|
||||
console.log(`✓ Skipped global 'code' shim under Bun (existing: ${other})`);
|
||||
skippedCmds.push({ name: 'code', reason: `existing: ${other}` });
|
||||
} catch (e) {
|
||||
console.log(`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`);
|
||||
}
|
||||
} else if (!other) {
|
||||
// No conflict: create a wrapper that forwards to `coder`
|
||||
try {
|
||||
const wrapperPath = bunShim;
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"%~dp0coder" %*\r\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
chmodSync(wrapperPath, 0o755);
|
||||
}
|
||||
console.log("✓ Created 'code' wrapper -> coder (bun)");
|
||||
installedCmds.add('code');
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create 'code' wrapper (bun): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary for Bun
|
||||
const list = Array.from(installedCmds).sort().join(', ');
|
||||
console.log(`Commands installed (bun): ${list}`);
|
||||
if (skippedCmds.length) {
|
||||
for (const s of skippedCmds) console.error(`Commands skipped: ${s.name} (${s.reason})`);
|
||||
console.error('→ Use `coder` to run this tool.');
|
||||
}
|
||||
// Final friendly usage hint
|
||||
if (installedCmds.has('code')) {
|
||||
console.log("Use 'code' to launch Code.");
|
||||
} else {
|
||||
console.log("Use 'coder' to launch Code.");
|
||||
}
|
||||
} else {
|
||||
// npm/pnpm/yarn path
|
||||
const globalBin = resolveGlobalBinDir();
|
||||
const ourShim = globalBin ? join(globalBin, isWindows ? 'code.cmd' : 'code') : '';
|
||||
const candidates = resolveAllOnPath();
|
||||
const others = candidates.filter(p => p && (!ourShim || p !== ourShim));
|
||||
const ourShimExists = ourShim && existsSync(ourShim);
|
||||
const shimLooksOurs = ourShimExists && looksLikeOurCodeShim(ourShim);
|
||||
const conflictPaths = [
|
||||
...others,
|
||||
...(ourShimExists && !shimLooksOurs ? [ourShim] : []),
|
||||
];
|
||||
const collision = conflictPaths.length > 0;
|
||||
|
||||
const ensureWrapper = (name, args) => {
|
||||
if (!globalBin) return;
|
||||
try {
|
||||
const wrapperPath = join(globalBin, isWindows ? `${name}.cmd` : name);
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"%~dp0${collision ? 'coder' : 'code'}" ${args} %*\r\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/${collision ? 'coder' : 'code'}" ${args} "$@"\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
chmodSync(wrapperPath, 0o755);
|
||||
}
|
||||
console.log(`✓ Created wrapper '${name}' -> ${collision ? 'coder' : 'code'} ${args}`);
|
||||
installedCmds.add(name);
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Always create legacy wrappers so existing scripts keep working
|
||||
ensureWrapper('code-tui', '');
|
||||
ensureWrapper('code-exec', 'exec');
|
||||
|
||||
if (collision) {
|
||||
console.error('⚠ Detected existing `code` on PATH:');
|
||||
for (const p of conflictPaths) console.error(` - ${p}`);
|
||||
if (globalBin) {
|
||||
try {
|
||||
if (ourShimExists) {
|
||||
if (shimLooksOurs && others.length > 0) {
|
||||
unlinkSync(ourShim);
|
||||
console.error(`✓ Removed global 'code' shim (ours) at ${ourShim}`);
|
||||
const reason = others[0] || ourShim;
|
||||
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
||||
} else if (!shimLooksOurs) {
|
||||
console.error(`✓ Skipped global 'code' shim (different CLI at ${ourShim})`);
|
||||
const reason = conflictPaths[0] || ourShim;
|
||||
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
||||
}
|
||||
} else {
|
||||
const reason = conflictPaths[0] || 'another command on PATH';
|
||||
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
|
||||
}
|
||||
console.error('→ Use `coder` to run this tool.');
|
||||
} else {
|
||||
console.log('Note: could not determine npm global bin; skipping alias creation.');
|
||||
}
|
||||
} else {
|
||||
// No collision; ensure a 'code' wrapper exists forwarding to 'coder'
|
||||
if (globalBin) {
|
||||
try {
|
||||
const content = isWindows
|
||||
? `@echo off\r\n"%~dp0coder" %*\r\n`
|
||||
: `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
||||
writeFileSync(ourShim, content);
|
||||
if (!isWindows) chmodSync(ourShim, 0o755);
|
||||
console.log("✓ Created 'code' wrapper -> coder");
|
||||
installedCmds.add('code');
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary for npm/pnpm/yarn
|
||||
const list = Array.from(installedCmds).sort().join(', ');
|
||||
console.log(`Commands installed: ${list}`);
|
||||
if (skippedCmds.length) {
|
||||
for (const s of skippedCmds) console.log(`Commands skipped: ${s.name} (${s.reason})`);
|
||||
}
|
||||
// Final friendly usage hint
|
||||
if (installedCmds.has('code')) {
|
||||
console.log("Use 'code' to launch Code.");
|
||||
} else {
|
||||
console.log("Use 'coder' to launch Code.");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Installation failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Windows-friendly preinstall: proactively free file locks from prior installs
|
||||
// so npm/yarn/pnpm can stage the new package. No-ops on non-Windows.
|
||||
|
||||
import { platform } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, readdirSync, rmSync, readFileSync, statSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
function isWSL() {
|
||||
if (platform() !== 'linux') return false;
|
||||
try {
|
||||
const rel = readFileSync('/proc/version', 'utf8').toLowerCase();
|
||||
return rel.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
const isWin = platform() === 'win32';
|
||||
const wsl = isWSL();
|
||||
const isWinLike = isWin || wsl;
|
||||
|
||||
// Scope: only run for global installs, unless explicitly forced. Allow opt-out.
|
||||
const isGlobal = process.env.npm_config_global === 'true';
|
||||
const force = process.env.CODE_FORCE_PREINSTALL === '1';
|
||||
const skip = process.env.CODE_SKIP_PREINSTALL === '1';
|
||||
if (!isWinLike || skip || (!isGlobal && !force)) process.exit(0);
|
||||
|
||||
function tryExec(cmd, opts = {}) {
|
||||
try { execSync(cmd, { stdio: ['ignore', 'ignore', 'ignore'], shell: true, ...opts }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 1) Stop our native binary if it is holding locks. Avoid killing unrelated tools.
|
||||
// Only available on native Windows; skip entirely on WSL to avoid noise.
|
||||
if (isWin) {
|
||||
tryExec('taskkill /IM code-x86_64-pc-windows-msvc.exe /F');
|
||||
}
|
||||
|
||||
// 2) Remove stale staging dirs from previous failed installs under the global
|
||||
// @just-every scope, which npm will reuse (e.g., .code-XXXXX). Remove only
|
||||
// old entries and never the current staging or live package.
|
||||
try {
|
||||
let scopeDir = '';
|
||||
try {
|
||||
const root = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], shell: true }).toString().trim();
|
||||
scopeDir = path.join(root, '@just-every');
|
||||
} catch {
|
||||
// Fall back to guessing from this script location: <staging>\..\..\
|
||||
const here = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
||||
scopeDir = path.resolve(here, '..');
|
||||
}
|
||||
if (existsSync(scopeDir)) {
|
||||
const now = Date.now();
|
||||
const maxAgeMs = 2 * 60 * 60 * 1000; // 2 hours
|
||||
const currentDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
for (const name of readdirSync(scopeDir)) {
|
||||
if (!name.startsWith('.code-')) continue;
|
||||
const p = path.join(scopeDir, name);
|
||||
if (path.resolve(p) === currentDir) continue; // never remove our current dir
|
||||
try {
|
||||
const st = statSync(p);
|
||||
const age = now - st.mtimeMs;
|
||||
if (age > maxAgeMs) rmSync(p, { recursive: true, force: true });
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
process.exit(0);
|
||||
@@ -1,32 +0,0 @@
|
||||
<#
|
||||
Helper to recover from EBUSY/EPERM during global npm upgrades on Windows.
|
||||
Closes running processes and removes stale package folders.
|
||||
|
||||
Usage (PowerShell):
|
||||
Set-ExecutionPolicy -Scope Process Bypass -Force
|
||||
./codex-cli/scripts/windows-cleanup.ps1
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
Write-Host "Stopping running Code/Coder processes..."
|
||||
taskkill /IM code-x86_64-pc-windows-msvc.exe /F 2>$null | Out-Null
|
||||
taskkill /IM code.exe /F 2>$null | Out-Null
|
||||
taskkill /IM coder.exe /F 2>$null | Out-Null
|
||||
|
||||
Write-Host "Removing old global package (if present)..."
|
||||
$npmRoot = (& npm root -g).Trim()
|
||||
$pkgPath = Join-Path $npmRoot "@just-every\code"
|
||||
if (Test-Path $pkgPath) {
|
||||
try { Remove-Item -LiteralPath $pkgPath -Recurse -Force -ErrorAction Stop } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Removing temp staging directories (if present)..."
|
||||
Get-ChildItem -LiteralPath (Join-Path $npmRoot "@just-every") -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like '.code-*' } |
|
||||
ForEach-Object {
|
||||
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Cleanup complete. You can now run: npm install -g @just-every/code@latest"
|
||||
|
||||
+27
-27
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Unified entry point for the Code CLI (fork of OpenAI Codex).
|
||||
// Unified entry point for the Code CLI.
|
||||
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -14,7 +14,7 @@ const __dirname = path.dirname(__filename);
|
||||
const { platform, arch } = process;
|
||||
|
||||
// Important: Never delegate to another system's `code` binary (e.g., VS Code).
|
||||
// When users run via `npx @just-every/code`, we must always execute our
|
||||
// When users run via `npx @hanzo/dev`, we must always execute our
|
||||
// packaged native binary by absolute path to avoid PATH collisions.
|
||||
|
||||
const isWSL = () => {
|
||||
@@ -73,8 +73,8 @@ if (!targetTriple) {
|
||||
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
||||
}
|
||||
|
||||
// Prefer new 'code-*' binary names; fall back to legacy 'coder-*' if missing.
|
||||
let binaryPath = path.join(__dirname, "..", "bin", `code-${targetTriple}`);
|
||||
// Prefer new 'dev-*' binary names; fall back to legacy 'coder-*' if missing.
|
||||
let binaryPath = path.join(__dirname, "..", "bin", `dev-${targetTriple}`);
|
||||
let legacyBinaryPath = path.join(__dirname, "..", "bin", `coder-${targetTriple}`);
|
||||
|
||||
// --- Bootstrap helper (runs if the binary is missing, e.g. Bun blocked postinstall) ---
|
||||
@@ -120,7 +120,7 @@ const getCacheDir = (version) => {
|
||||
} else {
|
||||
base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
|
||||
}
|
||||
const dir = path.join(base, "just-every", "code", version);
|
||||
const dir = path.join(base, "hanzoai", "dev", version);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
};
|
||||
@@ -129,7 +129,7 @@ const getCachedBinaryPath = (version) => {
|
||||
// targetTriple already includes the proper extension on Windows ("...msvc.exe").
|
||||
// Do not append another suffix; just use the exact targetTriple-derived name.
|
||||
const cacheDir = getCacheDir(version);
|
||||
return path.join(cacheDir, `code-${targetTriple}`);
|
||||
return path.join(cacheDir, `dev-${targetTriple}`);
|
||||
};
|
||||
|
||||
let lastBootstrapError = null;
|
||||
@@ -188,20 +188,20 @@ const tryBootstrapBinary = async () => {
|
||||
try {
|
||||
const req = (await import("module")).createRequire(import.meta.url);
|
||||
const name = (() => {
|
||||
if (platform === "win32") return "@just-every/code-win32-x64"; // may be unpublished; falls through
|
||||
if (platform === "win32") return "@hanzo/dev-win32-x64"; // may be unpublished; falls through
|
||||
const plt = nodePlatform();
|
||||
const cpu = nodeArch();
|
||||
if (plt === "darwin" && cpu === "arm64") return "@just-every/code-darwin-arm64";
|
||||
if (plt === "darwin" && cpu === "x64") return "@just-every/code-darwin-x64";
|
||||
if (plt === "linux" && cpu === "x64") return "@just-every/code-linux-x64-musl";
|
||||
if (plt === "linux" && cpu === "arm64") return "@just-every/code-linux-arm64-musl";
|
||||
if (plt === "darwin" && cpu === "arm64") return "@hanzo/dev-darwin-arm64";
|
||||
if (plt === "darwin" && cpu === "x64") return "@hanzo/dev-darwin-x64";
|
||||
if (plt === "linux" && cpu === "x64") return "@hanzo/dev-linux-x64-musl";
|
||||
if (plt === "linux" && cpu === "arm64") return "@hanzo/dev-linux-arm64-musl";
|
||||
return null;
|
||||
})();
|
||||
if (name) {
|
||||
try {
|
||||
const pkgJson = req.resolve(`${name}/package.json`);
|
||||
const pkgDir = path.dirname(pkgJson);
|
||||
const src = path.join(pkgDir, "bin", `code-${targetTriple}`);
|
||||
const src = path.join(pkgDir, "bin", `dev-${targetTriple}`);
|
||||
if (existsSync(src)) {
|
||||
// Always ensure cache has the binary; on Unix mirror into node_modules
|
||||
copyFileSync(src, cachePath);
|
||||
@@ -218,9 +218,9 @@ const tryBootstrapBinary = async () => {
|
||||
// 4) Download from GitHub release
|
||||
const isWin = platform === "win32";
|
||||
const archiveName = isWin
|
||||
? `code-${targetTriple}.zip`
|
||||
: (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `code-${targetTriple}.zst`; } catch { return `code-${targetTriple}.tar.gz`; } })();
|
||||
const url = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
|
||||
? `dev-${targetTriple}.zip`
|
||||
: (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `dev-${targetTriple}.zst`; } catch { return `dev-${targetTriple}.tar.gz`; } })();
|
||||
const url = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
||||
const tmp = path.join(binDir, `.${archiveName}.part`);
|
||||
return httpsDownload(url, tmp)
|
||||
.then(() => {
|
||||
@@ -314,11 +314,11 @@ if (existsSync(binaryPath)) {
|
||||
console.error(`Bootstrap error: ${msg}`);
|
||||
}
|
||||
console.error(`Please try reinstalling the package:`);
|
||||
console.error(` npm uninstall -g @just-every/code`);
|
||||
console.error(` npm install -g @just-every/code`);
|
||||
console.error(` npm uninstall -g @hanzo/dev`);
|
||||
console.error(` npm install -g @hanzo/dev`);
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Install inside WSL (Ubuntu) separately:");
|
||||
console.error(" npx -y @just-every/code@latest (run inside WSL)");
|
||||
console.error(" npx -y @hanzo/dev@latest (run inside WSL)");
|
||||
console.error("If installed globally on Windows, those binaries are not usable from WSL.");
|
||||
}
|
||||
process.exit(1);
|
||||
@@ -332,15 +332,15 @@ if (!validation.ok) {
|
||||
console.error(`The native binary at ${binaryPath} appears invalid: ${validation.reason}`);
|
||||
console.error("This can happen if the download failed or was modified by antivirus/proxy.");
|
||||
console.error("Please try reinstalling:");
|
||||
console.error(" npm uninstall -g @just-every/code");
|
||||
console.error(" npm install -g @just-every/code");
|
||||
console.error(" npm uninstall -g @hanzo/dev");
|
||||
console.error(" npm install -g @hanzo/dev");
|
||||
if (platform === "win32") {
|
||||
console.error("If the issue persists, clear npm cache and disable antivirus temporarily:");
|
||||
console.error(" npm cache clean --force");
|
||||
}
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Ensure you install/run inside WSL, not Windows:");
|
||||
console.error(" npx -y @just-every/code@latest (inside WSL)");
|
||||
console.error(" npx -y @hanzo/dev@latest (inside WSL)");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -359,15 +359,15 @@ try {
|
||||
cmd,
|
||||
], { encoding: "utf8" });
|
||||
const line = (out.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
||||
if (line && !line.includes("@just-every/code")) {
|
||||
if (line && !line.includes("@hanzo/dev")) {
|
||||
otherCode = line;
|
||||
}
|
||||
} catch {}
|
||||
if (otherCode) {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
|
||||
console.error(`Note: a different 'code' exists at ${otherCode}; not delegating.`);
|
||||
} else {
|
||||
console.error(`@just-every/code: running bundled binary -> ${binaryPath}`);
|
||||
console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -390,18 +390,18 @@ child.on("error", (err) => {
|
||||
if (code === 'EACCES') {
|
||||
console.error(`Permission denied: ${binaryPath}`);
|
||||
console.error(`Try running: chmod +x "${binaryPath}"`);
|
||||
console.error(`Or reinstall the package with: npm install -g @just-every/code`);
|
||||
console.error(`Or reinstall the package with: npm install -g @hanzo/dev`);
|
||||
} else if (code === 'EFTYPE' || code === 'ENOEXEC') {
|
||||
console.error(`Failed to execute native binary: ${binaryPath}`);
|
||||
console.error("The file may be corrupt or of the wrong type. Reinstall usually fixes this:");
|
||||
console.error(" npm uninstall -g @just-every/code && npm install -g @just-every/code");
|
||||
console.error(" npm uninstall -g @hanzo/dev && npm install -g @hanzo/dev");
|
||||
if (platform === 'win32') {
|
||||
console.error("On Windows, ensure the .exe downloaded correctly (proxy/AV can interfere).");
|
||||
console.error("Try clearing cache: npm cache clean --force");
|
||||
}
|
||||
if (isWSL()) {
|
||||
console.error("Detected WSL. Windows binaries cannot be executed from WSL.");
|
||||
console.error("Install inside WSL and run there: npx -y @just-every/code@latest");
|
||||
console.error("Install inside WSL and run there: npx -y @hanzo/dev@latest");
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@just-every/code",
|
||||
"name": "@hanzo/dev",
|
||||
"version": "0.2.188",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex",
|
||||
"description": "Local AI coding agent for your terminal, powered by Hanzo AI",
|
||||
"bin": {
|
||||
"coder": "bin/coder.js"
|
||||
"dev": "bin/coder.js"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/just-every/code.git"
|
||||
"url": "git+https://github.com/hanzoai/dev.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -35,10 +35,10 @@
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@just-every/code-darwin-arm64": "0.2.188",
|
||||
"@just-every/code-darwin-x64": "0.2.188",
|
||||
"@just-every/code-linux-x64-musl": "0.2.188",
|
||||
"@just-every/code-linux-arm64-musl": "0.2.188",
|
||||
"@just-every/code-win32-x64": "0.2.188"
|
||||
"@hanzo/dev-darwin-arm64": "0.2.188",
|
||||
"@hanzo/dev-darwin-x64": "0.2.188",
|
||||
"@hanzo/dev-linux-x64-musl": "0.2.188",
|
||||
"@hanzo/dev-linux-arm64-musl": "0.2.188",
|
||||
"@hanzo/dev-win32-x64": "0.2.188"
|
||||
}
|
||||
}
|
||||
|
||||
+54
-59
@@ -43,7 +43,7 @@ function getCacheDir(version) {
|
||||
} else {
|
||||
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
||||
}
|
||||
const dir = join(base, 'just-every', 'code', version);
|
||||
const dir = join(base, 'hanzoai', 'dev', version);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
@@ -51,21 +51,22 @@ function getCacheDir(version) {
|
||||
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
||||
const ext = isWindows ? '.exe' : '';
|
||||
const cacheDir = getCacheDir(version);
|
||||
return join(cacheDir, `code-${targetTriple}${ext}`);
|
||||
return join(cacheDir, `dev-${targetTriple}${ext}`);
|
||||
}
|
||||
|
||||
const CODE_SHIM_SIGNATURES = [
|
||||
'@just-every/code',
|
||||
'bin/coder.js',
|
||||
'$(dirname "$0")/coder',
|
||||
'%~dp0coder'
|
||||
// Substrings that appear only in shims we wrote ourselves. Used before removing
|
||||
// or replacing a `code` on PATH, so a third party's `code` is never touched.
|
||||
const SHIM_SIGNATURES = [
|
||||
'@hanzo/dev',
|
||||
'$(dirname "$0")/dev',
|
||||
'%~dp0dev'
|
||||
];
|
||||
|
||||
function shimContentsLookOurs(contents) {
|
||||
return CODE_SHIM_SIGNATURES.some(sig => contents.includes(sig));
|
||||
return SHIM_SIGNATURES.some(sig => contents.includes(sig));
|
||||
}
|
||||
|
||||
function looksLikeOurCodeShim(path) {
|
||||
function looksLikeOurShim(path) {
|
||||
try {
|
||||
const contents = readFileSync(path, 'utf8');
|
||||
return shimContentsLookOurs(contents);
|
||||
@@ -309,9 +310,9 @@ async function main() {
|
||||
if (!looksLikeOurs) {
|
||||
console.warn('[notice] Found an existing `code` on PATH at:');
|
||||
console.warn(` ${resolved}`);
|
||||
console.warn('[notice] We will still install our CLI, also available as `coder`.');
|
||||
console.warn(' If `code` runs another tool, prefer using: coder');
|
||||
console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
|
||||
console.warn('[notice] We will still install our CLI, also available as `dev`.');
|
||||
console.warn(' If `code` runs another tool, prefer using: dev');
|
||||
console.warn(' Or run our CLI explicitly via: npx -y @hanzo/dev');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -333,9 +334,9 @@ async function main() {
|
||||
const version = packageJson.version;
|
||||
|
||||
// Download only the primary binary; we'll create wrappers for legacy names.
|
||||
const binaries = ['code'];
|
||||
|
||||
console.log(`Installing @just-every/code v${version} for ${targetTriple}...`);
|
||||
const binaries = ['dev'];
|
||||
|
||||
console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
|
||||
|
||||
for (const binary of binaries) {
|
||||
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
||||
@@ -373,13 +374,13 @@ async function main() {
|
||||
const require = createRequire(import.meta.url);
|
||||
const platformPkg = (() => {
|
||||
const name = (() => {
|
||||
if (isWindows) return '@just-every/code-win32-x64';
|
||||
if (isWindows) return '@hanzo/dev-win32-x64';
|
||||
const plt = platform();
|
||||
const cpu = arch();
|
||||
if (plt === 'darwin' && cpu === 'arm64') return '@just-every/code-darwin-arm64';
|
||||
if (plt === 'darwin' && cpu === 'x64') return '@just-every/code-darwin-x64';
|
||||
if (plt === 'linux' && cpu === 'x64') return '@just-every/code-linux-x64-musl';
|
||||
if (plt === 'linux' && cpu === 'arm64') return '@just-every/code-linux-arm64-musl';
|
||||
if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
|
||||
if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
|
||||
if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
|
||||
if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
|
||||
return null;
|
||||
})();
|
||||
if (!name) return null;
|
||||
@@ -420,15 +421,8 @@ async function main() {
|
||||
// - Windows: .zip
|
||||
// - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
|
||||
const isWin = isWindows;
|
||||
const isWSL = (() => {
|
||||
if (platform() !== 'linux') return false;
|
||||
try {
|
||||
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
||||
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
||||
} catch { return false; }
|
||||
})();
|
||||
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
||||
const mirrorToLocal = !(isWin || (isWSL && isPathOnWindowsFs(binDirReal)));
|
||||
const mirrorToLocal = !(isWin || (isWSL() && isPathOnWindowsFs(binDirReal)));
|
||||
let useZst = false;
|
||||
if (!isWin) {
|
||||
try {
|
||||
@@ -439,12 +433,12 @@ async function main() {
|
||||
}
|
||||
}
|
||||
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
||||
const downloadUrl = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
|
||||
const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
||||
|
||||
console.log(`Downloading ${archiveName}...`);
|
||||
try {
|
||||
const needsIsolation = isWin || (!isWin && !mirrorToLocal); // Windows or WSL-on-NTFS
|
||||
let safeTempDir = needsIsolation ? join(tmpdir(), 'just-every', 'code', version) : binDir;
|
||||
let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzoai', 'dev', version) : binDir;
|
||||
// Ensure staging dir exists; if tmp fails (permissions/space), fall back to user cache.
|
||||
if (needsIsolation) {
|
||||
try {
|
||||
@@ -547,7 +541,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Create platform-specific symlink/copy for main binary
|
||||
const mainBinary = `code-${targetTriple}${binaryExt}`;
|
||||
const mainBinary = `dev-${targetTriple}${binaryExt}`;
|
||||
const mainBinaryPath = join(binDir, mainBinary);
|
||||
|
||||
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
||||
@@ -557,26 +551,25 @@ async function main() {
|
||||
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
||||
const valid = validateDownloadedBinary(probePath);
|
||||
if (!valid.ok) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${valid.reason}`);
|
||||
console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠ Main code binary appears invalid: ${e.message}`);
|
||||
console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
|
||||
console.warn(' Try reinstalling or check your network/proxy settings.');
|
||||
}
|
||||
console.log('Setting up main code binary...');
|
||||
console.log('Setting up main dev binary...');
|
||||
|
||||
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
||||
// On Unix, the JS wrapper will find the correct binary
|
||||
console.log('✓ Installation complete!');
|
||||
} else {
|
||||
console.warn('⚠ Main code binary not found. You may need to build from source.');
|
||||
console.warn('⚠ Main dev binary not found. You may need to build from source.');
|
||||
}
|
||||
|
||||
// Handle collisions (e.g., VS Code) and add wrappers. We no longer publish a
|
||||
// `code` bin in package.json. Instead, for global installs we create a `code`
|
||||
// wrapper only when there is no conflicting `code` earlier on PATH. This avoids
|
||||
// hijacking the VS Code CLI while still giving users a friendly name when safe.
|
||||
// `dev` is the only bin in package.json, so the package manager always creates
|
||||
// it and every wrapper below forwards to it. `code` is an alias we create only
|
||||
// when nothing else owns that name on PATH, so we never hijack another CLI.
|
||||
// For upgrades from older versions that published a `code` bin, we also remove
|
||||
// our old shim if a conflict is detected.
|
||||
if (isGlobal && !isNpx) try {
|
||||
@@ -585,7 +578,7 @@ async function main() {
|
||||
const ua = process.env.npm_config_user_agent || '';
|
||||
const isBun = ua.includes('bun') || !!process.env.BUN_INSTALL;
|
||||
|
||||
const installedCmds = new Set(['coder']); // global install always exposes coder via package manager
|
||||
const installedCmds = new Set(['dev']); // global install always exposes dev via package manager
|
||||
const skippedCmds = [];
|
||||
|
||||
// Helper to resolve all 'code' on PATH
|
||||
@@ -630,18 +623,18 @@ async function main() {
|
||||
console.log(`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`);
|
||||
}
|
||||
} else if (!other) {
|
||||
// No conflict: create a wrapper that forwards to `coder`
|
||||
// No conflict: create a wrapper that forwards to `dev`
|
||||
try {
|
||||
const wrapperPath = bunShim;
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"%~dp0coder" %*\r\n`;
|
||||
const content = `@echo off\r\n"%~dp0dev" %*\r\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
chmodSync(wrapperPath, 0o755);
|
||||
}
|
||||
console.log("✓ Created 'code' wrapper -> coder (bun)");
|
||||
console.log("✓ Created 'code' wrapper -> dev (bun)");
|
||||
installedCmds.add('code');
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create 'code' wrapper (bun): ${e.message}`);
|
||||
@@ -653,13 +646,13 @@ async function main() {
|
||||
console.log(`Commands installed (bun): ${list}`);
|
||||
if (skippedCmds.length) {
|
||||
for (const s of skippedCmds) console.error(`Commands skipped: ${s.name} (${s.reason})`);
|
||||
console.error('→ Use `coder` to run this tool.');
|
||||
console.error('→ Use `dev` to run this tool.');
|
||||
}
|
||||
// Final friendly usage hint
|
||||
if (installedCmds.has('code')) {
|
||||
console.log("Use 'code' to launch Code.");
|
||||
console.log("Use 'code' to launch Hanzo Dev.");
|
||||
} else {
|
||||
console.log("Use 'coder' to launch Code.");
|
||||
console.log("Use 'dev' to launch Hanzo Dev.");
|
||||
}
|
||||
} else {
|
||||
// npm/pnpm/yarn path
|
||||
@@ -668,7 +661,7 @@ async function main() {
|
||||
const candidates = resolveAllOnPath();
|
||||
const others = candidates.filter(p => p && (!ourShim || p !== ourShim));
|
||||
const ourShimExists = ourShim && existsSync(ourShim);
|
||||
const shimLooksOurs = ourShimExists && looksLikeOurCodeShim(ourShim);
|
||||
const shimLooksOurs = ourShimExists && looksLikeOurShim(ourShim);
|
||||
const conflictPaths = [
|
||||
...others,
|
||||
...(ourShimExists && !shimLooksOurs ? [ourShim] : []),
|
||||
@@ -680,21 +673,23 @@ async function main() {
|
||||
try {
|
||||
const wrapperPath = join(globalBin, isWindows ? `${name}.cmd` : name);
|
||||
if (isWindows) {
|
||||
const content = `@echo off\r\n"%~dp0${collision ? 'coder' : 'code'}" ${args} %*\r\n`;
|
||||
const content = `@echo off\r\n"%~dp0dev" ${args} %*\r\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
} else {
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/${collision ? 'coder' : 'code'}" ${args} "$@"\n`;
|
||||
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" ${args} "$@"\n`;
|
||||
writeFileSync(wrapperPath, content);
|
||||
chmodSync(wrapperPath, 0o755);
|
||||
}
|
||||
console.log(`✓ Created wrapper '${name}' -> ${collision ? 'coder' : 'code'} ${args}`);
|
||||
console.log(`✓ Created wrapper '${name}' -> dev ${args}`);
|
||||
installedCmds.add(name);
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Always create legacy wrappers so existing scripts keep working
|
||||
// Always create legacy wrappers so existing scripts keep working.
|
||||
// These are distinct commands and keep their own names; only their target
|
||||
// changed from `code` to `dev`.
|
||||
ensureWrapper('code-tui', '');
|
||||
ensureWrapper('code-exec', 'exec');
|
||||
|
||||
@@ -721,20 +716,20 @@ async function main() {
|
||||
} catch (e) {
|
||||
console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
|
||||
}
|
||||
console.error('→ Use `coder` to run this tool.');
|
||||
console.error('→ Use `dev` to run this tool.');
|
||||
} else {
|
||||
console.log('Note: could not determine npm global bin; skipping alias creation.');
|
||||
}
|
||||
} else {
|
||||
// No collision; ensure a 'code' wrapper exists forwarding to 'coder'
|
||||
// No collision; ensure a 'code' wrapper exists forwarding to 'dev'
|
||||
if (globalBin) {
|
||||
try {
|
||||
const content = isWindows
|
||||
? `@echo off\r\n"%~dp0coder" %*\r\n`
|
||||
: `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
||||
? `@echo off\r\n"%~dp0dev" %*\r\n`
|
||||
: `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
|
||||
writeFileSync(ourShim, content);
|
||||
if (!isWindows) chmodSync(ourShim, 0o755);
|
||||
console.log("✓ Created 'code' wrapper -> coder");
|
||||
console.log("✓ Created 'code' wrapper -> dev");
|
||||
installedCmds.add('code');
|
||||
} catch (e) {
|
||||
console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
|
||||
@@ -750,9 +745,9 @@ async function main() {
|
||||
}
|
||||
// Final friendly usage hint
|
||||
if (installedCmds.has('code')) {
|
||||
console.log("Use 'code' to launch Code.");
|
||||
console.log("Use 'code' to launch Hanzo Dev.");
|
||||
} else {
|
||||
console.log("Use 'coder' to launch Code.");
|
||||
console.log("Use 'dev' to launch Hanzo Dev.");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -70,22 +70,18 @@ ARTIFACTS_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$ARTIFACTS_DIR"' EXIT
|
||||
|
||||
# NB: The GitHub CLI `gh` must be installed and authenticated.
|
||||
gh run download --dir "$ARTIFACTS_DIR" --repo just-every/code "$WORKFLOW_ID"
|
||||
gh run download --dir "$ARTIFACTS_DIR" --repo hanzoai/dev "$WORKFLOW_ID"
|
||||
|
||||
# x64 Linux
|
||||
zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/code-x86_64-unknown-linux-musl.zst" \
|
||||
-o "$BIN_DIR/code-x86_64-unknown-linux-musl"
|
||||
# ARM64 Linux
|
||||
zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/code-aarch64-unknown-linux-musl.zst" \
|
||||
-o "$BIN_DIR/code-aarch64-unknown-linux-musl"
|
||||
# x64 macOS
|
||||
zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/code-x86_64-apple-darwin.zst" \
|
||||
-o "$BIN_DIR/code-x86_64-apple-darwin"
|
||||
# ARM64 macOS
|
||||
zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/code-aarch64-apple-darwin.zst" \
|
||||
-o "$BIN_DIR/code-aarch64-apple-darwin"
|
||||
# x64 Windows
|
||||
zstd -d "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/code-x86_64-pc-windows-msvc.exe.zst" \
|
||||
-o "$BIN_DIR/code-x86_64-pc-windows-msvc.exe"
|
||||
# Each build job uploads its artifact as `binaries-<target>` containing
|
||||
# `dev-<target>[.exe].zst`.
|
||||
for target in \
|
||||
x86_64-unknown-linux-musl \
|
||||
aarch64-unknown-linux-musl \
|
||||
x86_64-apple-darwin \
|
||||
aarch64-apple-darwin \
|
||||
x86_64-pc-windows-msvc.exe
|
||||
do
|
||||
zstd -d "$ARTIFACTS_DIR/binaries-${target%.exe}/dev-$target.zst" -o "$BIN_DIR/dev-$target"
|
||||
done
|
||||
|
||||
echo "Installed native dependencies into $BIN_DIR"
|
||||
|
||||
@@ -33,17 +33,17 @@ function tryExec(cmd, opts = {}) {
|
||||
// 1) Stop our native binary if it is holding locks. Avoid killing unrelated tools.
|
||||
// Only available on native Windows; skip entirely on WSL to avoid noise.
|
||||
if (isWin) {
|
||||
tryExec('taskkill /IM code-x86_64-pc-windows-msvc.exe /F');
|
||||
tryExec('taskkill /IM dev-x86_64-pc-windows-msvc.exe /F');
|
||||
}
|
||||
|
||||
// 2) Remove stale staging dirs from previous failed installs under the global
|
||||
// @just-every scope, which npm will reuse (e.g., .code-XXXXX). Remove only
|
||||
// @hanzo scope, which npm will reuse (e.g., .dev-XXXXX). Remove only
|
||||
// old entries and never the current staging or live package.
|
||||
try {
|
||||
let scopeDir = '';
|
||||
try {
|
||||
const root = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], shell: true }).toString().trim();
|
||||
scopeDir = path.join(root, '@just-every');
|
||||
scopeDir = path.join(root, '@hanzo');
|
||||
} catch {
|
||||
// Fall back to guessing from this script location: <staging>\..\..\
|
||||
const here = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
||||
@@ -54,7 +54,7 @@ try {
|
||||
const maxAgeMs = 2 * 60 * 60 * 1000; // 2 hours
|
||||
const currentDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
for (const name of readdirSync(scopeDir)) {
|
||||
if (!name.startsWith('.code-')) continue;
|
||||
if (!name.startsWith('.dev-')) continue;
|
||||
const p = path.join(scopeDir, name);
|
||||
if (path.resolve(p) === currentDir) continue; // never remove our current dir
|
||||
try {
|
||||
|
||||
@@ -9,24 +9,23 @@ Usage (PowerShell):
|
||||
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
Write-Host "Stopping running Code/Coder processes..."
|
||||
taskkill /IM code-x86_64-pc-windows-msvc.exe /F 2>$null | Out-Null
|
||||
taskkill /IM code.exe /F 2>$null | Out-Null
|
||||
taskkill /IM coder.exe /F 2>$null | Out-Null
|
||||
Write-Host "Stopping running Hanzo Dev processes..."
|
||||
taskkill /IM dev-x86_64-pc-windows-msvc.exe /F 2>$null | Out-Null
|
||||
taskkill /IM dev.exe /F 2>$null | Out-Null
|
||||
|
||||
Write-Host "Removing old global package (if present)..."
|
||||
$npmRoot = (& npm root -g).Trim()
|
||||
$pkgPath = Join-Path $npmRoot "@just-every\code"
|
||||
$pkgPath = Join-Path $npmRoot "@hanzo\dev"
|
||||
if (Test-Path $pkgPath) {
|
||||
try { Remove-Item -LiteralPath $pkgPath -Recurse -Force -ErrorAction Stop } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Removing temp staging directories (if present)..."
|
||||
Get-ChildItem -LiteralPath (Join-Path $npmRoot "@just-every") -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like '.code-*' } |
|
||||
Get-ChildItem -LiteralPath (Join-Path $npmRoot "@hanzo") -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like '.dev-*' } |
|
||||
ForEach-Object {
|
||||
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Cleanup complete. You can now run: npm install -g @just-every/code@latest"
|
||||
Write-Host "Cleanup complete. You can now run: npm install -g @hanzo/dev@latest"
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ Homebrew (macOS)
|
||||
|
||||
This repository now includes a helper script to generate a Homebrew formula
|
||||
from the latest GitHub release artifacts. Publishing to Homebrew requires a
|
||||
tap repository (for example, `just-every/homebrew-tap`). Once a tap exists,
|
||||
tap repository (for example, `hanzoai/homebrew-tap`). Once a tap exists,
|
||||
you can generate and publish the formula like so:
|
||||
|
||||
1) Generate the formula for the latest version:
|
||||
@@ -17,7 +17,7 @@ and update the `url`/`sha256` if needed.
|
||||
3) Users can then install with:
|
||||
|
||||
```
|
||||
brew tap just-every/tap
|
||||
brew tap hanzoai/tap
|
||||
brew install code
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ To point Zed at Code's ACP server, add this block to `settings.json`:
|
||||
"agent_servers": {
|
||||
"Code": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@just-every/code", "acp"]
|
||||
"args": ["-y", "@hanzo/dev", "acp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ Adjust the `command` or `args` only if you pin a different version or use a glob
|
||||
|
||||
- Zed Stable `0.201.5` (released August 27, 2025) or newer adds ACP support with the Agent Panel. Update via `Zed → Check for Updates` before wiring Code in. Zed’s docs call out ACP as the mechanism powering Gemini CLI and other external agents.
|
||||
- External agents live inside the Agent Panel (`cmd-?`). Use the `+` button to start a new thread and pick `Code` from the external agent list. Zed runs our CLI as a subprocess over JSON‑RPC, so all prompts and diff previews stay local.
|
||||
- Zed installs dependencies per entry automatically. If you keep `command = "npx"`, Zed will download the published `@just-every/code` package the first time you trigger the integration.
|
||||
- Zed installs dependencies per entry automatically. If you keep `command = "npx"`, Zed will download the published `@hanzo/dev` package the first time you trigger the integration.
|
||||
|
||||
## How Code implements ACP
|
||||
|
||||
@@ -36,7 +36,7 @@ write_text_file = { mcp_server = "zed", tool_name = "writeTextFile" }
|
||||
```
|
||||
|
||||
Zed wires these tools automatically when you add the Code agent, so the identifiers above match the defaults.
|
||||
- The CLI entry point (`npx @just-every/code acp`) is a thin wrapper over the Rust binary (`cargo run -p code-mcp-server -- --stdio`) that ships alongside the rest of Code. Build-from-source workflows plug in by swapping `command` for an absolute path to that binary.
|
||||
- The CLI entry point (`npx @hanzo/dev acp`) is a thin wrapper over the Rust binary (`cargo run -p code-mcp-server -- --stdio`) that ships alongside the rest of Code. Build-from-source workflows plug in by swapping `command` for an absolute path to that binary.
|
||||
|
||||
## Tips and troubleshooting
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
class Code < Formula
|
||||
desc "Terminal coding agent"
|
||||
homepage "https://github.com/just-every/code"
|
||||
version "v0.2.67"
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/just-every/code/releases/download/v0.2.67/code-aarch64-apple-darwin.tar.gz"
|
||||
sha256 "ed8fbb68d8cff0f76d28c9c9b69445dab66f8e645613e9145061e950e8cf7507"
|
||||
else
|
||||
url "https://github.com/just-every/code/releases/download/v0.2.67/code-x86_64-apple-darwin.tar.gz"
|
||||
sha256 "642f656b1d45fe305738f519b5d44c8329bd9963f3e3781ee542a4313d2102f7"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["code-*"].first => "code"
|
||||
# Provide a compatibility shim
|
||||
(bin/"coder").write <<~EOS
|
||||
#!/bin/bash
|
||||
exec "#{bin}/code" "$@"
|
||||
EOS
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/code", "--help"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
class HanzoDev < Formula
|
||||
desc "Local AI coding agent for your terminal, powered by Hanzo AI"
|
||||
homepage "https://github.com/hanzoai/dev"
|
||||
version "0.6.91"
|
||||
license "Apache-2.0"
|
||||
|
||||
on_macos do
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-apple-darwin.tar.gz"
|
||||
sha256 "2a4f5eb12bba0aac65f7d9cc7d577ea962913bea9c4ef2a21b2a4825fc814913"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-apple-darwin.tar.gz"
|
||||
sha256 "264992535a1c7f027fb67769baaa7183ca24155f45bb7519302dc3ebfd30739d"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-unknown-linux-musl.tar.gz"
|
||||
sha256 "1bb091624b6458087a79929c653dc8239d34eead9a7d012bb3b10207e817eeca"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-unknown-linux-musl.tar.gz"
|
||||
sha256 "69730db8e3261b6a3bd2ca5bad27b4c04480e28d0e341a7c7dcc663f84a3c8f7"
|
||||
end
|
||||
end
|
||||
|
||||
livecheck do
|
||||
url :stable
|
||||
strategy :github_latest
|
||||
end
|
||||
|
||||
def install
|
||||
# Installs `dev` ONLY. It must not also write a `hanzo` shim: `hanzo` is the
|
||||
# Hanzo CLI (Formula/hanzo.rb), a different program. A shim here would make
|
||||
# `hanzo` mean "the dev coding assistant" for anyone who installed this
|
||||
# formula first, and would collide with hanzo.rb on the same filename.
|
||||
bin.install Dir["dev-*"].first => "dev"
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/dev", "--help"
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,11 @@
|
||||
# just-every/homebrew-tap
|
||||
# hanzoai/homebrew-tap
|
||||
|
||||
Unofficial tap for Code (terminal coding agent).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
brew tap just-every/tap
|
||||
brew tap hanzoai/tap
|
||||
brew install code
|
||||
```
|
||||
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@
|
||||
"scripts": {
|
||||
"format": "prettier --check *.json *.md .github/workflows/*.yml **/*.js",
|
||||
"format:fix": "prettier --write *.json *.md .github/workflows/*.yml **/*.js",
|
||||
"build": "cd code-rs && cargo build --release --bin code --bin code-tui --bin code-exec",
|
||||
"build:dev": "cd code-rs && cargo build --bin code --bin code-tui --bin code-exec",
|
||||
"build:quick": "cd code-rs && cargo build --release --bin code",
|
||||
"build": "cd code-rs && cargo build --release --bin dev --bin code-tui --bin code-exec",
|
||||
"build:dev": "cd code-rs && cargo build --bin dev --bin code-tui --bin code-exec",
|
||||
"build:quick": "cd code-rs && cargo build --release --bin dev",
|
||||
"test:local": "if [ -d code-rs ]; then (cd code-rs && cargo run -p code-cli -- --version); elif [ -d codex-cli ]; then (cd codex-cli && npm link && codex --version); else echo 'no CLI workspace found' >&2; exit 1; fi",
|
||||
"fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Please use gh to review this issue in https://github.com/just-every/code/issues/{issue_number}
|
||||
Please use gh to review this issue in https://github.com/hanzoai/dev/issues/{issue_number}
|
||||
|
||||
1. Please assess if the issue is genuine and needs addressing
|
||||
2. If it does, work on a solution;
|
||||
|
||||
@@ -24,8 +24,8 @@ if [ -z "$package_version" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
expected_header="## @just-every/code v${package_version}"
|
||||
actual_header=$(grep -m1 '^## @just-every/code v' "$notes_file" || true)
|
||||
expected_header="## @hanzo/dev v${package_version}"
|
||||
actual_header=$(grep -m1 '^## @hanzo/dev v' "$notes_file" || true)
|
||||
|
||||
if [ "$actual_header" != "$expected_header" ]; then
|
||||
echo "release notes header mismatch" >&2
|
||||
|
||||
+6
-6
@@ -18,19 +18,19 @@ fi
|
||||
echo "[ci-tests] CLI smokes with host binary..."
|
||||
BIN="${CI_CLI_BIN:-}"
|
||||
if [[ -z "${BIN}" ]]; then
|
||||
if [[ -x ./code-rs/target/dev-fast/code ]]; then
|
||||
BIN=./code-rs/target/dev-fast/code
|
||||
elif [[ -x ./code-rs/target/debug/code ]]; then
|
||||
BIN=./code-rs/target/debug/code
|
||||
if [[ -x ./code-rs/target/dev-fast/dev ]]; then
|
||||
BIN=./code-rs/target/dev-fast/dev
|
||||
elif [[ -x ./code-rs/target/debug/dev ]]; then
|
||||
BIN=./code-rs/target/debug/dev
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${BIN}" || ! -x "${BIN}" ]]; then
|
||||
echo "[ci-tests] CLI binary not found; building debug binary..."
|
||||
pushd code-rs >/dev/null
|
||||
cargo build --bin code -q
|
||||
cargo build --bin dev -q
|
||||
popd >/dev/null
|
||||
BIN=./code-rs/target/debug/code
|
||||
BIN=./code-rs/target/debug/dev
|
||||
fi
|
||||
|
||||
"${BIN}" --version >/dev/null
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Generate a minimal Homebrew formula from the latest GitHub release.
|
||||
# Writes Formula/Code.rb into the repo root (not a tap); you can copy it
|
||||
# into a tap repo to publish.
|
||||
# Generate the Homebrew formula from the latest GitHub release.
|
||||
# Writes Formula/hanzo-dev.rb into the repo root (not a tap); release.yml copies
|
||||
# it into hanzoai/homebrew-tap as Formula/hanzo-dev.rb.
|
||||
|
||||
owner_repo="just-every/code"
|
||||
owner_repo="hanzoai/dev"
|
||||
version="${1:-}"
|
||||
if [ -z "$version" ] && [ -f "code-rs/Cargo.toml" ]; then
|
||||
version="$(awk -F '"' '/^\[workspace.package\]/{f=1; next} f && $1 ~ /version/ {print $2; exit}' code-rs/Cargo.toml)"
|
||||
@@ -21,11 +21,25 @@ fi
|
||||
# Optional directory where CI placed artifacts (step: Prepare release assets)
|
||||
RELEASE_ASSETS_DIR=${RELEASE_ASSETS_DIR:-"release-assets"}
|
||||
|
||||
# Every platform the tap formula serves. Dropping one silently narrows the
|
||||
# formula, so all four are required to produce a publishable file.
|
||||
assets=(
|
||||
"code-aarch64-apple-darwin.tar.gz"
|
||||
"code-x86_64-apple-darwin.tar.gz"
|
||||
"dev-aarch64-apple-darwin.tar.gz"
|
||||
"dev-x86_64-apple-darwin.tar.gz"
|
||||
"dev-aarch64-unknown-linux-musl.tar.gz"
|
||||
"dev-x86_64-unknown-linux-musl.tar.gz"
|
||||
)
|
||||
|
||||
placeholder_for() {
|
||||
case "$1" in
|
||||
dev-aarch64-apple-darwin.tar.gz) echo "__SHA_MACOS_ARM__" ;;
|
||||
dev-x86_64-apple-darwin.tar.gz) echo "__SHA_MACOS_INTEL__" ;;
|
||||
dev-aarch64-unknown-linux-musl.tar.gz) echo "__SHA_LINUX_ARM__" ;;
|
||||
dev-x86_64-unknown-linux-musl.tar.gz) echo "__SHA_LINUX_INTEL__" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
sha256_file() {
|
||||
local f="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
@@ -51,43 +65,61 @@ retry() {
|
||||
}
|
||||
|
||||
mkdir -p Formula
|
||||
cat > Formula/Code.rb <<'RUBY'
|
||||
class Code < Formula
|
||||
desc "Terminal coding agent"
|
||||
homepage "https://github.com/just-every/code"
|
||||
RUBY
|
||||
formula="Formula/hanzo-dev.rb"
|
||||
|
||||
echo " version \"v${version}\"" >> Formula/Code.rb
|
||||
cat > "$formula" <<'RUBY'
|
||||
class HanzoDev < Formula
|
||||
desc "Local AI coding agent for your terminal, powered by Hanzo AI"
|
||||
homepage "https://github.com/hanzoai/dev"
|
||||
version "__VERSION__"
|
||||
license "Apache-2.0"
|
||||
|
||||
cat >> Formula/Code.rb <<'RUBY'
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "__URL_ARM64__"
|
||||
sha256 "__SHA_ARM64__"
|
||||
else
|
||||
url "__URL_X64__"
|
||||
sha256 "__SHA_X64__"
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-apple-darwin.tar.gz"
|
||||
sha256 "__SHA_MACOS_ARM__"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-apple-darwin.tar.gz"
|
||||
sha256 "__SHA_MACOS_INTEL__"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_arm do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-aarch64-unknown-linux-musl.tar.gz"
|
||||
sha256 "__SHA_LINUX_ARM__"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/hanzoai/dev/releases/download/v#{version}/dev-x86_64-unknown-linux-musl.tar.gz"
|
||||
sha256 "__SHA_LINUX_INTEL__"
|
||||
end
|
||||
end
|
||||
|
||||
livecheck do
|
||||
url :stable
|
||||
strategy :github_latest
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["code-*"].first => "code"
|
||||
# Provide a compatibility shim
|
||||
(bin/"coder").write <<~EOS
|
||||
#!/bin/bash
|
||||
exec "#{bin}/code" "$@"
|
||||
EOS
|
||||
# Installs `dev` ONLY. It must not also write a `hanzo` shim: `hanzo` is the
|
||||
# Hanzo CLI (Formula/hanzo.rb), a different program. A shim here would make
|
||||
# `hanzo` mean "the dev coding assistant" for anyone who installed this
|
||||
# formula first, and would collide with hanzo.rb on the same filename.
|
||||
bin.install Dir["dev-*"].first => "dev"
|
||||
end
|
||||
|
||||
test do
|
||||
system "#{bin}/code", "--help"
|
||||
system "#{bin}/dev", "--help"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
sed -i.bak "s#__VERSION__#${version}#" "$formula"
|
||||
|
||||
for a in "${assets[@]}"; do
|
||||
url="https://github.com/${owner_repo}/releases/download/v${version}/${a}"
|
||||
tmp="/tmp/${a}"
|
||||
tmp="${TMPDIR:-/tmp}/${a}"
|
||||
sha=""
|
||||
|
||||
# Prefer local artifact if available to avoid CDN propagation races
|
||||
@@ -101,25 +133,26 @@ for a in "${assets[@]}"; do
|
||||
if [ -z "$sha" ]; then
|
||||
echo "Downloading ${url} (fallback for sha256)..." >&2
|
||||
if ! retry 12 5 curl -fsSL "${url}" -o "${tmp}"; then
|
||||
echo "WARN: Could not download ${url} to compute sha256 (possible CDN delay)." >&2
|
||||
echo " Proceeding without sha; Homebrew step will still push formula referencing the URL." >&2
|
||||
echo "ERROR: could not download ${url} to compute sha256." >&2
|
||||
else
|
||||
sha=$(sha256_file "$tmp" || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply URL (always), and sha when available
|
||||
if [[ "${a}" == *"aarch64-apple-darwin"* ]]; then
|
||||
sed -i.bak "s#__URL_ARM64__#${url}#" Formula/Code.rb
|
||||
if [ -n "$sha" ]; then sed -i.bak "s#__SHA_ARM64__#${sha}#" Formula/Code.rb; fi
|
||||
else
|
||||
sed -i.bak "s#__URL_X64__#${url}#" Formula/Code.rb
|
||||
if [ -n "$sha" ]; then sed -i.bak "s#__SHA_X64__#${sha}#" Formula/Code.rb; fi
|
||||
# A formula shipped with an unresolved placeholder is a broken tap: `brew
|
||||
# install` fails on every machine until the next release. Refuse to emit one.
|
||||
if [ -z "$sha" ]; then
|
||||
echo "ERROR: no sha256 for ${a}; refusing to write a formula with an unresolved placeholder." >&2
|
||||
rm -f "$formula" "$formula.bak"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ph="$(placeholder_for "$a")"
|
||||
sed -i.bak "s#${ph}#${sha}#" "$formula"
|
||||
done
|
||||
|
||||
rm -f Formula/Code.rb.bak
|
||||
echo "Wrote Formula/Code.rb for v${version}" >&2
|
||||
rm -f "$formula.bak"
|
||||
echo "Wrote ${formula} for v${version}" >&2
|
||||
|
||||
# Optional: best-effort HEAD check to surface propagation status without failing CI
|
||||
for a in "${assets[@]}"; do
|
||||
|
||||
@@ -33,24 +33,24 @@ try {
|
||||
} catch { Warn ("npm config read failed: " + $_) }
|
||||
|
||||
# 2) Resolve versions and tarballs from NPM
|
||||
Step "Resolve @just-every/code@$Version (npm)"
|
||||
Step "Resolve @hanzo/dev@$Version (npm)"
|
||||
$mainVersion = $null
|
||||
try { $mainVersion = (npm view "@just-every/code@$Version" version) } catch {}
|
||||
if (-not $mainVersion) { Err "Could not resolve @just-every/code@$Version"; exit 1 }
|
||||
try { $mainVersion = (npm view "@hanzo/dev@$Version" version) } catch {}
|
||||
if (-not $mainVersion) { Err "Could not resolve @hanzo/dev@$Version"; exit 1 }
|
||||
$mainTar = $null
|
||||
try { $mainTar = (npm view "@just-every/code@$mainVersion" dist.tarball) } catch {}
|
||||
try { $mainTar = (npm view "@hanzo/dev@$mainVersion" dist.tarball) } catch {}
|
||||
Info ("main version = " + $mainVersion)
|
||||
Info ("main tarball = " + ($mainTar ?? "<none>"))
|
||||
|
||||
Step "Resolve @just-every/code-win32-x64@$mainVersion (npm)"
|
||||
Step "Resolve @hanzo/dev-win32-x64@$mainVersion (npm)"
|
||||
$winVersion = $null; $winTar = $null
|
||||
try { $winVersion = (npm view "@just-every/code-win32-x64@$mainVersion" version) } catch {}
|
||||
try { $winVersion = (npm view "@hanzo/dev-win32-x64@$mainVersion" version) } catch {}
|
||||
if ($winVersion) {
|
||||
try { $winTar = (npm view "@just-every/code-win32-x64@$mainVersion" dist.tarball) } catch {}
|
||||
try { $winTar = (npm view "@hanzo/dev-win32-x64@$mainVersion" dist.tarball) } catch {}
|
||||
Info ("win version = " + $winVersion)
|
||||
Info ("win tarball = " + ($winTar ?? "<none>"))
|
||||
} else {
|
||||
Warn ("@just-every/code-win32-x64@$mainVersion not found on npm")
|
||||
Warn ("@hanzo/dev-win32-x64@$mainVersion not found on npm")
|
||||
}
|
||||
|
||||
# 3) Download + inspect npm tarballs
|
||||
@@ -80,7 +80,7 @@ if ($gotMain) {
|
||||
try {
|
||||
mkdir p1 | Out-Null
|
||||
tar -xzf $t1 -C p1
|
||||
Step ("@just-every/code@" + $mainVersion + " contents")
|
||||
Step ("@hanzo/dev@" + $mainVersion + " contents")
|
||||
dir p1/package
|
||||
Info "\nHead of bin/coder.js:"
|
||||
Get-Content p1/package/bin/coder.js -TotalCount 30 | ForEach-Object { " " + $_ }
|
||||
@@ -97,7 +97,7 @@ if ($gotWin) {
|
||||
try {
|
||||
mkdir p2 | Out-Null
|
||||
tar -xzf $t2 -C p2
|
||||
Step ("@just-every/code-win32-x64@" + $mainVersion + " contents")
|
||||
Step ("@hanzo/dev-win32-x64@" + $mainVersion + " contents")
|
||||
dir p2/package/bin
|
||||
} catch {
|
||||
Warn ("Could not extract or inspect win tarball: " + $_)
|
||||
@@ -106,7 +106,7 @@ if ($gotWin) {
|
||||
|
||||
# 4) Compute target triple and cache paths
|
||||
$triple = "x86_64-pc-windows-msvc.exe" # Windows x64
|
||||
$cacheDir = Join-Path $env:LOCALAPPDATA ("just-every\code\" + $mainVersion)
|
||||
$cacheDir = Join-Path $env:LOCALAPPDATA ("hanzoai\dev\" + $mainVersion)
|
||||
$expectOK = Join-Path $cacheDir ("code-" + $triple) # correct
|
||||
$expectBAD = $expectOK + ".exe" # incorrect (double .exe)
|
||||
Step "Cache expectations"
|
||||
@@ -117,7 +117,7 @@ Info ("exists OK? " + ([string](Test-Path $expectOK)))
|
||||
Info ("exists BAD? " + ([string](Test-Path $expectBAD)))
|
||||
|
||||
# 5) GitHub release asset (fallback path)
|
||||
$ghZip = "https://github.com/just-every/code/releases/download/v$mainVersion/code-$triple.zip"
|
||||
$ghZip = "https://github.com/hanzoai/dev/releases/download/v$mainVersion/dev-$triple.zip"
|
||||
Step "GitHub asset (fallback) HEAD check"
|
||||
try {
|
||||
$resp = Invoke-WebRequest -UseBasicParsing -Uri $ghZip -Method Head
|
||||
@@ -160,7 +160,7 @@ try {
|
||||
Step "Global npm prefix + expected node_modules bin path"
|
||||
$prefix = $null
|
||||
try { $prefix = (npm prefix -g 2>$null) } catch {}
|
||||
$nmBin = if ($prefix) { Join-Path $prefix "node_modules\@just-every\code\bin" } else { "<unknown>" }
|
||||
$nmBin = if ($prefix) { Join-Path $prefix "node_modules\@hanzo\dev\bin" } else { "<unknown>" }
|
||||
Info ("prefix = " + ($prefix ?? "<none>"))
|
||||
Info ("bin dir= " + $nmBin)
|
||||
if ($prefix -and (Test-Path $nmBin)) { dir $nmBin } else { Info "(bin dir not present - expected for npx, present for global installs)" }
|
||||
|
||||
Reference in New Issue
Block a user