Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.7
0.1.8
2 changes: 1 addition & 1 deletion plugins/cmd/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cmd",
"version": "0.1.7",
"version": "0.1.8",
"description": "Prompt-based wrapper commands for explicit agent workflows in Codex.",
"author": {
"name": "Sam Wang",
Expand Down
2 changes: 1 addition & 1 deletion plugins/dev/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dev",
"version": "0.1.7",
"version": "0.1.8",
"description": "Software development utilities for Codex.",
"author": {
"name": "Sam Wang",
Expand Down
280 changes: 54 additions & 226 deletions plugins/dev/skills/sdkman-switch-jdk/SKILL.md

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions plugins/dev/skills/sdkman-switch-jdk/references/default-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# SDKMAN 永久 Java default 變更

只有在使用者明確要求變更或回復 SDKMAN Java default 時,才讀取本參考。一般 build、test 或單次命令使用 `SKILL.md` 的無狀態 `run-java.sh`。

## 記錄原始 default

在隔離的 Bash process 中載入 SDKMAN、停用本次初始化的 auto-env,並記錄 `java/current`:

```bash
bash -c '
set -e -o pipefail
unset SDKMAN_ENV
export SDKMAN_OLD_PWD="$PWD"
source "${SDKMAN_DIR:-$HOME/.sdkman}/bin/sdkman-init.sh"
sdk version
sdkman_switch_jdk_current="${SDKMAN_CANDIDATES_DIR}/java/current"
if [[ -L "$sdkman_switch_jdk_current" ]]; then
printf "SDKMAN Java default state: link-hex:"
LC_ALL=C readlink -n "$sdkman_switch_jdk_current" | \
LC_ALL=C od -An -v -tx1 | LC_ALL=C tr -d "[:space:]"
printf "\n"
elif [[ -e "$sdkman_switch_jdk_current" ]]; then
printf "ERROR: current is not a symlink: %s\n" "$sdkman_switch_jdk_current" >&2
exit 1
else
printf "SDKMAN Java default state: absent\n"
fi
'
```

將 `SDKMAN Java default state:` 後的完整值逐字保存為 `<previous-default-state>`;值只會是 `link-hex:<raw-readlink-target 的逐 byte 十六進位>` 或 `absent`。編碼避免 shell command substitution 遺失 target 尾端 newline,也讓 state 能安全地作為單一 argv 傳遞。若初始化失敗,停止並說明 SDKMAN 尚未安裝或不可用;取得同意前不安裝 SDKMAN。

## 設定永久 default

將 identifier 與保存的 state 分別作為單一 argv 傳入。設定前再次確認 default 沒有漂移,然後保存輸出的 `<created-default-state>`:

```bash
bash -c '
set -e -o pipefail
unset SDKMAN_ENV
export SDKMAN_OLD_PWD="$PWD"
source "${SDKMAN_DIR:-$HOME/.sdkman}/bin/sdkman-init.sh"
sdkman_switch_jdk_current="${SDKMAN_CANDIDATES_DIR}/java/current"
sdkman_switch_jdk_link_state() {
printf "link-hex:"
LC_ALL=C readlink -n "$1" | LC_ALL=C od -An -v -tx1 | \
LC_ALL=C tr -d "[:space:]" || return 1
printf "\n"
}
sdkman_switch_jdk_default_state() {
if [[ -L "$sdkman_switch_jdk_current" ]]; then
sdkman_switch_jdk_link_state "$sdkman_switch_jdk_current"
elif [[ -e "$sdkman_switch_jdk_current" ]]; then
printf "unsupported\n"
else
printf "absent\n"
fi
}
sdkman_switch_jdk_before="$(sdkman_switch_jdk_default_state)"
if [[ "$sdkman_switch_jdk_before" != "$2" ]]; then
printf "Refusing to change a default that drifted.\nExpected: %s\nActual: %s\n" \
"$2" "$sdkman_switch_jdk_before" >&2
exit 1
fi
sdk default java "$1"
sdkman_switch_jdk_created="$(sdkman_switch_jdk_default_state)"
if [[ "$sdkman_switch_jdk_created" != link-hex:* ]]; then
printf "SDKMAN did not create a Java default symlink.\n" >&2
exit 1
fi
printf "Created default state: %s\n" "$sdkman_switch_jdk_created"
' bash <identifier> <previous-default-state>

bash -c '
set -e -o pipefail
unset SDKMAN_ENV
export SDKMAN_OLD_PWD="$PWD"
source "${SDKMAN_DIR:-$HOME/.sdkman}/bin/sdkman-init.sh"
java -version 2>&1
command -v java
'
```

不要將 state 當作 SDKMAN identifier,也不要用 `eval`。永久變更期間不得並行執行其他會改寫 `java/current` 的 SDKMAN 操作。

## 依要求回復

只在使用者要求回復時執行以下命令。它先要求目前 state 逐字等於 `<created-default-state>`,避免覆蓋後續變更;原本有 default 時,以 `java` 目錄內的暫存 symlink 原子還原 raw target,原本為 `absent` 時則移除本次建立的 symlink:

```bash
bash -c '
set -e -o pipefail
unset SDKMAN_ENV
export SDKMAN_OLD_PWD="$PWD"
source "${SDKMAN_DIR:-$HOME/.sdkman}/bin/sdkman-init.sh"
sdkman_switch_jdk_java_dir="${SDKMAN_CANDIDATES_DIR}/java"
sdkman_switch_jdk_current="${SDKMAN_CANDIDATES_DIR}/java/current"
sdkman_switch_jdk_link_state() {
printf "link-hex:"
LC_ALL=C readlink -n "$1" | LC_ALL=C od -An -v -tx1 | \
LC_ALL=C tr -d "[:space:]" || return 1
printf "\n"
}
sdkman_switch_jdk_default_state() {
if [[ -L "$sdkman_switch_jdk_current" ]]; then
sdkman_switch_jdk_link_state "$sdkman_switch_jdk_current"
elif [[ -e "$sdkman_switch_jdk_current" ]]; then
printf "unsupported\n"
else
printf "absent\n"
fi
}
sdkman_switch_jdk_decode_link_state() {
local sdkman_switch_jdk_encoded="${1#link-hex:}"
local sdkman_switch_jdk_escaped=""
if [[ "$1" != link-hex:* || -z "$sdkman_switch_jdk_encoded" || \
$(( ${#sdkman_switch_jdk_encoded} % 2 )) -ne 0 || \
! "$sdkman_switch_jdk_encoded" =~ ^[[:xdigit:]]+$ ]]; then
return 1
fi
while [[ -n "$sdkman_switch_jdk_encoded" ]]; do
sdkman_switch_jdk_escaped="${sdkman_switch_jdk_escaped}\\x${sdkman_switch_jdk_encoded:0:2}"
sdkman_switch_jdk_encoded="${sdkman_switch_jdk_encoded:2}"
done
printf -v sdkman_switch_jdk_decoded_target "%b" \
"$sdkman_switch_jdk_escaped"
}
sdkman_switch_jdk_expected="$1"
sdkman_switch_jdk_previous="$2"
if [[ "$sdkman_switch_jdk_expected" != link-hex:* ]] || \
[[ "$sdkman_switch_jdk_previous" != link-hex:* && \
"$sdkman_switch_jdk_previous" != absent ]]; then
printf "Invalid saved default state.\n" >&2
exit 2
fi
sdkman_switch_jdk_actual="$(sdkman_switch_jdk_default_state)"
if [[ "$sdkman_switch_jdk_actual" != "$sdkman_switch_jdk_expected" ]]; then
printf "Refusing to overwrite a default that drifted.\nExpected: %s\nActual: %s\n" \
"$sdkman_switch_jdk_expected" "$sdkman_switch_jdk_actual" >&2
exit 1
fi
if [[ "$sdkman_switch_jdk_previous" == absent ]]; then
unlink "$sdkman_switch_jdk_current"
else
if ! sdkman_switch_jdk_decode_link_state "$sdkman_switch_jdk_previous"; then
printf "Invalid encoded previous default state.\n" >&2
exit 2
fi
sdkman_switch_jdk_previous_target="$sdkman_switch_jdk_decoded_target"
sdkman_switch_jdk_temp_dir="$(
mktemp -d "$sdkman_switch_jdk_java_dir/.sdkman-switch-jdk-restore.XXXXXX"
)"
sdkman_switch_jdk_temp_link="$sdkman_switch_jdk_temp_dir/current"
sdkman_switch_jdk_cleanup() {
if [[ -L "${sdkman_switch_jdk_temp_link:-}" ]]; then
unlink "$sdkman_switch_jdk_temp_link" 2>/dev/null || true
fi
if [[ -d "${sdkman_switch_jdk_temp_dir:-}" ]]; then
rmdir "$sdkman_switch_jdk_temp_dir" 2>/dev/null || true
fi
}
trap sdkman_switch_jdk_cleanup EXIT
ln -s -- "$sdkman_switch_jdk_previous_target" "$sdkman_switch_jdk_temp_link"
if [[ ! -L "$sdkman_switch_jdk_temp_link" ]] || \
[[ "$(sdkman_switch_jdk_link_state "$sdkman_switch_jdk_temp_link")" != \
"$sdkman_switch_jdk_previous" ]]; then
printf "Could not create the exact rollback symlink.\n" >&2
exit 1
fi
if [[ "$(sdkman_switch_jdk_default_state)" != \
"$sdkman_switch_jdk_expected" ]]; then
printf "Refusing to overwrite a default that drifted during rollback.\n" >&2
exit 1
fi
if mv -fh "$sdkman_switch_jdk_temp_link" \
"$sdkman_switch_jdk_current" 2>/dev/null; then
:
elif [[ -L "$sdkman_switch_jdk_temp_link" ]] && \
mv -Tf "$sdkman_switch_jdk_temp_link" \
"$sdkman_switch_jdk_current" 2>/dev/null; then
:
else
printf "Could not atomically restore the Java default.\n" >&2
exit 1
fi
rmdir "$sdkman_switch_jdk_temp_dir"
sdkman_switch_jdk_temp_dir=
sdkman_switch_jdk_temp_link=
trap - EXIT
fi
sdkman_switch_jdk_actual="$(sdkman_switch_jdk_default_state)"
if [[ "$sdkman_switch_jdk_actual" != "$sdkman_switch_jdk_previous" ]]; then
printf "Java default rollback verification failed.\nExpected: %s\nActual: %s\n" \
"$sdkman_switch_jdk_previous" "$sdkman_switch_jdk_actual" >&2
exit 1
fi
printf "Restored default state: %s\n" "$sdkman_switch_jdk_actual"
' bash <created-default-state> <previous-default-state>
```

**完成條件:**永久 default 的建立與驗證成功,並回報 `<previous-default-state>`、`<created-default-state>` 及可逐 byte 回復的命令。若 default 在任一比較點漂移,保留外部狀態並停止。
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

1. 從 `.sdkmanrc` 讀出完整 `java=` identifier。
2. 必要時用 `scripts/install-java.sh` 只安裝該 Java。
3. 回到 `SKILL.md` 步驟 5 的暫時分支;default 不存在時使用其中指向的 SDK home 分支
3. 回到 `SKILL.md` 步驟 3 的無狀態暫時分支

此分支不執行 `sdk env install`,因為它會安裝並切換 `.sdkmanrc` 中的每一個 candidate。

Expand All @@ -28,7 +28,7 @@

2. 修改前計算有效的 `java=` 項目。沒有時新增 `java=<identifier>`;只有一個時更新該項並保留註解與其他 candidate。若超過一個,停止並列出衝突;只有在使用者明確要求修正重複項目時,才能更新第一項並移除其餘項目。
3. 修改後驗證恰好只有一個有效的 `java=` 項目,再檢查 diff,確認只包含預期的 Java 項目變更。
4. 必要時用 `scripts/install-java.sh` 安裝該 Java,再回到 `SKILL.md` 步驟 5 的暫時分支執行與驗證
4. 必要時用 `scripts/install-java.sh` 安裝該 Java,再回到 `SKILL.md` 步驟 3 的無狀態暫時分支執行與驗證

回復時,既有檔案應還原為保存的完整內容;若本次建立了新檔,先確認它仍是本次產物,再依使用者要求執行 `unlink .sdkmanrc`。不要使用會覆寫既有內容的固定 `.sdkmanrc.bak`。

Expand All @@ -52,7 +52,7 @@ bash <skill-dir>/scripts/run-sdkman-env.sh \

`--allow-default` 只適用於執行前為 `absent`、且存在於 `.sdkmanrc` 的 candidate;它只允許保留 `.sdkmanrc` 指定的 exact raw version target。它不允許改寫既有 default,也不接受重複或未列於 `.sdkmanrc` 的 candidate。沒有任何建立 default 的授權時,省略所有 `--allow-default`,runner 會還原每一個新建的 default。

需要 compound command 時,將 `bash -lc 'mvn test && mvn package'` 當成 `<actual-command> [args...]` 傳入;不要把命令文字插入 `bash -c` 程式本文
需要 compound command 時,將 `bash -c 'mvn test && mvn package'` 當成 `<actual-command> [args...]` 傳入。不要使用 `bash -lc`;login shell 可能重新讀取 profile 並覆寫 runner 選定的 SDKMAN 環境。不要拼接或 `eval` 使用者輸入

runner 只在所有 candidate 的比較、還原、最終驗證與 lock release 都成功後才以 `exec` 執行 payload。執行 SDKMAN 期間若收到 `HUP`、`INT` 或 `TERM`,會先以相同的 operation-owned 比較完成 reconciliation,再保留對應 signal status;lock metadata 初始化期間的 signal 也會延後到安全清理後處理。SDKMAN 失敗且安全 reconciliation 成功時,保留 SDKMAN 的原始非零 status;CLI 或 `.sdkmanrc` 格式錯誤回傳 `2`,其他安全拒絕回傳 `1`。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ SDKMAN 輸出可能包含 ANSI escape。將 `sdk list java` 當作顯示資料

## 維持 default 不變

`scripts/install-java.sh` 會預先設定 `USE=n` 並對提示送入 `n`;`scripts/run-java.sh` 則在所有情況下將精確 candidate 的 `bin` 放到 `PATH` 最前方,且不會在缺少 Java default 時建立它。兩者都會比較執行前後的 `java/current`。`scripts/run-sdkman-env.sh` 會一次解析並驗證 `.sdkmanrc`,再以已驗證的記憶體 candidate 清單逐項明確執行 `sdk install` 與 `sdk use`;它不會讓 SDKMAN 重新讀取檔案,也會對每個 candidate 執行相同的 raw-state 保護
`scripts/run-java.sh` 直接使用 `${SDKMAN_DIR:-$HOME/.sdkman}/candidates/java/<identifier>`,不載入 SDKMAN、不呼叫 `sdk use`、不讀寫 `java/current`,也不取得 default-state lock。它會將 exact candidate 的 `bin` 放到 `PATH` 最前方;caller 的 `PATH` 為空時不加入會代表目前目錄的空 segment。這條路徑刻意略過 SDKMAN extensions/native override,且不與同一 candidate 的並行替換序列化

三個 runner 共用 `${SDKMAN_CANDIDATES_DIR}/.sdkman-switch-jdk.lock`,讓 SDKMAN 操作與 reconciliation 不會彼此交錯。live owner 會使新呼叫 fail fast;metadata 完整、同一 EUID 且 PID 已不存在的 stale lock 會在重新核對後回收。metadata 不完整、ownership 不符或 PID 仍存活時一律保留 lock 並停止;先確認沒有相關 runner 後再人工檢查,不要直接遞迴刪除。
`scripts/install-java.sh` 會預先設定 `USE=n` 並對提示送入 `n`,同時比較安裝前後的 `java/current`。`scripts/run-sdkman-env.sh` 會一次解析並驗證 `.sdkmanrc`,再以記憶體 candidate 清單逐項執行 `sdk install` 與 `sdk use`;它不讓 SDKMAN 第二次讀取檔案,並對每個 candidate 執行 raw-state 保護。

這兩個會改變 SDKMAN 狀態的 bundled runner 共用 `${SDKMAN_CANDIDATES_DIR}/.sdkman-switch-jdk.lock`,讓 SDKMAN 操作與 reconciliation 不會彼此交錯。live owner 會使新呼叫 fail fast;metadata 完整、同一 EUID 且 PID 已不存在的 stale lock 會在重新核對後回收。metadata 不完整、ownership 不符或 PID 仍存活時一律保留 lock 並停止;先確認沒有相關 runner 後再人工檢查,不要直接遞迴刪除。

此 lock 是 bundled runner 間的合作式協調邊界。不要在 runner 執行期間以裸 `sdk default`、手動改寫 `current` 或其他不取得同一 lock 的流程並行變更 default;runner 會在每次還原寫入前重查漂移,但 portable Bash 無法把 state comparison 與 filesystem mutation 合併成單一 kernel operation。`HUP`、`INT` 與 `TERM` 會先執行相同 reconciliation 再釋放 lock;`SIGKILL` 或系統崩潰無法由 shell trap 處理,後續呼叫只會依完整 owner metadata 的 stale-lock 規則回收 lock,不會猜測或覆寫 default。

runner 已同時設定 `JAVA_HOME` 與 `PATH`。若它回報 active Java 不符,檢查 `${SDKMAN_CANDIDATES_DIR}/java/<identifier>/bin/java` 是否存在且可執行。不要將 `JAVA_HOME` 指向 `java/current`;該 symlink 代表 default,不一定是要求的 identifier。
`run-java.sh` 已同時設定 `JAVA_HOME` 與 `PATH`。若它回報 active Java 不符,檢查 `${SDKMAN_DIR:-$HOME/.sdkman}/candidates/java/<identifier>/bin/java` 是否為可執行的 regular file。不要將 `JAVA_HOME` 指向 `java/current`;該 symlink 代表 default,不一定是要求的 identifier。compound command 使用 `bash -c`;`bash -lc` 可能載入 profile 並覆寫這兩個值

## 失敗處理

| 觀察 | 動作 |
| --- | --- |
| SDKMAN init script 不存在 | 停止並在安裝 SDKMAN 前取得同意。 |
| exact candidate 已安裝但 SDKMAN init script 不存在 | 直接使用 `run-java.sh`;無狀態分支不需要 init。 |
| 需要安裝/完整環境但 SDKMAN init script 不存在 | 停止並在安裝 SDKMAN 前取得同意。 |
| 沒有唯一 identifier 符合主版本 | 列出候選並要求選定 distribution;不要取清單第一筆。 |
| `Stop! <id> is not available.` | 重新查看 `sdk list java` 並選定完整可用 identifier。 |
| runner 回報 active Java 不符 | 檢查 candidate 的 `bin/java` 與 shell 執行環境;不要繞過 runner,修正原因後重跑。 |
| stateful runner 回報無法建立 lock | 確認 lock parent 存在且可寫;不要把 absent path 當成 stale lock。 |
| stateful runner 回報 unsafe lock path | 保留該 regular file 或 symlink,確認來源後人工處理;runner 不會覆寫或刪除。 |
| runner 回報 project `.sdkmanrc` 在 activation 期間改變 | 保留並檢查並行修改;runner 只會執行已驗證的記憶體 entries,不會覆寫或第二次讀取 project 檔案,確認內容穩定後再重跑。 |
| 專案命令連帶影響 Maven、Gradle 或其他 SDK | 回到 Java-only 分支;只在明確要求完整環境時使用專用 runner。 |
Loading