first commit

This commit is contained in:
2026-09-05 21:08:13 +08:00
commit 7f8d810ccf
14 changed files with 781 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
# image-gen-custom-project
这是一个面向 Codex 的本地技能包仓库,核心内容是 `image-gen-custom`:它通过兼容 OpenAI 的 Images API 生成图片,并把结果稳定地保存为本地 `PNG`、`JPEG`、`GIF` 或 `WebP` 文件。
## 仓库内容
```text
skill/image-gen-custom/
├─ SKILL.md
├─ agents/openai.yaml
└─ scripts/
├─ configure.ps1 / configure.sh
├─ generate-image.ps1 / generate-image.sh
├─ install-skill.ps1 / install-skill.sh
└─ 其他辅助脚本
```
## 主要能力
- 支持自定义 OpenAI-compatible API base URL
- 支持指定模型、尺寸、质量和输出路径
- 自动校验返回内容是否为有效图片
- 以原子写入方式保存输出文件
- Windows 上使用 DPAPI 保存密钥,macOS/Linux 使用系统 keyring
## 安装
在对应平台运行安装脚本:
```powershell
skill\image-gen-custom\scripts\install-skill.ps1
```
```bash
bash skill/image-gen-custom/scripts/install-skill.sh
```
如需覆盖已有安装,可加 `-Force` 或 `--force`。
## 配置
首次使用前先配置 API 基础地址和密钥:
```powershell
skill\image-gen-custom\scripts\configure.ps1
```
```bash
bash skill/image-gen-custom/scripts/configure.sh
```
也可以临时使用环境变量:
- `OPENAI_API_KEY`
- `OPENAI_BASE_URL`
## 生成图片
Windows:
```powershell
skill\image-gen-custom\scripts\generate-image.ps1 `
-Prompt "A concise prompt..." `
-Out "outputs\generated-image.png" `
-Size "1024x1024" `
-Quality "low" `
-Force
```
macOS / Linux:
```bash
bash skill/image-gen-custom/scripts/generate-image.sh \
--prompt "A concise prompt..." \
--out outputs/generated-image.png \
--size 1024x1024 \
--quality low \
--force
```
默认模型是 `gpt-image-2`,可通过 `-Model` / `--model` 覆盖。
## 运行时说明
- 图片 API 支持 `b64_json` 或图片 URL 返回
- URL 下载上限为 50 MB
- 输出会先写入临时文件,再替换到目标路径
- 可通过 `CODEX_IMAGEGEN_VENV` 指定私有虚拟环境位置
- 可通过 `CODEX_IMAGEGEN_PYTHON` 指定用于引导环境的 Python
## 安全提示
- 不要把密钥写进脚本、提交记录或这个 README
- Windows 密钥保存到 DPAPI 保护的本地文件
- 其他平台保存到系统 keyring
+5
View File
@@ -0,0 +1,5 @@
scripts/connection.local.json
*.env
**/__pycache__/
*.py[cod]
venv/
+55
View File
@@ -0,0 +1,55 @@
---
name: image-gen-custom
description: Generate images through any OpenAI-compatible Images API and save reproducible local PNG, JPEG, GIF, or WebP files. Use when Codex needs a custom image endpoint, explicit model and output path, or a dependable cross-platform image generation workflow.
---
# Image Gen Custom
Use `scripts/generate-image.ps1` on Windows or `scripts/generate-image.sh` on macOS/Linux. The wrappers create or reuse a private virtual environment; generation uses only the Python standard library. The Python core reads the API key, calls the Images API, validates the returned image, and writes it atomically.
```powershell
& "$env:USERPROFILE\.codex\skills\image-gen-custom\scripts\generate-image.ps1" `
-Prompt "A concise prompt..." `
-Out "outputs\generated-image.png" `
-Size "1024x1024" `
-Quality "low" `
-Force
```
```bash
"$HOME/.codex/skills/image-gen-custom/scripts/generate-image.sh" \
--prompt "A concise prompt..." \
--out outputs/generated-image.png \
--size 1024x1024 \
--quality low \
--force
```
## Credentials
Run the matching `configure` wrapper once. It prompts for the API base URL first, then prompts for the key in visible text so you can paste it. On Windows, the key is saved in a DPAPI-protected local file; on macOS and Linux, it uses the platform credential store.
```powershell
& "$env:USERPROFILE\.codex\skills\image-gen-custom\scripts\configure.ps1"
```
```bash
"$HOME/.codex/skills/image-gen-custom/scripts/configure.sh"
```
The URL and Windows key file are saved under the local Codex shared configuration folder. The normal image command then needs no URL argument. An explicit `-BaseUrl` on the image command still overrides the saved value.
Set `OPENAI_API_KEY` instead when an ephemeral environment variable is preferred. Set `OPENAI_BASE_URL` for a custom OpenAI-compatible root; `/v1` is appended when omitted. Never put a key in `SKILL.md`, scripts, shell history, command arguments, ZIP files, or source control.
## Options and behavior
- Default model: `gpt-image-2`; use `-Model` or `--model` to override it.
- Use low quality for drafts and high quality for final images.
- Accept `b64_json` and absolute HTTP(S) URL responses; URL downloads are capped at 50 MB and checked for image signatures.
- Use `CODEX_IMAGEGEN_VENV` to choose the private virtual environment location.
- Use `CODEX_IMAGEGEN_PYTHON` to choose the Python executable used to bootstrap that environment.
- Report the concrete failure category without exposing credentials: missing runtime/dependency, keyring or authentication, network/permission, unsupported model/endpoint, quota, response parsing, download, or output writing.
## Install a shared package
Extract the package and run `scripts/install-skill.ps1` on Windows or `scripts/install-skill.sh` on macOS/Linux. Add `-Force` or `--force` only when updating an existing installation; the installer never overwrites the destination's keyring data or any local secret file. Start a new Codex task after installation.
@@ -0,0 +1,4 @@
interface:
display_name: "Image Gen Custom"
short_description: "Cross-platform custom image generation."
default_prompt: "Use $image-gen-custom to generate an image and save it to an explicit local file."
@@ -0,0 +1,25 @@
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Security
$documents = if ($env:CODEX_DOCUMENTS) { $env:CODEX_DOCUMENTS } else { Join-Path $env:USERPROFILE "Documents" }
$configDir = Join-Path $documents "Codex\.shared\image-gen-custom"
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
$baseUrl = Read-Host "OpenAI-compatible API base URL"
if ([string]::IsNullOrWhiteSpace($baseUrl)) { throw "API base URL cannot be empty." }
[IO.File]::WriteAllText((Join-Path $configDir "base_url.txt"), $baseUrl.Trim().TrimEnd("/"), [Text.UTF8Encoding]::new($false))
Write-Host "API base URL stored in the local configuration."
$apiKey = Read-Host "OpenAI-compatible API key"
if ([string]::IsNullOrWhiteSpace($apiKey)) { throw "API key cannot be empty." }
$plain = [Text.Encoding]::UTF8.GetBytes($apiKey.Trim())
$protected = [System.Security.Cryptography.ProtectedData]::Protect(
$plain,
$null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
[Convert]::ToBase64String($protected) | Set-Content -LiteralPath (Join-Path $configDir "api_key.dpapi") -Encoding ascii
Write-Host "API key stored in a Windows DPAPI-protected local file."
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
python_cmd="${CODEX_IMAGEGEN_PYTHON:-}"
if [[ -z "$python_cmd" ]]; then
embedded_python="$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe"
if [[ -x "$embedded_python" ]]; then
python_cmd="$embedded_python"
else
python_cmd="$(command -v python3 || command -v python || true)"
fi
fi
if [[ -z "$python_cmd" ]]; then
printf '%s\n' "Python 3 not found; install Python or set CODEX_IMAGEGEN_PYTHON." >&2
exit 1
fi
runtime="$($python_cmd "$script_dir/runtime.py" --keyring)"
read -r -p "OpenAI-compatible API base URL: " base_url
if [[ -z "${base_url// }" ]]; then
printf '%s\n' "API base URL cannot be empty." >&2
exit 1
fi
"$runtime" "$script_dir/configure_key.py" --base-url "$base_url"
printf '%s\n' "Now enter the API key."
"$runtime" "$script_dir/configure_key.py"
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Store or remove the API key using a protected local store."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import sys
SERVICE = "codex-image-gen-custom"
def config_dir() -> Path:
documents = os.environ.get("CODEX_DOCUMENTS") or str(Path.home() / "Documents")
return Path(documents) / "Codex" / ".shared" / "image-gen-custom"
def base_url_path() -> Path:
return config_dir() / "base_url.txt"
def api_key_path() -> Path:
return config_dir() / "api_key.xml"
def store_api_key(key: str, user: str) -> None:
if os.name == "nt":
raise RuntimeError("Use configure.ps1 on Windows to store the API key securely.")
keyring = secure_keyring()
keyring.set_password(SERVICE, user, key)
def delete_api_key(user: str) -> None:
if os.name == "nt":
api_key_path().unlink(missing_ok=True)
return
keyring = secure_keyring()
keyring.delete_password(SERVICE, user)
def load_saved_api_key(user: str) -> str:
if os.name == "nt":
raise RuntimeError("Use generate-image.ps1 on Windows to read the saved API key securely.")
keyring = secure_keyring()
return (keyring.get_password(SERVICE, user) or "").strip()
def secure_keyring():
import keyring
backend = keyring.get_keyring()
backend_id = f"{backend.__class__.__module__}.{backend.__class__.__name__}".lower()
allowed = ("windows", "macos", "secretservice", "libsecret", "kwallet")
if not any(marker in backend_id for marker in allowed):
raise RuntimeError("The configured keyring is not a recognized protected OS credential store")
return keyring
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--user", default=os.environ.get("IMAGE_GEN_KEYRING_USER", "default"))
parser.add_argument("--delete", action="store_true")
parser.add_argument("--base-url", default="")
parser.add_argument("--delete-base-url", action="store_true")
args = parser.parse_args()
if args.delete_base_url:
base_url_path().unlink(missing_ok=True)
print("API base URL removed from the local configuration.")
return 0
if args.base_url:
path = base_url_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(args.base_url.rstrip("/") + "\n", encoding="utf-8")
print("API base URL stored in the local configuration.")
return 0
try:
if args.delete:
delete_api_key(args.user)
print("API key removed from the local secret store.")
else:
key = input("OpenAI-compatible API key: ").strip()
if not key:
raise ValueError("API key cannot be empty")
store_api_key(key, args.user)
if load_saved_api_key(args.user) != key:
raise RuntimeError("Stored key could not be read back")
print("API key stored in the local secret store.")
except Exception as exc:
print(f"Error: {exc}. Set OPENAI_API_KEY for a one-off run or run configure again.", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,36 @@
param(
[Parameter(Mandatory = $true)][string] $Prompt,
[string] $Out = "outputs\imagegen.png",
[string] $Size = "1024x1024",
[ValidateSet("low", "medium", "high", "auto")][string] $Quality = "low",
[string] $Model = "gpt-image-2",
[string] $BaseUrl = "",
[switch] $Force
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$pythonSelector = Join-Path $scriptDir "runtime.py"
$generator = Join-Path $scriptDir "generate_image.py"
function Get-ImageGenPython {
if ($env:CODEX_IMAGEGEN_PYTHON) {
$command = Get-Command $env:CODEX_IMAGEGEN_PYTHON -ErrorAction SilentlyContinue
if ($command) { return $command.Source }
}
$embedded = Join-Path $env:USERPROFILE ".cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
if (Test-Path -LiteralPath $embedded) { return $embedded }
$command = Get-Command python.exe -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) { return $command.Source }
$command = Get-Command py.exe -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) { return $command.Source }
throw "Python 3 not found; install Python or set CODEX_IMAGEGEN_PYTHON."
}
$python = Get-ImageGenPython
$runtime = & $python $pythonSelector
if ($LASTEXITCODE -ne 0 -or -not $runtime) { throw "Could not bootstrap Python runtime." }
$args = @($generator, "--model", $Model, "--prompt", $Prompt, "--size", $Size, "--quality", $Quality, "--out", $Out)
if ($BaseUrl) { $args += @("--base-url", $BaseUrl) }
if ($Force) { $args += "--force" }
& $runtime @args
if ($LASTEXITCODE -ne 0) { throw "Image generation failed with exit code $LASTEXITCODE." }
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
python_cmd="${CODEX_IMAGEGEN_PYTHON:-}"
if [[ -z "$python_cmd" ]]; then
embedded_python="$HOME/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe"
if [[ -x "$embedded_python" ]]; then
python_cmd="$embedded_python"
else
python_cmd="$(command -v python3 || command -v python || true)"
fi
fi
if [[ -z "$python_cmd" ]]; then
printf '%s\n' "Python 3 not found; install Python or set CODEX_IMAGEGEN_PYTHON." >&2
exit 1
fi
runtime="$($python_cmd "$script_dir/runtime.py" --keyring)"
exec "$runtime" "$script_dir/generate_image.py" "$@"
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Generate an image through an OpenAI-compatible Images API."""
from __future__ import annotations
import argparse
import base64
import binascii
import json
import os
import shutil
import subprocess
from pathlib import Path
import sys
import tempfile
import time
from typing import Any, Callable
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
MAX_IMAGE_BYTES = 50 * 1024 * 1024
DOWNLOAD_TIMEOUT_SECONDS = 120
API_TIMEOUT_SECONDS = 180
KEYRING_SERVICE = "codex-image-gen-custom"
KEYRING_USER = "default"
def config_dir() -> Path:
documents = os.environ.get("CODEX_DOCUMENTS") or str(Path.home() / "Documents")
return Path(documents) / "Codex" / ".shared" / "image-gen-custom"
def _windows_saved_key_path() -> Path:
return config_dir() / "api_key.dpapi"
def _decrypt_windows_saved_key(path: Path) -> str:
shell = shutil.which("powershell.exe") or shutil.which("pwsh")
if not shell:
raise RuntimeError("PowerShell is required to read the saved Windows API key")
script = r"""
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Security
$protected = [Convert]::FromBase64String((Get-Content -Raw -LiteralPath $env:CODEX_IMAGEGEN_KEY_PATH).Trim())
$plain = [System.Security.Cryptography.ProtectedData]::Unprotect(
$protected,
$null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
[Text.Encoding]::UTF8.GetString($plain)
"""
env = os.environ.copy()
env["CODEX_IMAGEGEN_KEY_PATH"] = str(path)
command = [shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script]
result = subprocess.run(command, check=False, capture_output=True, text=True, env=env)
if result.returncode != 0:
raise RuntimeError((result.stderr or "PowerShell could not decrypt the saved API key").strip())
return result.stdout.strip()
def _secure_keyring():
try:
import keyring
except ImportError as exc:
raise RuntimeError("OPENAI_API_KEY is not set and the keyring package is unavailable") from exc
backend = keyring.get_keyring()
backend_id = f"{backend.__class__.__module__}.{backend.__class__.__name__}".lower()
allowed = ("windows", "macos", "secretservice", "libsecret", "kwallet")
if not any(marker in backend_id for marker in allowed):
raise RuntimeError("The configured keyring is not a recognized protected OS credential store")
return keyring
def _field(item: Any, name: str) -> Any:
if isinstance(item, dict):
return item.get(name)
return getattr(item, name, None)
def _validate_image(raw: bytes) -> None:
signatures = (
raw.startswith(b"\x89PNG\r\n\x1a\n"),
raw.startswith(b"\xff\xd8\xff"),
raw.startswith((b"GIF87a", b"GIF89a")),
len(raw) >= 12 and raw[:4] in (b"RIFF", b"RIFX") and raw[8:12] == b"WEBP",
)
if not raw or not any(signatures):
raise ValueError("Image API returned data without a supported image signature")
def _download_image(url: str, *, opener: Callable[..., Any] = urlopen) -> bytes:
parsed = urlsplit(url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError("Image API returned an invalid image URL")
request = Request(url, headers={"User-Agent": "codex-image-gen-custom/1"})
with opener(request, timeout=DOWNLOAD_TIMEOUT_SECONDS) as response:
content_length = response.headers.get("Content-Length")
if content_length and int(content_length) > MAX_IMAGE_BYTES:
raise ValueError(f"Image URL exceeds the {MAX_IMAGE_BYTES}-byte limit")
raw = response.read(MAX_IMAGE_BYTES + 1)
if len(raw) > MAX_IMAGE_BYTES:
raise ValueError(f"Downloaded image exceeds the {MAX_IMAGE_BYTES}-byte limit")
return raw
def extract_image_bytes(item: Any, *, opener: Callable[..., Any] = urlopen) -> bytes:
image_b64 = _field(item, "b64_json")
if image_b64:
try:
raw = base64.b64decode(image_b64, validate=True)
except (binascii.Error, TypeError, ValueError) as exc:
raise ValueError("Image API returned invalid b64_json") from exc
else:
image_url = _field(item, "url")
if not image_url:
raise ValueError("Image API response contained neither b64_json nor url")
raw = _download_image(str(image_url), opener=opener)
_validate_image(raw)
return raw
def write_image(raw: bytes, output: Path, *, force: bool) -> None:
if output.exists() and not force:
raise FileExistsError(f"Output already exists: {output} (use --force to overwrite)")
output.parent.mkdir(parents=True, exist_ok=True)
fd, temporary_name = tempfile.mkstemp(prefix=f".{output.name}.", suffix=".tmp", dir=output.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(raw)
stream.flush()
os.fsync(stream.fileno())
temporary.replace(output)
finally:
temporary.unlink(missing_ok=True)
def load_api_key() -> str:
environment_key = os.environ.get("OPENAI_API_KEY", "").strip()
if environment_key:
return environment_key
if os.name == "nt":
key_path = _windows_saved_key_path()
if not key_path.exists():
raise RuntimeError("No API key found. Run configure or set OPENAI_API_KEY")
key = _decrypt_windows_saved_key(key_path)
if not key:
raise RuntimeError("No API key found. Run configure or set OPENAI_API_KEY")
return key
try:
keyring = _secure_keyring()
key = keyring.get_password(KEYRING_SERVICE, KEYRING_USER)
except Exception as exc:
raise RuntimeError("The OS keyring is unavailable; set OPENAI_API_KEY for this run") from exc
if not key:
raise RuntimeError("No API key found. Run configure or set OPENAI_API_KEY")
return key
def _configured_base_url() -> str:
environment_url = os.environ.get("OPENAI_BASE_URL", "").strip()
if environment_url:
return environment_url
config_path = config_dir() / "base_url.txt"
try:
configured_url = config_path.read_text(encoding="utf-8-sig").strip()
except OSError:
configured_url = ""
return configured_url
def _base_url(override: str = "") -> str:
base_url = override.strip() or _configured_base_url() or "https://api.openai.com/v1"
base_url = base_url.rstrip("/")
if not base_url.lower().endswith("/v1"):
base_url += "/v1"
return base_url
def _request_generation(*, api_key: str, base_url: str, payload: dict[str, Any]) -> dict[str, Any]:
endpoint = urljoin(f"{base_url.rstrip('/')}/", "images/generations")
request = Request(
endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "codex-image-gen-custom/1",
},
method="POST",
)
try:
with urlopen(request, timeout=API_TIMEOUT_SECONDS) as response:
raw = response.read()
except HTTPError as exc:
detail = exc.read(4096).decode("utf-8", errors="replace").strip()
suffix = f": {detail}" if detail else ""
raise RuntimeError(f"Image API returned HTTP {exc.code}{suffix}") from exc
except URLError as exc:
raise RuntimeError(f"Could not reach the Image API: {exc.reason}") from exc
try:
result = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError("Image API returned invalid JSON") from exc
if not isinstance(result, dict):
raise ValueError("Image API returned a non-object JSON response")
return result
def generate(args: argparse.Namespace) -> None:
api_key = load_api_key()
base_url = _base_url(args.base_url)
started = time.time()
print("Calling Image API. This can take up to a couple of minutes.", file=sys.stderr)
result = _request_generation(
api_key=api_key,
base_url=base_url,
payload={
"model": args.model,
"prompt": args.prompt,
"size": args.size,
"quality": args.quality,
},
)
print(f"Generation completed in {time.time() - started:.1f}s.", file=sys.stderr)
data = result.get("data")
if not isinstance(data, list) or not data:
raise ValueError("Image API returned an empty data list")
write_image(extract_image_bytes(data[0]), Path(args.out), force=args.force)
print(f"Wrote {args.out}")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default="", help="OpenAI-compatible API root")
parser.add_argument("--model", default="gpt-image-2")
parser.add_argument("--prompt", required=True)
parser.add_argument("--size", default="1024x1024")
parser.add_argument("--quality", choices=("low", "medium", "high", "auto"), default="low")
parser.add_argument("--out", required=True)
parser.add_argument("--force", action="store_true")
return parser
def main() -> int:
try:
generate(build_parser().parse_args())
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,15 @@
param([string] $CodexHome, [switch] $Force)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$pythonCommand = if ($env:CODEX_IMAGEGEN_PYTHON) {
Get-Command $env:CODEX_IMAGEGEN_PYTHON -ErrorAction SilentlyContinue
} else {
Get-Command python.exe -ErrorAction SilentlyContinue | Select-Object -First 1
}
if (-not $pythonCommand) { $pythonCommand = Get-Command py.exe -ErrorAction SilentlyContinue | Select-Object -First 1 }
if (-not $pythonCommand) { throw "Python 3 not found; install Python or set CODEX_IMAGEGEN_PYTHON." }
$args = @((Join-Path $scriptDir "install_skill.py"))
if ($CodexHome) { $args += @("--codex-home", $CodexHome) }
if ($Force) { $args += "--force" }
& $pythonCommand.Source @args
if ($LASTEXITCODE -ne 0) { throw "Skill installation failed with exit code $LASTEXITCODE." }
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
python_cmd="${CODEX_IMAGEGEN_PYTHON:-}"
if [[ -z "$python_cmd" ]]; then
python_cmd="$(command -v python3 || command -v python || true)"
fi
if [[ -z "$python_cmd" ]]; then
printf '%s\n' "Python 3 not found; install Python or set CODEX_IMAGEGEN_PYTHON." >&2
exit 1
fi
exec "$python_cmd" "$script_dir/install_skill.py" "$@"
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Install this skill without copying local secrets or runtime caches."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
EXCLUDED_FILES = {"connection.local.json"}
EXCLUDED_DIRS = {".git", "__pycache__", "venv"}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--codex-home", type=Path)
parser.add_argument("--force", action="store_true")
args = parser.parse_args()
source = Path(__file__).resolve().parent.parent
codex_home = args.codex_home or Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
destination = codex_home.expanduser() / "skills" / "image-gen-custom"
if source == destination.resolve():
print(f"Skill is already installed at: {destination}")
return 0
if destination.exists() and not args.force:
raise SystemExit(f"Skill already exists at {destination}; use --force to update it")
destination.mkdir(parents=True, exist_ok=True)
for item in source.rglob("*"):
relative = item.relative_to(source)
if item.name in EXCLUDED_FILES or any(part in EXCLUDED_DIRS for part in relative.parts):
continue
target = destination / relative
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target)
print(f"Installed image-gen-custom to: {destination}")
print(f"Configure the API with: python {destination / 'scripts' / 'configure_key.py'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Create the private runtime used by the platform wrappers."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess
import sys
import venv
def default_venv() -> Path:
override = os.environ.get("CODEX_IMAGEGEN_VENV", "").strip()
if override:
return Path(override).expanduser()
if os.name == "nt":
documents = os.environ.get("CODEX_DOCUMENTS") or (Path.home() / "Documents")
return Path(documents) / "Codex" / ".shared" / "image-gen-custom" / "venv"
if sys.platform == "darwin":
return Path.home() / "Library" / "Caches" / "Codex" / "image-gen-custom" / "venv"
root = os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")
return Path(root) / "codex" / "image-gen-custom" / "venv"
def bootstrap_python() -> list[str]:
override = os.environ.get("CODEX_IMAGEGEN_PYTHON", "").strip()
if override:
return [override]
candidates: list[list[str]] = []
if os.name == "nt":
cache_root = Path.home() / ".cache" / "codex-runtimes"
for path in sorted(cache_root.glob("**/dependencies/python/python.exe")):
if path.is_file():
candidates.append([str(path)])
for candidate in ("py", "python", "python3"):
found = shutil.which(candidate)
if found:
candidates.append([found, "-3"] if candidate == "py" else [found])
else:
for candidate in ("python3", "python"):
found = shutil.which(candidate)
if found:
candidates.append([found])
probe = "import sys"
for candidate in candidates:
try:
result = subprocess.run([*candidate, "-c", probe], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError:
continue
if result.returncode == 0:
return candidate
raise RuntimeError("Python 3 was not found; install Python or set CODEX_IMAGEGEN_PYTHON")
def ensure_runtime(require_keyring: bool = False) -> Path:
target = default_venv()
python_path = target / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
if not python_path.exists():
target.parent.mkdir(parents=True, exist_ok=True)
command = bootstrap_python() + ["-m", "venv", str(target)]
subprocess.run(command, check=True)
if require_keyring:
check = subprocess.run([str(python_path), "-c", "import keyring"], check=False)
else:
check = None
if check is not None and check.returncode:
packages = ["keyring"]
subprocess.run(
[str(python_path), "-m", "pip", "install", "--disable-pip-version-check", *packages],
check=True,
stdout=subprocess.DEVNULL,
)
return python_path
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--keyring", action="store_true")
options = parser.parse_args()
try:
print(ensure_runtime(options.keyring))
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
raise SystemExit(1)