Add a wizzard to configure .sloptrap files
This commit is contained in:
263
sloptrap
263
sloptrap
@@ -126,7 +126,6 @@ status_line() {
|
||||
comment_line() {
|
||||
print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@"
|
||||
}
|
||||
|
||||
warn_line() {
|
||||
print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@"
|
||||
}
|
||||
@@ -154,7 +153,9 @@ EOF
|
||||
MANIFEST_BASENAME=".sloptrap"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VALID_NAME_REGEX='^[A-Za-z0-9_.-]+$'
|
||||
DEFAULT_CODEX_ARGS=(--sandbox workspace-write)
|
||||
DEFAULT_CODEX_ARGS=(--sandbox danger-full-access --ask-for-approval never)
|
||||
DEFAULT_CODEX_ARGS_DISPLAY=$(printf '%s ' "${DEFAULT_CODEX_ARGS[@]}")
|
||||
DEFAULT_CODEX_ARGS_DISPLAY=${DEFAULT_CODEX_ARGS_DISPLAY% }
|
||||
SLOPTRAP_IMAGE_LABEL_KEY="net.sk4nz.sloptrap.managed"
|
||||
SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1"
|
||||
|
||||
@@ -170,13 +171,13 @@ usage() {
|
||||
info_line "Example manifest entries:\n"
|
||||
comment_line " name=my-project\n"
|
||||
comment_line " packages_extra=kubectl helm\n"
|
||||
comment_line " default_targets=run\n"
|
||||
comment_line " codex_args=--sandbox workspace-write\n"
|
||||
comment_line " codex_args=--sandbox danger-full-access --ask-for-approval never\n"
|
||||
info_line "\n"
|
||||
info_line "Example targets:\n"
|
||||
comment_line " run Build if needed, then launch Codex\n"
|
||||
comment_line " resume <id> Build if needed, then run 'codex resume <id>'\n"
|
||||
comment_line " shell Drop into an interactive /bin/bash session\n"
|
||||
comment_line " wizzard Create or update %s interactively\n" "$MANIFEST_BASENAME"
|
||||
comment_line " clean Remove the project container/image cache\n"
|
||||
comment_line " prune Remove dangling/unused sloptrap images\n"
|
||||
}
|
||||
@@ -670,6 +671,145 @@ parse_manifest() {
|
||||
done < "$manifest_path"
|
||||
}
|
||||
|
||||
manifest_default_value() {
|
||||
local key=$1
|
||||
local fallback=$2
|
||||
if [[ -v MANIFEST["$key"] ]]; then
|
||||
printf '%s' "${MANIFEST[$key]}"
|
||||
else
|
||||
printf '%s' "$fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_manifest_value() {
|
||||
local label=$1
|
||||
local default_value=$2
|
||||
local input
|
||||
local tty_path="/dev/tty"
|
||||
printf '%s' "$PREFIX_TEXT" >"$tty_path"
|
||||
printf '%b' "$COLOR_TEXT" >"$tty_path"
|
||||
printf '%s [%s]: ' "$label" "$default_value" >"$tty_path"
|
||||
printf '%b' "$RESET" >"$tty_path"
|
||||
if ! IFS= read -r input <"$tty_path"; then
|
||||
error "wizzard requires an interactive terminal"
|
||||
fi
|
||||
printf '%s' "$input"
|
||||
}
|
||||
|
||||
validate_wizzard_name() {
|
||||
local value=$1
|
||||
[[ -n $value ]] || error "$MANIFEST_PATH: name must not be empty"
|
||||
if [[ ! $value =~ $VALID_NAME_REGEX ]]; then
|
||||
error "$MANIFEST_PATH: invalid project name '$value' (allowed: letters, digits, ., _, -)"
|
||||
fi
|
||||
}
|
||||
|
||||
normalize_wizzard_allow_host_network() {
|
||||
local value=${1,,}
|
||||
case "$value" in
|
||||
1|true|yes) printf 'true' ;;
|
||||
0|false|no) printf 'false' ;;
|
||||
*) error "$MANIFEST_PATH: allow_host_network must be true or false (got '$1')" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_wizzard_codex_args() {
|
||||
local value=$1
|
||||
ensure_safe_for_make "codex_args" "$value"
|
||||
local -a args=("${DEFAULT_CODEX_ARGS[@]}")
|
||||
local -a tokens=()
|
||||
if [[ -n $value ]]; then
|
||||
read -r -a tokens <<< "$value"
|
||||
args+=("${tokens[@]}")
|
||||
fi
|
||||
ensure_safe_sandbox "${args[@]}"
|
||||
}
|
||||
|
||||
run_wizzard() {
|
||||
local manifest_path=$1
|
||||
if [[ -L $manifest_path ]]; then
|
||||
error "$manifest_path: manifest must not be a symlink"
|
||||
fi
|
||||
if [[ ! -t 0 ]]; then
|
||||
error "wizzard requires an interactive terminal"
|
||||
fi
|
||||
if [[ ! -f $manifest_path ]]; then
|
||||
print_banner
|
||||
fi
|
||||
|
||||
local default_name
|
||||
local default_packages_extra
|
||||
local default_codex_args
|
||||
local default_allow_host_network
|
||||
|
||||
default_name=$(manifest_default_value "name" "$(basename "$CODE_DIR")")
|
||||
default_packages_extra=$(manifest_default_value "packages_extra" "")
|
||||
default_codex_args=$(manifest_default_value "codex_args" "$DEFAULT_CODEX_ARGS_DISPLAY")
|
||||
default_allow_host_network=$(manifest_default_value "allow_host_network" "false")
|
||||
|
||||
local action="Creating"
|
||||
if [[ -f $manifest_path ]]; then
|
||||
action="Updating"
|
||||
fi
|
||||
info_line "%s %s interactively.\n" "$action" "$MANIFEST_BASENAME"
|
||||
|
||||
local value
|
||||
while true; do
|
||||
info_line "name: Labels the project, container, and image names.\n"
|
||||
value=$(prompt_manifest_value "name" "$default_name")
|
||||
value=$(trim "$value")
|
||||
[[ -n $value ]] || value=$default_name
|
||||
validate_wizzard_name "$value"
|
||||
default_name=$value
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
info_line "packages_extra: Extra Debian packages to install during image build.\n"
|
||||
value=$(prompt_manifest_value "packages_extra" "$default_packages_extra")
|
||||
value=$(trim "$value")
|
||||
[[ -n $value ]] || value=$default_packages_extra
|
||||
if [[ -n $value ]]; then
|
||||
validate_package_list "packages_extra" "$value" "$manifest_path"
|
||||
fi
|
||||
default_packages_extra=$value
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
info_line "codex_args: Extra CLI flags passed to Codex at runtime.\n"
|
||||
value=$(prompt_manifest_value "codex_args" "$default_codex_args")
|
||||
value=$(trim "$value")
|
||||
[[ -n $value ]] || value=$default_codex_args
|
||||
validate_wizzard_codex_args "$value"
|
||||
default_codex_args=$value
|
||||
break
|
||||
done
|
||||
|
||||
while true; do
|
||||
info_line "allow_host_network: Use host networking instead of an isolated bridge.\n"
|
||||
value=$(prompt_manifest_value "allow_host_network" "$default_allow_host_network")
|
||||
value=$(trim "$value")
|
||||
[[ -n $value ]] || value=$default_allow_host_network
|
||||
default_allow_host_network=$(normalize_wizzard_allow_host_network "$value")
|
||||
break
|
||||
done
|
||||
|
||||
assert_path_within_code_dir "$manifest_path"
|
||||
cat > "$manifest_path" <<EOF
|
||||
name=$default_name
|
||||
packages_extra=$default_packages_extra
|
||||
codex_args=$default_codex_args
|
||||
allow_host_network=$default_allow_host_network
|
||||
EOF
|
||||
info_line "Wrote %s\n" "$manifest_path"
|
||||
local ignore_path="$CODE_DIR/.sloptrapignore"
|
||||
if [[ ! -f $ignore_path ]]; then
|
||||
info_line "Hint: create %s to hide files from the container (e.g., .git/ or secrets/).\n" "$ignore_path"
|
||||
fi
|
||||
info_line "Hint: run 'sloptrap %s build' or 'sloptrap %s rebuild' to apply changes.\n" "$CODE_DIR" "$CODE_DIR"
|
||||
}
|
||||
|
||||
print_config() {
|
||||
local manifest_path=$1
|
||||
info_line "manifest_path=%s\n" "$manifest_path"
|
||||
@@ -705,6 +845,15 @@ print_config() {
|
||||
done
|
||||
}
|
||||
|
||||
print_manifest_summary() {
|
||||
highlight_line "Manifest summary\n"
|
||||
comment_line " manifest_path=%s\n" "$MANIFEST_PATH"
|
||||
comment_line " name=%s\n" "$PROJECT_NAME"
|
||||
comment_line " packages_extra=%s\n" "$PACKAGES_EXTRA"
|
||||
comment_line " codex_args=%s\n" "$CODEX_ARGS_DISPLAY"
|
||||
comment_line " allow_host_network=%s\n" "$ALLOW_HOST_NETWORK"
|
||||
}
|
||||
|
||||
declare -a CONTAINER_SHARED_OPTS=()
|
||||
declare -a BASE_CONTAINER_CMD=()
|
||||
SLOPTRAP_IMAGE_NAME=""
|
||||
@@ -865,20 +1014,20 @@ ensure_safe_sandbox() {
|
||||
while [[ $i -lt ${#args[@]} ]]; do
|
||||
if [[ ${args[$i]} == "--sandbox" ]]; then
|
||||
if (( i + 1 >= ${#args[@]} )); then
|
||||
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
|
||||
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write, workspace-read-only, or danger-full-access)"
|
||||
fi
|
||||
sandbox_mode="${args[$((i + 1))]}"
|
||||
fi
|
||||
((i+=1))
|
||||
done
|
||||
if [[ -z $sandbox_mode ]]; then
|
||||
error "$MANIFEST_PATH: codex_args must include '--sandbox <mode>' (workspace-write or workspace-read-only)"
|
||||
error "$MANIFEST_PATH: codex_args must include '--sandbox <mode>' (workspace-write, workspace-read-only, or danger-full-access)"
|
||||
fi
|
||||
case "$sandbox_mode" in
|
||||
workspace-write|workspace-read-only)
|
||||
workspace-write|workspace-read-only|danger-full-access)
|
||||
;;
|
||||
*)
|
||||
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write or workspace-read-only)"
|
||||
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write, workspace-read-only, or danger-full-access)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -892,6 +1041,22 @@ normalize_package_list() {
|
||||
printf '%s' "${tokens[*]}"
|
||||
}
|
||||
|
||||
targets_need_build() {
|
||||
local -a targets=("$@")
|
||||
if [[ ${#targets[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
local target
|
||||
for target in "${targets[@]}"; do
|
||||
case "$target" in
|
||||
build|rebuild|build-if-missing|run|login|shell|resume)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_container_runtime() {
|
||||
resolve_container_workdir
|
||||
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA
|
||||
@@ -1051,7 +1216,10 @@ prepare_container_runtime() {
|
||||
|
||||
build_image() {
|
||||
ensure_codex_binary
|
||||
print_banner
|
||||
if [[ $SKIP_BUILD_BANNER != true ]]; then
|
||||
print_banner
|
||||
fi
|
||||
print_manifest_summary
|
||||
local extra_packages_arg
|
||||
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
|
||||
if ! $DRY_RUN; then
|
||||
@@ -1088,13 +1256,17 @@ build_image() {
|
||||
|
||||
rebuild_image() {
|
||||
ensure_codex_binary
|
||||
if [[ $SKIP_BUILD_BANNER != true ]]; then
|
||||
print_banner
|
||||
fi
|
||||
print_manifest_summary
|
||||
if ! $DRY_RUN; then
|
||||
status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME"
|
||||
fi
|
||||
local extra_packages_arg
|
||||
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
|
||||
local -a cmd=(
|
||||
"$CONTAINER_ENGINE" build --no-cache
|
||||
"$CONTAINER_ENGINE" build --no-cache --quiet
|
||||
-t "$SLOPTRAP_IMAGE_NAME"
|
||||
-f "$SLOPTRAP_DOCKERFILE_PATH"
|
||||
--label "$SLOPTRAP_IMAGE_LABEL"
|
||||
@@ -1128,6 +1300,20 @@ build_if_missing() {
|
||||
return 0
|
||||
fi
|
||||
if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then
|
||||
local created
|
||||
if created=$("$CONTAINER_ENGINE" image inspect --format '{{.Created}}' "$SLOPTRAP_IMAGE_NAME" 2>/dev/null); then
|
||||
local created_epoch
|
||||
created_epoch=$(date -d "$created" +%s 2>/dev/null || true)
|
||||
if [[ -n $created_epoch ]]; then
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
local age_days=$(( (now_epoch - created_epoch) / 86400 ))
|
||||
if (( age_days > 30 )); then
|
||||
warn "image '$SLOPTRAP_IMAGE_NAME' is ${age_days} days old; rebuilding"
|
||||
rebuild_image
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
build_image
|
||||
@@ -1250,6 +1436,10 @@ dispatch_target() {
|
||||
build_if_missing
|
||||
run_shell_target
|
||||
;;
|
||||
wizzard)
|
||||
run_wizzard "$MANIFEST_PATH"
|
||||
exit 0
|
||||
;;
|
||||
stop)
|
||||
stop_container
|
||||
;;
|
||||
@@ -1267,6 +1457,7 @@ dispatch_target() {
|
||||
|
||||
DRY_RUN=false
|
||||
PRINT_CONFIG=false
|
||||
SKIP_BUILD_BANNER=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -1313,12 +1504,41 @@ if [[ $CODE_DIR == "/" ]]; then
|
||||
error "project root may not be '/'"
|
||||
fi
|
||||
MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME"
|
||||
TARGETS_INPUT=("$@")
|
||||
|
||||
if [[ -f $MANIFEST_PATH ]]; then
|
||||
MANIFEST_PRESENT=true
|
||||
parse_manifest "$MANIFEST_PATH"
|
||||
fi
|
||||
|
||||
if [[ ${#TARGETS_INPUT[@]} -gt 0 ]]; then
|
||||
target_index=0
|
||||
while (( target_index < ${#TARGETS_INPUT[@]} )); do
|
||||
if [[ ${TARGETS_INPUT[$target_index]} == "wizzard" ]]; then
|
||||
if (( ${#TARGETS_INPUT[@]} > 1 )); then
|
||||
warn "wizzard runs standalone; ignoring other targets"
|
||||
fi
|
||||
run_wizzard "$MANIFEST_PATH"
|
||||
exit 0
|
||||
fi
|
||||
((target_index+=1))
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! -f $MANIFEST_PATH ]]; then
|
||||
if targets_need_build "${TARGETS_INPUT[@]}"; then
|
||||
if [[ -t 0 ]]; then
|
||||
run_wizzard "$MANIFEST_PATH"
|
||||
SKIP_BUILD_BANNER=true
|
||||
MANIFEST=()
|
||||
MANIFEST_PRESENT=true
|
||||
parse_manifest "$MANIFEST_PATH"
|
||||
else
|
||||
warn "missing $MANIFEST_BASENAME; proceeding with defaults (run '$0 $CODE_DIR wizzard' to create one)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
PROJECT_NAME=${MANIFEST[name]-$(basename "$CODE_DIR")}
|
||||
if [[ -z $PROJECT_NAME ]]; then
|
||||
error "$MANIFEST_PATH: project name resolved to empty string"
|
||||
@@ -1341,13 +1561,7 @@ fi
|
||||
|
||||
TARGETS=("$@")
|
||||
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
||||
if [[ -n ${MANIFEST[default_targets]-} ]]; then
|
||||
read -r -a TARGETS <<< "${MANIFEST[default_targets]}"
|
||||
elif [[ -n ${MANIFEST[default_target]-} ]]; then
|
||||
read -r -a TARGETS <<< "${MANIFEST[default_target]}"
|
||||
else
|
||||
TARGETS=("run")
|
||||
fi
|
||||
TARGETS=("run")
|
||||
fi
|
||||
|
||||
DEFAULT_TARGETS=("${TARGETS[@]}")
|
||||
@@ -1367,7 +1581,7 @@ if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
|
||||
esac
|
||||
fi
|
||||
|
||||
forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough)
|
||||
forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough default_targets default_target)
|
||||
for forbidden_key in "${forbidden_keys[@]}"; do
|
||||
if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then
|
||||
error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons"
|
||||
@@ -1382,10 +1596,13 @@ CONTAINER_ENGINE="$(detect_container_engine)"
|
||||
CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}")
|
||||
if [[ -n ${MANIFEST[codex_args]-} ]]; then
|
||||
ensure_safe_for_make "codex_args" "${MANIFEST[codex_args]}"
|
||||
declare -a manifest_codex_args=()
|
||||
read -r -a manifest_codex_args <<< "${MANIFEST[codex_args]}"
|
||||
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
|
||||
unset -v manifest_codex_args
|
||||
manifest_codex_args_value=$(trim "${MANIFEST[codex_args]}")
|
||||
if [[ $manifest_codex_args_value != "$DEFAULT_CODEX_ARGS_DISPLAY" ]]; then
|
||||
declare -a manifest_codex_args=()
|
||||
read -r -a manifest_codex_args <<< "$manifest_codex_args_value"
|
||||
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
|
||||
unset -v manifest_codex_args
|
||||
fi
|
||||
fi
|
||||
declare -a sanitized_codex_args=()
|
||||
declare -a sandbox_pair=()
|
||||
@@ -1393,7 +1610,7 @@ codex_args_index=0
|
||||
while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do
|
||||
if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then
|
||||
if (( codex_args_index + 1 >= ${#CODEX_ARGS_ARRAY[@]} )); then
|
||||
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
|
||||
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write, workspace-read-only, or danger-full-access)"
|
||||
fi
|
||||
sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}")
|
||||
((codex_args_index+=2))
|
||||
|
||||
Reference in New Issue
Block a user