diff --git a/Dockerfile b/Dockerfile
index 68ac4c2..f056a8f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,11 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
- bash \
ca-certificates \
- coreutils \
- gawk \
- jq \
nodejs \
npm \
&& npm install -g @openai/codex \
@@ -17,7 +13,7 @@ RUN apt-get update \
WORKDIR /workspace
-COPY app/codex_account.py /usr/local/bin/codex-account
+COPY app/codex_account.js /usr/local/bin/codex-account
RUN chmod +x /usr/local/bin/codex-account
CMD ["codex-account"]
diff --git a/README.md b/README.md
index 55a8ef9..11ef4f2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Codex CLI Composite Action
-提供一個 Gitea/GitHub Actions composite action,用於安裝 Codex CLI、寫入 OAuth token、執行指定提示詞,並將 CLI 輸出寫入 action output;另提供透過 `codex app-server` 取得目前登入帳號 email 的 shell helper。
+提供一個 Gitea/GitHub Actions composite action,用於安裝 Codex CLI、寫入 OAuth token、執行指定提示詞,並將 CLI 輸出寫入 action output;另提供透過 `codex app-server` 取得目前登入帳號 email 的 Node.js helper。
> 更新時間:2026/06/29 14:18:26
@@ -10,7 +10,7 @@
| 專案名稱 | 專案描述 |
| --- | --- |
-| [codex](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/) | 提供 Codex CLI composite action(安裝 `@openai/codex`、寫入 OAuth token、執行提示詞並輸出文字),並提供透過 `codex app-server` JSON-RPC `account/read` 取得目前登入帳號 email 的 shell helper。 |
+| [codex](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/) | 提供 Codex CLI composite action(安裝 `@openai/codex`、寫入 OAuth token、執行提示詞並輸出文字),並提供透過 `codex app-server` JSON-RPC `account/read` 取得目前登入帳號 email 的 Node.js helper。 |
### 參考專案
@@ -30,29 +30,29 @@
| 功能名稱 | 功能描述 |
| --- | --- |
-| [app/codex_account.py](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/app/codex_account.py#L1) | [透過 codex app-server 的 JSON-RPC `account/read` 取得目前登入帳號的 email,取不到或逾時輸出空字串。](#appcodex_accountpy) |
+| [app/codex_account.js](https://gitea.jsc.idv.tw/composite-actions/codex/src/branch/develop/app/codex_account.js#L1) | [透過 codex app-server 的 JSON-RPC `account/read` 取得目前登入帳號的 email,取不到或逾時輸出空字串。](#appcodex_accountjs) |
## 使用範例
-
-### app/codex_account.py
+
+### app/codex_account.js
-透過 `codex app-server` 的 JSON-RPC `account/read` 取得目前登入帳號的 email。腳本會啟動 `codex app-server` 子行程,依序送出 `initialize` / `initialized` / `account/read` 三筆 JSON-RPC 訊息,使用 `jq` 讀取回應中 `result.account.email` 並輸出到 stdout;讀到 EOF、JSON 解析失敗或超過逾時都會停止,取不到時輸出空字串。
+透過 `codex app-server` 的 JSON-RPC `account/read` 取得目前登入帳號的 email。腳本會啟動 `codex app-server` 子行程,依序送出 `initialize` / `initialized` / `account/read` 三筆 JSON-RPC 訊息,使用 Node.js 標準函式庫讀取回應中 `result.account.email` 並輸出到 stdout;讀到 EOF、JSON 解析失敗或超過逾時都會停止,取不到時輸出空字串。
前置條件:
- `codex` CLI 已安裝(例如本 action 的安裝步驟已執行)。
-- `jq` 已安裝。
+- Node.js 已安裝。
- `$HOME/.codex/auth.json` 已寫入有效的 OAuth token。
典型呼叫方式:
```bash
# 取得目前登入帳號 email;預設等待 25 秒
-bash app/codex_account.py
+node app/codex_account.js
# 自訂等待 app-server 回應的逾時秒數
-bash app/codex_account.py 10
+node app/codex_account.js 10
```
預期結果:
@@ -63,12 +63,12 @@ bash app/codex_account.py 10
此模組亦可直接以指令執行,會將 email 印到 stdout(取不到時印空字串):
```bash
-bash app/codex_account.py
+node app/codex_account.js
```
## Docker
-此專案提供以 `ubuntu:latest` 為基底的 Dockerfile,會安裝 `bash`、`jq`、Node.js/npm 與 `@openai/codex`:
+此專案提供以 `ubuntu:latest` 為基底的 Dockerfile,會安裝 Node.js/npm 與 `@openai/codex`:
```bash
docker build -t codex-account .
diff --git a/action.yml b/action.yml
index 7843853..bc78299 100644
--- a/action.yml
+++ b/action.yml
@@ -7,7 +7,7 @@
# 讓 codex CLI 取得登入憑證。
# 3. 執行 inputs.prompt 指定的提示詞,並把 Codex 的輸出寫入
# steps.codex.outputs.text 供後續 step 使用。
-# - 若未提供 prompt(PROMPT 為空),則改呼叫 app/codex_account.py 取得目前
+# - 若未提供 prompt(PROMPT 為空),則改呼叫 app/codex_account.js 取得目前
# 登入帳號的 email,寫入 text 輸出後結束(用於驗證身分)。
# 更新日期:2026/06/29 14:18:26
# =============================================================================
@@ -62,21 +62,6 @@ runs:
exit 1
fi
- # 安裝 jq,供 app/codex_account.py 以 shell 解析 JSON-RPC 回應。
- if ! command -v jq >/dev/null 2>&1; then
- if command -v apt-get >/dev/null 2>&1; then
- apt_get='apt-get'
- if command -v sudo >/dev/null 2>&1; then
- apt_get='sudo apt-get'
- fi
- $apt_get update
- $apt_get install -y jq
- else
- echo 'jq is required but apt-get is not available.' >&2
- exit 1
- fi
- fi
-
# 全域安裝 Codex CLI 套件,安裝後可直接使用 codex 指令
npm install -g @openai/codex
@@ -111,8 +96,8 @@ runs:
fi
# 當 PROMPT 為空時,改走「取得登入帳號 email」分支
if [ -z "$PROMPT" ]; then
- # 呼叫 codex_account.py 取得目前登入帳號的 email($GITHUB_ACTION_PATH 為 action 根目錄)
- email="$(bash "$GITHUB_ACTION_PATH/app/codex_account.py")"
+ # 呼叫 codex_account.js 取得目前登入帳號的 email($GITHUB_ACTION_PATH 為 action 根目錄)
+ email="$(node "$GITHUB_ACTION_PATH/app/codex_account.js")"
# 將 email 寫入 text 輸出($GITHUB_OUTPUT 為步驟輸出檔)
echo "text=$email" >> "$GITHUB_OUTPUT"
# 以 0 正常退出,不再執行 codex exec
diff --git a/app/codex_account.js b/app/codex_account.js
new file mode 100755
index 0000000..e97b717
--- /dev/null
+++ b/app/codex_account.js
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+// 透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。
+//
+// 這是 TUI `/status` Account 欄位的程式化來源,不解析本地 OAuth token,
+// 而是由 codex 自身回報登入帳號。將 email 印到 stdout(取不到時印空字串)。
+
+const { spawn } = require("node:child_process");
+const readline = require("node:readline");
+
+const timeoutSeconds = Number(process.argv[2] ?? "25");
+if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
+ console.error("timeout_seconds must be a non-negative number.");
+ process.exit(2);
+}
+
+function sendJson(processHandle, message) {
+ try {
+ processHandle.stdin.write(`${JSON.stringify(message)}\n`);
+ } catch {
+ return false;
+ }
+ return true;
+}
+
+async function readAccountEmail() {
+ const processHandle = spawn("codex", ["app-server"], {
+ stdio: ["pipe", "pipe", "ignore"],
+ });
+
+ let settled = false;
+ let timer;
+
+ const cleanup = () => {
+ if (!processHandle.killed) {
+ processHandle.kill();
+ }
+ };
+
+ process.once("exit", cleanup);
+ process.once("SIGINT", () => {
+ cleanup();
+ process.exit(130);
+ });
+ process.once("SIGTERM", () => {
+ cleanup();
+ process.exit(143);
+ });
+
+ return await new Promise((resolve) => {
+ const done = (email = "") => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ clearTimeout(timer);
+ cleanup();
+ resolve(email);
+ };
+
+ timer = setTimeout(() => done(""), timeoutSeconds * 1000);
+
+ processHandle.once("error", () => done(""));
+ processHandle.once("exit", () => done(""));
+ processHandle.stdin.once("error", () => done(""));
+
+ const lines = readline.createInterface({
+ input: processHandle.stdout,
+ crlfDelay: Infinity,
+ });
+
+ lines.on("line", (line) => {
+ let message;
+ try {
+ message = JSON.parse(line);
+ } catch {
+ return;
+ }
+
+ if (message.id !== 2) {
+ return;
+ }
+
+ const email = message?.result?.account?.email;
+ done(typeof email === "string" ? email : "");
+ });
+
+ const messages = [
+ {
+ jsonrpc: "2.0",
+ id: 1,
+ method: "initialize",
+ params: { clientInfo: { name: "ci", version: "1.0" } },
+ },
+ {
+ jsonrpc: "2.0",
+ method: "initialized",
+ params: {},
+ },
+ {
+ jsonrpc: "2.0",
+ id: 2,
+ method: "account/read",
+ params: {},
+ },
+ ];
+
+ for (const message of messages) {
+ if (!sendJson(processHandle, message)) {
+ done("");
+ break;
+ }
+ }
+ });
+}
+
+readAccountEmail()
+ .then((email) => {
+ process.stdout.write(`${email}\n`);
+ })
+ .catch(() => {
+ process.stdout.write("\n");
+ });
diff --git a/app/codex_account.py b/app/codex_account.py
deleted file mode 100755
index 6087e9f..0000000
--- a/app/codex_account.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/env bash
-# 透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。
-#
-# 這是 TUI `/status` Account 欄位的程式化來源,不解析本地 OAuth token,
-# 而是由 codex 自身回報登入帳號。將 email 印到 stdout(取不到時印空字串)。
-
-set -uo pipefail
-
-timeout_seconds="${1:-25}"
-if ! [[ "$timeout_seconds" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
- echo "timeout_seconds must be a number." >&2
- exit 2
-fi
-
-if ! command -v jq >/dev/null 2>&1; then
- echo "jq is required." >&2
- exit 2
-fi
-
-email=""
-
-cleanup() {
- if [[ -n "${CODEX_SERVER_PID:-}" ]]; then
- kill "$CODEX_SERVER_PID" >/dev/null 2>&1 || true
- wait "$CODEX_SERVER_PID" >/dev/null 2>&1 || true
- fi
-}
-trap cleanup EXIT
-
-coproc CODEX_SERVER { codex app-server 2>/dev/null; }
-exec {codex_server_stdin}>&"${CODEX_SERVER[1]}"
-exec {codex_server_stdout}<&"${CODEX_SERVER[0]}"
-
-send_json() {
- printf '%s\n' "$1" >&"$codex_server_stdin"
-}
-
-send_json '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"ci","version":"1.0"}}}'
-send_json '{"jsonrpc":"2.0","method":"initialized","params":{}}'
-send_json '{"jsonrpc":"2.0","id":2,"method":"account/read","params":{}}'
-
-deadline="$(
- awk -v now="$(date +%s)" -v timeout="$timeout_seconds" 'BEGIN { printf "%.3f", now + timeout }'
-)"
-
-while awk -v now="$(date +%s)" -v deadline="$deadline" 'BEGIN { exit !(now < deadline) }'; do
- remaining="$(
- awk -v now="$(date +%s)" -v deadline="$deadline" 'BEGIN {
- remaining = deadline - now
- if (remaining < 1) {
- remaining = 1
- }
- printf "%.0f", remaining
- }'
- )"
-
- if ! IFS= read -r -t "$remaining" line <&"$codex_server_stdout"; then
- break
- fi
-
- message_id="$(jq -r 'try .id catch empty' <<<"$line" 2>/dev/null || true)"
- if [[ "$message_id" != "2" ]]; then
- continue
- fi
-
- email="$(jq -r 'try (.result.account.email // "") catch ""' <<<"$line" 2>/dev/null || true)"
- break
-done
-
-printf '%s\n' "$email"