From 3315399e14741104c48b0840c6e137e16d6ba121 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 14:03:42 -0400 Subject: [PATCH 1/8] fix(updates): gate self-update to direct binaries Refs #295 --- README.md | 4 +- code-rs/cli/src/update.rs | 109 +++++------------- code-rs/core/src/config.rs | 5 +- .../tui/src/bottom_pane/settings_overlay.rs | 4 +- code-rs/tui/src/updates.rs | 53 +++++---- ...napshot__settings_help_overlay_closed.snap | 2 +- ...hot__settings_overlay_overview_layout.snap | 2 +- ...apshot__settings_overview_hints_clean.snap | 2 +- docs/settings.md | 2 +- docs/upstream-import-policy.md | 5 +- 10 files changed, 72 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index c876267ffd8..650f6219580 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ code Use `code update-check` to inspect the current GitHub Release manifest and `code update --yes` to install a newer verified direct binary when one is -available. npm and Homebrew publishing are deferred unless package-manager -distribution becomes intentional again. +available. Every Code releases are distributed through GitHub Releases; npm and +Homebrew publishing are not part of the current release path. **Authenticate** (one of the following): - **Sign in with ChatGPT** (Plus/Pro/Team; uses models available to your plan) diff --git a/code-rs/cli/src/update.rs b/code-rs/cli/src/update.rs index ae291f81cc7..3192d9a2188 100644 --- a/code-rs/cli/src/update.rs +++ b/code-rs/cli/src/update.rs @@ -86,14 +86,12 @@ pub async fn run_update(args: UpdateCommand) -> anyhow::Result<()> { } let install_target = resolve_install_target(&exe); - let install = detect_install_source_for_path(&install_target); println!("command: {}", identity.command_name); println!("install target: {}", install_target.display()); - println!("install source: {}", install.description()); - if !install.can_self_update() { + println!("install mode: {}", install_mode_description(&install_target)); + if !is_direct_binary_install_path(&install_target) { bail!( - "refusing self-update for {} install; install the release manually instead", - install.description() + "refusing self-update because this executable is not an Every Code direct binary; install the latest GitHub Release manually instead" ); } @@ -366,55 +364,24 @@ fn current_target() -> anyhow::Result { } } -#[derive(Debug)] -enum InstallSource { - Direct, - Homebrew, - Npm, - Cargo, - Unknown(PathBuf), +fn resolve_install_target(exe: &Path) -> PathBuf { + fs::canonicalize(exe).unwrap_or_else(|_| exe.to_path_buf()) } -impl InstallSource { - fn can_self_update(&self) -> bool { - matches!(self, InstallSource::Direct) - } - - fn description(&self) -> String { - match self { - InstallSource::Direct => "direct binary".to_string(), - InstallSource::Homebrew => "Homebrew".to_string(), - InstallSource::Npm => "npm/pnpm/bun".to_string(), - InstallSource::Cargo => "cargo".to_string(), - InstallSource::Unknown(path) => format!("unknown ({})", path.display()), - } +fn install_mode_description(exe: &Path) -> &'static str { + if is_direct_binary_install_path(exe) { + "Every Code direct binary" + } else { + "unsupported for self-update" } } -fn resolve_install_target(exe: &Path) -> PathBuf { - fs::canonicalize(exe).unwrap_or_else(|_| exe.to_path_buf()) -} - -fn detect_install_source_for_path(exe: &Path) -> InstallSource { +fn is_direct_binary_install_path(exe: &Path) -> bool { let path = exe.to_string_lossy(); - if path.contains("/Cellar/") || path.contains("/Homebrew/") || path.contains("/homebrew/") { - return InstallSource::Homebrew; - } - if path.contains("/node_modules/") || path.contains("/.bun/") { - return InstallSource::Npm; - } - if path.contains("/.cargo/bin/") { - return InstallSource::Cargo; - } - if path.contains("/.code/bin/") + path.contains("/.code/bin/") || path.contains("/.local/bin/") || path.contains("/usr/local/bin/") || path.contains("/code-rs/target/release/") - { - return InstallSource::Direct; - } - - InstallSource::Unknown(exe.to_path_buf()) } fn compare_versions(current: &str, latest: &str) -> VersionOrdering { @@ -546,31 +513,22 @@ mod tests { } #[test] - fn detect_install_source_classifies_managed_paths() { - assert!(matches!( - detect_install_source_for_path(Path::new( - "/opt/homebrew/Cellar/code/0.6.101/bin/code" - )), - InstallSource::Homebrew - )); - assert!(matches!( - detect_install_source_for_path(Path::new( - "/Users/me/.npm-global/lib/node_modules/@just-every/code/bin/code" - )), - InstallSource::Npm - )); - assert!(matches!( - detect_install_source_for_path(Path::new("/Users/me/.cargo/bin/code")), - InstallSource::Cargo - )); - assert!(matches!( - detect_install_source_for_path(Path::new("/Users/me/.local/bin/code")), - InstallSource::Direct - )); - assert!(matches!( - detect_install_source_for_path(Path::new("/usr/local/bin/chris-code")), - InstallSource::Direct - )); + fn direct_binary_install_path_allows_owned_locations_only() { + assert!(!is_direct_binary_install_path(Path::new( + "/opt/homebrew/Cellar/code/0.6.101/bin/code" + ))); + assert!(!is_direct_binary_install_path(Path::new( + "/Users/me/.npm-global/lib/node_modules/@just-every/code/bin/code" + ))); + assert!(!is_direct_binary_install_path(Path::new( + "/Users/me/.cargo/bin/code" + ))); + assert!(is_direct_binary_install_path(Path::new( + "/Users/me/.local/bin/code" + ))); + assert!(is_direct_binary_install_path(Path::new( + "/usr/local/bin/chris-code" + ))); } #[test] @@ -626,13 +584,10 @@ mod tests { } #[test] - fn detect_install_source_refuses_build_cache_binaries() { - assert!(matches!( - detect_install_source_for_path(Path::new( - "/Users/me/Developer/code/.code/working/_target-cache/code/main/code-rs/dev-fast/code" - )), - InstallSource::Unknown(_) - )); + fn direct_binary_install_path_refuses_build_cache_binaries() { + assert!(!is_direct_binary_install_path(Path::new( + "/Users/me/Developer/code/.code/working/_target-cache/code/main/code-rs/dev-fast/code" + ))); } #[cfg(target_family = "unix")] diff --git a/code-rs/core/src/config.rs b/code-rs/core/src/config.rs index 1ce58fe8206..5a6cc1bea4a 100644 --- a/code-rs/core/src/config.rs +++ b/code-rs/core/src/config.rs @@ -403,9 +403,8 @@ pub struct Config { pub otel: crate::config_types::OtelConfig, /// When true, Code will silently install updates on startup whenever a newer - /// release is available. Upgrades are performed using the package manager - /// that originally installed the CLI (Homebrew or npm). Manual installs are - /// never upgraded automatically. + /// GitHub Release direct binary is available. Executables outside explicit + /// Every Code direct-binary locations are not self-updated. pub auto_upgrade_enabled: bool, /// User-provided instructions from AGENTS.md. diff --git a/code-rs/tui/src/bottom_pane/settings_overlay.rs b/code-rs/tui/src/bottom_pane/settings_overlay.rs index 139b118e0ac..e688ecd5a58 100644 --- a/code-rs/tui/src/bottom_pane/settings_overlay.rs +++ b/code-rs/tui/src/bottom_pane/settings_overlay.rs @@ -64,7 +64,7 @@ impl SettingsSection { SettingsSection::Model => "Choose the language model used for new completions.", SettingsSection::Theme => "Switch between preset color palettes and adjust contrast.", SettingsSection::Planning => "Choose the model used in Plan Mode (Read Only).", - SettingsSection::Updates => "Control CLI auto-update cadence and release channels.", + SettingsSection::Updates => "Control GitHub Release update checks and automatic upgrades.", SettingsSection::Accounts => "Configure account switching behavior under rate and usage limits.", SettingsSection::Agents => "Configure linked agents and default task permissions.", SettingsSection::Memories => "Control whether future turns use cross-session memory guidance.", @@ -85,7 +85,7 @@ impl SettingsSection { SettingsSection::Model => "Model settings coming soon.", SettingsSection::Theme => "Theme settings coming soon.", SettingsSection::Planning => "Planning settings coming soon.", - SettingsSection::Updates => "Upgrade Codex and manage automatic updates.", + SettingsSection::Updates => "Upgrade Every Code and manage automatic updates.", SettingsSection::Accounts => "Account switching settings coming soon.", SettingsSection::Agents => "Agents configuration coming soon.", SettingsSection::Memories => "Enable or disable cross-session memories.", diff --git a/code-rs/tui/src/updates.rs b/code-rs/tui/src/updates.rs index 917724e433b..3d8adf6ce06 100644 --- a/code-rs/tui/src/updates.rs +++ b/code-rs/tui/src/updates.rs @@ -175,13 +175,6 @@ pub enum UpgradeResolution { Manual { instructions: String }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum UpgradeInstallSource { - Direct, - Managed, - Unknown, -} - fn version_filepath(config: &Config) -> PathBuf { config.code_home.join(VERSION_FILENAME) } @@ -198,7 +191,7 @@ pub fn resolve_upgrade_resolution() -> UpgradeResolution { fn resolve_upgrade_resolution_for_exe(exe_path: &Path) -> UpgradeResolution { let install_target = resolve_install_target(exe_path); - if detect_upgrade_install_source(&install_target) == UpgradeInstallSource::Direct { + if is_direct_binary_install_path(&install_target) { let command_name = upgrade_command_name(exe_path, &install_target); return UpgradeResolution::Command { command: vec![ @@ -249,24 +242,12 @@ fn resolve_install_target(exe_path: &Path) -> PathBuf { fs::canonicalize(exe_path).unwrap_or_else(|_| exe_path.to_path_buf()) } -fn detect_upgrade_install_source(exe_path: &Path) -> UpgradeInstallSource { +fn is_direct_binary_install_path(exe_path: &Path) -> bool { let path = exe_path.to_string_lossy(); - if path.contains("/Cellar/") || path.contains("/Homebrew/") || path.contains("/homebrew/") { - return UpgradeInstallSource::Managed; - } - if path.contains("/node_modules/") || path.contains("/.bun/") || path.contains("/.cargo/bin/") - { - return UpgradeInstallSource::Managed; - } - if path.contains("/.code/bin/") + path.contains("/.code/bin/") || path.contains("/.local/bin/") || path.contains("/usr/local/bin/") || path.contains("/code-rs/target/release/") - { - return UpgradeInstallSource::Direct; - } - - UpgradeInstallSource::Unknown } #[derive(Debug, Clone, Default, PartialEq, Eq)] @@ -287,7 +268,7 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result (command, command_display), _ => { - info!("auto-upgrade enabled but no managed installer detected; skipping"); + info!("auto-upgrade enabled but no direct binary install detected; skipping"); return Ok(AutoUpgradeOutcome::default()); } }; @@ -320,7 +301,7 @@ pub async fn auto_upgrade_if_enabled(config: &Config) -> anyhow::Result { + panic!("unexpected command resolution: {display}"); + } + UpgradeResolution::Manual { instructions } => { + assert!(!instructions.contains("code update --yes")); + assert!(instructions.contains(CODE_RELEASE_URL)); + } + } + + match resolve_upgrade_resolution_for_exe(Path::new("/Users/me/.cargo/bin/code")) { + UpgradeResolution::Command { display, .. } => { + panic!("unexpected command resolution: {display}"); + } + UpgradeResolution::Manual { instructions } => { + assert!(!instructions.contains("code update --yes")); + assert!(instructions.contains(CODE_RELEASE_URL)); + } + } } struct EnvReset { diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap index e4da351e4c8..2c25b496bd0 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap @@ -14,7 +14,7 @@ expression: closed | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control CLI auto-update cadence and release channels. | + | + Control GitHub Release update checks and automatic upgrades. | | | | | diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap index 087b6202b7e..af57b8f8846 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap @@ -14,7 +14,7 @@ expression: output | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control CLI auto-update cadence and release channels. | + | + Control GitHub Release update checks and automatic upgrades. | | | | | diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap index b1cebbd7b30..87dfe1cc361 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap @@ -14,7 +14,7 @@ expression: output | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control CLI auto-update cadence and release channels. | + | + Control GitHub Release update checks and automatic upgrades. | | ↑ ↓ Move Enter Open Esc Close ? Help | | | +------------------------------------------------------------------------------------------------+ diff --git a/docs/settings.md b/docs/settings.md index e65c71e7aa7..91c0f013a47 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -15,7 +15,7 @@ Full-screen settings panel for Every Code’s TUI. Use it to change models, them ## Sections - **Model**: pick the default chat model and reasoning effort. - **Theme**: choose a theme and spinner; applies immediately. -- **Updates**: view upgrade channel/status. `/update` opens here before running installers. +- **Updates**: view GitHub Release update status, run upgrades, and toggle automatic upgrades. `/update` opens here. - **Agents**: see built-in/custom agents, enable/disable, force read-only, add per-agent instructions. Open the Subagent editor to configure `/plan`/`/solve`/`/code` or custom slash commands. - **Prompts**: edit saved prompt snippets. - **Auto Drive**: set review/agents/QA/cross-check toggles, continue mode (manual/immediate/ten-seconds/sixty-seconds), model override, or “use chat model.” Updates apply to active runs. diff --git a/docs/upstream-import-policy.md b/docs/upstream-import-policy.md index 3af89d4695a..0ea3bf8ee68 100644 --- a/docs/upstream-import-policy.md +++ b/docs/upstream-import-policy.md @@ -95,9 +95,8 @@ original/direct upstream and provenance source. - **Patch harness:** local validation for changed files, project tool discovery, and workspace-aware validator execution. - **Release workflow:** GitHub Releases and local PATH rebuilds are Every Code - infrastructure. GitHub Releases are the canonical internal update source; - npm and Homebrew publishing are deferred unless package-manager distribution - becomes intentional again. + infrastructure. GitHub Releases are the canonical update source; npm and + Homebrew publishing are not part of the current release path. - **Model defaults:** local defaults may intentionally differ from upstream, but request wire compatibility should use upstream model metadata when available. From 0877493a67d4b505755017c43ed5de2ae321c0bb Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 14:08:54 -0400 Subject: [PATCH 2/8] chore(release): move version ownership to VERSION Refs #296 --- .github/github.json | 6 ++-- .github/workflows/README.md | 15 +++++----- .github/workflows/release.yml | 33 +++++---------------- VERSION | 1 + docs/update-manifest.md | 2 +- docs/upstream-import-policy.md | 27 +++++++++-------- scripts/check-release-notes-version.sh | 14 ++++----- scripts/local/fork-health.sh | 4 +-- scripts/local/rebuild-path-code.sh | 4 +-- scripts/local/release-notes.sh | 10 +++---- scripts/release/determine-release-intent.sh | 13 +++++--- 11 files changed, 58 insertions(+), 71 deletions(-) create mode 100644 VERSION diff --git a/.github/github.json b/.github/github.json index 74e81017329..0cd0b6dae25 100644 --- a/.github/github.json +++ b/.github/github.json @@ -85,12 +85,12 @@ }, "release": { "intent": { - "kind": "package-version", - "file": "codex-cli/package.json", + "kind": "version-file", + "file": "VERSION", "workflow": "Release Intent" }, "metadataFiles": [ - "codex-cli/package.json", + "VERSION", "CHANGELOG.md", "docs/release-notes/RELEASE_NOTES.md" ], diff --git a/.github/workflows/README.md b/.github/workflows/README.md index cb3a844ee7d..cc34aec1929 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -7,18 +7,17 @@ Rust-specific `rust-ci*.yml` workflows are intentionally removed for Every Code. - `./build-fast.sh` is the required local Rust verification gate. - `preview-build.yml` builds preview binaries for pull requests. - `release.yml` is displayed in GitHub Actions as `Release Intent`. It runs - after relevant `main` pushes, determines whether the committed - `codex-cli/package.json` version has an existing `v` tag, and either - exits successfully as a no-op or publishes the GitHub Release. + after relevant `main` pushes, determines whether the committed `VERSION` has + an existing `v` tag, and either exits successfully as a no-op or + publishes the GitHub Release. ## Release Operator Notes - A successful `Release Intent` run does not always mean a release was cut. It - means the workflow either published the new package version or confirmed that - the current version tag already exists. -- To cut a release, merge a release metadata PR that bumps - `codex-cli/package.json` and updates `CHANGELOG.md` plus - `docs/release-notes/RELEASE_NOTES.md`. + means the workflow either published the new `VERSION` or confirmed that the + current version tag already exists. +- To cut a release, merge a release metadata PR that bumps `VERSION` and updates + `CHANGELOG.md` plus `docs/release-notes/RELEASE_NOTES.md`. - To verify publishing, check for the tag or release directly. Example: ```sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 598e0a9623b..991d040e74d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,8 @@ on: push: branches: [ main ] # Ignore common non-release paths. For other main pushes, the workflow - # determines whether the committed package version represents release - # intent. Runs with no new package version are successful no-ops. + # determines whether the committed VERSION represents release intent. Runs + # with no new VERSION are successful no-ops. paths-ignore: - '.github/workflows/issue-triage.yml' - '.github/workflows/preview-build.yml' @@ -486,41 +486,27 @@ jobs: fetch-depth: 0 token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Update package.json version + - name: Prepare release tag id: version - working-directory: codex-cli shell: bash run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" NEW_VERSION="${{ needs.determine-version.outputs.version }}" - npm version "$NEW_VERSION" --no-git-tag-version --allow-same-version - echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" - git add package.json - if git diff --staged --quiet; then - echo "skip_push=true" >> "$GITHUB_OUTPUT" - else - git commit -m "chore(release): ${NEW_VERSION}" - echo "skip_push=false" >> "$GITHUB_OUTPUT" + if [[ "$(tr -d '[:space:]' < VERSION)" != "$NEW_VERSION" ]]; then + echo "VERSION does not match release intent ${NEW_VERSION}" >&2 + exit 1 fi + echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" if ! git rev-parse "v${NEW_VERSION}" >/dev/null 2>&1; then git tag "v${NEW_VERSION}" fi echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Download all artifacts - if: steps.version.outputs.skip_push == 'true' uses: actions/download-artifact@v4 with: path: artifacts/ - name: Prepare release assets - if: steps.version.outputs.skip_push == 'true' shell: bash run: | set -euo pipefail @@ -534,26 +520,22 @@ jobs: ls -la release-assets/ || true - name: Verify local release notes metadata - if: steps.version.outputs.skip_push == 'true' shell: bash run: | set -euo pipefail scripts/check-release-notes-version.sh --version "${{ steps.version.outputs.version }}" - name: Push tag - if: steps.version.outputs.skip_push == 'true' shell: bash run: git push origin "v${{ steps.version.outputs.version }}" || true - name: Verify release notes header matches version - if: steps.version.outputs.skip_push == 'true' shell: bash run: | set -euo pipefail scripts/check-release-notes-version.sh - name: Generate update manifest - if: steps.version.outputs.skip_push == 'true' shell: bash env: NEW_VERSION: ${{ steps.version.outputs.version }} @@ -567,7 +549,6 @@ jobs: --output release-assets/update-manifest.json - name: Create GitHub Release - if: steps.version.outputs.skip_push == 'true' uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.version }} diff --git a/VERSION b/VERSION new file mode 100644 index 00000000000..e9a4d0bfef8 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.6.113 diff --git a/docs/update-manifest.md b/docs/update-manifest.md index b071aab4d6d..c2f9b82de2a 100644 --- a/docs/update-manifest.md +++ b/docs/update-manifest.md @@ -7,7 +7,7 @@ can discover and verify newer dogfood builds without npm or Homebrew. The manifest is generated by `scripts/release/generate-update-manifest.sh` during the publish pass of the `Release Intent` workflow, immediately before the GitHub Release is published. Metadata preparation does not generate the manifest. A -successful no-op run, where the package version already has a tag, does not +successful no-op run, where `VERSION` already has a tag, does not create or update a manifest. The publish pass fails if any expected platform archive is missing. diff --git a/docs/upstream-import-policy.md b/docs/upstream-import-policy.md index 0ea3bf8ee68..a4946521081 100644 --- a/docs/upstream-import-policy.md +++ b/docs/upstream-import-policy.md @@ -157,18 +157,19 @@ defer until nearby release-worthy fixes settle into one batch. The active release workflow file is `.github/workflows/release.yml`, and GitHub displays it as `Release Intent`. That name is intentional: the workflow runs -after relevant `main` pushes, first decides whether the committed package -version represents a new release, and only then publishes GitHub Release assets. +after relevant `main` pushes, first decides whether the committed `VERSION` +represents a new release, and only then publishes GitHub +Release assets. Prepare release metadata locally with the Every Code harness only when cutting a -release intentionally: the local command bumps `codex-cli/package.json`, updates -`CHANGELOG.md`, and writes `docs/release-notes/RELEASE_NOTES.md`. For this repo, -the committed `codex-cli/package.json` version is release intent. If that -version already has a tag, the workflow exits successfully as a no-op; if the -tag does not exist, it validates the metadata and publishes exactly that -committed version instead of generating fallback notes in CI. The full preflight, -macOS/Linux release matrix, and Windows asset build run only on the publish pass -after the metadata PR has merged. +release intentionally: the local command bumps `VERSION`, updates `CHANGELOG.md`, +and writes `docs/release-notes/RELEASE_NOTES.md`. For this repo, the committed +`VERSION` value is release intent. If that version already has a tag, the +workflow exits successfully as a no-op; if the tag does not exist, it validates +the metadata and publishes exactly that committed version instead of generating +fallback notes in CI. The full preflight, macOS/Linux release matrix, and +Windows asset build run only on the publish pass after the metadata PR has +merged. Release tags use the plain `v` format, for example `v0.6.101`. @@ -189,7 +190,7 @@ code exec -m gpt-5.5 --sandbox read-only --max-seconds 30 "Reply with exactly OK Run `just local-code-rebuild` after any release-readiness `./build-fast.sh` run: the fast build creates dev-fast artifacts for validation, while the rebuild -recipe owns the PATH-resolved release binary and embeds the package version. +recipe owns the PATH-resolved release binary and embeds `VERSION`. Generate and review release metadata before pushing the release PR: @@ -198,7 +199,7 @@ just local-release-notes scripts/check-release-notes-version.sh ``` -Commit the resulting `codex-cli/package.json`, `CHANGELOG.md`, and +Commit the resulting `VERSION`, `CHANGELOG.md`, and `docs/release-notes/RELEASE_NOTES.md` changes on a release metadata branch. If an old manual Homebrew link exists, remove it so PATH resolution stays @@ -217,7 +218,7 @@ scripts/wait-for-gh-run.sh --workflow 'Release Intent' --branch main ``` A successful workflow run is not enough evidence that a release was published: -non-release runs also complete successfully as no-ops when the package version +non-release runs also complete successfully as no-ops when `VERSION` already has a tag. When cutting a release, verify the tag or GitHub Release directly after the workflow succeeds: diff --git a/scripts/check-release-notes-version.sh b/scripts/check-release-notes-version.sh index f1b09c66817..510d718c278 100755 --- a/scripts/check-release-notes-version.sh +++ b/scripts/check-release-notes-version.sh @@ -27,27 +27,27 @@ while [ "$#" -gt 0 ]; do done notes_file="${REPO_ROOT}/docs/release-notes/RELEASE_NOTES.md" -pkg_json="${REPO_ROOT}/codex-cli/package.json" +version_file="${REPO_ROOT}/VERSION" if [ ! -f "$notes_file" ]; then echo "release notes file missing: $notes_file" >&2 exit 1 fi -if [ ! -f "$pkg_json" ]; then - echo "package.json missing: $pkg_json" >&2 +if [ ! -f "$version_file" ]; then + echo "VERSION file missing: $version_file" >&2 exit 1 fi -package_version=$(jq -r '.version // empty' "$pkg_json") +package_version=$(tr -d '[:space:]' <"$version_file") -if [ -z "$package_version" ]; then - echo "Failed to read version from $pkg_json" >&2 +if ! [[ "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Failed to read semver from $version_file" >&2 exit 1 fi if [ -n "$expected_version" ] && [ "$package_version" != "$expected_version" ]; then - echo "package version mismatch" >&2 + echo "release version mismatch" >&2 echo " expected: $expected_version" >&2 echo " actual: $package_version" >&2 echo "Run 'just local-release-notes' before publishing this release." >&2 diff --git a/scripts/local/fork-health.sh b/scripts/local/fork-health.sh index 8c22f5d340e..3d88ff2c500 100755 --- a/scripts/local/fork-health.sh +++ b/scripts/local/fork-health.sh @@ -10,7 +10,7 @@ product_branch="${PRODUCT_BRANCH:-$(git symbolic-ref --quiet --short refs/remote product_branch="${product_branch:-main}" latest_product_tag="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" latest_upstream_tag="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)" -package_version="$(node -p "require('$repo_root/codex-cli/package.json').version" 2>/dev/null || true)" +package_version="$(tr -d '[:space:]' <"$repo_root/VERSION" 2>/dev/null || true)" echo "Every Code health" echo "=================" @@ -20,7 +20,7 @@ echo "upstream ref: $upstream_ref" echo "head: $(git rev-parse --short HEAD)" echo "origin product: $(git rev-parse --short "origin/$product_branch" 2>/dev/null || echo unknown)" echo "upstream head: $(git rev-parse --short "$upstream_ref" 2>/dev/null || echo unknown)" -echo "package version: ${package_version:-unknown}" +echo "release version: ${package_version:-unknown}" echo "latest upstream tag: ${latest_upstream_tag:-none}" echo "latest product tag: ${latest_product_tag:-none}" echo diff --git a/scripts/local/rebuild-path-code.sh b/scripts/local/rebuild-path-code.sh index 3dd224a8814..50173a80b1e 100755 --- a/scripts/local/rebuild-path-code.sh +++ b/scripts/local/rebuild-path-code.sh @@ -11,7 +11,7 @@ fi cargo_release_bin="$cargo_target_dir/release/code" resolve_code_version() { - node -p "require('$repo_root/codex-cli/package.json').version" + tr -d '[:space:]' <"$repo_root/VERSION" } code_version="${CODE_VERSION:-$(resolve_code_version || true)}" @@ -38,7 +38,7 @@ if [[ -n "$code_version" ]]; then echo "Embedding CODE_VERSION=$code_version" CODE_VERSION="$code_version" cargo build --manifest-path "$code_rs_root/Cargo.toml" -p code-cli --release else - echo "warning: could not resolve CODE_VERSION from package metadata; building without override" >&2 + echo "warning: could not resolve CODE_VERSION from VERSION; building without override" >&2 cargo build --manifest-path "$code_rs_root/Cargo.toml" -p code-cli --release fi trap - EXIT diff --git a/scripts/local/release-notes.sh b/scripts/local/release-notes.sh index 0db1fff7055..9d4a07411eb 100755 --- a/scripts/local/release-notes.sh +++ b/scripts/local/release-notes.sh @@ -11,13 +11,13 @@ if ! command -v code >/dev/null 2>&1; then exit 1 fi -if ! git diff --quiet -- codex-cli/package.json CHANGELOG.md docs/release-notes/RELEASE_NOTES.md || - ! git diff --cached --quiet -- codex-cli/package.json CHANGELOG.md docs/release-notes/RELEASE_NOTES.md; then +if ! git diff --quiet -- VERSION CHANGELOG.md docs/release-notes/RELEASE_NOTES.md || + ! git diff --cached --quiet -- VERSION CHANGELOG.md docs/release-notes/RELEASE_NOTES.md; then echo "release metadata files have uncommitted changes; commit or stash them first" >&2 exit 1 fi -current_version=$(jq -r '.version // empty' codex-cli/package.json) +current_version=$(tr -d '[:space:]' tag does not exist yet. +Determines whether the committed Every Code version should publish a GitHub +Release. A release is intended when VERSION names a version whose v tag +does not exist yet. USAGE } @@ -45,7 +45,12 @@ while [[ $# -gt 0 ]]; do done repo_root="$(git rev-parse --show-toplevel)" -current_version="$(node -p "require('${repo_root}/codex-cli/package.json').version")" +version_file="${repo_root}/VERSION" +current_version="$(tr -d '[:space:]' <"$version_file")" +if [[ ! "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "invalid VERSION: ${current_version:-}" >&2 + exit 1 +fi new_version="$current_version" if git rev-parse "v${new_version}" >/dev/null 2>&1; then From 12b28aeb8476248e0dbd76d6c0ddf31929878586 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 14:09:32 -0400 Subject: [PATCH 3/8] docs(release): use Every Code release headers Refs #297 --- docs/release-notes/RELEASE_NOTES.md | 2 +- scripts/check-release-notes-version.sh | 4 ++-- scripts/local/release-notes.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/RELEASE_NOTES.md b/docs/release-notes/RELEASE_NOTES.md index fe7ea3bd784..20a52b60c8d 100644 --- a/docs/release-notes/RELEASE_NOTES.md +++ b/docs/release-notes/RELEASE_NOTES.md @@ -1,4 +1,4 @@ -## @just-every/code v0.6.113 +## Every Code v0.6.113 This release ships a small reliability fix for skill metadata and upstream cursor checks. diff --git a/scripts/check-release-notes-version.sh b/scripts/check-release-notes-version.sh index 510d718c278..dfbdaa0e9eb 100755 --- a/scripts/check-release-notes-version.sh +++ b/scripts/check-release-notes-version.sh @@ -54,8 +54,8 @@ if [ -n "$expected_version" ] && [ "$package_version" != "$expected_version" ]; exit 1 fi -expected_header="## @just-every/code v${package_version}" -actual_header=$(grep -m1 '^## @just-every/code v' "$notes_file" || true) +expected_header="## Every Code v${package_version}" +actual_header=$(grep -m1 '^## Every Code v' "$notes_file" || true) if [ "$actual_header" != "$expected_header" ]; then echo "release notes header mismatch" >&2 diff --git a/scripts/local/release-notes.sh b/scripts/local/release-notes.sh index 9d4a07411eb..0f7328df5d4 100755 --- a/scripts/local/release-notes.sh +++ b/scripts/local/release-notes.sh @@ -82,7 +82,7 @@ CHANGELOG.md House Style Release Notes - Write exactly these sections in order: - 1. Title: ## @just-every/code v${new_version} + 1. Title: ## Every Code v${new_version} 2. One brief intro sentence. 3. Section header: ### Changes - Curate the most interesting release highlights for readers of a GitHub release page. From 4cf583fe205e99d3d40eb909f28466052ceeeaf4 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 14:10:30 -0400 Subject: [PATCH 4/8] chore(release): detach npm staging from active workspace Refs #298 --- .github/dependabot.yaml | 6 ------ build-fast.sh | 7 +------ package.json | 2 +- pnpm-workspace.yaml | 1 - 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 9eeb8f10285..5a6db6ddc26 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -22,12 +22,6 @@ updates: interval: weekly cooldown: default-days: 7 - - package-ecosystem: docker - directory: codex-cli - schedule: - interval: weekly - cooldown: - default-days: 7 - package-ecosystem: github-actions directory: / schedule: diff --git a/build-fast.sh b/build-fast.sh index c0e7366c9b0..e3e41fd2de6 100755 --- a/build-fast.sh +++ b/build-fast.sh @@ -705,8 +705,6 @@ if [ $? -eq 0 ]; then } CLI_TARGET_CODE="../../target/${BIN_SUBDIR}/${BIN_FILENAME}" - CLI_TARGET_CODEX="../../${WORKSPACE_DIR}/target/${BIN_SUBDIR}/${BIN_FILENAME}" - CLI_LINK_ABSOLUTE="" if [ "${TARGET_DIR_ABS}" != "${REPO_TARGET_ABS}" ]; then release_link_target="${BIN_PATH}" @@ -731,10 +729,7 @@ if [ $? -eq 0 ]; then ln -sf "${release_link_target}" "./target/release/${CRATE_PREFIX}" fi - # Update the symlinks in CLI wrapper directories - if [ -d "../codex-cli/bin" ]; then - create_cli_symlinks "../codex-cli/bin" "${CLI_TARGET_CODEX}" - fi + # Update the symlinks in active CLI wrapper directories. if [ -d "./code-cli/bin" ]; then create_cli_symlinks "./code-cli/bin" "${CLI_TARGET_CODE}" fi diff --git a/package.json b/package.json index b724c93f8bd..0ed43ebe729 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "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", - "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 && coder --version); else echo 'no CLI workspace found' >&2; exit 1; fi", + "test:local": "cd code-rs && cargo run -p code-cli -- --version", "fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty" }, "devDependencies": { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bc3454d7b51..d73fc674a08 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,4 @@ packages: - - codex-cli - codex-rs/responses-api-proxy/npm - sdk/typescript - shell-tool-mcp From d96d16c9a71f80807cffca63a2a102a3d00908c9 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 15:17:24 -0400 Subject: [PATCH 5/8] fix(release): preserve npm wrapper compatibility Refs #298 --- .github/workflows/release.yml | 28 ++++++++++++++++++++++++++++ build-fast.sh | 6 +++++- package.json | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 991d040e74d..48b47cbccac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -501,6 +501,34 @@ jobs: fi echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT" + - name: Sync compatibility npm package metadata + shell: bash + run: | + set -euo pipefail + NEW_VERSION="${{ steps.version.outputs.version }}" + node - "$NEW_VERSION" <<'NODE' + const fs = require('fs'); + const path = require('path'); + + const version = process.argv[2]; + const repoRoot = process.cwd(); + const npmFiles = [ + path.join(repoRoot, 'codex-cli', 'package.json'), + path.join(repoRoot, 'codex-cli', '.pack', 'package', 'package.json'), + ].filter((file) => fs.existsSync(file)); + + for (const file of npmFiles) { + const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); + pkg.version = version; + if (pkg.optionalDependencies) { + for (const key of Object.keys(pkg.optionalDependencies)) { + pkg.optionalDependencies[key] = version; + } + } + fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`); + } + NODE + - name: Download all artifacts uses: actions/download-artifact@v4 with: diff --git a/build-fast.sh b/build-fast.sh index e3e41fd2de6..7b3bc1d0338 100755 --- a/build-fast.sh +++ b/build-fast.sh @@ -705,6 +705,7 @@ if [ $? -eq 0 ]; then } CLI_TARGET_CODE="../../target/${BIN_SUBDIR}/${BIN_FILENAME}" + CLI_TARGET_CODEX="../../${WORKSPACE_DIR}/target/${BIN_SUBDIR}/${BIN_FILENAME}" CLI_LINK_ABSOLUTE="" if [ "${TARGET_DIR_ABS}" != "${REPO_TARGET_ABS}" ]; then release_link_target="${BIN_PATH}" @@ -729,7 +730,10 @@ if [ $? -eq 0 ]; then ln -sf "${release_link_target}" "./target/release/${CRATE_PREFIX}" fi - # Update the symlinks in active CLI wrapper directories. + # Update the symlinks in active and compatibility CLI wrapper directories. + if [ -d "../codex-cli/bin" ]; then + create_cli_symlinks "../codex-cli/bin" "${CLI_TARGET_CODEX}" + fi if [ -d "./code-cli/bin" ]; then create_cli_symlinks "./code-cli/bin" "${CLI_TARGET_CODE}" fi diff --git a/package.json b/package.json index 0ed43ebe729..cddc03a1324 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "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", - "test:local": "cd code-rs && cargo run -p code-cli -- --version", + "test:local": "cd code-rs && cargo run -p code-cli --bin code -- --version", "fix": "cargo fmt && cargo fix --allow-dirty && cargo clippy --fix --allow-dirty" }, "devDependencies": { From dd183e88935cf999e28b014c1dc6de83cab9fa99 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 15:23:17 -0400 Subject: [PATCH 6/8] chore(release): remove old npm wrapper Refs #298 --- .github/blob-size-allowlist.txt | 1 - .github/merge-policy.json | 9 +- .github/workflows/release.yml | 28 - .github/workflows/upstream-merge.yml | 15 +- .gitignore | 8 +- .prettierignore | 2 - AGENTS.md | 2 - build-fast.sh | 9 +- code-rs/core/src/parse_command.rs | 6 +- codex-cli/.gitignore | 1 - codex-cli/.pack/package/README.md | 358 -------- codex-cli/.pack/package/bin/coder.js | 470 ----------- codex-cli/.pack/package/package.json | 43 - codex-cli/.pack/package/postinstall.js | 786 ------------------ codex-cli/.pack/package/scripts/preinstall.js | 69 -- .../.pack/package/scripts/windows-cleanup.ps1 | 32 - codex-cli/bin/coder.js | 477 ----------- codex-cli/package-lock.json | 61 -- codex-cli/package.json | 43 - codex-cli/postinstall.js | 786 ------------------ codex-cli/scripts/README.md | 6 - codex-cli/scripts/init_firewall.sh | 115 --- codex-cli/scripts/install_native_deps.sh | 91 -- codex-cli/scripts/preinstall.js | 69 -- codex-cli/scripts/run_in_container.sh | 95 --- codex-cli/scripts/windows-cleanup.ps1 | 31 - codex-cli/test/postinstall.test.js | 14 - docs/exec.md | 2 +- pnpm-lock.yaml | 2 - scripts/upstream-merge/verify.sh | 4 +- 30 files changed, 15 insertions(+), 3620 deletions(-) delete mode 100644 codex-cli/.gitignore delete mode 100644 codex-cli/.pack/package/README.md delete mode 100755 codex-cli/.pack/package/bin/coder.js delete mode 100644 codex-cli/.pack/package/package.json delete mode 100644 codex-cli/.pack/package/postinstall.js delete mode 100644 codex-cli/.pack/package/scripts/preinstall.js delete mode 100644 codex-cli/.pack/package/scripts/windows-cleanup.ps1 delete mode 100755 codex-cli/bin/coder.js delete mode 100644 codex-cli/package-lock.json delete mode 100644 codex-cli/package.json delete mode 100644 codex-cli/postinstall.js delete mode 100644 codex-cli/scripts/README.md delete mode 100644 codex-cli/scripts/init_firewall.sh delete mode 100755 codex-cli/scripts/install_native_deps.sh delete mode 100644 codex-cli/scripts/preinstall.js delete mode 100755 codex-cli/scripts/run_in_container.sh delete mode 100644 codex-cli/scripts/windows-cleanup.ps1 delete mode 100644 codex-cli/test/postinstall.test.js diff --git a/.github/blob-size-allowlist.txt b/.github/blob-size-allowlist.txt index a1ef72e0fed..23fb9d39734 100644 --- a/.github/blob-size-allowlist.txt +++ b/.github/blob-size-allowlist.txt @@ -1,7 +1,6 @@ # Paths are matched exactly, relative to the repository root. # Keep this list short and limited to intentional large checked-in assets. -.github/codex-cli-splash.png MODULE.bazel.lock codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json diff --git a/.github/merge-policy.json b/.github/merge-policy.json index 001388b4e92..cbe8194f03d 100644 --- a/.github/merge-policy.json +++ b/.github/merge-policy.json @@ -1,7 +1,6 @@ { "prefer_ours_globs": [ "codex-rs/tui/**", - "codex-cli/**", "codex-rs/core/src/openai_tools.rs", "codex-rs/core/src/codex.rs", "codex-rs/core/src/agent_tool.rs", @@ -18,13 +17,9 @@ "codex-rs/exec/**", "codex-rs/file-search/**" ], - "purge_globs": [ - ".github/codex-cli-*.png", - ".github/codex-cli-*.jpg", - ".github/codex-cli-*.jpeg", - ".github/codex-cli-*.webp" - ], + "purge_globs": [], "perma_removed_paths": [ + "codex-cli/**", ".github/workflows/rust-ci.yml", ".github/workflows/rust-ci-full.yml" ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48b47cbccac..991d040e74d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -501,34 +501,6 @@ jobs: fi echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT" - - name: Sync compatibility npm package metadata - shell: bash - run: | - set -euo pipefail - NEW_VERSION="${{ steps.version.outputs.version }}" - node - "$NEW_VERSION" <<'NODE' - const fs = require('fs'); - const path = require('path'); - - const version = process.argv[2]; - const repoRoot = process.cwd(); - const npmFiles = [ - path.join(repoRoot, 'codex-cli', 'package.json'), - path.join(repoRoot, 'codex-cli', '.pack', 'package', 'package.json'), - ].filter((file) => fs.existsSync(file)); - - for (const file of npmFiles) { - const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); - pkg.version = version; - if (pkg.optionalDependencies) { - for (const key of Object.keys(pkg.optionalDependencies)) { - pkg.optionalDependencies[key] = version; - } - } - fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`); - } - NODE - - name: Download all artifacts uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/upstream-merge.yml b/.github/workflows/upstream-merge.yml index 61366874838..9df21e83692 100644 --- a/.github/workflows/upstream-merge.yml +++ b/.github/workflows/upstream-merge.yml @@ -426,14 +426,13 @@ jobs: comm -13 .github/auto/DEFAULT_CRATES.txt .github/auto/UPSTREAM_CRATES.txt > .github/auto/DELETED_ON_DEFAULT.txt || true git diff --name-only "origin/${DEFAULT_BRANCH}..upstream/${UPSTREAM_BRANCH}" > .github/auto/DELTA_FILES.txt || true - awk 'BEGIN{tui=cli=core=docs=tests=other=0} + awk 'BEGIN{tui=core=docs=tests=other=0} /^codex-rs\/tui\//{tui++; next} - /^codex-cli\//{cli++; next} /^codex-rs\/(core|common|protocol|exec|file-search)\//{core++; next} /^docs\//{docs++; next} /(^|\/)tests?\//{tests++; next} {other++} - END{printf("tui=%d cli=%d core=%d docs=%d tests=%d other=%d\n",tui,cli,core,docs,tests,other)}' \ + END{printf("tui=%d core=%d docs=%d tests=%d other=%d\n",tui,core,docs,tests,other)}' \ .github/auto/DELTA_FILES.txt > .github/auto/CHANGE_HISTOGRAM.txt FILES_COUNT=$(wc -l < .github/auto/DELTA_FILES.txt | tr -d ' ') @@ -515,7 +514,6 @@ jobs: MERGE_MODE: ${{ steps.prep.outputs.merge_mode || 'one-shot' }} OURS_GLOBS: | codex-rs/tui/** - codex-cli/** .github/workflows/** AGENTS.md README.md @@ -794,11 +792,6 @@ jobs: # Remove any accidentally committed artifacts under .github/auto/** (these should be uploaded, not tracked) auto_tracked=$(git ls-files -- '.github/auto/**' || true) if [ -n "$auto_tracked" ]; then echo "$auto_tracked" | xargs -r git rm -f --; removed=true; fi - # Remove any tracked upstream images disallowed by local policy - for p in .github/codex-cli-*.png .github/codex-cli-*.jpg .github/codex-cli-*.jpeg .github/codex-cli-*.webp; do - files=$(git ls-files -- "$p" || true) - if [ -n "$files" ]; then echo "$files" | xargs -r git rm -f --; removed=true; fi - done # Belt-and-suspenders: drop any accidentally committed cargo cache dirs # Cover both repo-root and nested workspace (e.g., codex-rs/.cargo-home) for d in .cargo-home .cargo2 codex-rs/.cargo-home codex-rs/.cargo2; do @@ -831,10 +824,10 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} run: | set -euo pipefail - # Guide-only branding report: detect user-visible 'Codex' strings under TUI/CLI affected by this merge. + # Guide-only branding report: detect user-visible 'Codex' strings under TUI affected by this merge. mkdir -p .github/auto : > .github/auto/VERIFY_branding.log - changed_files=$(git diff --name-only "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- 'codex-rs/tui/**' 'codex-cli/**' | tr '\n' ' ' || true) + changed_files=$(git diff --name-only "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- 'codex-rs/tui/**' | tr '\n' ' ' || true) if [ -n "${changed_files:-}" ]; then echo "[branding] scanning changed TUI/CLI files for user-visible 'Codex' strings..." | tee -a .github/auto/VERIFY_branding.log git diff -U0 --no-color "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- $changed_files \ diff --git a/.gitignore b/.gitignore index 66eaddc6593..099516dd8ed 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,6 @@ build/ out/ storybook-static/ -# ignore README for publishing -codex-cli/README.md - # ignore Nix derivation results result @@ -67,9 +64,6 @@ result /.aider.tags.cache.*/ # Binary symlinks generated by dev-fast builds -codex-cli/bin/*-darwin -codex-cli/bin/*-linux-* -codex-cli/bin/*-windows-* code-rs/code-cli/bin/ codex-rs/codex-cli/bin/ code-rs/bin/ @@ -97,7 +91,7 @@ code-rs/bin/ /npm-binaries/ /.github/auto/ -# Homebrew formula staging (generated) +# Legacy Homebrew formula staging scratch Formula/Code.rb homebrew-tap/Formula/Code.rb diff --git a/.prettierignore b/.prettierignore index f5b50f6ba22..123ded6aaca 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,3 @@ -/codex-cli/dist -/codex-cli/node_modules pnpm-lock.yaml prompt.md diff --git a/AGENTS.md b/AGENTS.md index 8896115d48e..3b1ee7ed3f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,7 +140,6 @@ When the user asks you to "push" local work: - Never rebase in this flow. Do not use `git pull --rebase` or attempt to replay local commits. - Prefer a simple merge of `origin/main` into the current branch, keeping our local history intact. -- If the remote only has trivial release metadata changes (e.g., `codex-cli/package.json` version bumps), adopt the remote version for those files and keep ours for everything else unless the user specifies otherwise. - If in doubt or if conflicts touch non-trivial areas, pause and ask before resolving. Quick procedure (merge-only): @@ -152,7 +151,6 @@ Quick procedure (merge-only): - Merge without auto-commit: `git merge --no-ff --no-commit origin/main` (stops before committing so you can choose sides) - Resolve policy: - Default to ours: `git checkout --ours .` - - Take remote for trivial package/version files as needed, e.g.: `git checkout --theirs codex-cli/package.json` - Stage and commit the merge with a descriptive message, e.g.: - `git add -A && git commit -m "Merge origin/main: adopt remote version bumps; keep ours elsewhere ()"` - Run `./build-fast.sh` and then `git push` diff --git a/build-fast.sh b/build-fast.sh index 7b3bc1d0338..f958b86d964 100755 --- a/build-fast.sh +++ b/build-fast.sh @@ -674,8 +674,7 @@ if [ $? -eq 0 ]; then echo "Binary location: ${BIN_DISPLAY_PATH}" echo "" - # Keep old symlink locations working for compatibility - # Create symlink in target/release for npm wrapper expectations + # Keep old target symlink locations working for compatibility. release_link_target="../${BIN_SUBDIR}/${BIN_FILENAME}" dev_fast_link_target="../${BIN_SUBDIR}/${BIN_FILENAME}" @@ -705,7 +704,6 @@ if [ $? -eq 0 ]; then } CLI_TARGET_CODE="../../target/${BIN_SUBDIR}/${BIN_FILENAME}" - CLI_TARGET_CODEX="../../${WORKSPACE_DIR}/target/${BIN_SUBDIR}/${BIN_FILENAME}" CLI_LINK_ABSOLUTE="" if [ "${TARGET_DIR_ABS}" != "${REPO_TARGET_ABS}" ]; then release_link_target="${BIN_PATH}" @@ -730,10 +728,7 @@ if [ $? -eq 0 ]; then ln -sf "${release_link_target}" "./target/release/${CRATE_PREFIX}" fi - # Update the symlinks in active and compatibility CLI wrapper directories. - if [ -d "../codex-cli/bin" ]; then - create_cli_symlinks "../codex-cli/bin" "${CLI_TARGET_CODEX}" - fi + # Update the symlinks in active CLI wrapper directories. if [ -d "./code-cli/bin" ]; then create_cli_symlinks "./code-cli/bin" "${CLI_TARGET_CODE}" fi diff --git a/code-rs/core/src/parse_command.rs b/code-rs/core/src/parse_command.rs index 99865e9e433..21be9e67137 100644 --- a/code-rs/core/src/parse_command.rs +++ b/code-rs/core/src/parse_command.rs @@ -378,11 +378,11 @@ mod tests { #[test] fn supports_jq_with_directory_path() { assert_parsed( - &vec_str(&["jq", "-r", ".name, .bin", "codex-cli/package.json"]), + &vec_str(&["jq", "-r", ".name, .bin", "sdk/typescript/package.json"]), vec![ParsedCommand::Search { - cmd: "jq -r '.name, .bin' codex-cli/package.json".to_string(), + cmd: "jq -r '.name, .bin' sdk/typescript/package.json".to_string(), query: Some(".name, .bin".to_string()), - path: Some("codex-cli/package.json".to_string()), + path: Some("sdk/typescript/package.json".to_string()), }], ); } diff --git a/codex-cli/.gitignore b/codex-cli/.gitignore deleted file mode 100644 index 57872d0f1e5..00000000000 --- a/codex-cli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/vendor/ diff --git a/codex-cli/.pack/package/README.md b/codex-cli/.pack/package/README.md deleted file mode 100644 index 9b0aaab18ad..00000000000 --- a/codex-cli/.pack/package/README.md +++ /dev/null @@ -1,358 +0,0 @@ -Every Code Logo - -  - -**Every Code** (Code for short) 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. - -  -## What's new in v0.6.0 (December 2025) - -- **Auto Review** – background ghost-commit watcher runs reviews in a separate worktree whenever a turn changes code; uses `codex-5.1-mini-high` and reports issues plus ready-to-apply fixes without blocking the main thread. -- **Code Bridge** – Sentry-style local bridge that streams errors, console, screenshots, and control from running apps into Code; ships an MCP server; install by asking Code to pull `https://github.com/just-every/code-bridge`. -- **Plays well with Auto Drive** – reviews run in parallel with long Auto Drive tasks so quality checks land while the flow keeps moving. -- **Quality-first focus** – the release shifts emphasis from "can the model write this file" to "did we verify it works". -- _From v0.5.0:_ rename to Every Code, upgraded `/auto` planning/recovery, unified `/settings`, faster streaming/history with card-based activity, and more reliable `/resume` + `/undo`. - - [Read the full notes in RELEASE_NOTES.md](docs/release-notes/RELEASE_NOTES.md) - -  -## Why Every Code - -- 🚀 **Auto Drive orchestration** – Multi-agent automation that now self-heals and ships complete tasks. -- 🌐 **Browser Integration** – CDP support, headless browsing, screenshots captured inline. -- 🤖 **Multi-agent commands** – `/plan`, `/code` and `/solve` coordinate multiple CLI agents. -- 🧭 **Unified settings hub** – `/settings` overlay for limits, theming, approvals, and provider wiring. -- 🎨 **Theme system** – Switch between accessible presets, customize accents, and preview live via `/themes`. -- 🔌 **MCP support** – Extend with filesystem, DBs, APIs, or your own tools. -- 🔒 **Safety modes** – Read-only, approvals, and workspace sandboxing. - -  -## AI Videos - -  -

