From f966d092c55eca0701fd1720de3badebd8a00a09 Mon Sep 17 00:00:00 2001 From: yimwoo Date: Thu, 26 Mar 2026 19:56:30 -0700 Subject: [PATCH] Replace copied-bundle Codex plugin with source checkout model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install.sh --codex-plugin now clones to ~/.codex/plugins/hotl-source/ instead of rsyncing a bundle to ~/.codex/plugins/hotl/ - install.sh --codex-plugin --local points marketplace at current checkout - update.sh --codex-plugin updates the source checkout with backup protection - update.sh (no flags) also auto-detects and updates the plugin source checkout, so the curl one-liner covers all install modes - Delete .codex-plugin/marketplace.json — installer generates entries from plugin.json; template was drift-prone and used undocumented source type - --status now reports source checkout health alongside marketplace registration - Migration: old copied bundle at ~/.codex/plugins/hotl/ is detected and removed during re-install Co-Authored-By: Claude Opus 4.6 (1M context) --- .codex-plugin/marketplace.json | 24 ----- README.md | 2 +- docs/README.codex.md | 66 +++++++++--- install.sh | 71 ++++++------- test/smoke.bats | 8 +- update.ps1 | 102 ++++++++++++++++--- update.sh | 181 ++++++++++++++++++++++++--------- 7 files changed, 314 insertions(+), 140 deletions(-) delete mode 100644 .codex-plugin/marketplace.json diff --git a/.codex-plugin/marketplace.json b/.codex-plugin/marketplace.json deleted file mode 100644 index 1ffe361..0000000 --- a/.codex-plugin/marketplace.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://openai.com/codex/marketplace.schema.json", - "name": "hotl-plugin", - "description": "Human-on-the-Loop (HOTL) operating model for AI-native development", - "owner": { - "name": "yimwoo" - }, - "plugins": [ - { - "name": "hotl", - "description": "Human-on-the-Loop operating model: skills, workflows, and adapters for AI-native development", - "version": "2.10.0", - "author": { - "name": "yimwoo" - }, - "source": { - "source": "url", - "url": "https://github.com/yimwoo/hotl-plugin.git" - }, - "category": "productivity", - "strict": false - } - ] -} diff --git a/README.md b/README.md index 5c8d920..ec5e825 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Want to create or modify HOTL skills? See [Authoring Skills vs Agents](docs/auth curl -fsSL https://raw.githubusercontent.com/yimwoo/hotl-plugin/main/update.sh | bash ``` -Covers Claude Code, native-skills Codex installs, and Cline, and skips tools that are not installed. Codex plugin installs are updated through Codex's plugin lifecycle instead. In Claude Code, you can also run `/hotl:check-update`. For backup behavior, manual checks, and `--force-codex`, see [Updating HOTL](docs/updating.md). +Covers Claude Code, Codex (both native-skills and plugin source checkout), and Cline. Skips tools that are not installed. In Claude Code, you can also run `/hotl:check-update`. For backup behavior, manual checks, and `--force-codex`, see [Updating HOTL](docs/updating.md). ## Supported Tools diff --git a/docs/README.codex.md b/docs/README.codex.md index 6215a41..b4d8a31 100644 --- a/docs/README.codex.md +++ b/docs/README.codex.md @@ -11,9 +11,9 @@ HOTL for Codex can be installed either as a native Codex plugin or as native ski ### Plugin Install (Recommended) -Requires a Codex version with plugin support. Register HOTL as a Codex plugin for -stable, versioned team distribution. Codex manages the plugin cache and lifecycle — -no manual symlinks needed. +Requires a Codex version with plugin support. The installer clones the HOTL repo +to a source checkout at `~/.codex/plugins/hotl-source/` and registers it in +`~/.agents/plugins/marketplace.json` as a local Codex plugin. 1. Clone the repo (or use an existing checkout): @@ -27,23 +27,42 @@ git clone https://github.com/yimwoo/hotl-plugin /tmp/hotl-plugin bash /tmp/hotl-plugin/install.sh --codex-plugin ``` -This copies the HOTL plugin bundle into `~/.codex/plugins/hotl` and registers it -in `~/.agents/plugins/marketplace.json` as a local Codex plugin. +This clones HOTL to `~/.codex/plugins/hotl-source/` and writes a marketplace +entry with `"source": "local"` pointing at that checkout. 3. Restart Codex. 4. Open the Codex plugin directory, switch the source to **Local Plugins**, and click **Add to Codex** for HOTL. -**For contributors** testing the plugin packaging from a repo checkout, use `--local` -to install the plugin under the repo and write to the repo-local marketplace -instead of your user-global config: +**For contributors** testing the plugin from a working copy, use `--local` +to point the marketplace at your current checkout without cloning: ```bash bash install.sh --codex-plugin --local ``` -Plugin updates are handled through Codex's plugin refresh flow, not `update.sh`. +This writes a repo-local marketplace entry at `.agents/plugins/marketplace.json` +pointing at your checkout directory. + +**Generated marketplace entry shape:** + +```json +{ + "name": "hotl", + "source": { + "source": "local", + "path": "~/.codex/plugins/hotl-source" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" +} +``` + +Plugin updates are handled via `update.sh --codex-plugin` (see Updating below). ### Native Skills Install (Fallback / Development) @@ -284,9 +303,23 @@ Restart Codex after updating so it re-discovers the latest skill files. ### Plugin Install -Plugin updates are managed through Codex's plugin lifecycle. When a new HOTL -version is available, refresh or reinstall the plugin through Codex's plugin UI. -`update.sh` does not apply to plugin installs. +The standard curl one-liner also updates the plugin source checkout if it exists: + +```bash +curl -fsSL https://raw.githubusercontent.com/yimwoo/hotl-plugin/main/update.sh | bash +``` + +Or update only the plugin checkout: + +```bash +bash ~/.codex/plugins/hotl-source/update.sh --codex-plugin +``` + +If the source checkout has local changes, they are backed up to +`~/.codex/backups/hotl-plugin//` before resetting. +Use `--force-codex-plugin` to skip the backup. + +Restart Codex after updating so it picks up the new plugin files. ## Codex Manual Canary @@ -343,8 +376,13 @@ Remove-Item -Recurse -Force "$env:USERPROFILE\.codex\hotl" ### Plugin Install -Uninstall HOTL through Codex's plugin UI. To also remove the marketplace entry: +Remove the source checkout and marketplace entry: -**macOS / Linux:** Edit `~/.agents/plugins/marketplace.json` and remove the `hotl` entry. +**macOS / Linux:** + +```bash +rm -rf ~/.codex/plugins/hotl-source +# Edit ~/.agents/plugins/marketplace.json and remove the hotl entry +``` **Repo-local:** Delete `.agents/plugins/marketplace.json` or remove the `hotl` entry from it. diff --git a/install.sh b/install.sh index 9621dbc..bd6a6d9 100755 --- a/install.sh +++ b/install.sh @@ -40,37 +40,49 @@ if [ "$CODEX_PLUGIN" = true ]; then exit 1 fi + GIT_REPO_URL="https://github.com/yimwoo/hotl-plugin.git" + if [ "$LOCAL" = true ]; then - INSTALL_ROOT="${SCRIPT_DIR}" - MARKETPLACE_DIR="${INSTALL_ROOT}/.agents/plugins" + # Local/contributor mode: point marketplace at the current checkout + MARKETPLACE_DIR="${SCRIPT_DIR}/.agents/plugins" MARKETPLACE_FILE="${MARKETPLACE_DIR}/marketplace.json" - PLUGIN_ROOT="${INSTALL_ROOT}/.codex/plugins/${PLUGIN_NAME}" - PLUGIN_SOURCE_PATH="./.codex/plugins/${PLUGIN_NAME}" - echo "Installing HOTL in repo-local Codex plugin directory: ${PLUGIN_ROOT}" + PLUGIN_SOURCE_PATH="${SCRIPT_DIR}" echo "Registering HOTL in repo-local Codex marketplace: ${MARKETPLACE_FILE}" + echo "Plugin source: current checkout at ${SCRIPT_DIR}" else - INSTALL_ROOT="${HOME}" + # User-global mode: clone source checkout MARKETPLACE_DIR="${HOME}/.agents/plugins" MARKETPLACE_FILE="${MARKETPLACE_DIR}/marketplace.json" - PLUGIN_ROOT="${HOME}/.codex/plugins/${PLUGIN_NAME}" - PLUGIN_SOURCE_PATH="./.codex/plugins/${PLUGIN_NAME}" - echo "Installing HOTL in user-global Codex plugin directory: ${PLUGIN_ROOT}" - echo "Registering HOTL in user-global Codex marketplace: ${MARKETPLACE_FILE}" + PLUGIN_SOURCE_PATH="${HOME}/.codex/plugins/hotl-source" + + if [ -d "${PLUGIN_SOURCE_PATH}/.git" ]; then + echo "Updating existing source checkout at ${PLUGIN_SOURCE_PATH}..." + git -C "${PLUGIN_SOURCE_PATH}" fetch origin main + git -C "${PLUGIN_SOURCE_PATH}" reset --hard origin/main + else + echo "Cloning HOTL to ${PLUGIN_SOURCE_PATH}..." + mkdir -p "$(dirname "${PLUGIN_SOURCE_PATH}")" + git clone "${GIT_REPO_URL}" "${PLUGIN_SOURCE_PATH}" + fi + + # Migration: remove old copied-bundle install if present + OLD_BUNDLE="${HOME}/.codex/plugins/hotl" + if [ -d "${OLD_BUNDLE}" ] && [ ! -d "${OLD_BUNDLE}/.git" ]; then + echo "" + echo "Migrating from copied-bundle plugin install at ${OLD_BUNDLE}" + echo "to source checkout at ${PLUGIN_SOURCE_PATH}..." + rm -rf "${OLD_BUNDLE}" + echo "Old bundle removed." + fi fi - PLUGIN_MANIFEST="${SCRIPT_DIR}/.codex-plugin/plugin.json" + PLUGIN_MANIFEST="${PLUGIN_SOURCE_PATH}/.codex-plugin/plugin.json" if [ ! -f "$PLUGIN_MANIFEST" ]; then echo "Error: ${PLUGIN_MANIFEST} not found." >&2 - echo "Are you running install.sh from the hotl-plugin repository?" >&2 exit 1 fi - mkdir -p "$MARKETPLACE_DIR" "$(dirname "${PLUGIN_ROOT}")" - rsync -a --delete \ - --exclude '.git' \ - --exclude '.codex/plugins' \ - --exclude '.agents/plugins' \ - "${SCRIPT_DIR}/" "${PLUGIN_ROOT}/" + mkdir -p "$MARKETPLACE_DIR" python3 -c " import json, sys, os @@ -83,20 +95,6 @@ owner_name = os.environ.get('USER', 'unknown') with open(manifest_path) as f: manifest = json.load(f) -manifest.setdefault('interface', { - 'displayName': 'HOTL', - 'shortDescription': 'Human-on-the-Loop workflows for structured AI development', - 'longDescription': 'Use HOTL to brainstorm, plan, execute, review, and verify software changes with explicit human-on-the-loop contracts.', - 'developerName': owner_name, - 'category': 'Productivity', - 'capabilities': ['Interactive', 'Read', 'Write'], - 'defaultPrompt': 'Plan, execute, review, and verify coding tasks with HOTL workflows' -}) - -with open(manifest_path, 'w') as f: - json.dump(manifest, f, indent=2) - f.write('\n') - hotl_entry = { 'name': manifest['name'], 'description': manifest['description'], @@ -113,6 +111,9 @@ hotl_entry = { 'category': 'Productivity', } +if manifest.get('interface'): + hotl_entry['interface'] = manifest['interface'] + # Read or create the destination marketplace file if os.path.exists(dest_path): with open(dest_path) as f: @@ -148,7 +149,7 @@ with open(dest_path, 'w') as f: action = 'Updated' if updated else 'Added' print(f'{action} HOTL plugin entry (version {hotl_entry[\"version\"]})') -" "$PLUGIN_ROOT/.codex-plugin/plugin.json" "$MARKETPLACE_FILE" "$PLUGIN_SOURCE_PATH" +" "$PLUGIN_MANIFEST" "$MARKETPLACE_FILE" "$PLUGIN_SOURCE_PATH" # Warn if an existing native-skills install is detected if [ -L "${HOME}/.agents/skills/hotl" ] || [ -d "${HOME}/.codex/hotl" ]; then @@ -169,12 +170,12 @@ print(f'{action} HOTL plugin entry (version {hotl_entry[\"version\"]})') fi echo "" - echo "HOTL Codex plugin prepared. Next steps:" + echo "HOTL Codex plugin registered. Next steps:" echo " 1. Restart Codex to discover the plugin" echo " 2. Open the Codex plugin directory, switch to Local Plugins," echo " and click Add to Codex for HOTL" echo "" - echo "To update later, use Codex's plugin refresh flow." + echo "To update later, run: update.sh (updates all installs) or update.sh --codex-plugin (plugin only)" echo "For native skills install (dev/iteration), see docs/README.codex.md" exit 0 fi diff --git a/test/smoke.bats b/test/smoke.bats index 2413e26..d5282b7 100644 --- a/test/smoke.bats +++ b/test/smoke.bats @@ -296,18 +296,20 @@ assert 'sessionStartNotice' not in data.get('hookSpecificOutput', {}) ver=$(cat "$REPO_ROOT/VERSION" | tr -d '[:space:]') [ -n "$ver" ] || { echo "VERSION file is empty"; return 1; } - local claude_ver cursor_ver market_ver codex_ver codex_market_ver + local claude_ver cursor_ver market_ver codex_ver claude_ver=$(python3 -c "import json; print(json.load(open('$REPO_ROOT/.claude-plugin/plugin.json'))['version'])") cursor_ver=$(python3 -c "import json; print(json.load(open('$REPO_ROOT/.cursor-plugin/plugin.json'))['version'])") market_ver=$(python3 -c "import json; print(json.load(open('$REPO_ROOT/.claude-plugin/marketplace.json'))['plugins'][0]['version'])") codex_ver=$(python3 -c "import json; print(json.load(open('$REPO_ROOT/.codex-plugin/plugin.json'))['version'])") - codex_market_ver=$(python3 -c "import json; print(json.load(open('$REPO_ROOT/.codex-plugin/marketplace.json'))['plugins'][0]['version'])") [ "$ver" = "$claude_ver" ] || { echo "VERSION ($ver) != .claude-plugin/plugin.json ($claude_ver)"; return 1; } [ "$ver" = "$cursor_ver" ] || { echo "VERSION ($ver) != .cursor-plugin/plugin.json ($cursor_ver)"; return 1; } [ "$ver" = "$market_ver" ] || { echo "VERSION ($ver) != .claude-plugin/marketplace.json ($market_ver)"; return 1; } [ "$ver" = "$codex_ver" ] || { echo "VERSION ($ver) != .codex-plugin/plugin.json ($codex_ver)"; return 1; } - [ "$ver" = "$codex_market_ver" ] || { echo "VERSION ($ver) != .codex-plugin/marketplace.json ($codex_market_ver)"; return 1; } +} + +@test ".codex-plugin/marketplace.json does not exist (installer generates entries)" { + [ ! -f "$REPO_ROOT/.codex-plugin/marketplace.json" ] } # ── command/skill name collision guard ──────────────────────────────────────── diff --git a/update.ps1 b/update.ps1 index f3aea51..a196dc3 100644 --- a/update.ps1 +++ b/update.ps1 @@ -6,7 +6,9 @@ param( [switch]$ForceCodex, [switch]$Check, [switch]$NativeSkills, - [switch]$Status + [switch]$Status, + [switch]$CodexPlugin, + [switch]$ForceCodexPlugin ) $ErrorActionPreference = "Stop" @@ -138,6 +140,20 @@ if ($Status) { Write-Host "Codex plugin: not found" } + # Show source checkout health + $CodexSource = Join-Path $env:USERPROFILE ".codex\plugins\hotl-source" + if ($PluginFound) { + if (Test-Path (Join-Path $CodexSource ".git")) { + $SrcRev = try { (git -C $CodexSource rev-parse --short HEAD 2>$null) } catch { "unknown" } + Write-Host " source checkout (rev $SrcRev) at $CodexSource" + } else { + Write-Host " WARNING: source checkout not found at $CodexSource" + } + } elseif (Test-Path (Join-Path $CodexSource ".git")) { + $SrcRev = try { (git -C $CodexSource rev-parse --short HEAD 2>$null) } catch { "unknown" } + Write-Host "Codex plugin: source checkout (rev $SrcRev) at $CodexSource (no marketplace entry)" + } + # Warn if both Codex modes are present if ($CodexNativeFound -and $PluginFound) { Write-Host "" @@ -160,6 +176,50 @@ if ($Status) { exit 0 } +# -CodexPlugin: update the plugin source checkout, then exit +if ($CodexPlugin) { + $CodexPluginSource = Join-Path $env:USERPROFILE ".codex\plugins\hotl-source" + + if (-not (Test-Path (Join-Path $CodexPluginSource ".git"))) { + Write-Host "No HOTL plugin source checkout found at $CodexPluginSource." + Write-Host "Run install.sh --codex-plugin first." + $NativeSkillsDir = Join-Path $env:USERPROFILE ".codex\hotl" + if (Test-Path $NativeSkillsDir) { + Write-Host "" + Write-Host "Hint: a native-skills install exists at $NativeSkillsDir." + Write-Host "Use update.ps1 (without -CodexPlugin) to update that instead." + } + exit 1 + } + + Write-Host "Updating Codex plugin source checkout at $CodexPluginSource..." + + if (Test-HasLocalChanges $CodexPluginSource) { + if ($ForceCodexPlugin) { + Write-Host "Source checkout has local changes; -ForceCodexPlugin set, skipping backup." + } else { + $BackupRoot = Join-Path $env:USERPROFILE ".codex\backups\hotl-plugin" + $Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $BackupDir = Join-Path $BackupRoot $Timestamp + New-Item -ItemType Directory -Force -Path (Join-Path $BackupDir "worktree") | Out-Null + git -C $CodexPluginSource status --short --branch > (Join-Path $BackupDir "git-status.txt") 2>$null + Copy-Item -Recurse -Force -Path (Join-Path $CodexPluginSource "*") -Destination (Join-Path $BackupDir "worktree") -Exclude ".git" + Write-Host "Source checkout has local changes; backed them up to $BackupDir." + } + } + + git -C $CodexPluginSource fetch origin main + git -C $CodexPluginSource reset --hard origin/main + git -C $CodexPluginSource clean -fd + + $VerFile = Join-Path $CodexPluginSource "VERSION" + $Ver = if (Test-Path $VerFile) { (Get-Content $VerFile -Raw).Trim() } else { "unknown" } + Write-Host " Updated to version $Ver." + Write-Host "" + Write-Host "Restart Codex to pick up the updated plugin." + exit 0 +} + $ClaudePluginDir = Join-Path $env:USERPROFILE ".claude\plugins\hotl" $ClaudeCacheDir = Join-Path $env:USERPROFILE ".claude\plugins\cache\hotl-plugin\hotl" $CodexHotlDir = Join-Path $env:USERPROFILE ".codex\hotl" @@ -207,19 +267,35 @@ if (Test-GitWorkTree $ClaudePluginDir) { # -- Codex ------------------------------------------------------------------------- -# Detect if HOTL is registered as a Codex plugin (user-global or repo-local) -$CodexPluginRegistered = $false -foreach ($CodexMarketplace in @($CodexMarketplaceUser, $CodexMarketplaceLocal)) { - if (Test-HotlInMarketplace $CodexMarketplace) { - $CodexPluginRegistered = $true - $PluginVer = Get-HotlMarketplaceVersion $CodexMarketplace - if (-not $PluginVer) { $PluginVer = "unknown" } - Write-Host "Note: HOTL is also registered as a Codex plugin (v$PluginVer) in" - Write-Host " $CodexMarketplace" - Write-Host "Plugin updates are managed through Codex's plugin UI; this updater only" - Write-Host "updates the $CodexHotlDir native-skills install." - Write-Host "" +# Update Codex plugin source checkout if it exists +$CodexPluginSource = Join-Path $env:USERPROFILE ".codex\plugins\hotl-source" +if (Test-Path (Join-Path $CodexPluginSource ".git")) { + $Found++ + Write-Host "Updating Codex plugin source checkout at $CodexPluginSource..." + + if (Test-HasLocalChanges $CodexPluginSource) { + if ($ForceCodexPlugin) { + Write-Host " Source checkout has local changes; -ForceCodexPlugin set, skipping backup." + } else { + $BackupRoot = Join-Path $env:USERPROFILE ".codex\backups\hotl-plugin" + $Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $BackupDir = Join-Path $BackupRoot $Timestamp + New-Item -ItemType Directory -Force -Path (Join-Path $BackupDir "worktree") | Out-Null + git -C $CodexPluginSource status --short --branch > (Join-Path $BackupDir "git-status.txt") 2>$null + Copy-Item -Recurse -Force -Path (Join-Path $CodexPluginSource "*") -Destination (Join-Path $BackupDir "worktree") -Exclude ".git" + Write-Host " Source checkout has local changes; backed them up to $BackupDir." + } } + + git -C $CodexPluginSource fetch origin main + git -C $CodexPluginSource reset --hard origin/main + git -C $CodexPluginSource clean -fd + + $VerFile = Join-Path $CodexPluginSource "VERSION" + $Ver = if (Test-Path $VerFile) { (Get-Content $VerFile -Raw).Trim() } else { "unknown" } + $Updated++ + Write-Host " Codex plugin source updated (v$Ver)." + Write-Host "" } if (Test-GitWorkTree $CodexHotlDir) { diff --git a/update.sh b/update.sh index 4b02b77..6414709 100755 --- a/update.sh +++ b/update.sh @@ -8,6 +8,8 @@ FORCE_CODEX=0 CHECK_ONLY=0 SWITCH_NATIVE_SKILLS=0 STATUS_ONLY=0 +CODEX_PLUGIN=0 +FORCE_CODEX_PLUGIN=0 while [ $# -gt 0 ]; do case "$1" in @@ -23,9 +25,16 @@ while [ $# -gt 0 ]; do --status) STATUS_ONLY=1 ;; + --codex-plugin) + CODEX_PLUGIN=1 + ;; + --force-codex-plugin) + FORCE_CODEX_PLUGIN=1 + CODEX_PLUGIN=1 + ;; *) echo "Unknown option: $1" >&2 - echo "Usage: bash update.sh [--check] [--force-codex] [--native-skills] [--status]" >&2 + echo "Usage: bash update.sh [--check] [--force-codex] [--native-skills] [--status] [--codex-plugin] [--force-codex-plugin]" >&2 exit 1 ;; esac @@ -81,6 +90,43 @@ except Exception: " "${mkt_path}" 2>/dev/null } +is_git_work_tree() { + local path="$1" + [ -e "${path}" ] && git -C "${path}" rev-parse --is-inside-work-tree > /dev/null 2>&1 +} + +current_branch() { + git -C "$1" branch --show-current 2>/dev/null || true +} + +has_local_changes() { + local path="$1" + [ -n "$(git -C "${path}" status --short --untracked-files=all 2>/dev/null)" ] +} + +resolve_path() { + local path="$1" + ( + cd "${path}" > /dev/null 2>&1 && pwd -P + ) +} + +backup_codex_install() { + local path="$1" + local real_path backup_root timestamp backup_dir + + real_path="$(resolve_path "${path}")" + backup_root="${HOME}/.codex/backups/hotl" + timestamp="$(date +"%Y%m%d-%H%M%S")" + backup_dir="${backup_root}/${timestamp}" + + mkdir -p "${backup_dir}/worktree" + git -C "${path}" status --short --branch > "${backup_dir}/git-status.txt" 2>/dev/null || true + rsync -a --exclude '.git' "${real_path}/" "${backup_dir}/worktree/" + + printf '%s\n' "${backup_dir}" +} + # --status: read-only report of all HOTL install modes, then exit if [ "$STATUS_ONLY" -eq 1 ]; then echo "HOTL Installation Status" @@ -125,6 +171,20 @@ if [ "$STATUS_ONLY" -eq 1 ]; then echo "Codex plugin: not found" fi + # Show source checkout health + _codex_source="${HOME}/.codex/plugins/hotl-source" + if [ "${_codex_plugin_found}" -eq 1 ]; then + if [ -d "${_codex_source}/.git" ]; then + _src_rev="$(git -C "${_codex_source}" rev-parse --short HEAD 2>/dev/null || echo "unknown")" + echo " source checkout (rev ${_src_rev}) at ${_codex_source}" + else + echo " WARNING: source checkout not found at ${_codex_source}" + fi + elif [ -d "${_codex_source}/.git" ]; then + _src_rev="$(git -C "${_codex_source}" rev-parse --short HEAD 2>/dev/null || echo "unknown")" + echo "Codex plugin: source checkout (rev ${_src_rev}) at ${_codex_source} (no marketplace entry)" + fi + # Warn if both Codex modes are present if [ "${_codex_native_found}" -eq 1 ] && [ "${_codex_plugin_found}" -eq 1 ]; then echo "" @@ -148,6 +208,48 @@ if [ "$STATUS_ONLY" -eq 1 ]; then exit 0 fi +# --codex-plugin: update the plugin source checkout, then exit +if [ "$CODEX_PLUGIN" -eq 1 ]; then + CODEX_PLUGIN_SOURCE="${HOME}/.codex/plugins/hotl-source" + + if [ ! -d "${CODEX_PLUGIN_SOURCE}/.git" ]; then + echo "No HOTL plugin source checkout found at ${CODEX_PLUGIN_SOURCE}." + echo "Run install.sh --codex-plugin first." + if [ -d "${HOME}/.codex/hotl" ]; then + echo "" + echo "Hint: a native-skills install exists at ~/.codex/hotl." + echo "Use update.sh (without --codex-plugin) to update that instead." + fi + exit 1 + fi + + echo "Updating Codex plugin source checkout at ${CODEX_PLUGIN_SOURCE}..." + + if has_local_changes "${CODEX_PLUGIN_SOURCE}"; then + if [ "${FORCE_CODEX_PLUGIN}" -eq 1 ]; then + echo "Source checkout has local changes; --force-codex-plugin set, skipping backup." + else + _backup_root="${HOME}/.codex/backups/hotl-plugin" + _timestamp="$(date +"%Y%m%d-%H%M%S")" + _backup_dir="${_backup_root}/${_timestamp}" + mkdir -p "${_backup_dir}/worktree" + git -C "${CODEX_PLUGIN_SOURCE}" status --short --branch > "${_backup_dir}/git-status.txt" 2>/dev/null || true + rsync -a --exclude '.git' "${CODEX_PLUGIN_SOURCE}/" "${_backup_dir}/worktree/" + echo "Source checkout has local changes; backed them up to ${_backup_dir}." + fi + fi + + git -C "${CODEX_PLUGIN_SOURCE}" fetch origin main + git -C "${CODEX_PLUGIN_SOURCE}" reset --hard origin/main + git -C "${CODEX_PLUGIN_SOURCE}" clean -fd + + _ver="$(cat "${CODEX_PLUGIN_SOURCE}/VERSION" 2>/dev/null | tr -d '[:space:]')" + echo " Updated to version ${_ver:-unknown}." + echo "" + echo "Restart Codex to pick up the updated plugin." + exit 0 +fi + CLAUDE_PLUGIN_DIR="${HOME}/.claude/plugins/hotl" CLAUDE_CACHE_DIR="${HOME}/.claude/plugins/cache/hotl-plugin/hotl" CODEX_HOTL_DIR="${HOME}/.codex/hotl" @@ -161,43 +263,6 @@ FOUND=0 UPDATED=0 SKIPPED=0 -is_git_work_tree() { - local path="$1" - [ -e "${path}" ] && git -C "${path}" rev-parse --is-inside-work-tree > /dev/null 2>&1 -} - -current_branch() { - git -C "$1" branch --show-current 2>/dev/null || true -} - -has_local_changes() { - local path="$1" - [ -n "$(git -C "${path}" status --short --untracked-files=all 2>/dev/null)" ] -} - -resolve_path() { - local path="$1" - ( - cd "${path}" > /dev/null 2>&1 && pwd -P - ) -} - -backup_codex_install() { - local path="$1" - local real_path backup_root timestamp backup_dir - - real_path="$(resolve_path "${path}")" - backup_root="${HOME}/.codex/backups/hotl" - timestamp="$(date +"%Y%m%d-%H%M%S")" - backup_dir="${backup_root}/${timestamp}" - - mkdir -p "${backup_dir}/worktree" - git -C "${path}" status --short --branch > "${backup_dir}/git-status.txt" 2>/dev/null || true - rsync -a --exclude '.git' "${real_path}/" "${backup_dir}/worktree/" - - printf '%s\n' "${backup_dir}" -} - # ── Claude Code ─────────────────────────────────────────────────────────────── if is_git_work_tree "${CLAUDE_PLUGIN_DIR}"; then @@ -249,19 +314,35 @@ fi # ── Codex ───────────────────────────────────────────────────────────────────── -# Detect if HOTL is registered as a Codex plugin (user-global or repo-local) -CODEX_PLUGIN_REGISTERED=0 -for CODEX_MARKETPLACE in "${CODEX_MARKETPLACE_USER}" "${CODEX_MARKETPLACE_LOCAL}"; do - if hotl_in_marketplace "${CODEX_MARKETPLACE}"; then - CODEX_PLUGIN_REGISTERED=1 - _plugin_ver="$(hotl_marketplace_version "${CODEX_MARKETPLACE}")" - echo "Note: HOTL is also registered as a Codex plugin (v${_plugin_ver}) in" - echo " ${CODEX_MARKETPLACE}" - echo "Plugin updates are managed through Codex's plugin UI; update.sh only" - echo "updates the ${CODEX_HOTL_DIR} native-skills install." - echo "" +# Update Codex plugin source checkout if it exists +CODEX_PLUGIN_SOURCE="${HOME}/.codex/plugins/hotl-source" +if [ -d "${CODEX_PLUGIN_SOURCE}/.git" ]; then + FOUND=$((FOUND + 1)) + echo "Updating Codex plugin source checkout at ${CODEX_PLUGIN_SOURCE}..." + + if has_local_changes "${CODEX_PLUGIN_SOURCE}"; then + if [ "${FORCE_CODEX_PLUGIN}" -eq 1 ]; then + echo " Source checkout has local changes; --force-codex-plugin set, skipping backup." + else + _backup_root="${HOME}/.codex/backups/hotl-plugin" + _timestamp="$(date +"%Y%m%d-%H%M%S")" + _backup_dir="${_backup_root}/${_timestamp}" + mkdir -p "${_backup_dir}/worktree" + git -C "${CODEX_PLUGIN_SOURCE}" status --short --branch > "${_backup_dir}/git-status.txt" 2>/dev/null || true + rsync -a --exclude '.git' "${CODEX_PLUGIN_SOURCE}/" "${_backup_dir}/worktree/" + echo " Source checkout has local changes; backed them up to ${_backup_dir}." + fi fi -done + + git -C "${CODEX_PLUGIN_SOURCE}" fetch origin main + git -C "${CODEX_PLUGIN_SOURCE}" reset --hard origin/main + git -C "${CODEX_PLUGIN_SOURCE}" clean -fd + + _ver="$(cat "${CODEX_PLUGIN_SOURCE}/VERSION" 2>/dev/null | tr -d '[:space:]')" + UPDATED=$((UPDATED + 1)) + echo " Codex plugin source updated (v${_ver:-unknown})." + echo "" +fi if is_git_work_tree "${CODEX_HOTL_DIR}"; then FOUND=$((FOUND + 1))