Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fd3c37c40 | |||
| 5d7407e30f | |||
| 6566c8343d | |||
| 9f289ba31b | |||
| a5c19e842d | |||
| d84a35efce | |||
| 9564880ebf | |||
| d0299d3f98 | |||
| c1f1c4109f | |||
| 710b626f47 | |||
| 1c19c0de59 | |||
| e0c9ba384a | |||
| 7ae2e77a44 | |||
| 88ea660891 | |||
| 1fbdf7ef2d |
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–198 |
|
||||
| ## 3. Installation Flow | 199–252 |
|
||||
| ## 4. The `pos` CLI System | 253–328 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 329–360 |
|
||||
| ## 6. Docker Compose / ScaleTail | 361–403 |
|
||||
| ## 7. Optional Apps (`apps/`) | 404–433 |
|
||||
| ## 8. Entertainment Module | 434–447 |
|
||||
| ## 9. Systemd Services | 448–459 |
|
||||
| ## 10. Configuration Files | 460–486 |
|
||||
| ## 11. Coding Conventions | 487–519 |
|
||||
| ## 12. Development Workflow | 520–572 |
|
||||
| ## 13. Key File Quick Reference | 573–641 |
|
||||
| ## 14. Common Tasks for Agents | 642–675 |
|
||||
| ## 2. Directory Structure | 44–200 |
|
||||
| ## 3. Installation Flow | 201–254 |
|
||||
| ## 4. The `pos` CLI System | 255–332 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 333–364 |
|
||||
| ## 6. Docker Compose / ScaleTail | 365–407 |
|
||||
| ## 7. Optional Apps (`apps/`) | 408–437 |
|
||||
| ## 8. Entertainment Module | 438–451 |
|
||||
| ## 9. Systemd Services | 452–463 |
|
||||
| ## 10. Configuration Files | 464–490 |
|
||||
| ## 11. Coding Conventions | 491–523 |
|
||||
| ## 12. Development Workflow | 524–576 |
|
||||
| ## 13. Key File Quick Reference | 577–648 |
|
||||
| ## 14. Common Tasks for Agents | 649–682 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -61,6 +61,7 @@ Linux_post_install/
|
||||
├── bin/ # CLI tools — installed to /usr/local/bin/
|
||||
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
||||
<!-- GEN:START tree -->
|
||||
│ ├── pos-ai-alias # manage AI agent aliases
|
||||
│ ├── pos-ai-gemini # Forward to pos ai --provider gemini (backward compat)
|
||||
│ ├── pos-ai-openrouter # Forward to pos ai --provider openrouter (backward compat)
|
||||
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
|
||||
@@ -97,6 +98,7 @@ Linux_post_install/
|
||||
│ ├── pos-system-firewall # Interactive UFW management
|
||||
│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
|
||||
│ ├── pos-system-schedule # Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently
|
||||
│ ├── pos-system-uninstall # Remove pos toolkit binaries, services, shell integration, config, and data
|
||||
│ ├── pos-ai # AI assistant: ask, chat, sessions, capture, models, providers
|
||||
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
|
||||
@@ -206,7 +208,7 @@ User runs: ./install.sh [--apps|--full|--feature|--dry-run|--skip <phase>|--step
|
||||
│
|
||||
├─ Phase 2: install.sh (requires root)
|
||||
│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)
|
||||
│ └─ Copies lib/*.sh (common, flags, notify, entertainment-lib,
|
||||
│ └─ Copies lib/*.sh (common, flags, notify, registry, entertainment-lib,
|
||||
│ scheduler-lib, config-ui, user-timers-lib, entertainment-plugin-lib,
|
||||
│ usb-lib, share-lib, menu-lib) → /usr/local/bin/ (chmod 644)
|
||||
│ └─ Copies x64_bin/* → /usr/local/bin/ on x86_64 (arm64_bin/ on aarch64)
|
||||
@@ -269,6 +271,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| Category | Command | Script | Description |
|
||||
|----------|---------|--------|-------------|
|
||||
<!-- GEN:START dispatch -->
|
||||
| ai | alias | `pos-ai-alias` | manage AI agent aliases |
|
||||
| ai | gemini | `pos-ai-gemini` | Forward to pos ai --provider gemini (backward compat) |
|
||||
| ai | openrouter | `pos-ai-openrouter` | Forward to pos ai --provider openrouter (backward compat) |
|
||||
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages |
|
||||
@@ -305,6 +308,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| system | firewall | `pos-system-firewall` | Interactive UFW management |
|
||||
| system | health | `pos-system-health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| system | schedule | `pos-system-schedule` | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| system | uninstall | `pos-system-uninstall` | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||
| | ai | `pos-ai` | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| | tree | `pos-tree` | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
@@ -587,6 +591,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `lib/usb-lib.sh` | 205 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`; EFI system partitions excluded; picker shows size/label/fs) — used by `pos system backup` + `pos media sync` |
|
||||
| `lib/share-lib.sh` | 318 | Domain layer for the share suite (usbsrv/smbclient record parsers, folder+mountpoint candidates, remote listings, service/firewall advisories; EOF-safe) + compat shims to `lib/menu-lib.sh` — used by all five `pos share *` tools |
|
||||
| `lib/menu-lib.sh` | 169 | Category-neutral interactive menu primitives (`menu_guard` tty guard, `menu_run` looping boxed menu, `menu_pick` type-to-filter picker, `menu_ask_value` prompt-with-default; stderr render, fail-closed on non-tty/EOF) — sourced by `share-lib.sh`, open to any category |
|
||||
| `lib/registry.sh` | 199 | Shared query API for POS tool metadata headers (`# POS_*:`) — `reg_scan`/`reg_list`/`reg_lookup`/`reg_each`/config scope helpers; used by `pos-tree` and `gen-docs.sh` |
|
||||
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||
| `bin/flag-clear` | 21 | Unset a flag |
|
||||
@@ -594,6 +599,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
|
||||
<!-- GEN:START filetable -->
|
||||
| `bin/pos` | 295 | CLI dispatcher with smart arg matching + logging + category help |
|
||||
| `bin/pos-ai-alias` | 542 | manage AI agent aliases |
|
||||
| `bin/pos-ai-gemini` | 7 | Forward to pos ai --provider gemini (backward compat) |
|
||||
| `bin/pos-ai-openrouter` | 7 | Forward to pos ai --provider openrouter (backward compat) |
|
||||
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
|
||||
@@ -630,10 +636,11 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-system-firewall` | 325 | Interactive UFW management |
|
||||
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| `bin/pos-ai` | 642 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-system-uninstall` | 415 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||
| `bin/pos-ai` | 680 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 305 | Dynamic bash completion |
|
||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 307 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
|
||||
+4
-2
@@ -31,7 +31,7 @@ Each phase is independent and runs only if the corresponding script exists.
|
||||
|-----------|---------|-------------|
|
||||
| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |
|
||||
| `apps/<category>/` | Optional desktop app installers | run on demand |
|
||||
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
|
||||
| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `registry.sh` (shared query API for POS tool metadata headers), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI), `menu-lib.sh` (category-neutral menu primitives: guard/looping menu/filter picker/prompt), `share-lib.sh` (share-suite domain probes/listings + compat shims to menu-lib) | sourced at build time |
|
||||
| `config/` | Gitignored user config files | `~/.config/<app>/` (via postinstall) |
|
||||
| `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/bin` (via install.sh Phase 2) |
|
||||
| `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |
|
||||
@@ -128,8 +128,10 @@ esac
|
||||
# POS: <category> <command> — one-line description rendered by `make gen`
|
||||
# POS_FLAGS: --flag1 --flag2 # ONLY for flag-style tools
|
||||
# POS_SUBCMDS: sub1 sub2 # ONLY for multi-command tools
|
||||
# POS_DEPS: binary1 binary2 # Optional: runtime deps (space-separated binary names)
|
||||
# POS_EXAMPLES: pos <tool> <args> | Description # Optional: usage examples
|
||||
```
|
||||
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `make gen` only reads the text after the first `— ` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
|
||||
The description feeds the dispatch table, bin tree and file table in `DOC/AGENT_Context_Project.md`; `POS_FLAGS` feeds flag completion and `POS_SUBCMDS` feeds subcommand completion in `completions/pos.bash` (both update via `make gen`). `POS_DEPS` lists runtime binary names that `command -v` would check — use when the tool requires specific binaries beyond what `preinstall.sh` installs. `POS_EXAMPLES` provides curated usage examples (one per line, pipe-delimited `command | description`) shown in `pos tree` and future help views. Both are optional and degrade gracefully when absent. `make gen` only reads the text after the first `— ` — the `<category> <command>` words before it are convention-only (for nested tools, keep the full path there, e.g. `# POS: communication telegram-listener — …`).
|
||||
- **Category-less vs categorized:** most tools are `bin/pos-<category>-<command>`. Use category-less `bin/pos-<cmd>` (e.g. `pos-config`, `pos-tree`) only for dispatcher/dev-level commands that fit no category — they dispatch and document like any tool but show with an empty category in the generated tables.
|
||||
- Nested tools (e.g. `bin/pos-communication-telegram-listener`) are auto-detected from filenames: the trailing segment (`listener`) is offered as a subcommand of the parent tool (`communication-telegram`) in `pos <category> --help` and tab-completion, instead of appearing as a flat sibling (`telegram-listener`). The flat dash-form (`pos communication telegram-listener`) still dispatches.
|
||||
- Optionally add an EXAMPLES line in `bin/pos` `usage()` to showcase the tool in `pos --help`.
|
||||
|
||||
@@ -70,6 +70,14 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
|
||||
| `pos ai providers` | Lists available providers, their config status, and the active provider |
|
||||
| `pos ai --model <id> …` | Overrides the model for one invocation |
|
||||
| `pos ai --provider <name> …` | Selects the provider for one invocation (gemini\|openrouter) |
|
||||
| `pos ai alias` | Interactive alias manager (`bin/pos-ai-alias`): menu loop (create / edit / remove / list) that shows the alias table (Name/Provider/Session/Prompt, prompts truncated) between picks |
|
||||
| `pos ai alias create [name]` | Interactive 4-step wizard: alias name (leading letter, then letters/digits/-/_; unique across aliases), provider pick (from installed `lib/ai-providers/*.sh` adapters), session name (defaults to the alias name), optional system prompt (must not contain `\|`; warns above 500 chars); confirm defaults to yes, then the alias is saved |
|
||||
| `pos ai alias edit [name]` | Edits an existing alias (pick from list or pass the name): provider/session/prompt are re-prompted pre-filled with the current values — Enter keeps the current value; a per-field changed/unchanged summary is confirmed (default yes) before saving; nothing is written if nothing changed |
|
||||
| `pos ai alias remove [name]` | Removes an alias (pick from list or pass the name); the confirmation defaults to **no** and removal cannot be undone |
|
||||
| `pos ai alias list` | Non-interactive: prints all aliases as a Name/Provider/Session/Prompt table (prompts truncated at 42 chars) |
|
||||
| `pos ai alias show <name>` | Prints one alias's details including the resolved command: `pos ai <provider> ask --session <session>[ --system '<prompt>']` |
|
||||
|
||||
Alias storage: records live in `~/.config/linux_post_install/ai-aliases.env` — one `name\|provider\|session\|system_prompt` line per alias, chmod 600, managed by the tool (do not hand-edit). Every create/edit/remove also rewrites `~/.config/linux_post_install/ai-aliases.sh` (chmod 644), which defines one shell alias per record — `alias <name>='pos ai <provider> ask --session <session>[ --system '<prompt>']'`. An empty session falls back to the alias name, prompts are single-quote-escaped, and the generated file is syntax-checked before it replaces the previous version. Source this file from your shell rc (e.g. `.bashrc`) to activate the aliases in new shells.
|
||||
|
||||
Backward compatibility: `pos ai gemini` and `pos ai openrouter` still work as shorthands for `pos ai --provider gemini` and `pos ai --provider openrouter`.
|
||||
|
||||
@@ -252,6 +260,7 @@ reported as "N videos require sign-in — skipped" (escape hatch:
|
||||
| `pos system backup --service` | `bin/pos-system-backup` | Lists folders under `/srv` and `~/srv`, lets you pick one, then runs the same backup | Roots via `BACKUP_SERVICE_ROOTS` (space-separated, default `/srv $HOME/srv`) or `~/.config/linux_post_install/system.env` |
|
||||
| `pos system health` | `bin/pos-system-health` | Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | Console-only reporter — health itself never sends notifications; forward the output with a wrapper (e.g. the Telegram/Matrix listener map `/status=pos system health`) or schedule it via `pos system schedule` with a `NOTIFY` policy. `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2) and `BACKUP_SERVICE_ROOTS` come from `~/.config/linux_post_install/system.env`; `--help` shows the effective values |
|
||||
| `pos system schedule <cmd>` | `bin/pos-system-schedule` | Scheduled jobs — run a command on a timer, notify (or stay silent): `run [name\|all]`, `list`, `config`, `enable [name\|all]`, `disable [name\|all]`, `status`, `migrate`. Each job is a file in `~/.config/linux_post_install/schedule.d/<name>.env` with `INTERVAL` (`5m…59m`, `1h…23h`, `hourly`, `daily`, `weekly`, `OnCalendar=…`), `NOTIFY` policy, optional `MSG`, `RULE` (threshold only), and `COMMAND` = the literal rest of the line (pipes/quotes/`sudo` fine). Policies: `always` (full output every run), `onchange` (send when output differs from the last run; first run always sends), `onerror` (non-zero exit or empty output), `threshold` (first numeric output vs `RULE`, alert on false→true + one recovery — the old event-trigger behavior), `never` (side-effect jobs, no notify) | One systemd **user** timer pair per job (`pos-schedule-<name>.timer` + oneshot `.service`, `Persistent=true`), reconciled on `enable`/`disable`; the legacy single `pos-event-trigger` timer is auto-removed. `migrate` converts a pre-existing `event.env` rule set into `schedule.d/rule-N.env` threshold jobs. `config` is an interactive editor (add/edit/remove/enable/disable, validates interval + threshold); alerts via `lib/notify.sh`; `--dry-run` previews runs/writes/sends; jobs are arbitrary shell commands (chmod 600, same trust model as the Telegram map); starter jobs in `config/schedule.d/` auto-installed no-clobber by postinstall. Bare invocation on a terminal (or the `menu` subcommand) opens an interactive hub over these verbs (list, timer status, run-now, enable, disable, config editor) — a menu run-now asks y/N first and goes through the same `run <name>` path the systemd timers use |
|
||||
| `pos system uninstall` | `bin/pos-system-uninstall` | Safe, interactive uninstaller for the pos toolkit — scans and removes binaries, services, shell integration, config, and data in three tiers | Tier 1 (always): binaries in `/usr/local/bin/` (pos, pos-*, libs, ai-providers, entertainment plugins, prebuilt, features), systemd services (disable+remove), shell integration in `~/.bashrc` (PATH, completion, pos-ai-hook source), completion file. Tier 2 (`--config`): `~/.config/linux_post_install/` (.env files, schedule.d/, authorized_keys, rclone.conf). Tier 3 (`--data`): `~/.local/share/linux_post_install/` (ai sessions, logs, captured output). Flags: `--yes` (skip prompts, tier 1 only), `--config` (include tier 2), `--data` (include tier 3). Combine all three for nuclear removal. Git repo is never removed |
|
||||
|
||||
A scheduled job is the recommended way to run the health dashboard on a timer: a `daily` job with `COMMAND=pos system health` and `NOTIFY=always` sends the dashboard output as the alert — no separate systemd unit needed (the old `pos-health.{service,timer}` units are gone; a legacy install may still have them failed/leftover — disable and remove them).
|
||||
|
||||
|
||||
+33
-2
@@ -1,13 +1,14 @@
|
||||
# How-To: `pos system`
|
||||
|
||||
Host care: encrypted backups, firewall, and the health dashboard. Tools:
|
||||
`backup`, `firewall`, `health`.
|
||||
Host care: encrypted backups, firewall, health dashboard, and uninstall. Tools:
|
||||
`backup`, `firewall`, `health`, `uninstall`.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `pos system health` | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker) |
|
||||
| `pos system backup` | gpg-encrypted (AES-256) folder snapshots |
|
||||
| `pos system firewall` | Interactive UFW ("UFW POWER") management |
|
||||
| `pos system uninstall` | Safe, interactive uninstaller for the pos toolkit |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,6 +205,36 @@ not).
|
||||
`sudo ufw allow 22/tcp`, then `sudo ufw reload`.
|
||||
- `ufw reset` requires typing `RESET` — deliberate.
|
||||
|
||||
## `pos system uninstall` — remove the pos toolkit
|
||||
|
||||
```bash
|
||||
pos system uninstall # interactive scan + confirm tier 1
|
||||
pos system uninstall --yes # non-interactive, tier 1 only
|
||||
pos system uninstall --yes --config --data # remove everything (nuclear option)
|
||||
```
|
||||
|
||||
Scans the system for installed pos components and removes them in three tiers:
|
||||
|
||||
| Tier | What it removes | How to include |
|
||||
|------|----------------|----------------|
|
||||
| **Tier 1** | Binaries (`/usr/local/bin/pos*`, libs, entertainment plugins, prebuilt, features), systemd services (disable+remove), shell integration (`~/.bashrc` PATH/completion/pos-ai-hook entries), completion file | Always (default) |
|
||||
| **Tier 2** | Config files (`~/.config/linux_post_install/` — `.env` files, `schedule.d/`, `authorized_keys`, `rclone.conf`) | `--config` flag |
|
||||
| **Tier 3** | Session/log data (`~/.local/share/linux_post_install/` — AI sessions, logs, captured output) | `--data` flag |
|
||||
|
||||
The default mode is interactive: it shows what will be removed and asks for
|
||||
confirmation. The git repo is **never** removed — delete it manually if desired.
|
||||
|
||||
**Recipes:**
|
||||
- Quick cleanup: `pos system uninstall --yes`
|
||||
- Full wipe: `pos system uninstall --yes --config --data`
|
||||
- Safe preview: run `pos system uninstall` without `--yes` to see the plan first
|
||||
|
||||
**Troubleshooting:**
|
||||
- "Nothing to remove" → pos toolkit is not installed (or already removed)
|
||||
- After uninstall, run `source ~/.bashrc` or restart your shell
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- Reference: [DOC/POS.md → system](../POS.md)
|
||||
|
||||
@@ -259,7 +259,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||
|
||||
# Commands that read from stdin interactively — only log invocation
|
||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter system-schedule entertainment-config config"
|
||||
INTERACTIVE_CMDS="docker-compose docker-vbox network-hotspot system-firewall media-mp4 media-sync system-backup system-uninstall share-usb-server share-smb-server share-smb-client share-nfs-client share-nfs-server communication-telegram-listener communication-matrix-listener ai ai-gemini ai-openrouter system-schedule entertainment-config config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
+63
-25
@@ -3,7 +3,7 @@ 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
|
||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini|openrouter, default gemini) | AI_API_KEY=secret:API key for the active provider | AI_MODEL=:Model id (default per provider) | AI_SYSTEM_PROMPT=:Custom system prompt (overrides built-in; empty to reset)
|
||||
# POS_CONFIG: ai | ai.env | AI_PROVIDER=:Provider (gemini or openrouter, default gemini) | *providers | 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"
|
||||
|
||||
@@ -76,13 +76,9 @@ Options:
|
||||
|
||||
Config: $CONFIG_FILE (edit with 'pos config ai')
|
||||
AI_PROVIDER Provider to use (gemini|openrouter, default gemini)
|
||||
AI_API_KEY API key for the active provider (secret)
|
||||
AI_MODEL Model id (default depends on provider)
|
||||
AI_SYSTEM_PROMPT Custom system prompt (overrides built-in; empty to reset)
|
||||
AI_GEMINI_API_KEY Legacy: Gemini API key fallback
|
||||
AI_GEMINI_MODEL Legacy: Gemini model fallback
|
||||
OPENROUTER_API_KEY Legacy: OpenRouter API key fallback
|
||||
OPENROUTER_MODEL Legacy: OpenRouter model fallback
|
||||
Provider keys: auto-discovered from lib/ai-providers/*.sh
|
||||
(AI_GEMINI_API_KEY, OPENROUTER_API_KEY, etc.)
|
||||
|
||||
Notes:
|
||||
ask is terse by default: a built-in system instruction tells the model to
|
||||
@@ -164,16 +160,11 @@ load_config() {
|
||||
resolve_key() {
|
||||
load_config
|
||||
local p="${PROVIDER:-gemini}"
|
||||
# 1. Unified AI_API_KEY
|
||||
if [ -n "${AI_API_KEY:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
# 2. Provider-specific fallback
|
||||
# Each provider has its own API key — set AI_API_KEY internally for adapters
|
||||
case "$p" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && export AI_API_KEY="$AI_GEMINI_API_KEY" && return 0 ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && export AI_API_KEY="$OPENROUTER_API_KEY" && return 0 ;;
|
||||
esac
|
||||
# 3. No key found — return 1 (require_key handles the error message)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -181,10 +172,10 @@ require_key() {
|
||||
if ! resolve_key >/dev/null 2>&1; then
|
||||
local p="${PROVIDER:-gemini}"
|
||||
case "$p" in
|
||||
gemini) err "No Gemini API key — run 'pos config ai' or set AI_API_KEY / AI_GEMINI_API_KEY" ;;
|
||||
openrouter) err "No OpenRouter API key — run 'pos config ai' or set AI_API_KEY / OPENROUTER_API_KEY" ;;
|
||||
gemini) err "No Gemini API key — run 'pos config ai' and set AI_GEMINI_API_KEY" ;;
|
||||
openrouter) err "No OpenRouter API key — run 'pos config ai' and set OPENROUTER_API_KEY" ;;
|
||||
esac
|
||||
err "No API key for provider '$p' — run 'pos config ai' or set AI_API_KEY"
|
||||
err "No API key for provider '$p' — run 'pos config ai'"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -304,7 +295,10 @@ session_push() {
|
||||
# ── Terminal markdown rendering (tty-only; raw bytes otherwise) ──
|
||||
render_markdown() {
|
||||
local text="$1"
|
||||
if [ ! -t 1 ]; then
|
||||
# Check stdout tty OR controlling terminal (/dev/tty) — the shell hook
|
||||
# (pos-ai-hook.sh) redirects stdout through tee, breaking [ -t 1 ], but
|
||||
# /dev/tty remains writable in interactive shells.
|
||||
if [ ! -t 1 ] && [ ! -w /dev/tty ]; then
|
||||
printf '%s\n' "$text"
|
||||
return 0
|
||||
fi
|
||||
@@ -359,6 +353,46 @@ render_markdown() {
|
||||
printf '\n%s\n' "$rendered"
|
||||
}
|
||||
|
||||
# ── Command extraction from AI responses ────────────────────────
|
||||
_extract_commands() {
|
||||
local text="$1"
|
||||
printf '%s' "$text" | awk '
|
||||
/^```(bash|sh|shell)/ { in_block=1; next }
|
||||
/^```/ { if (in_block) in_block=0; next }
|
||||
in_block && NF > 0 { lines[++n] = $0 }
|
||||
END {
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (i > 1) printf "\n"
|
||||
printf "%s", lines[i]
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# ── Interactive prompt to run extracted commands ─────────────────
|
||||
_prompt_run_command() {
|
||||
local cmd="$1"
|
||||
# Only prompt on interactive terminals with a controlling tty
|
||||
[ -w /dev/tty ] || return 0
|
||||
printf '\n%s\n' "Command detected:" >&2
|
||||
printf ' %s\n\n' "$cmd" >&2
|
||||
printf 'Run this command? [Y/n] ' >&2
|
||||
local choice
|
||||
IFS= read -r choice </dev/tty || choice=""
|
||||
case "${choice,,}" in
|
||||
n|N)
|
||||
# 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
|
||||
}
|
||||
|
||||
# ── Machine context appended to the built-in default prompt ─────
|
||||
mc_clean() {
|
||||
sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g' \
|
||||
@@ -477,6 +511,10 @@ cmd_ask() {
|
||||
messages="$(session_push "$messages" assistant "$out")"
|
||||
session_save "$messages"
|
||||
render_markdown "$out"
|
||||
# Command execution prompt: extract commands from response and offer to run
|
||||
local _cmd
|
||||
_cmd="$(_extract_commands "$out")"
|
||||
[ -n "$_cmd" ] && _prompt_run_command "$_cmd"
|
||||
}
|
||||
|
||||
cmd_chat() {
|
||||
@@ -510,6 +548,10 @@ cmd_chat() {
|
||||
session_save "$messages"
|
||||
printf '\n'
|
||||
render_markdown "$answer"
|
||||
# Command execution prompt: extract commands from response and offer to run
|
||||
local _cmd
|
||||
_cmd="$(_extract_commands "$answer")"
|
||||
[ -n "$_cmd" ] && _prompt_run_command "$_cmd"
|
||||
printf '\n\n'
|
||||
done
|
||||
echo
|
||||
@@ -566,14 +608,10 @@ cmd_providers() {
|
||||
[ -n "$pmodel" ] || pmodel="unknown"
|
||||
# Check if API key exists for this provider
|
||||
configured="not configured"
|
||||
if [ -n "${AI_API_KEY:-}" ]; then
|
||||
configured="configured"
|
||||
else
|
||||
case "$name" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && configured="configured" ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && configured="configured" ;;
|
||||
esac
|
||||
fi
|
||||
case "$name" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && configured="configured" ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && configured="configured" ;;
|
||||
esac
|
||||
current=""
|
||||
[ "$name" = "$active" ] && current=" ← active"
|
||||
printf ' %-16s %s (model: %s)%s\n' "$name" "$configured" "$pmodel" "$current"
|
||||
|
||||
Executable
+542
@@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai alias — manage AI agent aliases
|
||||
# POS_SUBCMDS: create edit remove list show
|
||||
|
||||
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"
|
||||
|
||||
# ── Paths & constants ──────────────────────────────────────────
|
||||
ENV_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.env"
|
||||
SH_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/ai-aliases.sh"
|
||||
|
||||
# Provider discovery — same pattern as bin/pos-ai (lines 17-18)
|
||||
PROVIDER_DIR="$(dirname "$0")/../lib/ai-providers"
|
||||
[ -d "$PROVIDER_DIR" ] || PROVIDER_DIR="$(dirname "$0")/ai-providers"
|
||||
|
||||
# ── Core helpers ───────────────────────────────────────────────
|
||||
|
||||
_alias_load() {
|
||||
_ALIAS_NAMES=(); _ALIAS_PROVIDERS=(); _ALIAS_SESSIONS=(); _ALIAS_PROMPTS=()
|
||||
[ -f "$ENV_FILE" ] || return 0
|
||||
while IFS='|' read -r name provider session prompt _rest; do
|
||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${name// /}" ]] && continue
|
||||
name="${name## }"; name="${name%% }"
|
||||
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
||||
provider="${provider## }"; provider="${provider%% }"
|
||||
session="${session## }"; session="${session%% }"
|
||||
_ALIAS_NAMES+=("$name")
|
||||
_ALIAS_PROVIDERS+=("$provider")
|
||||
_ALIAS_SESSIONS+=("$session")
|
||||
_ALIAS_PROMPTS+=("$prompt")
|
||||
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
||||
}
|
||||
|
||||
_alias_save() {
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
{
|
||||
printf '%s\n' "# AI aliases — managed by pos ai alias (do not hand-edit)"
|
||||
printf '%s\n' "# Format: alias_name|provider|session_name|system_prompt"
|
||||
printf '%s\n' "#"
|
||||
local i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
printf '%s|%s|%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||
"${_ALIAS_SESSIONS[$i]}" "${_ALIAS_PROMPTS[$i]}"
|
||||
done
|
||||
} >"$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
}
|
||||
|
||||
_alias_regen() {
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
printf '%s\n' "#!/usr/bin/env bash" >"$tmp"
|
||||
printf '%s\n\n' "# Auto-generated by pos ai alias — do not hand-edit." >>"$tmp"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
while IFS='|' read -r name provider session prompt _rest; do
|
||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${name// /}" ]] && continue
|
||||
name="${name## }"; name="${name%% }"
|
||||
[[ "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue
|
||||
provider="${provider## }"; provider="${provider%% }"
|
||||
session="${session## }"; session="${session%% }"
|
||||
[ -z "$session" ] && session="$name"
|
||||
local escaped_prompt="${prompt//\'/\'\\\'\'}"
|
||||
printf "alias %s='pos ai %s ask --session %s" "$name" "$provider" "$session" >>"$tmp"
|
||||
if [ -n "$prompt" ]; then
|
||||
printf " --system '%s'" "$escaped_prompt" >>"$tmp"
|
||||
fi
|
||||
printf "'\n" >>"$tmp"
|
||||
done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true)
|
||||
fi
|
||||
if ! bash -n "$tmp" 2>/dev/null; then
|
||||
warn "Generated alias file has syntax errors — keeping previous version"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
mv "$tmp" "$SH_FILE"
|
||||
chmod 644 "$SH_FILE"
|
||||
}
|
||||
|
||||
_alias_provider_pick() {
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
if [ ${#providers[@]} -eq 0 ]; then
|
||||
err "No AI providers installed — run 'pos ai' setup first"
|
||||
fi
|
||||
menu_pick "Pick provider" "${providers[@]}"
|
||||
}
|
||||
|
||||
_alias_find() {
|
||||
local name="$1" i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" = "$name" ]; then
|
||||
echo "$i"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "-1"
|
||||
return 0
|
||||
}
|
||||
|
||||
_alias_name_valid() {
|
||||
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
|
||||
}
|
||||
|
||||
_alias_prompt_truncate() {
|
||||
local p="$1"
|
||||
if [ ${#p} -gt 42 ]; then
|
||||
printf '%s…' "${p:0:42}"
|
||||
else
|
||||
printf '%s' "$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Non-interactive output ─────────────────────────────────────
|
||||
|
||||
_alias_list() {
|
||||
_alias_load
|
||||
local count=${#_ALIAS_NAMES[@]}
|
||||
printf 'Aliases (%d):\n' "$count"
|
||||
[ "$count" -eq 0 ] && return 0
|
||||
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt"
|
||||
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
|
||||
"------------------------------------------"
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||
"${_ALIAS_SESSIONS[$i]}" "$(_alias_prompt_truncate "${_ALIAS_PROMPTS[$i]}")"
|
||||
done
|
||||
}
|
||||
|
||||
_alias_show() {
|
||||
_alias_load
|
||||
local idx
|
||||
idx="$(_alias_find "$1")"
|
||||
[ "$idx" = "-1" ] && err "Alias '$1' not found"
|
||||
local name="${_ALIAS_NAMES[$idx]}" provider="${_ALIAS_PROVIDERS[$idx]}"
|
||||
local session="${_ALIAS_SESSIONS[$idx]}" prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
[ -z "$session" ] && session="$name"
|
||||
printf ' %-12s %s\n' "Alias:" "$name"
|
||||
printf ' %-12s %s\n' "Provider:" "$provider"
|
||||
printf ' %-12s %s\n' "Session:" "$session"
|
||||
printf ' %-12s %s\n' "Prompt:" "${prompt:-$(printf '%s' "(default)")}"
|
||||
# Show the resolved command
|
||||
local cmd="pos ai $provider ask --session $session"
|
||||
if [ -n "$prompt" ]; then
|
||||
local escaped="${prompt//\'/\'\\\'\'}"
|
||||
cmd="$cmd --system '$escaped'"
|
||||
fi
|
||||
printf ' %-12s %s\n' "Command:" "$cmd"
|
||||
}
|
||||
|
||||
# ── Interactive: main menu ─────────────────────────────────────
|
||||
|
||||
_alias_menu() {
|
||||
menu_guard || return 1
|
||||
while true; do
|
||||
{
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}"
|
||||
else
|
||||
local count=${#_ALIAS_NAMES[@]} i
|
||||
printf ' %-12s %-12s %-12s %s\n' "Name" "Provider" "Session" "Prompt"
|
||||
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
|
||||
"------------------------------------------"
|
||||
for ((i = 0; i < count; i++)); do
|
||||
local p="${_ALIAS_PROMPTS[$i]}"
|
||||
if [ ${#p} -gt 42 ]; then
|
||||
p="${p:0:42}…"
|
||||
fi
|
||||
printf ' %-12s %-12s %-12s %s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_PROVIDERS[$i]}" \
|
||||
"${_ALIAS_SESSIONS[$i]}" "$p"
|
||||
done
|
||||
printf ' %-12s %-12s %-12s %s\n' "------------" "------------" "------------" \
|
||||
"------------------------------------------"
|
||||
printf ' %d alias(es)\n' "$count"
|
||||
fi
|
||||
echo >&2
|
||||
} >&2
|
||||
local choice
|
||||
choice="$(menu_run "AI Agent Aliases" "Create new alias" "Edit existing alias" \
|
||||
"Remove alias" "List aliases")" || return 0
|
||||
case "$choice" in
|
||||
1) _alias_create ;;
|
||||
2) _alias_edit ;;
|
||||
3) _alias_remove ;;
|
||||
4) _alias_list ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Interactive: create ────────────────────────────────────────
|
||||
|
||||
_alias_create() {
|
||||
local preset_name="${1:-}"
|
||||
section "Create AI Agent Alias" >&2
|
||||
|
||||
# Step 1: Alias name
|
||||
local name="$preset_name"
|
||||
while true; do
|
||||
if [ -z "$name" ]; then
|
||||
step 1 4 "Alias Name" >&2
|
||||
name="$(menu_ask_value "Alias name" "")" || return 0
|
||||
fi
|
||||
[ -z "$name" ] && { warn "Alias name cannot be empty" >&2; name=""; continue; }
|
||||
if ! _alias_name_valid "$name"; then
|
||||
warn "Invalid name '$name' — use letters, digits, hyphens, underscores (start with a letter)" >&2
|
||||
name=""; continue
|
||||
fi
|
||||
_alias_load
|
||||
local existing
|
||||
existing="$(_alias_find "$name")"
|
||||
if [ "$existing" != "-1" ]; then
|
||||
warn "Alias '$name' already exists — use 'pos ai alias edit $name' instead" >&2
|
||||
[ -n "$preset_name" ] && return 1
|
||||
name=""; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# Step 2: Provider
|
||||
step 2 4 "Provider" >&2
|
||||
local pidx
|
||||
pidx="$(_alias_provider_pick)" || return 0
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
local provider="${providers[$((pidx - 1))]}"
|
||||
|
||||
# Step 3: Session name
|
||||
local session=""
|
||||
while true; do
|
||||
step 3 4 "Session Name" >&2
|
||||
session="$(menu_ask_value "Session name" "$name")" || return 0
|
||||
if [ -n "$session" ] && ! _alias_name_valid "$session"; then
|
||||
warn "Invalid session '$session' — use letters, digits, hyphens, underscores" >&2
|
||||
session=""; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
[ -z "$session" ] && session="$name"
|
||||
|
||||
# Step 4: System prompt
|
||||
local prompt=""
|
||||
while true; do
|
||||
step 4 4 "System Prompt" >&2
|
||||
prompt="$(menu_ask_value "System prompt (empty = use built-in)" "")" || return 0
|
||||
if [[ "$prompt" == *'|'* ]]; then
|
||||
warn "System prompt must not contain '|' characters" >&2
|
||||
prompt=""; continue
|
||||
fi
|
||||
if [ ${#prompt} -gt 500 ]; then
|
||||
warn "Prompt is ${#prompt} chars — consider keeping it concise" >&2
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# Confirmation
|
||||
{
|
||||
echo "────────────────────────────────────────────"
|
||||
printf ' Create alias '\''%s'\''?\n' "$name"
|
||||
printf ' Provider: %s\n' "$provider"
|
||||
printf ' Session: %s\n' "$session"
|
||||
local dp="$prompt"
|
||||
[ ${#dp} -gt 50 ] && dp="${dp:0:50}…"
|
||||
printf ' Prompt: %s\n' "${dp:-<built-in>}"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Create alias '$name'?" y; then
|
||||
log "Aborted." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_NAMES+=("$name")
|
||||
_ALIAS_PROVIDERS+=("$provider")
|
||||
_ALIAS_SESSIONS+=("$session")
|
||||
_ALIAS_PROMPTS+=("$prompt")
|
||||
_alias_save
|
||||
_alias_regen
|
||||
log "Alias '$name' created. Reload shell: source ~/.bashrc" >&2
|
||||
}
|
||||
|
||||
# ── Interactive: edit ──────────────────────────────────────────
|
||||
|
||||
_alias_edit() {
|
||||
local preset_name="${1:-}"
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
warn "No aliases to edit — create one first" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name="$preset_name"
|
||||
if [ -z "$name" ]; then
|
||||
section "Edit AI Agent Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local p="${_ALIAS_PROMPTS[$i]}"
|
||||
if [ ${#p} -gt 30 ]; then
|
||||
p="${p:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
|
||||
done
|
||||
local picked
|
||||
picked="$(menu_pick "Pick alias to edit" "${display_items[@]}")" || return 0
|
||||
name="${_ALIAS_NAMES[$((picked - 1))]}"
|
||||
fi
|
||||
|
||||
local idx
|
||||
idx="$(_alias_find "$name")"
|
||||
if [ "$idx" = "-1" ]; then
|
||||
err "Alias '$name' not found"
|
||||
fi
|
||||
|
||||
# Show current values
|
||||
{
|
||||
echo " Current values for '$name':"
|
||||
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
|
||||
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
|
||||
local cp="${_ALIAS_PROMPTS[$idx]}"
|
||||
[ -z "$cp" ] && cp="(default)"
|
||||
printf ' Prompt: %s\n' "$cp"
|
||||
echo >&2
|
||||
} >&2
|
||||
|
||||
local new_provider="${_ALIAS_PROVIDERS[$idx]}"
|
||||
local new_session="${_ALIAS_SESSIONS[$idx]}"
|
||||
local new_prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
local changed=0
|
||||
|
||||
# Edit provider
|
||||
step 1 3 "Provider" >&2
|
||||
local pidx
|
||||
pidx="$(_alias_provider_pick)" || return 0
|
||||
local providers=()
|
||||
for f in "$PROVIDER_DIR"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
providers+=("$(basename "$f" .sh)")
|
||||
done
|
||||
local picked_provider="${providers[$((pidx - 1))]}"
|
||||
if [ "$picked_provider" != "$new_provider" ]; then
|
||||
new_provider="$picked_provider"
|
||||
changed=1
|
||||
fi
|
||||
|
||||
# Edit session
|
||||
local tmp_session=""
|
||||
while true; do
|
||||
step 2 3 "Session Name" >&2
|
||||
tmp_session="$(menu_ask_value "Session name" "$new_session")" || return 0
|
||||
if [ -n "$tmp_session" ] && ! _alias_name_valid "$tmp_session"; then
|
||||
warn "Invalid session '$tmp_session' — use letters, digits, hyphens, underscores" >&2
|
||||
tmp_session=""; continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
[ -n "$tmp_session" ] && new_session="$tmp_session"
|
||||
[ "$new_session" != "${_ALIAS_SESSIONS[$idx]}" ] && changed=1
|
||||
|
||||
# Edit prompt
|
||||
local tmp_prompt=""
|
||||
while true; do
|
||||
step 3 3 "System Prompt" >&2
|
||||
local default_prompt="${_ALIAS_PROMPTS[$idx]}"
|
||||
[ ${#default_prompt} -gt 80 ] && default_prompt="${default_prompt:0:80}…"
|
||||
[ -z "$default_prompt" ] && default_prompt=""
|
||||
tmp_prompt="$(menu_ask_value "System prompt" "$default_prompt")" || return 0
|
||||
if [[ "$tmp_prompt" == *'|'* ]]; then
|
||||
warn "System prompt must not contain '|' characters" >&2
|
||||
tmp_prompt=""; continue
|
||||
fi
|
||||
if [ ${#tmp_prompt} -gt 500 ]; then
|
||||
warn "Prompt is ${#tmp_prompt} chars — consider keeping it concise" >&2
|
||||
fi
|
||||
break
|
||||
done
|
||||
# Keep full current if user pressed Enter (tmp_prompt = default_prompt value)
|
||||
if [ -n "$tmp_prompt" ]; then
|
||||
new_prompt="$tmp_prompt"
|
||||
fi
|
||||
[ "$new_prompt" != "${_ALIAS_PROMPTS[$idx]}" ] && changed=1
|
||||
|
||||
# No changes?
|
||||
if [ "$changed" -eq 0 ]; then
|
||||
log "No changes — nothing to save." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Show diff summary
|
||||
{
|
||||
echo "────────────────────────────────────────────"
|
||||
printf ' Save changes to '\''%s'\''?\n' "$name"
|
||||
local tag_p tag_s tag_pr
|
||||
[ "$new_provider" = "${_ALIAS_PROVIDERS[$idx]}" ] && tag_p="(unchanged)" || tag_p="(changed)"
|
||||
[ "$new_session" = "${_ALIAS_SESSIONS[$idx]}" ] && tag_s="(unchanged)" || tag_s="(changed)"
|
||||
[ "$new_prompt" = "${_ALIAS_PROMPTS[$idx]}" ] && tag_pr="(unchanged)" || tag_pr="(changed)"
|
||||
printf ' Provider: %-12s %s\n' "$new_provider" "$tag_p"
|
||||
printf ' Session: %-12s %s\n' "$new_session" "$tag_s"
|
||||
local dp="$new_prompt"
|
||||
[ -z "$dp" ] && dp="<built-in>"
|
||||
printf ' Prompt: %s %s\n' "${dp:0:40}" "$tag_pr"
|
||||
echo "────────────────────────────────────────────"
|
||||
} >&2
|
||||
if ! confirm "Save changes to '$name'?" y; then
|
||||
log "Discarded." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
_ALIAS_PROVIDERS[$idx]="$new_provider"
|
||||
_ALIAS_SESSIONS[$idx]="$new_session"
|
||||
_ALIAS_PROMPTS[$idx]="$new_prompt"
|
||||
_alias_save
|
||||
_alias_regen
|
||||
log "Alias '$name' updated." >&2
|
||||
}
|
||||
|
||||
# ── Interactive: remove ────────────────────────────────────────
|
||||
|
||||
_alias_remove() {
|
||||
local preset_name="${1:-}"
|
||||
_alias_load
|
||||
if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then
|
||||
warn "No aliases to remove" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name="$preset_name"
|
||||
if [ -z "$name" ]; then
|
||||
section "Remove AI Agent Alias" >&2
|
||||
local display_items=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
local p="${_ALIAS_PROMPTS[$i]}"
|
||||
if [ ${#p} -gt 30 ]; then
|
||||
p="${p:0:30}…"
|
||||
fi
|
||||
display_items+=("${_ALIAS_NAMES[$i]} [${_ALIAS_PROVIDERS[$i]}] ${p}")
|
||||
done
|
||||
local picked
|
||||
picked="$(menu_pick "Pick alias to remove" "${display_items[@]}")" || return 0
|
||||
name="${_ALIAS_NAMES[$((picked - 1))]}"
|
||||
fi
|
||||
|
||||
local idx
|
||||
idx="$(_alias_find "$name")"
|
||||
if [ "$idx" = "-1" ]; then
|
||||
err "Alias '$name' not found"
|
||||
fi
|
||||
|
||||
# Show alias detail
|
||||
{
|
||||
echo " Alias: $name"
|
||||
printf ' Provider: %s\n' "${_ALIAS_PROVIDERS[$idx]}"
|
||||
printf ' Session: %s\n' "${_ALIAS_SESSIONS[$idx]}"
|
||||
printf ' Prompt: %s\n' "${_ALIAS_PROMPTS[$idx]:-<built-in>}"
|
||||
echo >&2
|
||||
} >&2
|
||||
|
||||
if ! confirm "Remove alias '$name'? This cannot be undone." n; then
|
||||
log "Cancelled." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
_alias_load
|
||||
local new_names=() new_providers=() new_sessions=() new_prompts=() i
|
||||
for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do
|
||||
if [ "${_ALIAS_NAMES[$i]}" != "$name" ]; then
|
||||
new_names+=("${_ALIAS_NAMES[$i]}")
|
||||
new_providers+=("${_ALIAS_PROVIDERS[$i]}")
|
||||
new_sessions+=("${_ALIAS_SESSIONS[$i]}")
|
||||
new_prompts+=("${_ALIAS_PROMPTS[$i]}")
|
||||
fi
|
||||
done
|
||||
_ALIAS_NAMES=("${new_names[@]+"${new_names[@]}"}")
|
||||
_ALIAS_PROVIDERS=("${new_providers[@]+"${new_providers[@]}"}")
|
||||
_ALIAS_SESSIONS=("${new_sessions[@]+"${new_sessions[@]}"}")
|
||||
_ALIAS_PROMPTS=("${new_prompts[@]+"${new_prompts[@]}"}")
|
||||
_alias_save
|
||||
_alias_regen
|
||||
log "Alias '$name' removed." >&2
|
||||
}
|
||||
|
||||
# ── show <name> ───────────────────────────────────────────────
|
||||
|
||||
# (defined above as _alias_show)
|
||||
|
||||
# ── Usage ──────────────────────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos ai alias [subcommand] [args]
|
||||
|
||||
Manage named AI agent aliases — create, edit, remove, list, and show
|
||||
configured aliases. Each alias maps a name to a provider, session, and
|
||||
optional system prompt.
|
||||
|
||||
Subcommands:
|
||||
(no args) Interactive menu
|
||||
create [name] Create a new alias (interactive prompts for each field)
|
||||
edit [name] Edit an existing alias (interactive, Enter = keep)
|
||||
remove [name] Remove an alias (interactive, default = no)
|
||||
list List all aliases (non-interactive, machine-readable)
|
||||
show <name> Show one alias's details
|
||||
|
||||
Options:
|
||||
-h|--help Show this help.
|
||||
|
||||
Examples:
|
||||
pos ai alias # interactive menu
|
||||
pos ai alias list # show all aliases
|
||||
pos ai alias create # interactive create
|
||||
pos ai alias create mybot # create 'mybot' alias
|
||||
pos ai alias edit mybot # edit the 'mybot' alias
|
||||
pos ai alias remove mybot # remove 'mybot' (with confirm)
|
||||
pos ai alias show mybot # show alias details
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Main dispatch ──────────────────────────────────────────────
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
create) shift; _alias_create "${1:-}" ;;
|
||||
edit) shift; _alias_edit "${1:-}" ;;
|
||||
remove) shift; _alias_remove "${1:-}" ;;
|
||||
list) _alias_list ;;
|
||||
show)
|
||||
[ -n "${2:-}" ] || err "Usage: pos ai alias show <name>"
|
||||
_alias_show "$2"
|
||||
;;
|
||||
"") _alias_menu ;;
|
||||
*) err "Unknown subcommand '$1' (use -h for help)" ;;
|
||||
esac
|
||||
Executable
+415
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: system uninstall — Remove pos toolkit binaries, services, shell integration, config, and data
|
||||
# POS_FLAGS: --yes --config --data
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: pos system uninstall [--yes] [--config] [--data]
|
||||
|
||||
Remove the pos toolkit from this machine. Scans for installed components
|
||||
and removes them interactively (or non-interactively with --yes).
|
||||
|
||||
Tiers:
|
||||
Tier 1 (always): binaries, entertainment plugins, systemd services, shell integration
|
||||
Tier 2 (--config): config files in ~/.config/linux_post_install/
|
||||
Tier 3 (--data): session/log/capture data in ~/.local/share/linux_post_install/
|
||||
|
||||
Flags:
|
||||
--yes Skip confirmation prompts (removes defaults only; combine with --config/--data for more)
|
||||
--config Include config files (Tier 2) in removal
|
||||
--data Include session/log data (Tier 3) in removal
|
||||
|
||||
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
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Parse flags ─────────────────────────────────────────────────
|
||||
YES_MODE=0
|
||||
DEL_CONFIG=0
|
||||
DEL_DATA=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--yes) YES_MODE=1; shift ;;
|
||||
--config) DEL_CONFIG=1; shift ;;
|
||||
--data) DEL_DATA=1; shift ;;
|
||||
-*) err "Unknown option '$1'" ;;
|
||||
*) err "Unexpected argument: $1 (no subcommands)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Scan functions ──────────────────────────────────────────────
|
||||
scan_tier1() {
|
||||
local found=()
|
||||
|
||||
# ── Binaries in /usr/local/bin ──
|
||||
# Main dispatcher
|
||||
[ -f /usr/local/bin/pos ] && found+=("/usr/local/bin/pos")
|
||||
|
||||
# All pos-* tools (sorted for deterministic display)
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
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
|
||||
[ -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
|
||||
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)
|
||||
|
||||
# Legacy forwarders
|
||||
for f in wr-* mp3 mp4 vbox ssh-load-all; do
|
||||
if compgen -G "/usr/local/bin/$f" >/dev/null 2>&1; then
|
||||
while IFS= read -r lf; do
|
||||
found+=("$lf")
|
||||
done < <(compgen -G "/usr/local/bin/$f" 2>/dev/null)
|
||||
fi
|
||||
done
|
||||
|
||||
# Prebuilt binaries (hotspot)
|
||||
for f in wihotspot wihotspot-gui create_ap; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# Feature scripts installed by install.sh
|
||||
for f in autostart.sh usb-automount.sh; do
|
||||
[ -f "/usr/local/bin/$f" ] && found+=("/usr/local/bin/$f")
|
||||
done
|
||||
|
||||
# User-local binaries
|
||||
[ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && found+=("$HOME/.local/bin/pos-ai-hook.sh")
|
||||
|
||||
# Completion file
|
||||
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && found+=("/usr/local/share/bash-completion/completions/pos.bash")
|
||||
|
||||
# ~/.bash_completion entries
|
||||
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)
|
||||
fi
|
||||
|
||||
# ── Systemd services ──
|
||||
for svc in autostart.service ssh-agent.service usb-automount.service; do
|
||||
if systemctl is-enabled "$svc" &>/dev/null 2>&1; then
|
||||
found+=("service: $svc")
|
||||
elif [ -f "/etc/systemd/system/$svc" ]; then
|
||||
found+=("service: $svc")
|
||||
fi
|
||||
done
|
||||
# Catch any other linux_post_install-related services
|
||||
while IFS= read -r line; do
|
||||
local svc_name
|
||||
svc_name=$(echo "$line" | awk '{print $1}')
|
||||
# Skip already-listed services
|
||||
local already=0
|
||||
for listed in autostart.service ssh-agent.service usb-automount.service; do
|
||||
[ "$svc_name" = "$listed" ] && already=1 && break
|
||||
done
|
||||
[ "$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) ──
|
||||
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)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
scan_tier2() {
|
||||
local found=()
|
||||
local cfg="$HOME/.config/linux_post_install"
|
||||
if [ -d "$cfg" ]; then
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null | sort)
|
||||
fi
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
scan_tier3() {
|
||||
local found=()
|
||||
local data="$HOME/.local/share/linux_post_install"
|
||||
if [ -d "$data" ]; then
|
||||
while IFS= read -r f; do
|
||||
found+=("$f")
|
||||
done < <(find "$data" -maxdepth 2 \( -type f -o -type d \) 2>/dev/null | sort)
|
||||
fi
|
||||
printf '%s\n' "${found[@]}"
|
||||
}
|
||||
|
||||
# ── Display function ────────────────────────────────────────────
|
||||
display_plan() {
|
||||
local -a t1=() t2=() t3=()
|
||||
# Collect non-empty entries from scan output
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t1+=("$line")
|
||||
done <<< "${1:-}"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t2+=("$line")
|
||||
done <<< "${2:-}"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && t3+=("$line")
|
||||
done <<< "${3:-}"
|
||||
|
||||
echo
|
||||
echo "${BOLD}pos uninstall — what will be removed:${RESET}"
|
||||
echo
|
||||
|
||||
local n=1
|
||||
|
||||
if [ "${#t1[@]}" -gt 0 ]; then
|
||||
echo "${CYAN}Tier 1 (always):${RESET}"
|
||||
for item in "${t1[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${CYAN}Tier 1 (always):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ "${#t2[@]}" -gt 0 ]; then
|
||||
echo "${YELLOW}Tier 2 (--config to include):${RESET}"
|
||||
for item in "${t2[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${YELLOW}Tier 2 (--config to include):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ "${#t3[@]}" -gt 0 ]; then
|
||||
echo "${YELLOW}Tier 3 (--data to include):${RESET}"
|
||||
for item in "${t3[@]}"; do
|
||||
printf " %3d) %s\n" "$n" "$item"
|
||||
n=$((n + 1))
|
||||
done
|
||||
else
|
||||
echo "${YELLOW}Tier 3 (--data to include):${RESET} (nothing found)"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# ── Removal functions ───────────────────────────────────────────
|
||||
remove_tier1() {
|
||||
local count=0
|
||||
|
||||
# ── Binaries ──
|
||||
# Main dispatcher
|
||||
[ -f /usr/local/bin/pos ] && { rm -f /usr/local/bin/pos && count=$((count+1)); }
|
||||
|
||||
# All pos-* tools
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
[ -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)); }
|
||||
done
|
||||
|
||||
# AI providers directory
|
||||
if [ -d /usr/local/bin/ai-providers ]; then
|
||||
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
|
||||
|
||||
# Legacy forwarders
|
||||
for pat in 'wr-*' mp3 mp4 vbox ssh-load-all; do
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done < <(compgen -G "/usr/local/bin/$pat" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# Prebuilt binaries
|
||||
for f in /usr/local/bin/wihotspot /usr/local/bin/wihotspot-gui /usr/local/bin/create_ap; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# Feature scripts
|
||||
for f in /usr/local/bin/autostart.sh /usr/local/bin/usb-automount.sh; do
|
||||
[ -f "$f" ] && { rm -f "$f" && count=$((count+1)); }
|
||||
done
|
||||
|
||||
# User-local binaries
|
||||
[ -f "$HOME/.local/bin/pos-ai-hook.sh" ] && { rm -f "$HOME/.local/bin/pos-ai-hook.sh" && count=$((count+1)); }
|
||||
|
||||
# Completion file
|
||||
[ -f /usr/local/share/bash-completion/completions/pos.bash ] && { rm -f /usr/local/share/bash-completion/completions/pos.bash && count=$((count+1)); }
|
||||
|
||||
# ── Systemd services ──
|
||||
for svc in autostart.service ssh-agent.service usb-automount.service; do
|
||||
if systemctl is-enabled "$svc" &>/dev/null 2>&1; then
|
||||
systemctl disable --now "$svc" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/$svc"
|
||||
count=$((count+1))
|
||||
elif [ -f "/etc/systemd/system/$svc" ]; then
|
||||
rm -f "/etc/systemd/system/$svc"
|
||||
count=$((count+1))
|
||||
fi
|
||||
done
|
||||
# Additional linux_post_install services
|
||||
while IFS= read -r svc_file; do
|
||||
local svc_name
|
||||
svc_name=$(basename "$svc_file" .service)
|
||||
# Skip already-handled services
|
||||
local already=0
|
||||
for listed in autostart ssh-agent usb-automount; do
|
||||
[ "$svc_name" = "$listed" ] && already=1 && break
|
||||
done
|
||||
if [ "$already" -eq 0 ]; then
|
||||
systemctl disable --now "$svc_name.service" 2>/dev/null || true
|
||||
rm -f "$svc_file"
|
||||
count=$((count+1))
|
||||
fi
|
||||
done < <(find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*' 2>/dev/null || true)
|
||||
|
||||
# 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))
|
||||
fi
|
||||
|
||||
# ── Shell completion (~/.bash_completion) ──
|
||||
if [ -f "$HOME/.bash_completion" ]; then
|
||||
local before
|
||||
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))
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 1)"
|
||||
}
|
||||
|
||||
remove_tier2() {
|
||||
local cfg="$HOME/.config/linux_post_install"
|
||||
local count=0
|
||||
|
||||
if [ -d "$cfg" ]; then
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
rm -f "$f" && count=$((count+1))
|
||||
done < <(find "$cfg" -maxdepth 2 -type f 2>/dev/null)
|
||||
|
||||
# Remove empty directory tree
|
||||
rmdir "$cfg/schedule.d" 2>/dev/null || true
|
||||
rmdir "$cfg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 2)"
|
||||
}
|
||||
|
||||
remove_tier3() {
|
||||
local data="$HOME/.local/share/linux_post_install"
|
||||
local count=0
|
||||
|
||||
if [ -d "$data" ]; then
|
||||
# Remove files first
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
rm -f "$f" && count=$((count+1))
|
||||
done < <(find "$data" -maxdepth 2 -type f 2>/dev/null)
|
||||
|
||||
# Remove directories bottom-up
|
||||
while IFS= read -r d; do
|
||||
rmdir "$d" 2>/dev/null && count=$((count+1)) || true
|
||||
done < <(find "$data" -mindepth 1 -depth -type d 2>/dev/null)
|
||||
|
||||
# Remove top-level directory if empty
|
||||
rmdir "$data" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
ok "Removed $count items (tier 3)"
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────
|
||||
main() {
|
||||
section "pos system uninstall"
|
||||
|
||||
# Scan all tiers
|
||||
local scan1 scan2 scan3
|
||||
scan1="$(scan_tier1)"
|
||||
scan2="$(scan_tier2)"
|
||||
scan3="$(scan_tier3)"
|
||||
|
||||
# Display the plan
|
||||
display_plan "$scan1" "$scan2" "$scan3"
|
||||
|
||||
# Nothing to do at all?
|
||||
if [ -z "$scan1" ] && [ -z "$scan2" ] && [ -z "$scan3" ]; then
|
||||
ok "Nothing to remove — pos toolkit does not appear to be installed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Tier 1: always remove (confirm unless --yes) ──
|
||||
if [ -n "$scan1" ]; then
|
||||
if [ "$YES_MODE" -eq 1 ]; then
|
||||
log "Removing tier 1 items (--yes)..."
|
||||
else
|
||||
confirm "Remove tier 1 items (binaries, services, shell integration)?" || return 0
|
||||
fi
|
||||
remove_tier1
|
||||
fi
|
||||
|
||||
# ── Tier 2: config files ──
|
||||
if [ -n "$scan2" ]; then
|
||||
if [ "$DEL_CONFIG" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then
|
||||
confirm "Also remove config files (tier 2)?" && DEL_CONFIG=1 || true
|
||||
fi
|
||||
[ "$DEL_CONFIG" -eq 1 ] && remove_tier2
|
||||
fi
|
||||
|
||||
# ── Tier 3: session/log data ──
|
||||
if [ -n "$scan3" ]; then
|
||||
if [ "$DEL_DATA" -eq 0 ] && [ "$YES_MODE" -eq 0 ]; then
|
||||
confirm "Also remove session/log data (tier 3)?" && DEL_DATA=1 || true
|
||||
fi
|
||||
[ "$DEL_DATA" -eq 1 ] && remove_tier3
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Uninstall complete."
|
||||
echo " The git repo was NOT removed — delete it manually if desired."
|
||||
echo " Restart your shell or run: source ~/.bashrc"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -19,11 +19,13 @@ _pos_flags[network-hotspot]="--foreground"
|
||||
_pos_flags[share-usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version menu"
|
||||
_pos_flags[system-backup]="--service --no-encrypt"
|
||||
_pos_flags[system-schedule]="--dry-run"
|
||||
_pos_flags[system-uninstall]="--yes --config --data"
|
||||
_pos_flags[ai]="--provider --model --session --system --full --last"
|
||||
_pos_flags[tree]="--depth"
|
||||
# GEN:END posflags
|
||||
# GEN:START possubcmds
|
||||
declare -A _pos_subcmds
|
||||
_pos_subcmds[ai-alias]="create edit remove list show"
|
||||
_pos_subcmds[ai-gemini]="ask chat models sessions capture"
|
||||
_pos_subcmds[ai-openrouter]="ask chat sessions capture"
|
||||
_pos_subcmds[communication-matrix-sender]="send test login"
|
||||
@@ -40,7 +42,7 @@ _pos_subcmds[share-smb-client]="mount unmount list persist unpersist menu"
|
||||
_pos_subcmds[share-smb-server]="status share unshare list adduser deluser reload enable disable menu"
|
||||
_pos_subcmds[system-backup]="menu"
|
||||
_pos_subcmds[system-schedule]="run list config enable disable status migrate menu"
|
||||
_pos_subcmds[ai]="ask chat sessions capture models providers gemini openrouter"
|
||||
_pos_subcmds[ai]="ask chat sessions capture models providers alias gemini openrouter"
|
||||
# GEN:END possubcmds
|
||||
# GEN:START posconfigscopes
|
||||
declare -a _pos_config_scopes=(ai compose entertainment matrix notify scrcpy system telegram ytsync)
|
||||
|
||||
+12
-9
@@ -2,14 +2,17 @@
|
||||
# Copied to ~/.config/linux_post_install/ai.env on install (no clobber).
|
||||
# Edit with: pos config ai
|
||||
#
|
||||
# Syntax:
|
||||
# Provider selection:
|
||||
# AI_PROVIDER=gemini # provider: gemini|openrouter (default gemini)
|
||||
# AI_API_KEY=<key> # API key for the active provider (secret)
|
||||
# AI_MODEL=<model> # model id (default depends on provider)
|
||||
# AI_SYSTEM_PROMPT=<prompt> # custom system prompt (overrides built-in; empty to reset)
|
||||
#
|
||||
# Legacy provider-specific keys (still supported as fallback):
|
||||
# AI_GEMINI_API_KEY=<key> # Gemini API key (used when AI_API_KEY is empty)
|
||||
# AI_GEMINI_MODEL=<model> # Gemini model id (used when AI_MODEL is empty)
|
||||
# OPENROUTER_API_KEY=<key> # OpenRouter API key (used when AI_API_KEY is empty)
|
||||
# OPENROUTER_MODEL=<model> # OpenRouter model id (used when AI_MODEL is empty)
|
||||
# API keys (each provider needs its own):
|
||||
# AI_GEMINI_API_KEY=<key> # Gemini API key from aistudio.google.com
|
||||
# OPENROUTER_API_KEY=<key> # OpenRouter API key from openrouter.ai
|
||||
#
|
||||
# Model overrides (optional, defaults per provider):
|
||||
# AI_GEMINI_MODEL=<model> # Gemini model id (default: gemini-2.5-flash)
|
||||
# OPENROUTER_MODEL=<model> # OpenRouter model id (default: openrouter/auto)
|
||||
# AI_MODEL=<model> # Override for all providers (takes priority)
|
||||
#
|
||||
# System prompt:
|
||||
# AI_SYSTEM_PROMPT=<prompt> # Custom system prompt (overrides built-in; empty to reset)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
# Provider-specific: API call, auth, response parsing, models list
|
||||
# Part of the R8 provider-agnostic architecture (lib/ai-providers/).
|
||||
|
||||
# Provider-specific config variables (auto-discovered by pos config ai):
|
||||
# PROVIDER_CONFIG: AI_GEMINI_API_KEY=secret:Gemini API key from aistudio.google.com
|
||||
# PROVIDER_CONFIG: AI_GEMINI_MODEL=:Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
provider_name() { printf 'Google Gemini'; }
|
||||
provider_default_model() { printf 'gemini-2.5-flash'; }
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
# Provider-specific: API call, auth, response parsing, models list
|
||||
# Part of the R8 provider-agnostic architecture (lib/ai-providers/).
|
||||
|
||||
# Provider-specific config variables (auto-discovered by pos config ai):
|
||||
# PROVIDER_CONFIG: OPENROUTER_API_KEY=secret:OpenRouter API key from openrouter.ai
|
||||
# PROVIDER_CONFIG: OPENROUTER_MODEL=:OpenRouter model id (default: openrouter/auto)
|
||||
|
||||
provider_name() { printf 'OpenRouter'; }
|
||||
provider_default_model() { printf 'openrouter/auto'; }
|
||||
|
||||
|
||||
+38
-1
@@ -136,6 +136,42 @@ _cfg_plugin_keys() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# "*providers" expansion: keys declared by the installed AI provider
|
||||
# adapters' "# PROVIDER_CONFIG:" headers (lib/ai-providers/*.sh).
|
||||
_cfg_provider_keys() {
|
||||
local pdir line key desc flags
|
||||
# Repo layout: lib/config-ui.sh → ../lib/ai-providers/
|
||||
# Installed layout: /usr/local/bin/config-ui.sh → ./ai-providers/
|
||||
pdir=""
|
||||
for candidate in \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/../lib/ai-providers" \
|
||||
"$(dirname "${BASH_SOURCE[0]}")/ai-providers"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
pdir="$(cd "$candidate" 2>/dev/null && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$pdir" ] || return 0
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
# Format: KEY=flags:description (same as POS_CONFIG key fields)
|
||||
key="${line%%=*}"
|
||||
[ -n "$key" ] || continue
|
||||
[ -n "${_cfg_seen[$key]:-}" ] && continue
|
||||
_cfg_seen[$key]=1
|
||||
# Parse flags and description from the rest
|
||||
local rest="${line#*=}" flags="" desc=""
|
||||
if [[ "$rest" == *":"* ]]; then
|
||||
flags="${rest%%:*}"
|
||||
desc="${rest#*:}"
|
||||
else
|
||||
flags="$rest"
|
||||
fi
|
||||
printf '%s|%s|%s|\n' "$key" "$flags" "$desc"
|
||||
done < <(grep '^# PROVIDER_CONFIG:' "$pdir"/*.sh 2>/dev/null | sed 's/^.*# PROVIDER_CONFIG:[[:space:]]*//' || true)
|
||||
return 0
|
||||
}
|
||||
|
||||
# Declared keys for a scope: "KEY|flags|description" lines, deduped.
|
||||
cfg_scope_keys() {
|
||||
local scope="$1" dir line s keystring field
|
||||
@@ -155,7 +191,8 @@ cfg_scope_keys() {
|
||||
if [ -n "$field" ]; then
|
||||
if [[ "$field" == "*"* ]]; then
|
||||
case "$field" in
|
||||
*plugins*) _cfg_plugin_keys ;;
|
||||
*plugins*) _cfg_plugin_keys ;;
|
||||
*providers*) _cfg_provider_keys ;;
|
||||
esac
|
||||
else
|
||||
_cfg_key_line "$field"
|
||||
|
||||
@@ -39,6 +39,7 @@ PACKAGES=(
|
||||
python3 python3-pip rclone
|
||||
ffmpeg
|
||||
libqrencode4 libgtk-3-0 adb
|
||||
xdotool xclip
|
||||
)
|
||||
|
||||
spawn "apt update" sudo apt update
|
||||
|
||||
Reference in New Issue
Block a user