launcher: image auto-resolution, --user in-place config persistence, --debug...

launcher: image auto-resolution, --user in-place config persistence, --debug SPEC, install/uninstall image handling

run_oci.sh:
- Resolve the installed coderai image when none is given (single -> use it,
  several -> menu); no longer hardcodes a possibly-wrong tag.
- --debug accepts SPEC as the next token (not just --debug=SPEC) and won't
  swallow an image-looking arg, fixing "image: <debug spec>" mishaps.
- --local puts the runtime dir under ~/.config/coderai-runtime (override
  --data-dir).
- New --user[=UID[:GID]]: run as that user and switch a config dir to an
  IN-PLACE mount so the app's config edits persist (owned by you). Without
  --user, --local/--config-dir stay a throwaway copy. Banner shows user.

Non-destructive config persistence (no startup rewrite):
- codai/cli.py: new --host/--port that override config.server in memory only.
- codai/main.py: apply those overrides right after config load (never written
  to config.json).
- coderai-oci: pass --host/--port to the server and stop rewriting an existing
  config.json (only create one on true first run) — so an in-place-mounted
  config dir is never modified at startup.

install.sh / uninstall.sh:
- install: after loading, offer to remove OTHER coderai images (default no);
  no auto-launch.
- uninstall: resolve the installed coderai image(s) to remove (single ->
  confirm, several -> menu / all / none) instead of a hardcoded tag.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 69ff740e