- - Play Auto Review video -
- Auto Review -

- -  -

- - Play Introducing Auto Drive video -
- Auto Drive Overview -

- -  -

- - Play Multi-Agent Support video -
- Multi-Agent Promo -

- - - -  -## 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" -- **API key** (usage-based) - - Set `export OPENAI_API_KEY=xyz` and run `code` - -### Install Claude & Gemini (optional) - -Every Code supports orchestrating other AI CLI tools. Install these and config to use alongside Code. - -```bash -# Ensure Node.js 20+ is available locally (installs into ~/.n) -npm install -g n -export N_PREFIX="$HOME/.n" -export PATH="$N_PREFIX/bin:$PATH" -n 20.18.1 - -# Install the companion CLIs -export npm_config_prefix="${npm_config_prefix:-$HOME/.npm-global}" -mkdir -p "$npm_config_prefix/bin" -export PATH="$npm_config_prefix/bin:$PATH" -npm install -g @anthropic-ai/claude-code @google/gemini-cli @qwen-code/qwen-code - -# Quick smoke tests -claude --version -gemini --version -qwen --version -``` - -> ℹ️ Add `export N_PREFIX="$HOME/.n"` and `export PATH="$N_PREFIX/bin:$PATH"` (plus the `npm_config_prefix` bin path) to your shell profile so the CLIs stay on `PATH` in future sessions. - -  -## 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" -``` - -### Auto Drive -```bash -# Hand off a multi-step task; Auto Drive will coordinate agents and approvals -/auto "Refactor the auth flow and add device login" - -# Resume or inspect an active Auto Drive run -/auto status -``` - -### 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 Override the model for the active provider (e.g. gpt-5.1) - --read-only Prevent file modifications - --no-approval Skip approval prompts (use with caution) - --config Override config values - --oss Use local open source models - --sandbox Set sandbox level (read-only, workspace-write, etc.) - --help Show help information - --debug Log API requests and responses to file - --version Show version number -``` - -Note: `--model` only changes the model name sent to the active provider. To use a different provider, set `model_provider` in `config.toml`. Providers must expose an OpenAI-compatible API (Chat Completions or Responses). - -  -## Memory & project docs - -Every 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**: Every 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) - -Every 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` Define each server under a named table like `[mcp_servers.]` (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] -> Every 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 Every Code appears to miss settings after upgrading, copy your legacy `~/.codex/config.toml` into `~/.code/`. - -```toml -# Model settings -model = "gpt-5.1" -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.1" -model_provider = "openai" -approval_policy = "never" -model_reasoning_effort = "high" -model_reasoning_summary = "detailed" -``` - -### Environment variables - -- `CODE_HOME`: Override config directory location -- `OPENAI_API_KEY`: Use API key instead of ChatGPT auth -- `OPENAI_BASE_URL`: Use OpenAI-compatible API endpoints (chat or responses) -- `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. Every 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! Every Code 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 -./code-rs/target/dev-fast/code -``` - -#### Git hooks - -This repo ships shared hooks under `.githooks/`. To enable them locally: - -```bash -git config core.hooksPath .githooks -``` - -The `pre-push` hook runs `./pre-release.sh` automatically when pushing to `main`. - -### 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. -- **Every Code** (Code) is **not** affiliated with, sponsored by, or endorsed by OpenAI. - -### Your responsibilities -Using OpenAI, Anthropic or Google services through Every 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` -- 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. Every 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. - -Every Code 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. diff --git a/codex-cli/.pack/package/bin/coder.js b/codex-cli/.pack/package/bin/coder.js deleted file mode 100755 index 92f0adb1fbe..00000000000 --- a/codex-cli/.pack/package/bin/coder.js +++ /dev/null @@ -1,470 +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"; -import { runPostinstall } from "../postinstall.js"; - -// __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. - -function 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) -let binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); -if (!binaryReady) { - let runtimePostinstallError = null; - try { - await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true }); - } catch (err) { - runtimePostinstallError = err; - } - - binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); - if (!binaryReady) { - const ok = await tryBootstrapBinary(); - if (!ok) { - if (runtimePostinstallError && !lastBootstrapError) { - lastBootstrapError = runtimePostinstallError; - } - // 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"); - -// Make the resolved native binary path visible to spawned agents/subprocesses. -process.env.CODE_BINARY_PATH = binaryPath; - -const child = spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - env: { ...process.env, CODER_MANAGED_BY_NPM: "1", CODEX_MANAGED_BY_NPM: "1", CODE_BINARY_PATH: binaryPath }, -}); - -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); -} diff --git a/codex-cli/.pack/package/package.json b/codex-cli/.pack/package/package.json deleted file mode 100644 index 04e6edb240a..00000000000 --- a/codex-cli/.pack/package/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@just-every/code", - "version": "0.6.39", - "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": ">=16" - }, - "files": [ - "bin/coder.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.6.39", - "@just-every/code-darwin-x64": "0.6.39", - "@just-every/code-linux-x64-musl": "0.6.39", - "@just-every/code-linux-arm64-musl": "0.6.39", - "@just-every/code-win32-x64": "0.6.39" - } -} diff --git a/codex-cli/.pack/package/postinstall.js b/codex-cli/.pack/package/postinstall.js deleted file mode 100644 index 01a3b91fb5f..00000000000 --- a/codex-cli/.pack/package/postinstall.js +++ /dev/null @@ -1,786 +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, resolve } 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 }; - } -} - -export async function runPostinstall(options = {}) { - const { skipGlobalAlias = false, invokedByRuntime = false } = options; - if (process.env.CODE_POSTINSTALL_DRY_RUN === '1') { - return { skipped: true }; - } - - if (invokedByRuntime) { - process.env.CODE_RUNTIME_POSTINSTALL = process.env.CODE_RUNTIME_POSTINSTALL || '1'; - } - // 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 (!skipGlobalAlias && 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 detectedWSL = (() => { - 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 || (detectedWSL && 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 - } -} - -function isExecutedDirectly() { - const entry = process.argv[1]; - if (!entry) return false; - try { - return resolve(entry) === fileURLToPath(import.meta.url); - } catch { - return false; - } -} - -if (isExecutedDirectly()) { - runPostinstall().catch(error => { - console.error('Installation failed:', error); - process.exit(1); - }); -} diff --git a/codex-cli/.pack/package/scripts/preinstall.js b/codex-cli/.pack/package/scripts/preinstall.js deleted file mode 100644 index 295fdf93bc8..00000000000 --- a/codex-cli/.pack/package/scripts/preinstall.js +++ /dev/null @@ -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: \..\..\ - 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); diff --git a/codex-cli/.pack/package/scripts/windows-cleanup.ps1 b/codex-cli/.pack/package/scripts/windows-cleanup.ps1 deleted file mode 100644 index 56251e89e60..00000000000 --- a/codex-cli/.pack/package/scripts/windows-cleanup.ps1 +++ /dev/null @@ -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" - diff --git a/codex-cli/bin/coder.js b/codex-cli/bin/coder.js deleted file mode 100755 index de62445adea..00000000000 --- a/codex-cli/bin/coder.js +++ /dev/null @@ -1,477 +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"; -import { runPostinstall } from "../postinstall.js"; - -// __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. - -function 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) -let binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); -if (!binaryReady) { - let runtimePostinstallError = null; - try { - await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true }); - } catch (err) { - runtimePostinstallError = err; - } - - binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath); - if (!binaryReady) { - const ok = await tryBootstrapBinary(); - if (!ok) { - if (runtimePostinstallError && !lastBootstrapError) { - lastBootstrapError = runtimePostinstallError; - } - // 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(" Install and run Code from inside WSL, not from Windows."); - 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(" Use the supported Linux install path from 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"); - -// Make the resolved native binary path visible to spawned agents/subprocesses. -process.env.CODE_BINARY_PATH = binaryPath; -process.env.CODE_COMMAND_NAME = path.basename(process.argv[1] || "code"); - -const child = spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - env: { - ...process.env, - CODER_MANAGED_BY_NPM: "1", - CODEX_MANAGED_BY_NPM: "1", - CODE_BINARY_PATH: binaryPath, - CODE_COMMAND_NAME: process.env.CODE_COMMAND_NAME, - }, -}); - -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 and run Code from inside WSL using the Linux install path."); - } - } 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); -} diff --git a/codex-cli/package-lock.json b/codex-cli/package-lock.json deleted file mode 100644 index 787d81ab6c1..00000000000 --- a/codex-cli/package-lock.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@just-every/code", - "version": "0.6.106", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@just-every/code", - "version": "0.6.106", - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "coder": "bin/coder.js" - }, - "devDependencies": { - "prettier": "^3.3.3" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@just-every/code-darwin-arm64": "0.6.100", - "@just-every/code-darwin-x64": "0.6.100", - "@just-every/code-linux-arm64-musl": "0.6.100", - "@just-every/code-linux-x64-musl": "0.6.100", - "@just-every/code-win32-x64": "0.6.100" - } - }, - "node_modules/@just-every/code-darwin-arm64": { - "optional": true - }, - "node_modules/@just-every/code-darwin-x64": { - "optional": true - }, - "node_modules/@just-every/code-linux-arm64-musl": { - "optional": true - }, - "node_modules/@just-every/code-linux-x64-musl": { - "optional": true - }, - "node_modules/@just-every/code-win32-x64": { - "optional": true - }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - } - } -} diff --git a/codex-cli/package.json b/codex-cli/package.json deleted file mode 100644 index a781194ffdc..00000000000 --- a/codex-cli/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@just-every/code", - "version": "0.6.113", - "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": ">=16" - }, - "files": [ - "bin/coder.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.6.100", - "@just-every/code-darwin-x64": "0.6.100", - "@just-every/code-linux-x64-musl": "0.6.100", - "@just-every/code-linux-arm64-musl": "0.6.100", - "@just-every/code-win32-x64": "0.6.100" - } -} diff --git a/codex-cli/postinstall.js b/codex-cli/postinstall.js deleted file mode 100644 index 01a3b91fb5f..00000000000 --- a/codex-cli/postinstall.js +++ /dev/null @@ -1,786 +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, resolve } 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 }; - } -} - -export async function runPostinstall(options = {}) { - const { skipGlobalAlias = false, invokedByRuntime = false } = options; - if (process.env.CODE_POSTINSTALL_DRY_RUN === '1') { - return { skipped: true }; - } - - if (invokedByRuntime) { - process.env.CODE_RUNTIME_POSTINSTALL = process.env.CODE_RUNTIME_POSTINSTALL || '1'; - } - // 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 (!skipGlobalAlias && 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 detectedWSL = (() => { - 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 || (detectedWSL && 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 - } -} - -function isExecutedDirectly() { - const entry = process.argv[1]; - if (!entry) return false; - try { - return resolve(entry) === fileURLToPath(import.meta.url); - } catch { - return false; - } -} - -if (isExecutedDirectly()) { - runPostinstall().catch(error => { - console.error('Installation failed:', error); - process.exit(1); - }); -} diff --git a/codex-cli/scripts/README.md b/codex-cli/scripts/README.md deleted file mode 100644 index 83bfa96e61e..00000000000 --- a/codex-cli/scripts/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# npm releases - -Use the helpers in this directory to generate npm tarballs for a release. For example, -invoke `build_npm_package.py` after `codex-cli/scripts/install_native_deps.py` has -hydrated `vendor/` for the desired packages; point `--vendor-src` at the populated -`vendor/` tree. diff --git a/codex-cli/scripts/init_firewall.sh b/codex-cli/scripts/init_firewall.sh deleted file mode 100644 index 1251325f014..00000000000 --- a/codex-cli/scripts/init_firewall.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/bash -set -euo pipefail # Exit on error, undefined vars, and pipeline failures -IFS=$'\n\t' # Stricter word splitting - -# Read allowed domains from file -ALLOWED_DOMAINS_FILE="/etc/codex/allowed_domains.txt" -if [ -f "$ALLOWED_DOMAINS_FILE" ]; then - ALLOWED_DOMAINS=() - while IFS= read -r domain; do - ALLOWED_DOMAINS+=("$domain") - done < "$ALLOWED_DOMAINS_FILE" - echo "Using domains from file: ${ALLOWED_DOMAINS[*]}" -else - # Fallback to default domains - ALLOWED_DOMAINS=("api.openai.com") - echo "Domains file not found, using default: ${ALLOWED_DOMAINS[*]}" -fi - -# Ensure we have at least one domain -if [ ${#ALLOWED_DOMAINS[@]} -eq 0 ]; then - echo "ERROR: No allowed domains specified" - exit 1 -fi - -# Flush existing rules and delete existing ipsets -iptables -F -iptables -X -iptables -t nat -F -iptables -t nat -X -iptables -t mangle -F -iptables -t mangle -X -ipset destroy allowed-domains 2>/dev/null || true - -# First allow DNS and localhost before any restrictions -# Allow outbound DNS -iptables -A OUTPUT -p udp --dport 53 -j ACCEPT -# Allow inbound DNS responses -iptables -A INPUT -p udp --sport 53 -j ACCEPT -# Allow localhost -iptables -A INPUT -i lo -j ACCEPT -iptables -A OUTPUT -o lo -j ACCEPT - -# Create ipset with CIDR support -ipset create allowed-domains hash:net - -# Resolve and add other allowed domains -for domain in "${ALLOWED_DOMAINS[@]}"; do - echo "Resolving $domain..." - ips=$(dig +short A "$domain") - if [ -z "$ips" ]; then - echo "ERROR: Failed to resolve $domain" - exit 1 - fi - - while read -r ip; do - if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - echo "ERROR: Invalid IP from DNS for $domain: $ip" - exit 1 - fi - echo "Adding $ip for $domain" - ipset add allowed-domains "$ip" - done < <(echo "$ips") -done - -# Get host IP from default route -HOST_IP=$(ip route | grep default | cut -d" " -f3) -if [ -z "$HOST_IP" ]; then - echo "ERROR: Failed to detect host IP" - exit 1 -fi - -HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") -echo "Host network detected as: $HOST_NETWORK" - -# Set up remaining iptables rules -iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT -iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT - -# Set default policies to DROP first -iptables -P INPUT DROP -iptables -P FORWARD DROP -iptables -P OUTPUT DROP - -# First allow established connections for already approved traffic -iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT - -# Then allow only specific outbound traffic to allowed domains -iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT - -# Append final REJECT rules for immediate error responses -# For TCP traffic, send a TCP reset; for UDP, send ICMP port unreachable. -iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset -iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable -iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset -iptables -A OUTPUT -p udp -j REJECT --reject-with icmp-port-unreachable -iptables -A FORWARD -p tcp -j REJECT --reject-with tcp-reset -iptables -A FORWARD -p udp -j REJECT --reject-with icmp-port-unreachable - -echo "Firewall configuration complete" -echo "Verifying firewall rules..." -if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then - echo "ERROR: Firewall verification failed - was able to reach https://example.com" - exit 1 -else - echo "Firewall verification passed - unable to reach https://example.com as expected" -fi - -# Always verify OpenAI API access is working -if ! curl --connect-timeout 5 https://api.openai.com >/dev/null 2>&1; then - echo "ERROR: Firewall verification failed - unable to reach https://api.openai.com" - exit 1 -else - echo "Firewall verification passed - able to reach https://api.openai.com as expected" -fi diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh deleted file mode 100755 index 4b4835e6017..00000000000 --- a/codex-cli/scripts/install_native_deps.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash - -# Install native runtime dependencies for codex-cli. -# -# Usage -# install_native_deps.sh [--workflow-url URL] [CODEX_CLI_ROOT] -# -# The optional RELEASE_ROOT is the path that contains package.json. Omitting -# it installs the binaries into the repository's own bin/ folder to support -# local development. - -set -euo pipefail - -# ------------------ -# Parse arguments -# ------------------ - -CODEX_CLI_ROOT="" - -# Until we start publishing stable GitHub releases, we have to grab the binaries -# from the GitHub Action that created them. Update the URL below to point to the -# appropriate workflow run: -WORKFLOW_URL="" # Optional override; if empty, caller must pass --workflow-url - -while [[ $# -gt 0 ]]; do - case "$1" in - --workflow-url) - shift || { echo "--workflow-url requires an argument"; exit 1; } - if [ -n "$1" ]; then - WORKFLOW_URL="$1" - fi - ;; - *) - if [[ -z "$CODEX_CLI_ROOT" ]]; then - CODEX_CLI_ROOT="$1" - else - echo "Unexpected argument: $1" >&2 - exit 1 - fi - ;; - esac - shift -done - -# ---------------------------------------------------------------------------- -# Determine where the binaries should be installed. -# ---------------------------------------------------------------------------- - -if [ -n "$CODEX_CLI_ROOT" ]; then - # The caller supplied a release root directory. - BIN_DIR="$CODEX_CLI_ROOT/bin" -else - # No argument; fall back to the repo’s own bin directory. - # Resolve the path of this script, then walk up to the repo root. - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - CODEX_CLI_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - BIN_DIR="$CODEX_CLI_ROOT/bin" -fi - -# Make sure the destination directory exists. -mkdir -p "$BIN_DIR" - -# ---------------------------------------------------------------------------- -# Download and decompress the artifacts from the GitHub Actions workflow. -# ---------------------------------------------------------------------------- - -WORKFLOW_ID="${WORKFLOW_URL##*/}" - -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" - -# 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" - -echo "Installed native dependencies into $BIN_DIR" diff --git a/codex-cli/scripts/preinstall.js b/codex-cli/scripts/preinstall.js deleted file mode 100644 index 295fdf93bc8..00000000000 --- a/codex-cli/scripts/preinstall.js +++ /dev/null @@ -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: \..\..\ - 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); diff --git a/codex-cli/scripts/run_in_container.sh b/codex-cli/scripts/run_in_container.sh deleted file mode 100755 index 54f7e92e7c8..00000000000 --- a/codex-cli/scripts/run_in_container.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -set -e - -# Usage: -# ./run_in_container.sh [--work_dir directory] "COMMAND" -# -# Examples: -# ./run_in_container.sh --work_dir project/code "ls -la" -# ./run_in_container.sh "echo Hello, world!" - -# Default the work directory to WORKSPACE_ROOT_DIR if not provided. -WORK_DIR="${WORKSPACE_ROOT_DIR:-$(pwd)}" -# Default allowed domains - can be overridden with OPENAI_ALLOWED_DOMAINS env var -OPENAI_ALLOWED_DOMAINS="${OPENAI_ALLOWED_DOMAINS:-api.openai.com}" - -# Parse optional flag. -if [ "$1" = "--work_dir" ]; then - if [ -z "$2" ]; then - echo "Error: --work_dir flag provided but no directory specified." - exit 1 - fi - WORK_DIR="$2" - shift 2 -fi - -WORK_DIR=$(realpath "$WORK_DIR") - -# Generate a unique container name based on the normalized work directory -CONTAINER_NAME="codex_$(echo "$WORK_DIR" | sed 's/\//_/g' | sed 's/[^a-zA-Z0-9_-]//g')" - -# Define cleanup to remove the container on script exit, ensuring no leftover containers -cleanup() { - docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true -} -# Trap EXIT to invoke cleanup regardless of how the script terminates -trap cleanup EXIT - -# Ensure a command is provided. -if [ "$#" -eq 0 ]; then - echo "Usage: $0 [--work_dir directory] \"COMMAND\"" - exit 1 -fi - -# Check if WORK_DIR is set. -if [ -z "$WORK_DIR" ]; then - echo "Error: No work directory provided and WORKSPACE_ROOT_DIR is not set." - exit 1 -fi - -# Verify that OPENAI_ALLOWED_DOMAINS is not empty -if [ -z "$OPENAI_ALLOWED_DOMAINS" ]; then - echo "Error: OPENAI_ALLOWED_DOMAINS is empty." - exit 1 -fi - -# Kill any existing container for the working directory using cleanup(), centralizing removal logic. -cleanup - -# Run the container with the specified directory mounted at the same path inside the container. -docker run --name "$CONTAINER_NAME" -d \ - -e OPENAI_API_KEY \ - --cap-add=NET_ADMIN \ - --cap-add=NET_RAW \ - -v "$WORK_DIR:/app$WORK_DIR" \ - codex \ - sleep infinity - -# Write the allowed domains to a file in the container -docker exec --user root "$CONTAINER_NAME" bash -c "mkdir -p /etc/codex" -for domain in $OPENAI_ALLOWED_DOMAINS; do - # Validate domain format to prevent injection - if [[ ! "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - echo "Error: Invalid domain format: $domain" - exit 1 - fi - echo "$domain" | docker exec --user root -i "$CONTAINER_NAME" bash -c "cat >> /etc/codex/allowed_domains.txt" -done - -# Set proper permissions on the domains file -docker exec --user root "$CONTAINER_NAME" bash -c "chmod 444 /etc/codex/allowed_domains.txt && chown root:root /etc/codex/allowed_domains.txt" - -# Initialize the firewall inside the container as root user -docker exec --user root "$CONTAINER_NAME" bash -c "/usr/local/bin/init_firewall.sh" - -# Remove the firewall script after running it -docker exec --user root "$CONTAINER_NAME" bash -c "rm -f /usr/local/bin/init_firewall.sh" - -# Execute the provided command in the container, ensuring it runs in the work directory. -# We use a parameterized bash command to safely handle the command and directory. - -quoted_args="" -for arg in "$@"; do - quoted_args+=" $(printf '%q' "$arg")" -done -docker exec -it "$CONTAINER_NAME" bash -c "cd \"/app$WORK_DIR\" && coder --sandbox workspace-write --ask-for-approval on-request ${quoted_args}" diff --git a/codex-cli/scripts/windows-cleanup.ps1 b/codex-cli/scripts/windows-cleanup.ps1 deleted file mode 100644 index c6798e71410..00000000000 --- a/codex-cli/scripts/windows-cleanup.ps1 +++ /dev/null @@ -1,31 +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. Install the current GitHub Release or run: code update --yes" diff --git a/codex-cli/test/postinstall.test.js b/codex-cli/test/postinstall.test.js deleted file mode 100644 index 596b64afcd1..00000000000 --- a/codex-cli/test/postinstall.test.js +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; - -test('runPostinstall resolves in dry-run mode', async () => { - const { runPostinstall } = await import('../postinstall.js'); - process.env.CODE_POSTINSTALL_DRY_RUN = '1'; - try { - const result = await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true }); - assert.ok(result && result.skipped === true); - } finally { - delete process.env.CODE_POSTINSTALL_DRY_RUN; - delete process.env.CODE_RUNTIME_POSTINSTALL; - } -}); diff --git a/docs/exec.md b/docs/exec.md index d4df5355828..4f16354e54e 100644 --- a/docs/exec.md +++ b/docs/exec.md @@ -46,7 +46,7 @@ Sample output: {"type":"turn.started"} {"type":"item.completed","item":{"id":"item_0","item_type":"reasoning","text":"**Searching for README files**"}} {"type":"item.started","item":{"id":"item_1","item_type":"command_execution","command":"bash -lc ls","aggregated_output":"","status":"in_progress"}} -{"type":"item.completed","item":{"id":"item_1","item_type":"command_execution","command":"bash -lc ls","aggregated_output":"AGENTS.md\nCHANGELOG.md\nREADME.md\ncode-rs\ncodex-rs\ncodex-cli\ndocs\nscripts\nsdk\n","exit_code":0,"status":"completed"}} +{"type":"item.completed","item":{"id":"item_1","item_type":"command_execution","command":"bash -lc ls","aggregated_output":"AGENTS.md\nCHANGELOG.md\nREADME.md\ncode-rs\ncodex-rs\ndocs\nscripts\nsdk\n","exit_code":0,"status":"completed"}} {"type":"item.completed","item":{"id":"item_2","item_type":"reasoning","text":"**Checking repository root for README**"}} {"type":"item.completed","item":{"id":"item_3","item_type":"assistant_message","text":"Yep — there’s a `README.md` in the repository root."}} {"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122}} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 324cd3a50d7..23f501b37ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,6 @@ importers: specifier: ^5.9.2 version: 5.9.2 - codex-cli: {} - codex-rs/responses-api-proxy/npm: {} sdk/typescript: diff --git a/scripts/upstream-merge/verify.sh b/scripts/upstream-merge/verify.sh index 18c6a9877db..280f7207267 100755 --- a/scripts/upstream-merge/verify.sh +++ b/scripts/upstream-merge/verify.sh @@ -99,13 +99,13 @@ echo "guards=${status_guards}" >> "$guards_log" # STEP 4: Branding guard parity with CI (non-fixing check) { - echo "[verify] STEP 4: branding guard (TUI/CLI user-visible)" + echo "[verify] STEP 4: branding guard (TUI user-visible)" } DEFAULT_BRANCH_LOCAL=${DEFAULT_BRANCH:-main} # Try to fetch origin to ensure refs exist; ignore failure for local runs git fetch origin "$DEFAULT_BRANCH_LOCAL" >/dev/null 2>&1 || true range_ref="origin/${DEFAULT_BRANCH_LOCAL}..HEAD" -changed_files=$(git diff --name-only $range_ref -- 'codex-rs/tui/**' 'codex-cli/**' | tr '\n' ' ' || true) +changed_files=$(git diff --name-only $range_ref -- 'codex-rs/tui/**' | tr '\n' ' ' || true) branding_log=.github/auto/VERIFY_branding.log : > "$branding_log" if [ -n "${changed_files:-}" ]; then From 71507c5bdb3e323346430a878b8f9dd605ee6b6b Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 15:43:58 -0400 Subject: [PATCH 7/8] fix(updates): normalize direct install paths Refs #295 --- AGENTS.md | 2 +- code-rs/cli/src/update.rs | 8 +++++++- code-rs/tui/src/updates.rs | 14 +++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3b1ee7ed3f9..66614ede650 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ Examples: and `.github/github.json` PR workflow metadata when watching fix trains, auto-review lag, CI, review comments, and merge readiness. - Use `just local-code-rebuild` to rebuild the current branch into the PATH-resolved binary. -- After `./build-fast.sh`, run `just local-code-rebuild` again before release smoke checks; the fast build validates dev-fast artifacts, while the rebuild recipe owns the PATH-resolved release binary and embeds the package version. +- After `./build-fast.sh`, run `just local-code-rebuild` again before release smoke checks; the fast build validates dev-fast artifacts, while the rebuild recipe owns the PATH-resolved release binary and embeds the `VERSION` file value. - During active local work, run `just local-cleanup-space --apply --keep-current-fast-cache` when repo-local build caches grow large; this removes stale per-branch `./build-fast.sh` diff --git a/code-rs/cli/src/update.rs b/code-rs/cli/src/update.rs index 3192d9a2188..12aa829aa76 100644 --- a/code-rs/cli/src/update.rs +++ b/code-rs/cli/src/update.rs @@ -377,7 +377,7 @@ fn install_mode_description(exe: &Path) -> &'static str { } fn is_direct_binary_install_path(exe: &Path) -> bool { - let path = exe.to_string_lossy(); + let path = exe.to_string_lossy().replace('\\', "/"); path.contains("/.code/bin/") || path.contains("/.local/bin/") || path.contains("/usr/local/bin/") @@ -529,6 +529,12 @@ mod tests { assert!(is_direct_binary_install_path(Path::new( "/usr/local/bin/chris-code" ))); + assert!(is_direct_binary_install_path(Path::new( + r"C:\Users\me\.code\bin\code.exe" + ))); + assert!(!is_direct_binary_install_path(Path::new( + r"C:\Users\me\AppData\Roaming\npm\code.exe" + ))); } #[test] diff --git a/code-rs/tui/src/updates.rs b/code-rs/tui/src/updates.rs index 3d8adf6ce06..9b00cc7197f 100644 --- a/code-rs/tui/src/updates.rs +++ b/code-rs/tui/src/updates.rs @@ -243,7 +243,7 @@ fn resolve_install_target(exe_path: &Path) -> PathBuf { } fn is_direct_binary_install_path(exe_path: &Path) -> bool { - let path = exe_path.to_string_lossy(); + let path = exe_path.to_string_lossy().replace('\\', "/"); path.contains("/.code/bin/") || path.contains("/.local/bin/") || path.contains("/usr/local/bin/") @@ -707,6 +707,18 @@ mod tests { } } + #[test] + fn upgrade_resolution_allows_windows_direct_binary_paths() { + match resolve_upgrade_resolution_for_exe(Path::new( + r"C:\Users\me\.code\bin\chris-code.exe", + )) { + UpgradeResolution::Command { display, .. } => { + assert_eq!(display, "chris-code update --yes"); + } + UpgradeResolution::Manual { instructions } => panic!("unexpected manual resolution: {instructions}"), + } + } + #[test] fn upgrade_resolution_prefers_command_name_env() { let _lock = ENV_TEST_LOCK.lock().unwrap(); From 15d898d7fb6d0101481907f1cdb83c0dab12ec7c Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 1 Jun 2026 15:50:27 -0400 Subject: [PATCH 8/8] test(updates): cover direct binary path separators Refs #295 --- code-rs/cli/src/update.rs | 2 +- code-rs/tui/src/updates.rs | 6 +++--- ...0_chatwidget_snapshot__settings_help_overlay_closed.snap | 2 +- ...100_chatwidget_snapshot__settings_help_overlay_open.snap | 2 +- ...atwidget_snapshot__settings_overlay_overview_layout.snap | 2 +- ..._chatwidget_snapshot__settings_overview_hints_clean.snap | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/code-rs/cli/src/update.rs b/code-rs/cli/src/update.rs index 12aa829aa76..ce13a60dab2 100644 --- a/code-rs/cli/src/update.rs +++ b/code-rs/cli/src/update.rs @@ -247,7 +247,7 @@ fn command_name_from_path(path: &Path) -> Option { } fn valid_command_name(name: &str) -> Option { - let trimmed = name.trim(); + let trimmed = name.trim().trim_end_matches(".exe"); if trimmed.is_empty() || trimmed.contains(std::path::MAIN_SEPARATOR) { None } else { diff --git a/code-rs/tui/src/updates.rs b/code-rs/tui/src/updates.rs index 9b00cc7197f..57b33edaa64 100644 --- a/code-rs/tui/src/updates.rs +++ b/code-rs/tui/src/updates.rs @@ -218,13 +218,13 @@ fn upgrade_command_name(exe_path: &Path, install_target: &Path) -> String { } fn command_name_from_path(path: &Path) -> Option { - path.file_name() - .and_then(|name| name.to_str()) + path.to_str() + .and_then(|value| value.rsplit(['/', '\\']).next()) .and_then(valid_command_name) } fn valid_command_name(name: &str) -> Option { - let trimmed = name.trim(); + let trimmed = name.trim().trim_end_matches(".exe"); if trimmed.is_empty() || trimmed.contains(std::path::MAIN_SEPARATOR) { None } else { diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap index 2c25b496bd0..0949e8a68db 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_closed.snap @@ -14,7 +14,7 @@ expression: closed | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control GitHub Release update checks and automatic upgrades. | + | + Control GitHub Release update checks and automatic upgrades. | | | | | diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap index 0437f043e08..4d266457923 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_help_overlay_open.snap @@ -14,7 +14,7 @@ expression: open | + Switch between preset | | | | |• ↑/↓ Move between sections | | | Updates Auto |• Enter Open selected section | | - | + Control CLI auto-updat|• Tab Jump forward between sections | | + | + Control GitHub Release|• Tab Jump forward between sections | | | |• Esc Close settings | | | ------------------------|• ? Toggle this help |-------------------------- | | | | | diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap index af57b8f8846..fd97641ca4e 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overlay_overview_layout.snap @@ -14,7 +14,7 @@ expression: output | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control GitHub Release update checks and automatic upgrades. | + | + Control GitHub Release update checks and automatic upgrades. | | | | | diff --git a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap index 87dfe1cc361..863b31ac0e7 100644 --- a/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap +++ b/code-rs/tui/tests/snapshots/vt100_chatwidget_snapshot__settings_overview_hints_clean.snap @@ -14,7 +14,7 @@ expression: output | + Switch between preset color palettes and adjust contrast. | | | | Updates Auto update: Disabled | - | + Control GitHub Release update checks and automatic upgrades. | + | + Control GitHub Release update checks and automatic upgrades. | | ↑ ↓ Move Enter Open Esc Close ? Help | | | +------------------------------------------------------------------------------------------------+