fix: stabilization pass — fail-closed auth, ai flag validation, lint/config/security hardening, regression tests
gates / consistency-and-conventions (push) Successful in 26s
gates / consistency-and-conventions (push) Successful in 26s
17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):
- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
/dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
eval deny-by-default + --no-command-execution carried by both chat bridges,
tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
(requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
output byte-identical, :num restored); pos system uninstall covers all 12
libs + scale-tail + flags dir + systemd user units (|| true) + plugin
markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
contract; systemd-analyze verify on generated unit PASS
Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
This commit is contained in:
+40
-41
@@ -1,14 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai ask — AI assistant: ask, chat, sessions, capture, models, providers
|
||||
# POS_SUBCMDS: ask chat sessions capture models providers
|
||||
# POS_FLAGS: --provider --model --session --system --full --last --trust
|
||||
# POS_SUBCMDS: ask chat sessions capture models providers llamacpp
|
||||
# POS_FLAGS: --provider --model --session --system --full --last --trust --no-command-execution
|
||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini, openrouter or llamacpp, default gemini) | @[AI_PROVIDER=gemini|] Gemini | *providers=gemini | @[AI_PROVIDER=openrouter] OpenRouter | *providers=openrouter | @[AI_PROVIDER=llamacpp] llamacpp | LLAMACPP_PORT=:Server port (default 8088) | LLAMACPP_HOST=:Bind address (default 127.0.0.1) | LLAMACPP_MODEL=:Default model path (GGUF) | LLAMACPP_CTX_SIZE=num:Context window size (default 4096) | LLAMACPP_GPU_LAYERS=num:GPU layers (-1=auto, 0=CPU, default -1) | LLAMACPP_THREADS=num:CPU threads (default: nproc) | *providers=llamacpp | @General | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in, empty to reset)
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# ── Paths & constants ──────────────────────────────────────────
|
||||
CONFIG_FILE="$HOME/.config/linux_post_install/ai.env"
|
||||
CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/ai.env}"
|
||||
SESSION_DIR="$HOME/.local/share/linux_post_install/ai"
|
||||
DISPATCH_LOG_DIR="$HOME/.local/share/linux_post_install/logs" # bin/pos per-run logs
|
||||
LAST_CMD_OUTPUT_FILE="$HOME/.local/share/linux_post_install/last_cmd_output" # --last fallback for any command
|
||||
@@ -32,12 +35,12 @@ DEFAULT_SYSTEM_PROMPT_HARD="You are a Linux CLI assistant. Rules:
|
||||
5. Match the user's OS/package manager (apt/dnf/pacman)"
|
||||
|
||||
# Legacy: kept for session migration and backward compat config
|
||||
LEGACY_GEMINI_CONFIG="$HOME/.config/linux_post_install/ai.env"
|
||||
LEGACY_OPENROUTER_CONFIG="$HOME/.config/linux_post_install/ai-openrouter.env"
|
||||
LEGACY_GEMINI_CONFIG="${LEGACY_GEMINI_CONFIG:-$CONFIG_DIR/ai.env}"
|
||||
LEGACY_OPENROUTER_CONFIG="${LEGACY_OPENROUTER_CONFIG:-$CONFIG_DIR/ai-openrouter.env}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos ai [subcommand] [--provider <name>] [--model <id>] [--session <name>] [--system <text>] [--full] [--last] [--trust]
|
||||
Usage: pos ai [subcommand] [--provider <name>] [--model <id>] [--session <name>] [--system <text>] [--full] [--last] [--trust] [--no-command-execution]
|
||||
|
||||
AI assistant with pluggable providers (gemini, openrouter, llamacpp).
|
||||
|
||||
@@ -73,8 +76,16 @@ Options:
|
||||
'capture'. Notes on stderr which source was attached and
|
||||
its age; warns when stale (>60 min).
|
||||
--trust Auto-execute agent-detected commands without confirmation.
|
||||
Used by trusted alias wrappers — do NOT pass manually
|
||||
unless you fully trust the agent's output.
|
||||
Interactive terminals only — it has no effect when stdout/
|
||||
stdin is not a tty (e.g. chat bridges, cron), which never
|
||||
execute commands. Used by trusted alias wrappers — do NOT
|
||||
pass manually unless you fully trust the agent's output.
|
||||
--no-command-execution Never run agent-detected command blocks. The proposed
|
||||
command is neither printed nor executed; no
|
||||
confirmation prompt is shown (structural guard — used by
|
||||
chat bridges so a future refactor cannot auto-execute).
|
||||
Each of these two modes contradict the other: the last one
|
||||
given on the command line wins.
|
||||
-h|--help This help.
|
||||
|
||||
Config: $CONFIG_FILE (edit with 'pos config ai')
|
||||
@@ -126,36 +137,16 @@ load_provider() {
|
||||
source "$f"
|
||||
}
|
||||
|
||||
# ── ai.env loader (same pattern as telegram.env) ────────────────
|
||||
# ── ai.env loader (canonical loader + legacy files) ─────────────
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
# Legacy provider-specific config files (fallback for old configs)
|
||||
# Both files are loaded — env-var precedence means unified AI_API_KEY wins.
|
||||
local legacy_files="$LEGACY_GEMINI_CONFIG $LEGACY_OPENROUTER_CONFIG"
|
||||
local legacy_env
|
||||
for legacy_env in $legacy_files; do
|
||||
[ -f "$legacy_env" ] && [ "$legacy_env" != "$CONFIG_FILE" ] || continue
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in \#*) continue ;; esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$legacy_env" || true)
|
||||
load_env_file "$legacy_env"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -378,8 +369,14 @@ _extract_commands() {
|
||||
# ── Interactive prompt to run extracted commands ─────────────────
|
||||
_prompt_run_command() {
|
||||
local cmd="$1" trusted="${2:-0}"
|
||||
# Only prompt on interactive terminals with a controlling tty
|
||||
# Structural guards: commands never execute without a controlling tty, and
|
||||
# never when --no-command-execution was passed. Both apply BEFORE any
|
||||
# auto-execute path (including --trust), so a future refactor cannot
|
||||
# introduce a non-interactive or bridge-forced execution.
|
||||
[ -w /dev/tty ] || return 0
|
||||
if [ "${NO_EXEC:-0}" -eq 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
printf '\n%s\n' "Command detected:" >&2
|
||||
printf ' %s\n\n' "$cmd" >&2
|
||||
if [ "$trusted" -eq 1 ]; then
|
||||
@@ -388,20 +385,19 @@ _prompt_run_command() {
|
||||
run eval "$cmd"
|
||||
return
|
||||
fi
|
||||
printf 'Run this command? [Y/n] ' >&2
|
||||
printf 'Run this command? [y/N] ' >&2
|
||||
local choice
|
||||
IFS= read -r choice </dev/tty || choice=""
|
||||
case "${choice,,}" in
|
||||
n|N)
|
||||
case "$choice" in
|
||||
[yY])
|
||||
printf '%s\n' "$cmd"
|
||||
run eval "$cmd"
|
||||
;;
|
||||
*)
|
||||
# Add to shell history so user can press ↑ to recall, edit, run
|
||||
history -s "$cmd" 2>/dev/null || true
|
||||
printf '%s\n' "Command added to history — press ↑ to recall, edit, and run." >&2
|
||||
;;
|
||||
*)
|
||||
# Y or Enter: execute
|
||||
printf '%s\n' "$cmd"
|
||||
run eval "$cmd"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -642,6 +638,7 @@ MODEL_OVERRIDE=""
|
||||
FULL_MODE=0
|
||||
LAST_MODE=0
|
||||
TRUST_MODE=0
|
||||
NO_EXEC=0
|
||||
PROVIDER=""
|
||||
cmd=""
|
||||
args=()
|
||||
@@ -665,7 +662,9 @@ while [ $# -gt 0 ]; do
|
||||
--last)
|
||||
LAST_MODE=1; shift ;;
|
||||
--trust)
|
||||
TRUST_MODE=1; shift ;;
|
||||
TRUST_MODE=1; NO_EXEC=0; shift ;;
|
||||
--no-command-execution)
|
||||
NO_EXEC=1; TRUST_MODE=0; shift ;;
|
||||
-*) err "Unknown option '$1' (see --help)" ;;
|
||||
*)
|
||||
if [ -z "$cmd" ]; then
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai gemini — Forward to pos ai --provider gemini (backward compat)
|
||||
# POS_SUBCMDS: ask chat models sessions capture
|
||||
# POS_SUBCMDS: ask chat models sessions capture providers
|
||||
# Thin forwarder — all logic lives in bin/pos-ai + lib/ai-providers/gemini.sh
|
||||
case "${1:-}" in -h|--help) exec pos ai --provider gemini --help ;; esac
|
||||
exec pos ai --provider gemini "$@"
|
||||
|
||||
+24
-35
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai hf — Download AI models from Hugging Face (search, download, manage)
|
||||
# POS_SUBCMDS: search download list remove info files cache
|
||||
# POS_FLAGS: --branch --gguf --list --output --quant --include --exclude --revision
|
||||
# POS_DEPS: curl jq
|
||||
# POS_CONFIG: ai | ai.env | HF_TOKEN=secret:Hugging Face API token (https://huggingface.co/settings/tokens) | HF_DOWNLOAD_DIR=:Model download directory (default ~/.local/share/linux_post_install/ai/models)
|
||||
@@ -18,29 +19,20 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# ── Dependencies (before --help) ───────────────────────────────
|
||||
command -v curl &>/dev/null || err "curl not found (install curl)"
|
||||
command -v jq &>/dev/null || err "jq not found (install jq)"
|
||||
|
||||
# ── Config & defaults ──────────────────────────────────────────
|
||||
CONFIG_FILE="${CONFIG_FILE:-$HOME/.config/linux_post_install/ai.env}"
|
||||
CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/ai.env}"
|
||||
HF_TOKEN="${HF_TOKEN:-}"
|
||||
HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/.local/share/linux_post_install/ai/models}"
|
||||
|
||||
load_hf_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
load_hf_config
|
||||
@@ -698,7 +690,9 @@ cmd_download() {
|
||||
rm -rf "$temp_dir"
|
||||
trap - EXIT
|
||||
else
|
||||
# Single file download - use original sequential approach
|
||||
# Single-file download — the same honesty rules as the parallel path:
|
||||
# a failure is recorded in failed_files so the model is never marked
|
||||
# complete (.hf-meta suppressed) and the tool exits rc 1.
|
||||
while IFS= read -r file_json; do
|
||||
local fname fsize
|
||||
fname="$(printf '%s' "$file_json" | jq -r '.rfilename')"
|
||||
@@ -708,12 +702,8 @@ cmd_download() {
|
||||
local url="${HF_BASE}/${ns}/${repo}/resolve/${branch}/${fname}"
|
||||
local target="${target_dir}/${fname}"
|
||||
|
||||
if [ "$file_count" -gt 1 ]; then
|
||||
downloaded=$((downloaded + 1))
|
||||
printf '[%d/%d] Downloading %s...\n' "$downloaded" "$file_count" "$fname" >&2
|
||||
fi
|
||||
|
||||
if ! hf_download_with_progress "$url" "$target"; then
|
||||
failed_files+=("$fname")
|
||||
warn "Failed to download $fname"
|
||||
continue
|
||||
fi
|
||||
@@ -743,8 +733,15 @@ METAEOF
|
||||
warn "Not writing .hf-meta — ${repo_id} is incomplete (${#failed_files[@]} file(s) failed)"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
if [ "$file_count" -eq 1 ]; then
|
||||
# Summary — honest in both single-file and parallel paths: any failure
|
||||
# yields a success/failure count, never a false "downloaded" claim.
|
||||
if [ "${#failed_files[@]}" -gt 0 ]; then
|
||||
# Honest count: attempted = total files, success = total − failures
|
||||
local success_count=$((file_count - ${#failed_files[@]}))
|
||||
printf '📥 Downloaded: %s (%d of %d files, %d failed: %s)\n' \
|
||||
"$repo_id" "$success_count" "$file_count" "${#failed_files[@]}" "${failed_files[*]}"
|
||||
printf '📁 %s/\n' "$target_dir"
|
||||
elif [ "$file_count" -eq 1 ]; then
|
||||
local fname
|
||||
fname="$(printf '%s' "$filtered_files" | jq -r '.[0].rfilename')"
|
||||
local fsize
|
||||
@@ -754,22 +751,14 @@ METAEOF
|
||||
printf '📥 Downloaded: %s/%s (%s)\n' "$repo_id" "$fname" "$human_size"
|
||||
printf '📁 %s/%s\n' "$target_dir" "$fname"
|
||||
else
|
||||
if [ "${#failed_files[@]}" -gt 0 ]; then
|
||||
# Honest count: attempted = total files, success = total − failures
|
||||
local success_count=$((file_count - ${#failed_files[@]}))
|
||||
printf '📥 Downloaded: %s (%d of %d files, %d failed: %s)\n' \
|
||||
"$repo_id" "$success_count" "$file_count" "${#failed_files[@]}" "${failed_files[*]}"
|
||||
else
|
||||
local total_human
|
||||
total_human="$(hf_human_size "$total_size")"
|
||||
printf '📥 Downloaded: %s (%d files, %s)\n' "$repo_id" "$file_count" "$total_human"
|
||||
fi
|
||||
local total_human
|
||||
total_human="$(hf_human_size "$total_size")"
|
||||
printf '📥 Downloaded: %s (%d files, %s)\n' "$repo_id" "$file_count" "$total_human"
|
||||
printf '📁 %s/\n' "$target_dir"
|
||||
fi
|
||||
|
||||
# A partially-failed parallel batch must be detectable by scripts —
|
||||
# exit non-zero. The sequential single-file path is unchanged: it never
|
||||
# populates failed_files, so this clause only fires for the parallel path.
|
||||
# Any failure — single-file or parallel batch — must be detectable by
|
||||
# scripts: exit non-zero.
|
||||
if [ "${#failed_files[@]}" -gt 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai llamacpp — Forward to pos ai --provider llamacpp (backward compat)
|
||||
# POS_SUBCMDS: ask chat models sessions capture
|
||||
# POS_SUBCMDS: ask chat models sessions capture providers
|
||||
# Thin forwarder — all logic lives in bin/pos-ai + lib/ai-providers/llamacpp.sh
|
||||
case "${1:-}" in -h|--help) exec pos ai --provider llamacpp --help ;; esac
|
||||
exec pos ai --provider llamacpp "$@"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai openrouter — Forward to pos ai --provider openrouter (backward compat)
|
||||
# POS_SUBCMDS: ask chat sessions capture
|
||||
# POS_SUBCMDS: ask chat sessions capture models providers
|
||||
# Thin forwarder — all logic lives in bin/pos-ai + lib/ai-providers/openrouter.sh
|
||||
case "${1:-}" in -h|--help) exec pos ai --provider openrouter --help ;; esac
|
||||
exec pos ai --provider openrouter "$@"
|
||||
|
||||
+113
-29
@@ -7,35 +7,50 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# ── Dependencies (before --help) ───────────────────────────────
|
||||
command -v curl &>/dev/null || err "curl not found (install curl)"
|
||||
command -v jq &>/dev/null || err "jq not found (install jq)"
|
||||
|
||||
# ── Config / seams ─────────────────────────────────────────────
|
||||
CONFIG_FILE="${CONFIG_FILE:-$HOME/.config/linux_post_install/ai.env}"
|
||||
CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/ai.env}"
|
||||
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"
|
||||
SERVICE="pos-ai-server.service"
|
||||
HF_DOWNLOAD_DIR="${HF_DOWNLOAD_DIR:-$HOME/.local/share/linux_post_install/ai/models}"
|
||||
|
||||
# ── Config loader (env-var precedence, same pattern as pos-ai-hf) ──
|
||||
# ── Config loader (canonical env-var precedence, same pattern as pos-ai-hf) ──
|
||||
# Loaded keys are also recorded in LOADED_ENV_KEYS (see below); the D-F
|
||||
# requested-from-config tracking uses the exported values, so it is
|
||||
# unaffected by the loader implementation.
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
load_config
|
||||
|
||||
# ── Config/env-requested flags (D-F) ───────────────────────────
|
||||
# Flags the user explicitly configured (via ai.env or exported env) are
|
||||
# validated as "requested" (hard error if unsupported), distinct from the
|
||||
# always-emitted tool defaults (warn + omit). This is captured here, BEFORE
|
||||
# the CLI parse overwrites the LLAMACPP_* variables, so it reflects genuine
|
||||
# user intent rather than the final defaulted values.
|
||||
CONFIG_REQUESTED_FLAGS=()
|
||||
requested_from_env_config() {
|
||||
local var="$1" flag="$2"
|
||||
[ -n "${!var:-}" ] || return 0
|
||||
case " ${CONFIG_REQUESTED_FLAGS[*]:-} " in
|
||||
*" $flag "*) ;; # dedupe
|
||||
*) CONFIG_REQUESTED_FLAGS+=("$flag") ;;
|
||||
esac
|
||||
}
|
||||
requested_from_env_config LLAMACPP_PORT --port
|
||||
requested_from_env_config LLAMACPP_HOST --host
|
||||
requested_from_env_config LLAMACPP_CTX_SIZE --ctx-size
|
||||
requested_from_env_config LLAMACPP_GPU_LAYERS --n-gpu-layers
|
||||
requested_from_env_config LLAMACPP_THREADS --threads
|
||||
|
||||
# ── Binary detection ───────────────────────────────────────────
|
||||
find_llamacpp() {
|
||||
local candidates=("llama-server" "llama.cpp/server" "server" "llama-server-cuda")
|
||||
@@ -80,12 +95,62 @@ validate_requested_flags() {
|
||||
*" $flag "*) continue ;; # dedupe alias-mapped flags (e.g. --gpu → --n-gpu-layers)
|
||||
esac
|
||||
seen+=("$flag")
|
||||
if ! printf '%s' "$help_text" | grep -qF -- "$flag"; then
|
||||
# Word-boundary match: the flag token must appear as a whole word in
|
||||
# --help, not as a substring of a longer flag (D4 — e.g. --mmap must
|
||||
# not match a --no-mmap entry).
|
||||
if ! printf '%s' "$help_text" | grep -qE -- "(^|[[:space:]])${flag}([[:space:]]|=|$)"; then
|
||||
err "installed llama.cpp ${version} does not expose ${flag} — remove it or upgrade llama.cpp"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── Validate always-emitted default flags (D-F) ────────────────
|
||||
# validate_default_flags <binary> <version> <requested-flag...> — for every
|
||||
# flag the tool emits BY DEFAULT (with no explicit user intent), check it is
|
||||
# supported in --help. Requested flags (CLI or config/env) are excluded — they
|
||||
# were hard-validated upstream and must be kept. An unsupported DEFAULT is
|
||||
# omitted from ExecStart with a single warn (never a hard error — the tool
|
||||
# chose the default, not the user). Readable --help sets globals:
|
||||
# DEFAULT_PORT_OK DEFAULT_HOST_OK DEFAULT_GPU_OK DEFAULT_CTX_OK DEFAULT_THREADS_OK
|
||||
# (all 1 = keep; 0 = omit). Unreadable --help text sets all OK = 1 (warn).
|
||||
validate_default_flags() {
|
||||
local bin="$1" version="$2"
|
||||
shift 2
|
||||
local requested=("$@")
|
||||
local requested_str=" ${requested[*]:-} "
|
||||
|
||||
DEFAULT_PORT_OK=1; DEFAULT_HOST_OK=1; DEFAULT_GPU_OK=1; DEFAULT_CTX_OK=1; DEFAULT_THREADS_OK=1
|
||||
|
||||
local help_text
|
||||
help_text="$("$bin" --help 2>/dev/null)" || {
|
||||
warn "Cannot obtain llama-server --help output — skipping default flag validation"
|
||||
return 0
|
||||
}
|
||||
|
||||
local spec flag ok_var
|
||||
# flag | ok_var — the always-emitted defaults a user may not have requested.
|
||||
local specs=(
|
||||
"--port|DEFAULT_PORT_OK"
|
||||
"--host|DEFAULT_HOST_OK"
|
||||
"--n-gpu-layers|DEFAULT_GPU_OK"
|
||||
"--ctx-size|DEFAULT_CTX_OK"
|
||||
"--threads|DEFAULT_THREADS_OK"
|
||||
)
|
||||
for spec in "${specs[@]}"; do
|
||||
flag="${spec%%|*}"
|
||||
ok_var="${spec#*|}"
|
||||
case "$requested_str" in
|
||||
*" $flag "*) continue ;; # requested → already hard-validated, keep
|
||||
esac
|
||||
if printf '%s' "$help_text" | grep -qE -- "(^|[[:space:]])${flag}([[:space:]]|=|$)"; then
|
||||
eval "$ok_var=1"
|
||||
else
|
||||
warn "installed llama.cpp ${version} does not support default flag ${flag} — omitting it from the unit"
|
||||
eval "$ok_var=0"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── GPU detection ──────────────────────────────────────────────
|
||||
detect_gpu() {
|
||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null 2>&1; then
|
||||
@@ -126,9 +191,11 @@ human_size() {
|
||||
|
||||
# ── Health check ───────────────────────────────────────────────
|
||||
check_health() {
|
||||
local port="${LLAMACPP_PORT:-8088}"
|
||||
# Probe the SAME host/port the unit binds (HOST/PORT, defaults
|
||||
# 127.0.0.1/8088) — previously probes hardcoded 127.0.0.1 and missed a
|
||||
# non-localhost LLAMACPP_HOST bind.
|
||||
local resp
|
||||
resp="$(curl -sf "http://127.0.0.1:$port/health" 2>/dev/null)" || { echo "not running"; return 1; }
|
||||
resp="$(curl -sf "http://$HOST:$PORT/health" 2>/dev/null)" || { echo "not running"; return 1; }
|
||||
local status
|
||||
status="$(printf '%s' "$resp" | jq -r '.status // "unknown"' 2>/dev/null)"
|
||||
echo "$status"
|
||||
@@ -253,7 +320,7 @@ Config (~/.config/linux_post_install/ai.env):
|
||||
LLAMACPP_GPU_LAYERS GPU layers: -1=auto, 0=CPU only (default -1)
|
||||
LLAMACPP_THREADS CPU threads (default: nproc)
|
||||
|
||||
Requires: llama-server binary (install llama.cpp: https://github.com/ggerganov/llama.cpp)
|
||||
Requires: llama-server binary — install llama.cpp with the app installer: 'apps/ai/llamacpp.sh' (run 'bash apps/install.sh llamacpp', or pass '--apps'/'--full' to install.sh), see 'pos help ai server' (https://github.com/ggerganov/llama.cpp)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
@@ -394,7 +461,7 @@ systemd_quote() {
|
||||
cmd_start() {
|
||||
# Resolve the llama-server binary
|
||||
local llamacpp_bin
|
||||
llamacpp_bin="$(find_llamacpp)" || err "llama-server not found — install llama.cpp (https://github.com/ggerganov/llama.cpp)"
|
||||
llamacpp_bin="$(find_llamacpp)" || err "llama-server not found — install llama.cpp with the app installer: 'apps/ai/llamacpp.sh' (run 'bash apps/install.sh llamacpp', or pass '--apps'/'--full' to install.sh), see 'pos help ai server' (https://github.com/ggerganov/llama.cpp)"
|
||||
local llamacpp_full
|
||||
llamacpp_full="$(command -v "$llamacpp_bin")"
|
||||
|
||||
@@ -403,10 +470,21 @@ cmd_start() {
|
||||
local version
|
||||
version="$(detect_llama_version "$llamacpp_bin")"
|
||||
|
||||
# Validate explicitly requested flags against this binary's --help
|
||||
if [ "${#REQUESTED_FLAGS[@]}" -gt 0 ]; then
|
||||
validate_requested_flags "$llamacpp_bin" "$version" "${REQUESTED_FLAGS[@]}"
|
||||
# Validate EVERY flag that will appear in ExecStart (D-F):
|
||||
# - Requested flags (CLI OR config/env) → hard error if unsupported.
|
||||
# - Always-emitted defaults → warn + omit if unsupported.
|
||||
local all_requested=("${REQUESTED_FLAGS[@]}" "${CONFIG_REQUESTED_FLAGS[@]}")
|
||||
local deduped=() flag
|
||||
for flag in "${all_requested[@]}"; do
|
||||
case " ${deduped[*]:-} " in
|
||||
*" $flag "*) continue ;;
|
||||
esac
|
||||
deduped+=("$flag")
|
||||
done
|
||||
if [ "${#deduped[@]}" -gt 0 ]; then
|
||||
validate_requested_flags "$llamacpp_bin" "$version" "${deduped[@]}"
|
||||
fi
|
||||
validate_default_flags "$llamacpp_bin" "$version" "${deduped[@]}"
|
||||
|
||||
# Resolve model
|
||||
local explicit_model="${SUBCMD_ARGS[0]:-}"
|
||||
@@ -441,11 +519,17 @@ cmd_start() {
|
||||
# unquoted whitespace, so the binary and the model path — the only tokens
|
||||
# that may contain spaces — are systemd_quote()d; plain flag/number
|
||||
# tokens stay unquoted.
|
||||
# Build the command with ONLY the flags that passed validation. Requested
|
||||
# flags (hard-validated) and supported defaults are always emitted; an
|
||||
# unsupported DEFAULT is omitted here (validate_default_flags set the
|
||||
# DEFAULT_*_OK globals) so the unit never carries an unsupported flag.
|
||||
local exec_cmd
|
||||
exec_cmd="$(systemd_quote "$llamacpp_full") -m $(systemd_quote "$model") --port $PORT --host $HOST"
|
||||
exec_cmd+=" --n-gpu-layers $gpu_layers"
|
||||
exec_cmd+=" --ctx-size $CTX_SIZE"
|
||||
exec_cmd+=" --threads $THREADS"
|
||||
exec_cmd="$(systemd_quote "$llamacpp_full") -m $(systemd_quote "$model")"
|
||||
if [ "$DEFAULT_PORT_OK" -eq 1 ]; then exec_cmd+=" --port $PORT"; fi
|
||||
if [ "$DEFAULT_HOST_OK" -eq 1 ]; then exec_cmd+=" --host $HOST"; fi
|
||||
if [ "$DEFAULT_GPU_OK" -eq 1 ]; then exec_cmd+=" --n-gpu-layers $gpu_layers"; fi
|
||||
if [ "$DEFAULT_CTX_OK" -eq 1 ]; then exec_cmd+=" --ctx-size $CTX_SIZE"; fi
|
||||
if [ "$DEFAULT_THREADS_OK" -eq 1 ]; then exec_cmd+=" --threads $THREADS"; fi
|
||||
if [ -n "$GPU_THREADS" ]; then
|
||||
exec_cmd+=" --gpu-threads $GPU_THREADS"
|
||||
fi
|
||||
@@ -560,7 +644,7 @@ cmd_status() {
|
||||
# llama-server must be present for the version probe below — same
|
||||
# actionable deps message as `start`
|
||||
if ! find_llamacpp >/dev/null 2>&1; then
|
||||
err "llama-server not found — install llama.cpp (https://github.com/ggerganov/llama.cpp)"
|
||||
err "llama-server not found — install llama.cpp with the app installer: 'apps/ai/llamacpp.sh' (run 'bash apps/install.sh llamacpp', or pass '--apps'/'--full' to install.sh), see 'pos help ai server' (https://github.com/ggerganov/llama.cpp)"
|
||||
fi
|
||||
|
||||
# Service state
|
||||
@@ -573,7 +657,7 @@ cmd_status() {
|
||||
# Model (from health endpoint if running)
|
||||
if [ "$svc_state" = "running" ]; then
|
||||
local models_resp
|
||||
models_resp="$(curl -sf "http://127.0.0.1:$PORT/v1/models" 2>/dev/null)" || true
|
||||
models_resp="$(curl -sf "http://$HOST:$PORT/v1/models" 2>/dev/null)" || true
|
||||
local model_id
|
||||
model_id="$(printf '%s' "$models_resp" | jq -r '.data[0].id // "unknown"' 2>/dev/null)" || model_id="unknown"
|
||||
printf 'model: %s\n' "$model_id"
|
||||
|
||||
@@ -9,6 +9,9 @@ MAP_FILE="$CONFIG_DIR/matrix_commands.env"
|
||||
SERVICE="pos-matrix-listener.service"
|
||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# System prompt for the "ai " bridge: replies are posted straight into the
|
||||
# room, so ask for concise, emoji-friendly Matrix-style answers.
|
||||
AI_SYSTEM="You are a friendly assistant chatting in a Matrix room. Keep replies concise, use emojis and light formatting to make them lively, and never claim to send messages yourself."
|
||||
@@ -31,7 +34,8 @@ Commands:
|
||||
--run Run the polling loop in the foreground (used by the service)
|
||||
|
||||
Config: $CONFIG_FILE (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN,
|
||||
MATRIX_USER_ID — edit with 'pos config matrix')
|
||||
MATRIX_USER_ID, MATRIX_ROOM_ID — edit with
|
||||
'pos config matrix')
|
||||
Map: $MAP_FILE — '/cmd=bash command' per line (optional
|
||||
'/cmd::short description=bash command')
|
||||
|
||||
@@ -44,10 +48,11 @@ without '@quiet' and the listener forwards the output:
|
||||
'/status=pos system health'
|
||||
|
||||
The listener only reacts to messages from your own Matrix user
|
||||
(MATRIX_USER_ID). If MATRIX_ROOM_ID is set it only watches that room,
|
||||
otherwise it watches every room you've joined. Commands run as your user,
|
||||
so 'sudo' inside them needs a NOPASSWD rule. The interactive editor runs
|
||||
'bash -n' to syntax-check commands before saving.
|
||||
(MATRIX_USER_ID), and only in the room set by MATRIX_ROOM_ID — without it
|
||||
the listener refuses to run any command (fail-closed), so a bot account
|
||||
that has joined many rooms can't be tricked into executing commands.
|
||||
Commands run as your user, so 'sudo' inside them needs a NOPASSWD rule.
|
||||
The interactive editor runs 'bash -n' to syntax-check commands before saving.
|
||||
|
||||
Commands are matched with a leading '/' or '!' — '/status' and '!status'
|
||||
both resolve. There is no equivalent of Telegram's bot "/" menu on Matrix,
|
||||
@@ -63,18 +68,7 @@ EOF
|
||||
|
||||
# ── matrix.env (same pattern as pos-communication-matrix-sender) ──
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
api_base() {
|
||||
@@ -469,7 +463,7 @@ handle_message() {
|
||||
return
|
||||
fi
|
||||
log "ai: $prompt"
|
||||
if answer="$(timeout 120 pos ai gemini ask --session "$session" --system "$AI_SYSTEM" "$prompt" 2>&1)"; then
|
||||
if answer="$(timeout 120 pos ai gemini ask --no-command-execution --session "$session" --system "$AI_SYSTEM" "$prompt" 2>&1)"; then
|
||||
reply_room "$room" "$(strip_markdown "$answer")" "$event_id"
|
||||
else
|
||||
[ -n "$answer" ] || answer="timed out after 120s"
|
||||
@@ -513,6 +507,9 @@ run_daemon() {
|
||||
[ -n "${MATRIX_ACCESS_TOKEN:-}" ] || err "No access token — run 'pos communication matrix sender login'"
|
||||
local owner="${MATRIX_USER_ID:-}"
|
||||
local room_only="${MATRIX_ROOM_ID:-}"
|
||||
if [ -z "$room_only" ]; then
|
||||
warn "MATRIX_ROOM_ID unset — refusing to run commands (fail-closed); set it with 'pos config matrix'"
|
||||
fi
|
||||
if [ -z "$owner" ]; then
|
||||
local who
|
||||
who="$(curl -fsS -m 30 -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||
@@ -524,7 +521,7 @@ run_daemon() {
|
||||
|
||||
local since="" filter_enc resp nb
|
||||
filter_enc="$(printf '%s' "$SYNC_FILTER" | jq -sRr @uri)"
|
||||
log "listener running (owner ${owner}${room_only:+ — room ${room_only}}) — Ctrl+C to stop"
|
||||
log "listener running (owner ${owner}, room ${room_only:-none — fail-closed}) — Ctrl+C to stop"
|
||||
trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT
|
||||
while true; do
|
||||
local args=("$(api_base)/_matrix/client/v3/sync?timeout=30000&filter=${filter_enc}")
|
||||
@@ -536,7 +533,9 @@ run_daemon() {
|
||||
|
||||
local room ecount i roomid ev sender text eid
|
||||
for room in $(printf '%s' "$resp" | jq -r '.rooms.join // {} | to_entries[] | .key' 2>/dev/null); do
|
||||
if [ -n "$room_only" ] && [ "$room" != "$room_only" ]; then
|
||||
# Fail-closed: with MATRIX_ROOM_ID unset no room is watched, so no
|
||||
# command can ever run (previously every joined room was watched).
|
||||
if [ -z "$room_only" ] || [ "$room" != "$room_only" ]; then
|
||||
continue
|
||||
fi
|
||||
ecount="$(printf '%s' "$resp" | jq -r --arg r "$room" ".rooms.join[\"$room\"].timeline.events // [] | length")"
|
||||
|
||||
@@ -7,6 +7,9 @@ set -euo pipefail
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/matrix.env"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos communication matrix sender [command] [args]
|
||||
@@ -42,19 +45,7 @@ err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
log() { echo "[+] $*"; }
|
||||
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
save_config() {
|
||||
|
||||
@@ -6,25 +6,16 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
CONFIG_FILE="$HOME/.config/linux_post_install/scrcpy.env"
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
CONFIG_FILE="$CONFIG_DIR/scrcpy.env"
|
||||
|
||||
command -v scrcpy &>/dev/null || err "scrcpy not found — install the latest release with the app installer: 'apps/media/scrcpy.sh' (or 'sudo apt install scrcpy' if your distro ships it; it bundles adb), see 'pos help communication scrcpy'"
|
||||
command -v adb &>/dev/null || err "adb not found — install it: 'sudo apt install adb'"
|
||||
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
load_config
|
||||
|
||||
@@ -12,6 +12,9 @@ API="https://api.telegram.org"
|
||||
SERVICE="pos-telegram-listener.service"
|
||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# System prompt for the "<prefix> " AI bridge (default prefix: "ai"): replies
|
||||
# are posted straight into the chat, so ask for concise, emoji-friendly
|
||||
# Telegram-style answers. The trigger word is configurable via
|
||||
@@ -47,7 +50,8 @@ Commands:
|
||||
is set via 'pos config telegram' (TELEGRAM_AI_PREFIX).
|
||||
|
||||
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_AI_PREFIX — edit with 'pos config telegram')
|
||||
TELEGRAM_OWNER_ID, TELEGRAM_AI_PREFIX — edit with 'pos
|
||||
config telegram')
|
||||
Map: $MAP_FILE — '/cmd=bash command' per line (optional
|
||||
'/cmd::short description=bash command' shown in the bot menu)
|
||||
Prefix map: $PREFIX_FILE — '<word>=command' per line: a non-command
|
||||
@@ -65,7 +69,11 @@ To reply with a command's stdout (e.g. the health dashboard), just map it
|
||||
without '@quiet' and the listener forwards the output:
|
||||
'/status=pos system health'
|
||||
|
||||
The listener only reacts to the owner chat (TELEGRAM_CHAT_ID). Commands run
|
||||
The listener only reacts to messages sent to the owner chat
|
||||
(TELEGRAM_CHAT_ID) BY your account (TELEGRAM_OWNER_ID) — both must match,
|
||||
so an impersonator or a forwarded message can't trigger commands. If
|
||||
TELEGRAM_OWNER_ID is unset the daemon starts but refuses to run any chat
|
||||
command (fail-closed); set it with 'pos config telegram'. Commands run
|
||||
as your user, so 'sudo' inside them needs a NOPASSWD rule. The interactive
|
||||
editor runs 'bash -n' to syntax-check commands before saving.
|
||||
|
||||
@@ -84,18 +92,7 @@ EOF
|
||||
|
||||
# ── telegram.env (same pattern as pos-communication-telegram) ────
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
# ── command map (MAP_FILE) ──────────────────────────────────────
|
||||
@@ -731,7 +728,7 @@ handle_message() {
|
||||
if [ -n "$reply_text" ]; then
|
||||
prompt="[Reply context — the message you are replying to]\n${reply_text}\n\n${prompt}"
|
||||
fi
|
||||
if answer="$(timeout 120 pos ai gemini ask --session "$session" --system "$AI_SYSTEM" "$prompt" 2>&1)"; then
|
||||
if answer="$(timeout 120 pos ai gemini ask --no-command-execution --session "$session" --system "$AI_SYSTEM" "$prompt" 2>&1)"; then
|
||||
reply "$(strip_markdown "$answer")" "$msg_id"
|
||||
else
|
||||
[ -n "$answer" ] || answer="timed out after 120s"
|
||||
@@ -758,10 +755,15 @@ run_daemon() {
|
||||
load_config
|
||||
[ -n "${TELEGRAM_BOT_TOKEN:-}" ] || err "No bot token — run 'pos config telegram'"
|
||||
[ -n "${TELEGRAM_CHAT_ID:-}" ] || err "No chat id — run 'pos config telegram'"
|
||||
if [ -n "${TELEGRAM_OWNER_ID:-}" ]; then
|
||||
log "owner id ${TELEGRAM_OWNER_ID} — commands authorized"
|
||||
else
|
||||
warn "TELEGRAM_OWNER_ID unset — chat commands WILL BE IGNORED (fail-closed); set it with 'pos config telegram'"
|
||||
fi
|
||||
sync_bot_commands || true
|
||||
|
||||
local offset=0
|
||||
log "listener running (owner chat ${TELEGRAM_CHAT_ID}) — Ctrl+C to stop"
|
||||
log "listener running (chat ${TELEGRAM_CHAT_ID}, owner ${TELEGRAM_OWNER_ID:-unset}) — Ctrl+C to stop"
|
||||
trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT
|
||||
while true; do
|
||||
local resp n i
|
||||
@@ -784,7 +786,15 @@ run_daemon() {
|
||||
reply_text="$(printf '%s' "$resp" | jq -r ".result[$i].message.reply_to_message.text // .result[$i].message.reply_to_message.caption // empty")"
|
||||
offset=$((u + 1))
|
||||
[ -n "$text" ] || continue
|
||||
if [ -n "$chat" ] && [ "$chat" != "$TELEGRAM_CHAT_ID" ] && [ "$from_id" != "$TELEGRAM_CHAT_ID" ]; then
|
||||
if [ -z "${TELEGRAM_OWNER_ID:-}" ]; then
|
||||
warn "TELEGRAM_OWNER_ID unset — ignoring command (set it with 'pos config telegram')"
|
||||
continue
|
||||
fi
|
||||
# Fail-closed owner check: the message must be in the owner chat
|
||||
# AND sent by the owner account. Previously a chat-id OR sender-id
|
||||
# match was enough — anyone who knew the chat id could run commands.
|
||||
if [ "$chat" != "$TELEGRAM_CHAT_ID" ] || [ "$from_id" != "$TELEGRAM_OWNER_ID" ]; then
|
||||
warn "ignoring message in chat ${chat:-?} from ${from_id:-?} (not the owner chat/account)"
|
||||
continue
|
||||
fi
|
||||
handle_message "$text" "$msg_id" "$reply_text"
|
||||
|
||||
@@ -3,12 +3,15 @@ set -euo pipefail
|
||||
# POS: communication telegram-sender — Send Telegram messages/files/links/stickers via Bot API (send, test)
|
||||
# POS_FLAGS: --type --caption --parse-mode --no-preview --token --chat-id --markdown
|
||||
# POS_SUBCMDS: send test
|
||||
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot | TELEGRAM_AI_PREFIX=:AI-bridge trigger word in the telegram listener (default ai)::ai
|
||||
# POS_CONFIG: telegram | telegram.env | TELEGRAM_BOT_TOKEN=secret:Bot token from @BotFather | TELEGRAM_CHAT_ID=digits:Numeric chat id from @userinfobot | TELEGRAM_OWNER_ID=digits:Numeric Telegram user id (your account) allowed to run chat commands | TELEGRAM_AI_PREFIX=:AI-bridge trigger word in the telegram listener (default ai)::ai
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
API="https://api.telegram.org"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos communication telegram sender [command] [args]
|
||||
@@ -39,8 +42,8 @@ Options:
|
||||
--token <t> Override token for one send
|
||||
--chat-id <id> Override chat id for one send
|
||||
|
||||
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
|
||||
edit it with 'pos config telegram'
|
||||
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_OWNER_ID — edit it with 'pos config telegram')
|
||||
|
||||
Precedence: CLI flags > environment > config file.
|
||||
|
||||
@@ -59,19 +62,7 @@ EOF
|
||||
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
v="${v//$'\r'/}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
load_env_file "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
send_request() {
|
||||
|
||||
+4
-12
@@ -6,20 +6,12 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
# Load grab.env config (env-seam: GRAB_DEFAULT)
|
||||
load_grab_config() {
|
||||
local f="$HOME/.config/linux_post_install/grab.env" k v
|
||||
[ -f "$f" ] || return 0
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$f" || true)
|
||||
load_env_file "$CONFIG_DIR/grab.env"
|
||||
}
|
||||
|
||||
load_grab_config
|
||||
|
||||
@@ -130,7 +130,9 @@ port_meta() {
|
||||
# --- fallback engine (no nmap): bash /dev/tcp + nc ---------------------------
|
||||
check_tcp() {
|
||||
local ip="$1" port="$2" tmo="$3" rc=""
|
||||
timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port" 2>/dev/null
|
||||
# host/port are positional args ($1/$2), never interpolated into the
|
||||
# command source — a hostile host string stays a literal argument.
|
||||
timeout "$tmo" bash -c 'exec 3<>/dev/tcp/$1/$2' _ "$ip" "$port" 2>/dev/null
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ]; then printf 'open'
|
||||
elif [ "$rc" -eq 124 ]; then printf 'timeout'
|
||||
@@ -154,7 +156,7 @@ check_udp() {
|
||||
elif printf '%s' "$out" | grep -qiE 'refus|unreachable'; then printf 'closed'
|
||||
else printf 'no-reply'; fi
|
||||
else
|
||||
timeout "$tmo" bash -c "exec 3<>/dev/udp/$ip/$port; printf 'x' >&3" 2>/dev/null
|
||||
timeout "$tmo" bash -c 'exec 3<>/dev/udp/$1/$2; printf "x" >&3' _ "$ip" "$port" 2>/dev/null
|
||||
printf 'no-reply'
|
||||
fi
|
||||
}
|
||||
@@ -163,9 +165,9 @@ probe() {
|
||||
local ip="$1" port="$2" tmo="$3"
|
||||
case "$port" in
|
||||
80|8000|8080|8081|8888|3000|5000|9090|3128|10000)
|
||||
timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port; printf 'HEAD / HTTP/1.0\r\n\r\n' >&3; head -c 300 <&3" 2>/dev/null ;;
|
||||
timeout "$tmo" bash -c 'exec 3<>/dev/tcp/$1/$2; printf "HEAD / HTTP/1.0\r\n\r\n" >&3; head -c 300 <&3' _ "$ip" "$port" 2>/dev/null ;;
|
||||
*)
|
||||
timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port; head -c 200 <&3" 2>/dev/null ;;
|
||||
timeout "$tmo" bash -c 'exec 3<>/dev/tcp/$1/$2; head -c 200 <&3' _ "$ip" "$port" 2>/dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ set -euo pipefail
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
|
||||
|
||||
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
||||
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
||||
|
||||
command -v aria2c &>/dev/null || err "aria2c not found (install aria2)"
|
||||
command -v jq &>/dev/null || err "jq not found (install jq)"
|
||||
command -v curl &>/dev/null || err "curl not found (install curl)"
|
||||
@@ -25,14 +28,13 @@ SERVICE="pos-aria2.service"
|
||||
HEALER_SERVICE="pos-aria2-retry.service"
|
||||
HEALER_TIMER="pos-aria2-retry.timer"
|
||||
RETRY_STATE="$CONFIG_DIR/download.retry"
|
||||
NET_PROBE="${NET_PROBE:-timeout 3 bash -c '</dev/tcp/8.8.8.8/53>'}"
|
||||
NET_PROBE="${NET_PROBE:-timeout 3 bash -c 'exec 3<>/dev/tcp/\$1/\$2' _ 8.8.8.8 53}"
|
||||
RETRY_INTERVAL="${RETRY_INTERVAL:-30}"
|
||||
RETRY_VERIFY_SLEEP="${RETRY_VERIFY_SLEEP:-3}"
|
||||
|
||||
load_secret() {
|
||||
if [ -z "${RPC_SECRET:-}" ] && [ -f "$CONFIG_FILE" ]; then
|
||||
RPC_SECRET=$(grep -E '^RPC_SECRET=' "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
fi
|
||||
# Canonical loader: env-wins, so an exported RPC_SECRET always beats the file.
|
||||
load_env_file "$CONFIG_FILE"
|
||||
RPC_SECRET="${RPC_SECRET:-}"
|
||||
}
|
||||
load_secret
|
||||
@@ -150,8 +152,8 @@ rpc() { # rpc <method> [json-args...]
|
||||
daemon_active() { systemctl --user is-active --quiet "$SERVICE" 2>/dev/null; }
|
||||
|
||||
cmd_start() {
|
||||
if [ -z "$RPC_SECRET" ] && [ -f "$CONFIG_FILE" ]; then
|
||||
RPC_SECRET=$(grep -E '^RPC_SECRET=' "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
if [ -z "$RPC_SECRET" ]; then
|
||||
load_secret
|
||||
fi
|
||||
if [ -z "$RPC_SECRET" ]; then
|
||||
RPC_SECRET=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
|
||||
@@ -89,7 +89,9 @@ split_share() {
|
||||
# TCP connect probe against the server before touching passwords or mounts.
|
||||
probe_server() {
|
||||
local host="$1"
|
||||
if timeout 3 bash -c "exec 3<>/dev/tcp/${host}/${SMB_PORT}" 2>/dev/null; then
|
||||
# host/port are positional args, never interpolated into the command
|
||||
# source — a hostile share/address string stays a literal argument.
|
||||
if timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ "$host" "$SMB_PORT" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
err "Server ${host} not reachable on port ${SMB_PORT} (SMB) — nothing was mounted.
|
||||
|
||||
+10
-2
@@ -192,14 +192,22 @@ run_backup() {
|
||||
unset CONFIRM
|
||||
|
||||
log "Encrypting backup..."
|
||||
gpg --batch --yes --passphrase "$PASS" --symmetric --cipher-algo AES256 "$ARCHIVE"
|
||||
# The passphrase is fed on fd 3 (here-string), never via argv, so it
|
||||
# cannot leak through 'ps' / /proc/<pid>/cmdline while gpg runs.
|
||||
if ! gpg --batch --yes --passphrase-fd 3 --symmetric --cipher-algo AES256 "$ARCHIVE" 3<<<"$PASS"; then
|
||||
rm -f "$ARCHIVE"
|
||||
err "encryption failed — plaintext archive removed, nothing left behind"
|
||||
fi
|
||||
|
||||
rm -f "$ARCHIVE"
|
||||
ARCHIVE="${ARCHIVE}.gpg"
|
||||
chmod 600 "$ARCHIVE"
|
||||
|
||||
log "Verifying encrypted backup..."
|
||||
gpg --batch --quiet --passphrase "$PASS" --decrypt "$ARCHIVE" | tar -tzf - > /dev/null
|
||||
if ! gpg --batch --quiet --passphrase-fd 3 --decrypt "$ARCHIVE" 3<<<"$PASS" | tar -tzf - > /dev/null; then
|
||||
rm -f "$ARCHIVE"
|
||||
err "decryption verification failed — corrupt artifact removed"
|
||||
fi
|
||||
|
||||
unset PASS
|
||||
else
|
||||
|
||||
+118
-36
@@ -25,6 +25,14 @@ Examples:
|
||||
pos system uninstall # interactive, tier 1 only
|
||||
pos system uninstall --yes # non-interactive, tier 1 only
|
||||
pos system uninstall --yes --config --data # nuclear option
|
||||
|
||||
Intentionally NOT removed (user-managed):
|
||||
- apt packages (system packages installed by preinstall.sh)
|
||||
- /usr/local/bin/yt-dlp (manually installed)
|
||||
- ~/.config/rclone/ (rclone manages its own config)
|
||||
- ~/.ssh/authorized_keys additions (user SSH access)
|
||||
- pos-owned config files (removed by --config tier)
|
||||
- pos-owned data files (removed by --data tier)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
@@ -44,6 +52,27 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# ── pos-owned installed files (single source of truth, mirrors install.sh) ──
|
||||
# lib/*.sh list shipped by install.sh phase 2 to /usr/local/bin.
|
||||
POS_LIBS=(common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh \
|
||||
user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh registry.sh)
|
||||
# Exact lines postinstall.sh appends to ~/.bashrc.
|
||||
PATH_LINE='export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$HOME/.local/bin:$PATH"'
|
||||
COMPLETION_LINE='source /usr/local/share/bash-completion/completions/pos.bash 2>/dev/null || true'
|
||||
# Pos-ai-hook source lines are not written by the installer (user/legacy-added),
|
||||
# so they are matched by an anchored source-pattern, never a bare substring.
|
||||
HOOK_PATTERN='^[[:space:]]*source[[:space:]].*pos-ai-hook\.sh'
|
||||
|
||||
# Entertainment plugins installed by install.sh — discovered by POS_PLUGIN marker
|
||||
# so future plugins are removed without touching this file.
|
||||
installed_plugins() {
|
||||
local ep
|
||||
for ep in /usr/local/bin/*.sh; do
|
||||
[ -f "$ep" ] || continue
|
||||
grep -q '^# POS_PLUGIN:' "$ep" 2>/dev/null && echo "$ep"
|
||||
done | sort
|
||||
}
|
||||
|
||||
# ── Scan functions ──────────────────────────────────────────────
|
||||
scan_tier1() {
|
||||
local found=()
|
||||
@@ -58,20 +87,18 @@ scan_tier1() {
|
||||
found+=("$f")
|
||||
done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true)
|
||||
|
||||
# Lib files shipped by install.sh
|
||||
for f in common.sh menu-lib.sh share-lib.sh; do
|
||||
# Lib files shipped by install.sh (single source: POS_LIBS)
|
||||
for f in "${POS_LIBS[@]}"; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# AI providers subdirectory
|
||||
[ -d /usr/local/bin/ai-providers ] && found+=("/usr/local/bin/ai-providers/")
|
||||
|
||||
# Entertainment plugins installed by install.sh
|
||||
# Entertainment plugins installed by install.sh (discovered by POS_PLUGIN marker)
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(for ep in weather.sh gold.sh joke.sh; do
|
||||
[ -f "/usr/local/bin/$ep" ] && echo "/usr/local/bin/$ep"
|
||||
done | sort)
|
||||
[ -n "$f" ] && found+=("$f")
|
||||
done < <(installed_plugins)
|
||||
|
||||
# Legacy forwarders
|
||||
for f in wr-* mp3 mp4 vbox ssh-load-all; do
|
||||
@@ -107,11 +134,11 @@ scan_tier1() {
|
||||
# Completion file
|
||||
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash")
|
||||
|
||||
# ~/.bash_completion entries
|
||||
# ~/.bash_completion entries (anchored source lines only, never /pos/ substring)
|
||||
if [ -f "$HOME/.bash_completion" ]; then
|
||||
while IFS= read -r line; do
|
||||
found+=("~/.bash_completion: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)")
|
||||
done < <(grep -n 'pos' "$HOME/.bash_completion" 2>/dev/null || true)
|
||||
done < <(grep -nE '^[[:space:]]*source[[:space:]].*pos\.bash' "$HOME/.bash_completion" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── Systemd services ──
|
||||
@@ -134,11 +161,24 @@ scan_tier1() {
|
||||
[ "$already" -eq 0 ] && found+=("service: $svc_name")
|
||||
done < <(systemctl list-unit-files --type=service 2>/dev/null | grep -i 'linux_post_install\|pos-' || true)
|
||||
|
||||
# ── Shell integration (~/.bashrc) ──
|
||||
# ── ScaleTail templates + feature-flag store (install.sh phase 2/4) ──
|
||||
[ -d /usr/local/share/linux_post_install/scale-tail ] && found+=("/usr/local/share/linux_post_install/scale-tail/")
|
||||
[ -d /usr/local/share/linux_post_install/flags ] && found+=("/usr/local/share/linux_post_install/flags/")
|
||||
|
||||
# ── Runtime-created user units (pos-* in ~/.config/systemd/user) ──
|
||||
local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
if [ -d "$user_unit_dir" ]; then
|
||||
local ufile
|
||||
while IFS= read -r ufile; do
|
||||
[ -n "$ufile" ] && found+=("user-unit: $(basename "$ufile")")
|
||||
done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── Shell integration (~/.bashrc) — exact installer lines + anchored hook ──
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
while IFS= read -r line; do
|
||||
found+=("~/.bashrc: $(echo "$line" | sed 's/^[[:space:]]*//' | cut -c1-70)")
|
||||
done < <(grep -n 'source.*pos-ai-hook\|linux_post_install.*PATH\|source.*pos\.bash\|pos completion' "$HOME/.bashrc" 2>/dev/null || true)
|
||||
done < <({ grep -nF "$PATH_LINE" "$HOME/.bashrc"; grep -nF "$COMPLETION_LINE" "$HOME/.bashrc"; grep -nE "$HOOK_PATTERN" "$HOME/.bashrc"; } 2>/dev/null | sort -u || true)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${found[@]}"
|
||||
@@ -234,9 +274,9 @@ remove_tier1() {
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(compgen -G /usr/local/bin/pos-* 2>/dev/null | sort || true)
|
||||
|
||||
# Lib files
|
||||
for f in /usr/local/bin/common.sh /usr/local/bin/menu-lib.sh /usr/local/bin/share-lib.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
# Lib files (single source: POS_LIBS)
|
||||
for f in "${POS_LIBS[@]}"; do
|
||||
[ -f "/usr/local/bin/$f" ] && { rm -f "/usr/local/bin/$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# AI providers directory
|
||||
@@ -244,10 +284,10 @@ remove_tier1() {
|
||||
rm -rf /usr/local/bin/ai-providers && count=$((count+1))
|
||||
fi
|
||||
|
||||
# Entertainment plugins
|
||||
for f in /usr/local/bin/weather.sh /usr/local/bin/gold.sh /usr/local/bin/joke.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
# Entertainment plugins (discovered by POS_PLUGIN marker)
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] && [ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(installed_plugins)
|
||||
|
||||
# Legacy forwarders
|
||||
for pat in 'wr-*' mp3 mp4 vbox ssh-load-all; do
|
||||
@@ -313,28 +353,70 @@ remove_tier1() {
|
||||
# Reload daemon after service changes
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
|
||||
# ── Shell integration (~/.bashrc) ──
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
local before
|
||||
before=$(wc -l < "$HOME/.bashrc")
|
||||
sed -i '/source.*pos-ai-hook/d' "$HOME/.bashrc"
|
||||
sed -i '/linux_post_install.*PATH/d' "$HOME/.bashrc"
|
||||
sed -i '/source.*pos\.bash/d' "$HOME/.bashrc"
|
||||
local after
|
||||
after=$(wc -l < "$HOME/.bashrc")
|
||||
local removed=$((before - after))
|
||||
count=$((count + removed))
|
||||
# ── Runtime-created USER systemd units (pos-* in ~/.config/systemd/user) ──
|
||||
# Wrapped `|| true`: no user session (e.g. running as a scheduled task) is fine.
|
||||
local user_unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
if [ -d "$user_unit_dir" ]; then
|
||||
local ufile uniname
|
||||
while IFS= read -r ufile; do
|
||||
[ -n "$ufile" ] || continue
|
||||
uniname="$(basename "$ufile")"
|
||||
systemctl --user disable --now "$uniname" 2>/dev/null || true
|
||||
rm -f "$ufile" && count=$((count+1))
|
||||
done < <(find "$user_unit_dir" -maxdepth 1 -name 'pos-*' -type f 2>/dev/null || true)
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Shell completion (~/.bash_completion) ──
|
||||
# ── ScaleTail templates (submodule clone — guard existence) ──
|
||||
if [ -d /usr/local/share/linux_post_install/scale-tail ]; then
|
||||
rm -rf /usr/local/share/linux_post_install/scale-tail && count=$((count+1))
|
||||
fi
|
||||
|
||||
# ── Feature-flag store ──
|
||||
if [ -d /usr/local/share/linux_post_install/flags ]; then
|
||||
rm -rf /usr/local/share/linux_post_install/flags && count=$((count+1))
|
||||
fi
|
||||
|
||||
# Clean up parent dir if empty
|
||||
rmdir /usr/local/share/linux_post_install 2>/dev/null || true
|
||||
|
||||
# ── Shell integration (~/.bashrc) — exact installer lines + anchored hook ──
|
||||
# Only lines postinstall.sh itself added are removed by exact literal match;
|
||||
# pos-ai-hook lines are matched anchored (never a bare substring).
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
local before after removed tmp
|
||||
before=$(wc -l < "$HOME/.bashrc")
|
||||
tmp="$(mktemp)"
|
||||
awk -v p="$PATH_LINE" -v c="$COMPLETION_LINE" -v h="$HOOK_PATTERN" '
|
||||
$0 == p || $0 == c || $0 ~ h { next }
|
||||
{ print }
|
||||
' "$HOME/.bashrc" > "$tmp"
|
||||
after=$(wc -l < "$tmp")
|
||||
removed=$((before - after))
|
||||
if [ "$removed" -gt 0 ]; then
|
||||
chmod --reference="$HOME/.bashrc" "$tmp"
|
||||
mv "$tmp" "$HOME/.bashrc"
|
||||
count=$((count + removed))
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Shell completion (~/.bash_completion) — anchored source lines only ──
|
||||
if [ -f "$HOME/.bash_completion" ]; then
|
||||
local before
|
||||
local before after removed tmp
|
||||
before=$(wc -l < "$HOME/.bash_completion")
|
||||
sed -i '/pos/d' "$HOME/.bash_completion"
|
||||
local after
|
||||
after=$(wc -l < "$HOME/.bash_completion")
|
||||
local removed=$((before - after))
|
||||
count=$((count + removed))
|
||||
tmp="$(mktemp)"
|
||||
awk '/^[[:space:]]*source[[:space:]].*pos\.bash/ { next } { print }' "$HOME/.bash_completion" > "$tmp"
|
||||
after=$(wc -l < "$tmp")
|
||||
removed=$((before - after))
|
||||
if [ "$removed" -gt 0 ]; then
|
||||
chmod --reference="$HOME/.bash_completion" "$tmp"
|
||||
mv "$tmp" "$HOME/.bash_completion"
|
||||
count=$((count + removed))
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 1)"
|
||||
|
||||
Reference in New Issue
Block a user