...@@ -194,6 +194,23 @@ configuration directory (--config DIR, default: OS-specific CoderAI directory). ...@@ -194,6 +194,23 @@ configuration directory (--config DIR, default: OS-specific CoderAI directory).
"volume when /tmp is small — 4x upscaling can exhaust a small /tmp " "volume when /tmp is small — 4x upscaling can exhaust a small /tmp "
"('No space left on device').", "('No space left on device').",
) )
parser.add_argument(
"--host",
type=str,
default=None,
metavar="ADDR",
help="Override the server bind host (config.server.host) for this run only, "
"WITHOUT modifying config.json. Used by the container launcher so an "
"in-place-mounted config dir is never rewritten.",
)
parser.add_argument(
"--port",
type=int,
default=None,
metavar="PORT",
help="Override the server bind port (config.server.port) for this run only, "
"WITHOUT modifying config.json.",
)
parser.add_argument( parser.add_argument(
"--debug", "--debug",
action="store_true", action="store_true",
......
...@@ -321,6 +321,16 @@ def main(): ...@@ -321,6 +321,16 @@ def main():
config_mgr = ConfigManager(config_dir) config_mgr = ConfigManager(config_dir)
config = config_mgr.load() config = config_mgr.load()
# CLI --host/--port override the server bind for THIS run only, in memory —
# they are never written back to config.json. The container launcher uses this
# so a config dir mounted in place (persistent) is not modified at startup.
_cli_host = getattr(args, "host", None)
_cli_port = getattr(args, "port", None)
if _cli_host:
config.server.host = _cli_host
if _cli_port:
config.server.port = int(_cli_port)
# Configure the temporary-working-files directory as early as possible (before # Configure the temporary-working-files directory as early as possible (before
# any tempfile.* call). CLI --tmp overrides config.tmp_dir. Setting both # any tempfile.* call). CLI --tmp overrides config.tmp_dir. Setting both
# tempfile.tempdir and TMPDIR/TMP/TEMP makes every tempfile.* call AND child # tempfile.tempdir and TMPDIR/TMP/TEMP makes every tempfile.* call AND child
......
...@@ -55,7 +55,10 @@ if ! "$ENGINE" info >/dev/null 2>&1; then ...@@ -55,7 +55,10 @@ if ! "$ENGINE" info >/dev/null 2>&1; then
fi fi
say "[install] loading image from $IMAGE_TAR — this is large, please wait…" say "[install] loading image from $IMAGE_TAR — this is large, please wait…"
"${DK[@]}" load -i "$IMAGE_TAR" LOAD_OUT="$("${DK[@]}" load -i "$IMAGE_TAR")"
say "$LOAD_OUT"
# e.g. "Loaded image: coderai:full_all_0.1.0" — the tag we just installed.
LOADED_TAG="$(printf '%s\n' "$LOAD_OUT" | sed -n 's/^Loaded image: //p' | head -1)"
# Pick the install dir by privilege. # Pick the install dir by privilege.
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then
...@@ -85,6 +88,35 @@ if [ "$(id -u)" -ne 0 ]; then ...@@ -85,6 +88,35 @@ if [ "$(id -u)" -ne 0 ]; then
esac esac
fi fi
# Offer to remove OTHER coderai images (e.g. older versions) — default NO.
OTHERS=()
while IFS= read -r line; do
[ -n "$line" ] && [ "$line" != "$LOADED_TAG" ] && OTHERS+=("$line")
done < <(
"${DK[@]}" images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
| grep -E '^coderai:' | grep -v ':<none>$' | sort -u)
if [ "${#OTHERS[@]}" -gt 0 ]; then
say ""
say "[install] other coderai image(s) already present:"
for img in "${OTHERS[@]}"; do say " $img"; done
# Default NO — only remove on an explicit yes (and not under non-interactive --yes).
if [ "$ASSUME_YES" -ne 1 ]; then
printf '[install] remove the other image(s) above? [y/N] '
read -r reply </dev/tty || reply=""
case "$reply" in
y|Y|yes|YES)
for img in "${OTHERS[@]}"; do
"${DK[@]}" image rm "$img" >/dev/null 2>&1 && say "[install] removed $img" \
|| say "[install] could not remove $img (in use?)"
done ;;
*) say "[install] keeping the other image(s)." ;;
esac
else
say "[install] --yes given; keeping the other image(s) (default)."
fi
fi
say "" say ""
say "Done. Quick start:" say "Done. Quick start:"
say " coderai-docker --nvidia # or --vulkan / --cpu" say " coderai-docker --nvidia # or --vulkan / --cpu"
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
# CoderAI Docker distribution uninstaller — reverses install.sh. It: # CoderAI Docker distribution uninstaller — reverses install.sh. It:
# 1. Removes the `coderai-docker` runner from both the root and user install # 1. Removes the `coderai-docker` runner from both the root and user install
# dirs (/usr/local/bin and ~/.local/usr/bin). # dirs (/usr/local/bin and ~/.local/usr/bin).
# 2. Removes the loaded image (unless --keep-image). # 2. Removes the installed CoderAI image(s) (unless --keep-image). With no
# --image, it resolves the actual loaded coderai image(s): one -> that one,
# several -> asks which (or all). --image TAG targets a specific tag.
# 3. Points out the PATH line install.sh may have added to ~/.bashrc. # 3. Points out the PATH line install.sh may have added to ~/.bashrc.
# #
# It does NOT touch your runtime data (coderai-runtime/, ~/.coderai) — those are # It does NOT touch your runtime data (coderai-runtime/, ~/.coderai) — those are
...@@ -10,20 +12,22 @@ ...@@ -10,20 +12,22 @@
# #
# Usage: ./uninstall.sh [--keep-image] [--image TAG] [--yes] # Usage: ./uninstall.sh [--keep-image] [--image TAG] [--yes]
# Env: CONTAINER_ENGINE=docker|podman (default docker) # Env: CONTAINER_ENGINE=docker|podman (default docker)
# OCI_IMAGE=TAG image tag to remove (default coderai:dist) # OCI_IMAGE=TAG specific image tag to remove
set -euo pipefail set -euo pipefail
ENGINE="${CONTAINER_ENGINE:-docker}" ENGINE="${CONTAINER_ENGINE:-docker}"
IMAGE="${OCI_IMAGE:-coderai:dist}" IMAGE="${OCI_IMAGE:-}"
IMAGE_EXPLICIT=0
[ -n "$IMAGE" ] && IMAGE_EXPLICIT=1
KEEP_IMAGE=0 KEEP_IMAGE=0
ASSUME_YES=0 ASSUME_YES=0
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--keep-image) KEEP_IMAGE=1; shift ;; --keep-image) KEEP_IMAGE=1; shift ;;
--image) [[ $# -ge 2 ]] || { echo "Error: --image requires a tag" >&2; exit 2; }; IMAGE="$2"; shift 2 ;; --image) [[ $# -ge 2 ]] || { echo "Error: --image requires a tag" >&2; exit 2; }; IMAGE="$2"; IMAGE_EXPLICIT=1; shift 2 ;;
-y|--yes) ASSUME_YES=1; shift ;; -y|--yes) ASSUME_YES=1; shift ;;
-h|--help) sed -n '2,12p' "$0"; exit 0 ;; -h|--help) sed -n '2,16p' "$0"; exit 0 ;;
*) echo "Error: unknown option: $1" >&2; exit 2 ;; *) echo "Error: unknown option: $1" >&2; exit 2 ;;
esac esac
done done
...@@ -32,14 +36,23 @@ say(){ printf '%s\n' "$*"; } ...@@ -32,14 +36,23 @@ say(){ printf '%s\n' "$*"; }
die(){ printf 'Error: %s\n' "$*" >&2; exit 1; } die(){ printf 'Error: %s\n' "$*" >&2; exit 1; }
# Tell the user what this will do, and confirm before proceeding. # Tell the user what this will do, and confirm before proceeding.
say "This will remove the 'coderai-docker' runner and$([ "$KEEP_IMAGE" -eq 1 ] && echo ' keep' || echo ' (after a prompt) remove') the CoderAI image" if [ "$KEEP_IMAGE" -eq 1 ]; then _img_note="keep the CoderAI image(s)";
say "'$IMAGE' from $ENGINE. Your runtime data and config are not touched." elif [ "$IMAGE_EXPLICIT" -eq 1 ]; then _img_note="(after a prompt) remove the image '$IMAGE'";
else _img_note="(after a prompt) remove the installed CoderAI image(s)"; fi
say "This will remove the 'coderai-docker' runner and $_img_note from $ENGINE."
say "Your runtime data and config are not touched."
if [ "$ASSUME_YES" -ne 1 ]; then if [ "$ASSUME_YES" -ne 1 ]; then
printf 'Proceed? [y/N] ' printf 'Proceed? [y/N] '
read -r reply </dev/tty || reply="" read -r reply </dev/tty || reply=""
case "$reply" in y|Y|yes|YES) ;; *) die "aborted by user." ;; esac case "$reply" in y|Y|yes|YES) ;; *) die "aborted by user." ;; esac
fi fi
# Engine access (sudo only if needed for the daemon).
DK=("$ENGINE")
if ! "$ENGINE" info >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then DK=(sudo "$ENGINE"); fi
fi
# 1. Remove the runner from every place install.sh may have put it. # 1. Remove the runner from every place install.sh may have put it.
removed_any=0 removed_any=0
for d in /usr/local/bin "$HOME/.local/usr/bin"; do for d in /usr/local/bin "$HOME/.local/usr/bin"; do
...@@ -55,26 +68,64 @@ for d in /usr/local/bin "$HOME/.local/usr/bin"; do ...@@ -55,26 +68,64 @@ for d in /usr/local/bin "$HOME/.local/usr/bin"; do
done done
[ "$removed_any" -eq 1 ] || say "[uninstall] no coderai-docker runner found in the standard locations." [ "$removed_any" -eq 1 ] || say "[uninstall] no coderai-docker runner found in the standard locations."
# 2. Remove the image unless asked to keep it. # Remove one image tag (helper).
if [ "$KEEP_IMAGE" -eq 0 ]; then rm_image(){
DK=("$ENGINE") "${DK[@]}" image rm "$1" >/dev/null 2>&1 && say "[uninstall] removed image: $1" \
if ! "$ENGINE" info >/dev/null 2>&1; then || say "[uninstall] could not remove image: $1"
if command -v sudo >/dev/null 2>&1; then DK=(sudo "$ENGINE"); fi }
fi
# 2. Remove the image(s) unless asked to keep them.
if [ "$KEEP_IMAGE" -eq 1 ]; then
say "[uninstall] keeping image(s) (--keep-image)."
elif [ "$IMAGE_EXPLICIT" -eq 1 ]; then
# Explicit tag: remove just that one.
if "${DK[@]}" image inspect "$IMAGE" >/dev/null 2>&1; then if "${DK[@]}" image inspect "$IMAGE" >/dev/null 2>&1; then
if [ "$ASSUME_YES" -ne 1 ]; then if [ "$ASSUME_YES" -eq 1 ]; then rm_image "$IMAGE"; else
printf '[uninstall] remove image "%s"? [y/N] ' "$IMAGE" printf '[uninstall] remove image "%s"? [y/N] ' "$IMAGE"
read -r reply </dev/tty || reply="" read -r reply </dev/tty || reply=""
case "$reply" in y|Y|yes|YES) ;; *) say "[uninstall] keeping image $IMAGE."; KEEP_IMAGE=1 ;; esac case "$reply" in y|Y|yes|YES) rm_image "$IMAGE" ;; *) say "[uninstall] keeping image $IMAGE." ;; esac
fi
if [ "$KEEP_IMAGE" -eq 0 ]; then
"${DK[@]}" image rm "$IMAGE" && say "[uninstall] removed image: $IMAGE"
fi fi
else else
say "[uninstall] image '$IMAGE' not present (use --image TAG if you tagged it differently)." say "[uninstall] image '$IMAGE' not present."
fi fi
else else
say "[uninstall] keeping image (--keep-image)." # Resolve the installed coderai image(s).
IMGS=()
while IFS= read -r line; do [ -n "$line" ] && IMGS+=("$line"); done < <(
"${DK[@]}" images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
| grep -E '^coderai:' | grep -v ':<none>$' | sort -u)
case "${#IMGS[@]}" in
0) say "[uninstall] no coderai image found to remove." ;;
1)
if [ "$ASSUME_YES" -eq 1 ]; then rm_image "${IMGS[0]}"; else
printf '[uninstall] remove image "%s"? [y/N] ' "${IMGS[0]}"
read -r reply </dev/tty || reply=""
case "$reply" in y|Y|yes|YES) rm_image "${IMGS[0]}" ;; *) say "[uninstall] keeping image ${IMGS[0]}." ;; esac
fi
;;
*)
if [ "$ASSUME_YES" -eq 1 ]; then
say "[uninstall] multiple coderai images; --yes -> removing all:"
for img in "${IMGS[@]}"; do rm_image "$img"; done
else
say "Multiple coderai images found:"
i=1; for img in "${IMGS[@]}"; do printf ' %d) %s\n' "$i" "$img"; i=$((i+1)); done
printf 'Remove which? [1-%d / a=all / n=none, default n]: ' "${#IMGS[@]}"
read -r sel </dev/tty || sel=""
case "$sel" in
a|A|all) for img in "${IMGS[@]}"; do rm_image "$img"; done ;;
""|n|N|no|NO) say "[uninstall] keeping all images." ;;
*)
if printf '%s' "$sel" | grep -qE '^[0-9]+$' && [ "$sel" -ge 1 ] && [ "$sel" -le "${#IMGS[@]}" ]; then
rm_image "${IMGS[$((sel-1))]}"
else
die "invalid selection: $sel"
fi
;;
esac
fi
;;
esac
fi fi
# 3. Note the PATH line install.sh may have appended (we don't edit ~/.bashrc). # 3. Note the PATH line install.sh may have appended (we don't edit ~/.bashrc).
......
...@@ -32,8 +32,10 @@ mkdir -p "$CODERAI_CONFIG_DIR/coderai" "$CODERAI_MODELS_DIR/coderai" "$CODERAI_C ...@@ -32,8 +32,10 @@ mkdir -p "$CODERAI_CONFIG_DIR/coderai" "$CODERAI_MODELS_DIR/coderai" "$CODERAI_C
CONFIG_DIR="$CODERAI_CONFIG_DIR/coderai" CONFIG_DIR="$CODERAI_CONFIG_DIR/coderai"
CONFIG_FILE="$CONFIG_DIR/config.json" CONFIG_FILE="$CONFIG_DIR/config.json"
# Ensure the container binds to all interfaces even when config.json was created # Bind host/port are passed to coderai on the CLI (--host/--port below), which
# by the app default, which uses 127.0.0.1 for local desktop installs. # override config.server in memory WITHOUT modifying config.json. This keeps an
# in-place-mounted config dir untouched at startup. We only CREATE config.json on
# first run (when it doesn't exist yet) — that's not modifying user config.
if [ ! -f "$CONFIG_FILE" ]; then if [ ! -f "$CONFIG_FILE" ]; then
/opt/coderai/python/bin/python3 - "$CONFIG_FILE" "$CODERAI_HOST" "$CODERAI_PORT" <<'PY' /opt/coderai/python/bin/python3 - "$CONFIG_FILE" "$CODERAI_HOST" "$CODERAI_PORT" <<'PY'
import json import json
...@@ -45,31 +47,6 @@ host = sys.argv[2] ...@@ -45,31 +47,6 @@ host = sys.argv[2]
port = int(sys.argv[3]) port = int(sys.argv[3])
path.write_text(json.dumps({"server": {"host": host, "port": port}}, indent=2) + "\n") path.write_text(json.dumps({"server": {"host": host, "port": port}}, indent=2) + "\n")
PY PY
else
/opt/coderai/python/bin/python3 - "$CONFIG_FILE" "$CODERAI_HOST" "$CODERAI_PORT" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
host = sys.argv[2]
port = int(sys.argv[3])
try:
data = json.loads(path.read_text())
except Exception:
data = {}
server = data.setdefault("server", {})
changed = False
if server.get("host") in (None, "", "127.0.0.1", "localhost") or host != "0.0.0.0":
if server.get("host") != host:
server["host"] = host
changed = True
if server.get("port") != port:
server["port"] = port
changed = True
if changed:
path.write_text(json.dumps(data, indent=2) + "\n")
PY
fi fi
# Optional debug logging. CODERAI_DEBUG selects coderai's --debug* flags: # Optional debug logging. CODERAI_DEBUG selects coderai's --debug* flags:
...@@ -97,8 +74,9 @@ esac ...@@ -97,8 +74,9 @@ esac
# only sub-flags. # only sub-flags.
case " $DEBUG_ARGS " in *" --debug "*) : ;; *[!\ ]*) DEBUG_ARGS="--debug$DEBUG_ARGS" ;; esac case " $DEBUG_ARGS " in *" --debug "*) : ;; *[!\ ]*) DEBUG_ARGS="--debug$DEBUG_ARGS" ;; esac
# Assemble the server argv: --config, optional --tmp, debug flags, then passthrough. # Assemble the server argv: --config, --host/--port (override config.server in
set -- --config "$CONFIG_DIR" "$@" # memory, never written to config.json), optional --tmp, debug flags, passthrough.
set -- --config "$CONFIG_DIR" --host "$CODERAI_HOST" --port "$CODERAI_PORT" "$@"
[ -n "${CODERAI_TMP:-}" ] && set -- "$@" --tmp "$CODERAI_TMP" [ -n "${CODERAI_TMP:-}" ] && set -- "$@" --tmp "$CODERAI_TMP"
# Extra coderai CLI flags passed straight through from the host launcher # Extra coderai CLI flags passed straight through from the host launcher
......
...@@ -9,7 +9,14 @@ if [[ -f "$VERSIONS_FILE" ]]; then ...@@ -9,7 +9,14 @@ if [[ -f "$VERSIONS_FILE" ]]; then
fi fi
ENGINE="${CONTAINER_ENGINE:-docker}" ENGINE="${CONTAINER_ENGINE:-docker}"
# Default image tag (this literal is pinned by make_dist_bundle to the shipped
# tag). It's only a FALLBACK: when the user gives no explicit image (positional
# arg) and OCI_IMAGE is unset, we resolve the actual loaded coderai image below —
# auto-pick when there's exactly one, ask when there are several — so the runner
# always targets a real installed image instead of a possibly-wrong fixed tag.
IMAGE_TAG="${OCI_IMAGE:-coderai:dist}" IMAGE_TAG="${OCI_IMAGE:-coderai:dist}"
IMAGE_EXPLICIT=0
[[ -n "${OCI_IMAGE:-}" ]] && IMAGE_EXPLICIT=1
# Selected GPU backends. ADDITIVE: --nvidia --vulkan enables BOTH, so the # Selected GPU backends. ADDITIVE: --nvidia --vulkan enables BOTH, so the
# container gets the NVIDIA driver libs (libcuda.so.1 — needed even by a # container gets the NVIDIA driver libs (libcuda.so.1 — needed even by a
# CUDA-built llama-cpp running under Vulkan) AND /dev/dri. CPU always works. # CUDA-built llama-cpp running under Vulkan) AND /dev/dri. CPU always works.
...@@ -26,7 +33,19 @@ HOST_BIND="${CODERAI_HOST_BIND:-}" ...@@ -26,7 +33,19 @@ HOST_BIND="${CODERAI_HOST_BIND:-}"
# container (via CODERAI_EXTRA_ARGS, appended by the in-image coderai launcher). # container (via CODERAI_EXTRA_ARGS, appended by the in-image coderai launcher).
# Built from --coderai-arg (repeatable, one token each) and --coderai-args "...". # Built from --coderai-arg (repeatable, one token each) and --coderai-args "...".
CODERAI_EXTRA_ARGS="${CODERAI_EXTRA_ARGS:-}" CODERAI_EXTRA_ARGS="${CODERAI_EXTRA_ARGS:-}"
# Runtime dir (models/cache, and config when not using --local). Defaults to
# $PWD/coderai-runtime, but for --local it moves to a stable per-user location
# (~/.config/coderai-runtime) so it doesn't depend on the current directory —
# unless the user pins it with --data-dir. DATA_ROOT_EXPLICIT tracks --data-dir.
DATA_ROOT="$PWD/coderai-runtime" DATA_ROOT="$PWD/coderai-runtime"
DATA_ROOT_EXPLICIT=0
IS_LOCAL=0
# Run the container as a specific user. Empty = container default (root). When set
# (e.g. via --user with no value -> your uid:gid) AND a config dir is used, the
# config is mounted IN PLACE so the app's edits persist to it (files stay owned by
# you). Without --user, an in-place mount as root would create root-owned files,
# so we fall back to the throwaway temp copy.
USER_SPEC=""
DETACH=0 DETACH=0
NAME="coderai" NAME="coderai"
EXTRA_ARGS=() EXTRA_ARGS=()
...@@ -87,18 +106,26 @@ Options: ...@@ -87,18 +106,26 @@ Options:
--coderai-args STR Pass a raw string of extra coderai flags (space-separated), --coderai-args STR Pass a raw string of extra coderai flags (space-separated),
e.g. --coderai-args "--foo bar --baz". Appended after any e.g. --coderai-args "--foo bar --baz". Appended after any
--coderai-arg values. --coderai-arg values.
--data-dir PATH Directory for config/models/cache (default: ./coderai-runtime). --data-dir PATH Directory for config/models/cache. Default ./coderai-runtime,
or ~/.config/coderai-runtime when --local is used.
--name NAME Container name (default: coderai). --name NAME Container name (default: coderai).
-d, --detach Run in background. -d, --detach Run in background.
--config-dir PATH Use an EXISTING config dir (with config.json/models.json), --config-dir PATH Use an EXISTING config dir (with config.json/models.json),
mounted at /config/coderai. Copied to a temp dir by default mounted at /config/coderai. Copied to a temp dir by default
so the image's host/port rewrite leaves your dir untouched. so the image's host/port rewrite leaves your dir untouched.
--local Shortcut for --config-dir ~/.coderai. --local Shortcut for --config-dir ~/.coderai. Also puts the runtime
--inplace-config Mount --config-dir in place (the image WILL edit host/port). dir under ~/.config/coderai-runtime (override with --data-dir).
Add --user to persist the app's config edits back to it.
--user[=UID[:GID]] Run the container as that user (no value = your uid:gid). With
a config dir, switches to an IN-PLACE mount so config edits
persist there, owned by you. Without it, --config-dir/--local
use a throwaway copy (no persistence).
--inplace-config Mount --config-dir in place (config edits persist there).
--map HOST[:CONT] Bind-mount a host dir at the SAME path (or HOST:CONT) inside --map HOST[:CONT] Bind-mount a host dir at the SAME path (or HOST:CONT) inside
the container, so absolute paths in models.json resolve the container, so absolute paths in models.json resolve
(e.g. --map /AI/guffcache). Repeatable. (e.g. --map /AI/guffcache). Repeatable.
--debug[=SPEC] Run coderai with debug flags. SPEC (default 'all'): --debug[=SPEC] Run coderai with debug flags. SPEC (default 'all'); may be
given as --debug=SPEC or --debug SPEC:
all | engine,requests,ws,web,thermal,lora,engine-web all | engine,requests,ws,web,thermal,lora,engine-web
Also writes a host-tailable file log (see --log-file). Also writes a host-tailable file log (see --log-file).
--log-file PATH In-container log path (default /cache/logs/coderai.log, --log-file PATH In-container log path (default /cache/logs/coderai.log,
...@@ -144,19 +171,34 @@ while [[ $# -gt 0 ]]; do ...@@ -144,19 +171,34 @@ while [[ $# -gt 0 ]]; do
CODERAI_EXTRA_ARGS="${CODERAI_EXTRA_ARGS:+$CODERAI_EXTRA_ARGS }$2"; shift 2 ;; CODERAI_EXTRA_ARGS="${CODERAI_EXTRA_ARGS:+$CODERAI_EXTRA_ARGS }$2"; shift 2 ;;
--data-dir) --data-dir)
[[ $# -ge 2 ]] || { echo "Error: --data-dir requires a path" >&2; exit 2; } [[ $# -ge 2 ]] || { echo "Error: --data-dir requires a path" >&2; exit 2; }
DATA_ROOT="$2"; shift 2 ;; DATA_ROOT="$2"; DATA_ROOT_EXPLICIT=1; shift 2 ;;
--name) --name)
[[ $# -ge 2 ]] || { echo "Error: --name requires a value" >&2; exit 2; } [[ $# -ge 2 ]] || { echo "Error: --name requires a value" >&2; exit 2; }
NAME="$2"; shift 2 ;; NAME="$2"; shift 2 ;;
--config-dir) --config-dir)
[[ $# -ge 2 ]] || { echo "Error: --config-dir requires a path" >&2; exit 2; } [[ $# -ge 2 ]] || { echo "Error: --config-dir requires a path" >&2; exit 2; }
CONFIG_DIR_SRC="$2"; shift 2 ;; CONFIG_DIR_SRC="$2"; shift 2 ;;
--local) CONFIG_DIR_SRC="$HOME/.coderai"; shift ;; --local) CONFIG_DIR_SRC="$HOME/.coderai"; IS_LOCAL=1; shift ;;
--inplace-config) INPLACE_CONFIG=1; shift ;; --inplace-config) INPLACE_CONFIG=1; shift ;;
# --user [UID[:GID]] runs the container as that user (default: your uid:gid).
# With a config dir, this also switches the mount to IN PLACE so edits persist.
--user)
if [[ $# -ge 2 && "$2" =~ ^[0-9] ]]; then USER_SPEC="$2"; shift 2
else USER_SPEC="$(id -u):$(id -g)"; shift; fi ;;
--user=*) USER_SPEC="${1#*=}"; shift ;;
--map) --map)
[[ $# -ge 2 ]] || { echo "Error: --map requires HOST[:CONT]" >&2; exit 2; } [[ $# -ge 2 ]] || { echo "Error: --map requires HOST[:CONT]" >&2; exit 2; }
MAPS+=("$2"); shift 2 ;; MAPS+=("$2"); shift 2 ;;
--debug) DEBUG_SPEC="all"; shift ;; # --debug accepts an optional SPEC as the next token (e.g. --debug engine,ws)
# OR as --debug=SPEC. The next token is only taken as SPEC when it isn't
# another option and doesn't look like an image ref (no ':' or '/'), so e.g.
# `--debug coderai:tag` leaves the tag as the image, not the debug spec.
--debug)
if [[ $# -ge 2 && "$2" != -* && "$2" != *[:/]* ]]; then
DEBUG_SPEC="$2"; shift 2
else
DEBUG_SPEC="all"; shift
fi ;;
--debug=*) DEBUG_SPEC="${1#*=}"; shift ;; --debug=*) DEBUG_SPEC="${1#*=}"; shift ;;
--log-file) --log-file)
[[ $# -ge 2 ]] || { echo "Error: --log-file requires a path" >&2; exit 2; } [[ $# -ge 2 ]] || { echo "Error: --log-file requires a path" >&2; exit 2; }
...@@ -177,10 +219,54 @@ while [[ $# -gt 0 ]]; do ...@@ -177,10 +219,54 @@ while [[ $# -gt 0 ]]; do
break ;; break ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
-*) echo "Error: unknown option: $1" >&2; usage >&2; exit 2 ;; -*) echo "Error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) IMAGE_TAG="$1"; shift ;; *) IMAGE_TAG="$1"; IMAGE_EXPLICIT=1; shift ;;
esac esac
done done
# Resolve the image when the user didn't name one: prefer the single loaded
# coderai image; if several exist, ask (default = the pinned fallback if present,
# else the first). Keeps the runner pointed at a real installed image instead of
# a fixed tag that may not exist after a rebuild/retag.
if [[ "$IMAGE_EXPLICIT" -eq 0 ]]; then
_imgs=()
while IFS= read -r _l; do [[ -n "$_l" ]] && _imgs+=("$_l"); done < <(
"$ENGINE" images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
| grep -E '^coderai:' | grep -v ':<none>$' | sort -u)
if [[ "${#_imgs[@]}" -eq 1 ]]; then
IMAGE_TAG="${_imgs[0]}"
elif [[ "${#_imgs[@]}" -gt 1 ]]; then
# Default selection: the pinned fallback if it's one of them, else the first.
_default="${_imgs[0]}"
for _i in "${_imgs[@]}"; do [[ "$_i" == "$IMAGE_TAG" ]] && _default="$IMAGE_TAG"; done
if [[ -t 0 ]]; then
echo "Multiple coderai images found — choose one to run:" >&2
_n=1; for _i in "${_imgs[@]}"; do
_mark=""; [[ "$_i" == "$_default" ]] && _mark=" (default)"
printf ' %d) %s%s\n' "$_n" "$_i" "$_mark" >&2; _n=$((_n+1))
done
printf 'Selection [1-%d, default %s]: ' "${#_imgs[@]}" "$_default" >&2
read -r _sel </dev/tty || _sel=""
if [[ -z "$_sel" ]]; then
IMAGE_TAG="$_default"
elif [[ "$_sel" =~ ^[0-9]+$ ]] && (( _sel >= 1 && _sel <= ${#_imgs[@]} )); then
IMAGE_TAG="${_imgs[$((_sel-1))]}"
else
echo "Error: invalid selection: $_sel" >&2; exit 2
fi
else
IMAGE_TAG="$_default"
echo "Note: multiple coderai images; no TTY to choose — using $IMAGE_TAG" >&2
fi
fi
# 0 images: keep the pinned fallback (docker run will report if it's missing).
fi
# For --local, default the runtime dir to a stable per-user location under
# ~/.config instead of the current directory (unless --data-dir was given).
if [[ "$IS_LOCAL" -eq 1 && "$DATA_ROOT_EXPLICIT" -eq 0 ]]; then
DATA_ROOT="${XDG_CONFIG_HOME:-$HOME/.config}/coderai-runtime"
fi
mkdir -p "$DATA_ROOT/config" "$DATA_ROOT/models" "$DATA_ROOT/cache" mkdir -p "$DATA_ROOT/config" "$DATA_ROOT/models" "$DATA_ROOT/cache"
DATA_ROOT="$(cd "$DATA_ROOT" && pwd)" DATA_ROOT="$(cd "$DATA_ROOT" && pwd)"
...@@ -197,6 +283,9 @@ args=(run --rm --name "$NAME" --ipc=host -p "$PUBLISH" -e CODERAI_HOST=0.0.0.0 - ...@@ -197,6 +283,9 @@ args=(run --rm --name "$NAME" --ipc=host -p "$PUBLISH" -e CODERAI_HOST=0.0.0.0 -
if [[ -n "$CODERAI_EXTRA_ARGS" ]]; then if [[ -n "$CODERAI_EXTRA_ARGS" ]]; then
args+=(-e "CODERAI_EXTRA_ARGS=$CODERAI_EXTRA_ARGS") args+=(-e "CODERAI_EXTRA_ARGS=$CODERAI_EXTRA_ARGS")
fi fi
if [[ -n "$USER_SPEC" ]]; then
args+=(--user "$USER_SPEC")
fi
if [[ "$DETACH" == "1" ]]; then if [[ "$DETACH" == "1" ]]; then
args+=(-d) args+=(-d)
fi fi
...@@ -209,6 +298,15 @@ fi ...@@ -209,6 +298,15 @@ fi
if [[ -n "${MODES[nvidia]:-}" ]]; then if [[ -n "${MODES[nvidia]:-}" ]]; then
if [[ "$ENGINE" == "docker" ]]; then if [[ "$ENGINE" == "docker" ]]; then
args+=(--gpus all) args+=(--gpus all)
# Some hosts set `no-cgroups = true` in /etc/nvidia-container-runtime/config.toml.
# Then --gpus all injects the device nodes + driver libs but does NOT add them to
# the container's device-cgroup allowlist, so the kernel blocks GPU access and
# NVML fails ("Failed to initialize NVML: Unknown Error") -> torch.cuda sees no
# GPU -> coderai falls back to vulkan/cpu. Passing the nodes via --device adds
# them to the cgroup allowlist. Harmless when cgroups are managed normally.
for _dev in /dev/nvidia*; do
[[ -c "$_dev" ]] && args+=(--device "$_dev")
done
else else
args+=(--hooks-dir=/usr/share/containers/oci/hooks.d) args+=(--hooks-dir=/usr/share/containers/oci/hooks.d)
fi fi
...@@ -260,19 +358,25 @@ CONFIG_NOTE="$DATA_ROOT/config (fresh)" ...@@ -260,19 +358,25 @@ CONFIG_NOTE="$DATA_ROOT/config (fresh)"
if [[ -n "$CONFIG_DIR_SRC" ]]; then if [[ -n "$CONFIG_DIR_SRC" ]]; then
[[ -d "$CONFIG_DIR_SRC" ]] || { echo "Error: --config-dir '$CONFIG_DIR_SRC' not found" >&2; exit 2; } [[ -d "$CONFIG_DIR_SRC" ]] || { echo "Error: --config-dir '$CONFIG_DIR_SRC' not found" >&2; exit 2; }
CONFIG_DIR_SRC="$(cd "$CONFIG_DIR_SRC" && pwd)" CONFIG_DIR_SRC="$(cd "$CONFIG_DIR_SRC" && pwd)"
if [[ "$INPLACE_CONFIG" == "1" ]]; then # Mount IN PLACE (so the app's config edits persist back to the real dir) when
# explicitly asked (--inplace-config) OR when running as a specific --user (so
# the files written stay owned by you, not root). The in-image launcher no
# longer rewrites host/port into config.json (it passes them on the CLI), so an
# in-place mount is non-destructive. Without --user, fall back to a throwaway
# copy so a root container can't leave root-owned files in your real config.
if [[ "$INPLACE_CONFIG" == "1" || -n "$USER_SPEC" ]]; then
CFG_MOUNT="$CONFIG_DIR_SRC" CFG_MOUNT="$CONFIG_DIR_SRC"
CONFIG_NOTE="$CONFIG_DIR_SRC (in place — image rewrites host/port!)" CONFIG_NOTE="$CONFIG_DIR_SRC (in place — edits persist here)"
else else
# Copy ONLY the json config files to a throwaway dir so the image's host/port # Copy ONLY the json config files to a throwaway dir so nothing in the
# rewrite never touches your real config, and we don't copy big subdirs # container can touch your real config, and we don't copy big subdirs
# (e.g. ~/.coderai/ds4 weights). # (e.g. ~/.coderai/ds4 weights).
CFG_PARENT="$(mktemp -d "${TMPDIR:-/tmp}/coderai-cfg.XXXXXX")" CFG_PARENT="$(mktemp -d "${TMPDIR:-/tmp}/coderai-cfg.XXXXXX")"
CFG_MOUNT="$CFG_PARENT/coderai" CFG_MOUNT="$CFG_PARENT/coderai"
mkdir -p "$CFG_MOUNT" mkdir -p "$CFG_MOUNT"
cp -a "$CONFIG_DIR_SRC"/*.json "$CFG_MOUNT/" 2>/dev/null || true cp -a "$CONFIG_DIR_SRC"/*.json "$CFG_MOUNT/" 2>/dev/null || true
[[ -f "$CFG_MOUNT/config.json" ]] || { echo "Error: no config.json in '$CONFIG_DIR_SRC'" >&2; exit 2; } [[ -f "$CFG_MOUNT/config.json" ]] || { echo "Error: no config.json in '$CONFIG_DIR_SRC'" >&2; exit 2; }
CONFIG_NOTE="$CONFIG_DIR_SRC$CFG_MOUNT (copy; original untouched)" CONFIG_NOTE="$CONFIG_DIR_SRC$CFG_MOUNT (temporary copy; original untouched)"
fi fi
args+=(-v "$CFG_MOUNT:/config/coderai$volume_suffix" \ args+=(-v "$CFG_MOUNT:/config/coderai$volume_suffix" \
-v "$DATA_ROOT/models:/models$volume_suffix" -v "$DATA_ROOT/cache:/cache$volume_suffix") -v "$DATA_ROOT/models:/models$volume_suffix" -v "$DATA_ROOT/cache:/cache$volume_suffix")
...@@ -340,6 +444,7 @@ Starting CoderAI OCI container ...@@ -340,6 +444,7 @@ Starting CoderAI OCI container
debug: ${DEBUG_SPEC:-off} debug: ${DEBUG_SPEC:-off}
log: $LOG_HOST_NOTE log: $LOG_HOST_NOTE
tools: $TOOLS_NOTE tools: $TOOLS_NOTE
user: ${USER_SPEC:-container default (root)}
cdr-args:${CODERAI_EXTRA_ARGS:+ $CODERAI_EXTRA_ARGS} cdr-args:${CODERAI_EXTRA_ARGS:+ $CODERAI_EXTRA_ARGS}
EOF EOF
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment