Merge branch 'main' of https://gitea.skink-platy.ts.net/admin/Linux_post_install
.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
name: gates
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
consistency-and-conventions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run the repo gates (gen drift, check, lint)
|
||||
id: gates
|
||||
run: |
|
||||
set -e
|
||||
make gen
|
||||
git diff --exit-code
|
||||
make check
|
||||
make lint
|
||||
- name: Report gate result as a git tag (ci-ok/<sha> / ci-fail/<sha>)
|
||||
if: always() && github.event_name == 'push'
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
if [ "${{ steps.gates.conclusion }}" = "success" ]; then
|
||||
tag="ci-ok/$GITHUB_SHA"
|
||||
else
|
||||
tag="ci-fail/$GITHUB_SHA"
|
||||
fi
|
||||
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
|
||||
echo "tag already present: $tag"
|
||||
else
|
||||
git tag "$tag" "$GITHUB_SHA"
|
||||
git push "http://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git" "refs/tags/$tag"
|
||||
fi
|
||||
@@ -16,7 +16,7 @@ CRITICAL: real guidance lives in DOC/. When you encounter a reference below, use
|
||||
|
||||
- **Tool model:** `bin/pos-<category>-<command>`, or **category-less** `bin/pos-<cmd>` for dispatcher/dev-level commands that fit no category (`pos-config`, `pos-tree`) — they dispatch like any tool and show with an empty category in the generated tables. `bin/pos` dispatches by longest-prefix arg matching. New tools are auto-discovered but must be executable (`100755`) and carry a `# POS: <cat> <cmd> — <desc>` header right after the shebang; `make gen` only uses the text after the first `— ` (the leading words are convention-only), so keep the one-line description concise. `# POS_FLAGS:` / `# POS_SUBCMDS:` / `# POS_CONFIG:` headers feed tab-completion and the `pos config` scope registry. A missing `# POS:` header hard-fails `make gen`. Legacy `bin/wr-*`, `mp3`, `mp4`, `vbox`, `ssh-load-all` are thin forwarders to `pos` — keep them that way.
|
||||
- **Categories:** `ai`, `communication`, `docker`, `entertainment`, `media`, `network`, `share` (usb, nfs, smb), `ssh`, `system`, plus category-less `config`/`tree`. `pos tree` (bin/pos-tree) is the authoritative structure — it derives the hierarchy from `bin/pos-*` filenames + `# POS:`/`# POS_SUBCMDS:` headers.
|
||||
- **Generated code:** blocks between `GEN:START`/`GEN:END` markers in `DOC/AGENT_Context_Project.md` (tree, dispatch, selfcontained, filetable, docmap) and `completions/pos.bash` (flags, subcmds, config scopes) are `make gen` output — never hand-edit them. After touching `bin/pos-*`, run `make gen` then `make check` (bash -n + exec-bit check + doc-sync gate + dispatch smoke; definition of done). Hand-maintained, not gen-checked: `DOC/POS.md`, the line-count rows above the filetable marker in `DOC/AGENT_Context_Project.md` (the non-`pos-*` files — `install.sh`, `preinstall.sh`, `postinstall.sh`, `lib/*`, `features/*`; bump a row's count only when that file's length changes), `bin/pos` usage() EXAMPLES, root README. There is no CI — `make check` (`scripts/check-sync.sh`) is the only gate.
|
||||
- **Generated code:** blocks between `GEN:START`/`GEN:END` markers in `DOC/AGENT_Context_Project.md` (tree, dispatch, selfcontained, filetable, docmap) and `completions/pos.bash` (flags, subcmds, config scopes) are `make gen` output — never hand-edit them. Generators must be **byte-order deterministic** (sort with `LC_ALL=C`, as `scripts/gen-docs.sh` does) or CI's `git diff --exit-code` trips on a locale that collates differently. After touching `bin/pos-*`, run `make gen`, then `make check`, then `make lint` (definition of done: check green + lint ends `0 FAIL, 0 WARN`). `make check` (`scripts/check-sync.sh`) is the self-consistency gate — bash -n + exec-bit check + doc-sync + dispatch smoke; `make lint` (`scripts/lint-conventions.sh`) is the convention gate — it enforces every rule in this file (shebang/strict-mode, exec bits, `# POS:` headers, `-h|--help` present and after deps guards, stdin-readers in `INTERACTIVE_CMDS`, POS.md coverage, plugin/app/unit/wrapper/secrets/env-seam classes — see `DOC/DEV.md → Convention Lint Gate`). Hand-maintained, not gen-checked: `DOC/POS.md`, the line-count rows above the filetable marker in `DOC/AGENT_Context_Project.md` (the non-`pos-*` files — `install.sh`, `preinstall.sh`, `postinstall.sh`, `lib/*`, `features/*`; bump a row's count only when that file's length changes), `bin/pos` usage() EXAMPLES, root README. CI: `.gitea/workflows/lint.yml` runs `make gen` + `git diff --exit-code` + `make check` + `make lint` on every push/PR via the **live** Gitea act_runner (`linux-post-install`, registered on the Gitea host) — a red run means gen drift or a gate failure and is a merge-blocker; still run the gates locally too (lint isn't in the pre-commit hook).
|
||||
- **Stdin gotcha:** any tool that reads stdin must be added to `INTERACTIVE_CMDS` in `bin/pos` — otherwise the logging `tee` pipe hangs on (or swallows) the prompt.
|
||||
- **Deps:** apt packages → `PACKAGES` array in `preinstall.sh`; non-apt/manual installers (e.g. `usbsrv`) → `command -v <bin> || err "…"` guard inside the tool, never in PACKAGES.
|
||||
- **Secrets:** never commit keys/tokens. `config/authorized_keys` and `config/rclone.conf` are gitignored; runtime tool config is `~/.config/linux_post_install/<tool>.env` (chmod 600, env-var precedence). Mask tokens in `config` output.
|
||||
|
||||
+61
-81
@@ -14,78 +14,14 @@ summary (newest last).
|
||||
- When a task is completed: move it from Now/Next into **Done** (dated one-line)
|
||||
in the same commit that finishes the work.
|
||||
|
||||
## Done
|
||||
|
||||
- **2026-08-13** — Fix scrcpy apt install on the live box: preinstall `apt install` failed with `Unable to locate package scrcpy` (Debian/Ubuntu need contrib/universe for `scrcpy`, and the apt build is older anyway). Removed `scrcpy` from `preinstall.sh` PACKAGES (kept `adb`); `scrcpy` now installs via the existing optional app `apps/media/scrcpy.sh` (GitHub latest, bundles adb) — docs (POS.md, howto/communication.md) and the tool's deps-guard error reworded to lead with that path. Re-verified: `bash -n`, stub suite 21/21, `make gen && make check` green.
|
||||
|
||||
- **2026-08-13** — `pos communication scrcpy` (`bin/pos-communication-scrcpy`): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: bare `scrcpy` (mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints), `devices` (`adb devices -l`), `record [file] [--headless]` (default `$SCRCPY_RECORD_DIR/<device>_<date>.mp4`, `--headless` = `--no-playback` for headless servers), `tcpip [port]` (USB→wireless switch + prints `connect` with the auto-detected device IP), `connect <ip[:port]>` (adb connect + mirror `-s`), `push` (default `/sdcard/Download` = scrcpy's own default), `pull`, `screenshot` (`adb exec-out screencap -p` → PNG in RECORD_DIR), `info` (model/android/sdk/serial via getprop). Config scope `scrcpy` (`~/.config/linux_post_install/scrcpy.env`, `pos config scrcpy`): `SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS`, env-var precedence. Deps `scrcpy` + `adb` added to preinstall PACKAGES; docs note the apt build is older and point to the existing `apps/media/scrcpy.sh` app installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions: `# POS:`/`# POS_SUBCMDS:`/`# POS_CONFIG:` headers, deps guards before `-h|--help`, no stdin → no INTERACTIVE_CMDS. Verified: `bash -n`, stub-PATH suite `/tmp/opencode/scrcpy-run-test.sh` 21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes), `make gen && make check` green, dispatch via `pos communication scrcpy --help`. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable.
|
||||
|
||||
- **2026-08-13** — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to `notify_send` (platform follows `NOTIFY_PLATFORM`, default Telegram) via `lib/notify.sh` sourced by `bin/pos-entertainment-send`; a **last-run state** is recorded per plugin (`~/.local/share/linux_post_install/entertainment/last/<plugin>` — rc + timestamp) on every non-`--print` run and shown by `pos entertainment status`, which also lists installed-but-not-enabled plugins; a send that fails **while fired by a timer** (gated on `$INVOCATION_ID`) additionally notifies the configured platforms. New **message-safe plugin lib** `lib/entertainment-plugin-lib.sh` (defines only `plugin_*`, never writes stdout — the stdout contract stays "message only"): `plugin_load_config` (entertainment.env + env precedence), `plugin_have`, `plugin_require`, `plugin_err`, `plugin_http_json <url> [--key <jq>] [-H <header>]` (curl `--max-time 20 --retry 2`); `weather`/`joke`/`gold` refactored onto it. `pos entertainment config` gains `get|unset|ls|edit` (edit via the shared `pos config` UI — added to `INTERACTIVE_CMDS`). **Tier 2**: new shared lib `lib/user-timers-lib.sh` (only `ut_*`: `ut_interval_to_oncalendar`, `ut_interval_label`, `ut_unit_name`, `ut_write_unit_pair` incl. `TimeoutStopSec=5s` + `Persistent` + network-online deps, `ut_ensure_linger`, `USER_SYSTEMD_DIR`) dedupes the systemd user-timer machinery between `lib/entertainment-lib.sh` and `lib/scheduler-lib.sh` (the latter's `sched_*` duplicates deleted; both source it; collides-with-nothing). `install.sh` Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: `bash -n` everywhere; smoke-tested in an isolated `HOME=/tmp/enttest` (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); `make gen && make check` green.
|
||||
|
||||
- **2026-08-13** — Fast pos-unit shutdown: every systemd unit a pos tool writes (or `systemd/` ships) now sets `TimeoutStopSec=5s` (+ `KillMode=control-group` on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: `pos-communication-telegram-listener`, `pos-communication-matrix-listener` (also gained a `trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT` in `run_daemon` so stop returns sub-second), `pos-network-download` (aria2 + retry-healer units), `lib/scheduler-lib.sh` `sched_write_units`, `lib/entertainment-lib.sh` `write_units`, and `systemd/{ssh-agent,autostart,usb-automount}.service`. Legacy-unit cleanup: the repo no longer ships `pos-health.{service,timer}` / `pos-entertainment.service` (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the `pos-health.service` section + gating special-case, added a new **Stop behavior** section), POS.md, HOWTO.md, howto/system.md (now documents the `pos system schedule` job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a **Systemd units** convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: `bash -n` on all edited scripts; `make gen && make check` green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via `pos network download start`, `pos communication telegram listener --enable`, `pos system schedule enable <job>`, `pos entertainment enable <plugin>`, then `sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload`.
|
||||
|
||||
- **2026-08-13** — Bootstrap output transparency (`install.sh` / `preinstall.sh` / `postinstall.sh`): removed the redundant `apt update` (preinstall.sh owns it — install.sh previously ran it twice, showing two identical `OK apt update` lines); Phase 2 now names what it installs — libs line (`libs -> /usr/local/bin (644): common.sh flags.sh …`), plugin names in the count line, x64_bin names, per-feature `feature installed/overwritten` + `feature flag set` logs with a `N features installed: …` summary — and the misleading `"47 scripts + libs"` label is fixed to `47 scripts + 6 libs` (the 6 libs were outside the counter); preinstall prints `Installing N packages (apt install -y):` with the 40-name list wrapped at 80 cols; postinstall now logs silent skips — `config/authorized_keys is empty — nothing to add` (empty file previously looped zero times with no message), `schedule.d already exists, keeping it` (restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-service `service enabled: <name>` line. No output-layer changes (no `--verbose`, no log file — decided scope). Verified: `bash -n` + `--dry-run` smokes of phases 1/2/3 showing every new line (learned: `install.sh:19` hardcodes `export DRY_RUN=0`, so an env `DRY_RUN=1` is ignored — the flag `--dry-run` is required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168), `make gen && make check` green. usb-automount left live (user choice).
|
||||
|
||||
- **2026-08-13** — `usb-automount` feature, integrated exactly like `autostart`: `features/usb-automount.sh` (root-guard re-exec via sudo; first-root-run self-install of udev rule `/etc/udev/rules.d/99-usb-automount.rules` — `ACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service"` — + `udevadm control --reload` + `trigger --subsystem-match=block`; an existing/edited rule is never overwritten; scans `lsblk -J` for unmounted removable partitions/raw whole-disk filesystems, mounts each at `/media/<label>` — vfat/exfat/ntfs world-writable via `-o umask=000`, fallback plain mount, label-collision bump `-2`/`-3`, no label → `usb-<name>`, logs `${HOME:-/root}/.usb-automount.log`) + `systemd/usb-automount.service` (`Type=oneshot`, `WantedBy=multi-user.target` — boot + hotplug + manual `systemctl start usb-automount`), gated in postinstall.sh's systemd loop exactly like autostart (`flag_is_set usb-automount` → skip with hint). Purpose: a plugged-in stick is auto-mounted world-writable, ready for `pos system backup`'s post-verify USB copy. Docs: SYSTEMD.md (service section + gating code block), SCRIPTS.md (feature section + systemd bullet + TOC), AGENT_Context tree + filetable rows (postinstall.sh count corrected 152→163 — it was already 6 lines stale), README index rows. Verified with a stub suite (`/tmp/opencode/usb-automount-test` — lsblk JSON fixtures, mount/mountpoint/udevadm/sudo stubs, `MOUNT_BASE`/`UDEV_RULES_DIR` seams, HOME isolation): 47/47 green. `make gen && make check` green. Gotcha learned: `${VAR:-{...}}` with a `{` inside the parameter-expansion default mis-parses in bash (emits a stray `}` — printf of a multi-line value showed `}}`); avoid braces in `:-` defaults.
|
||||
- **2026-08-13** — `pos system backup` copies the finished backup to a USB stick. Detection runs **after** the archive verifies (so a stick plugged in while the backup ran is found; if none is mounted, one re-scan prompt before giving up — `s` skips, EOF from cron skips silently, rc stays 0). Single stick → y/N confirm; several → numbered pick (0 = skip). Copy lands in `<usb>/backups/` (mkdir -p; `chmod 600` best-effort — vfat chmod failures warn, never fail), and the transfer is proven 100% by **sha256 source-vs-copy** before any success is announced: mismatch → warn with both hashes + `notify_send "USB copy FAILED…"` + rc=1 (the ERR trap is re-armed mid-script so a USB-phase failure no longer notifies "Backup FAILED"). Detection: `lsblk -J` → recursive jq filter (rm==true && mounted && type part|disk, space-safe via JSON) or pinned `BACKUP_USB_ROOT` seam (= `<root>/backups/`, skips detection — also the test seam). Docs: usage() Environment, POS.md backup row, howto/system.md (USB section + env table + mismatch troubleshooting), DEV.md system.env list. Verified with a stub suite (`/tmp/opencode/backup-test` — sudo/gpg/lsblk/sender stubs, HOME isolation, per-test lsblk JSON fixtures, corrupting-cp + vfat-chmod override stubs): 40/40 green (skip s/EOF, seam y/n, detect single, multi pick 2/0, re-scan after replug, corrupt copy rc=1 + honest notify, vfat tolerance). `make gen && make check` green.
|
||||
- **2026-08-13** — `pos share smb-server share` now guards the two common `NT_STATUS_ACCESS_DENIED` causes at share time (warnings only): `--users` entries missing from the Samba passdb (`pdbedit -L`, cut to user column, `grep -qxF` per user — pointer to `pos share smb-server adduser <user>`), and ancestors of the share path lacking `other:+x` traversal (sticky dirs like `/tmp` count as traversable via the `t` slot; fix hint `chmod o+x <dir>`). Both wired into the `share` case after `require_root_dir`; howto/share.md SMB section + troubleshooting updated. Rooted in `reports/bug-report-smb-server-access-denied.md` (committed as the spec). Verified with a stub-PATH suite (`/tmp/opencode/smb-test` — pdbedit/systemctl/smbcontrol/testparm/smbpasswd stubs, `SMB_CONF` seam): 16/16 green.
|
||||
- **2026-08-13** — Docs hardening from the schedule-session review (sole-developer call: terse, session-learned). DEV.md §7 env-seam registry now lists `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh`) + the scheduler's `SCHEDULE_*` seams, and documents the missing-`:-`-guard gotcha (a `VAR="${XDG…:-…}"` without leading `VAR:-` overrides the seam — stub runs then silently write to the real `$HOME`; fix: `USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-…}"`). New-tool test checklist gains an env-seam review step (grep for unguarded config writes + prove with `VAR=/tmp/x`). §7 notes stub harnesses are throwaway by design — build in `/tmp/opencode/<tool>-test/`, leave there, keep only the pattern. howto/schedule.md documents that `migrate` copies the rule LHS **verbatim** as `COMMAND` (old tool never had `disk root`/`loadavg` shorthands — rewrite those jobs with real commands). `make check` green.
|
||||
- **2026-08-12** — `pos system event-trigger` (eventer) generalized into `pos system schedule` — the scheduler replaces the single-timer threshold monitor with **per-job systemd user timers** (`pos-schedule-<name>.{timer,service}`, `Persistent`, ExecStart `run <name>`, reconciled on `enable`/`disable` — orphan units + the legacy `pos-event-trigger` timer auto-removed). Each job is a chmod-600 file `~/.config/linux_post_install/schedule.d/<name>.env`: `INTERVAL` (`5m..59m`/`1h..23h`/`hourly`/`daily`/`weekly`/`OnCalendar=…`), `NOTIFY` policy, optional `MSG`, `RULE` (threshold only), and `COMMAND` = **literal remainder of the line** (pipes/quotes/`sudo` need no escaping). Policies: `always` (full output every run), `onchange` (diff vs last run, first run always sends), `onerror` (non-zero exit or empty output), `threshold` (old event-trigger behavior: first numeric vs `RULE`, alert on false→true + recovery, per-job firing state), `never` (silent side-effect jobs — no notify; run log + last-run record still kept). Per-run logs/state in `~/.local/share/linux_post_install/schedule/{logs,state}/`. Subcommands: `run [name|all]`, `list`, `config` (interactive add/edit/remove/enable/disable with validation), `enable [name|all]`, `disable [name|all]`, `status`, `migrate` (converts legacy `event.env` rules → `schedule.d/rule-N.env` threshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files: `bin/pos-system-event-trigger` → `bin/pos-system-schedule`, `lib/eventer-lib.sh` → `lib/scheduler-lib.sh` (git mv; installed by install.sh), `config/event.env` + `config/event-rules.template` → `config/schedule.d/` starter jobs (nvme-health via `sudo -n smartctl` with the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an empty `schedule.d/` (legacy `event.env` users get a migrate hint instead). `bin/pos` EXAMPLES + INTERACTIVE_CMDS (`system-schedule config`) updated. Supersedes the "Tier 2: watch plugins" backlog idea. Docs: POS.md system row rewritten, howto/event-trigger.md → howto/schedule.md (job syntax, policies, NVMe recipe, migration), HOWTO.md index row + config table + scheduling bullet, AGENT_Context lib row + Common Tasks row. `make gen && make check` green; stub-harness suite (fake `systemctl`/`sudo`/`smartctl`/`sensors`/`df` + fake telegram sender logging, env seams `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`USER_SYSTEMD_DIR`/`SCHED_LEGACY_ENV`) covers all 5 policies (threshold cross/recover/no-repeat, onchange first/diff/same, onerror, always, never-silent), COMMAND literal-pipe parsing, enable/disable/status + orphan/legacy cleanup, migrate (incl. skip-existing + dry-run), and dispatch.
|
||||
- **2026-08-12** — `pos network download replace <gid> <url>` + fresh-link status advisory. `status` now flags stopped errored downloads whose source is marked permanently failing in `download.retry` (`needs fresh link: <name> (<gid>) — pos network download replace … <new-url>`; one `tellStopped` RPC, id-match in jq). `replace <gid> <url>` re-queues a dead single-file HTTP/FTP download with a new URL keeping the same `dir` + file name (partial resumes via `--continue=true`), unmarks the old source (`retry_unmark`, literal `grep -vxF` — URL-safe), and reuses `retry_verify` so a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints; `--dir/--split/--tmux` supported. `retry_verify` hardened to `${quiet:-0}` so it works outside `cmd_retry`. Stub suite grew a `replace` section (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gained `uris` (real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe. `make gen && make check` green.
|
||||
- **2026-08-12** — `pos network download` grows outage resilience: `restart <gid>` (re-queue from history — torrents via rebuilt magnet `urn:btih:` + `&tr=` trackers, HTTP via original URIs with `dir`/`out` preserved, `--continue=true` resumes partials; options `--dir/--seed/--split/--tmux`), `retry <gid|all>` (smart retry — waits out internet outages via `NET_PROBE` seam, re-queues, `retry_verify` polls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in `~/.config/linux_post_install/download.retry` as `url:<uri>`/`bt:<infohash>`, `retry all` skips them, manual restart overrides; `--once`/`--quiet` timer mode; `--interval`/`--max-wait`), and the **retry healer** systemd user pair (`pos-aria2-retry.service` oneshot `retry all --once --quiet` + `pos-aria2-retry.timer` 2min, `Persistent`) that arms on download start (`add`/`torrent`/`metalink`/`restart`) and disables itself when nothing is left; `watch <gid>` now auto-restarts its download after an outage. Fixes from stub-suite review: `ensure_healer` was missing from the three submit paths; `RESTART_NAME` was lost across `do_restart`'s process-substitution subshell (now a `download_name()` helper); `restart` exited 1 because the `[ tmux -eq 1 ] && tmux_watch` test was the function's last statement. Verification: stub-based test harness (`/tmp/opencode/dl-test` — curl/systemctl stubs with tellStatus fixtures, `NET_PROBE` file-flip, unit enable/disable logging) 119/119 green, incl. new restart/retry/healer/watch-heal cases. Docs: POS.md download rows + outage-resilience paragraph, howto/network.md outage recipe, SYSTEMD.md per-user units section, AGENT_Context + completions regenerated. `make gen && make check` green.
|
||||
- **2026-08-11** — Docs: DEV.md / AGENTS.md / AGENT_Context improved from the SMB session's lessons. DEV.md: new "Testing tools that need root / systemd / missing deps" (env-override test seams — `FLAGS_DIR`/`SMB_CONF`/`SMB_CREDS_DIR`/`UNIT_DIR` precedents — + stub-PATH fakes + PTY prompt driving via `script`); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. the `inblock == 1` awk guard, validate-then-apply, hot reload); deps-guards-run-before-`--help` made explicit (previously only inferable by reading the NFS tools); "Update the docs" checklist completed (howto index/section, Common Tasks row, AGENTS.md Quick facts, AGENT_TODO Done move). AGENTS.md: clarified which filetable line-count rows are hand-maintained (non-`pos-*` files above the marker) + when to bump them; deps-guard clause added to Quick facts. AGENT_Context "Adding a New Tool" steps 6–7 mirror the above. `make gen && make check` green.
|
||||
- **2026-08-11** — `share` category grows SMB: `pos share smb server` (`bin/pos-share-smb-server`) + `pos share smb client` (`bin/pos-share-smb-client`), completing the share trio (usb/nfs/smb). **Server:** `status`/`share`/`unshare`/`list`/`adduser`/`deluser`/`reload`/`enable`/`disable`; idempotent marker blocks in `/etc/samba/smb.conf` (`# >>> pos-managed share: <name>` … `# <<< end pos-managed share` — hand edits outside markers survive; `inblock==1`-guarded awk so removing one block never eats another's end marker), `testparm` validation before apply + `smbcontrol smbd reload-config` hot reload; `--read-only`/`--guest`/`--users u1,u2` flags with unrestricted-share warnings; `smbpasswd` user management (prompts, requires system user first). **Client:** `mount`/`unmount`/`list`/`persist`/`unpersist`; password prompt via `/dev/tty`, throwaway chmod-600 credentials for one-shot mounts, persistent creds at `/etc/samba/credentials/<name>` (chmod 600); `persist` writes a systemd `.mount` unit (`systemd-escape`) with `x-systemd.automount` + `_netdev` — mounts on first access, never blocks boot. Both source `lib/notify.sh` for mutations; added to `INTERACTIVE_CMDS` (prompting subcommands). Deps: `samba` + `cifs-utils` added to preinstall PACKAGES. `SMB_CONF`/`SMB_CREDS_DIR`/`UNIT_DIR` env-overridable for tests (FLAGS_DIR precedent). Docs: POS.md share rows, howto/share.md SMB sections, HOWTO index row, AGENT_Context Common Tasks, AGENTS.md categories. `make gen && make check` green; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows).
|
||||
|
||||
- **2026-08-11** — `pos network checkport` nmap overhaul: two-pass engine — pass 1 = fast `-Pn -T4 --max-retries 1` scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (`--versions`, opt-in) = `-sV --version-light` on open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap `-sU` requires root and quit outright); IPv6 hosts get `-6`; `no output`/filtered states set rc=1; `--timeout` scales nmap host-timeouts. New `--versions` flag in `# POS_FLAGS:` (completions regenerated) + usage text; port-metadata fallback retained. `make gen && make check` green.
|
||||
|
||||
- **2026-08-11** — `pos communication matrix sender login` error reporting: captures HTTP status + Matrix `errcode`/`error` from the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends `@` when `--user` is bare (e.g. `--user alice:example.org` → `@alice:example.org`).
|
||||
|
||||
- **2026-08-11** — New `share` category — `usb` and `nfs` moved out of `pos usb` / `pos system` into `pos share`: `pos share usb server` (was `pos-usb-server`), `pos share nfs server` + `pos share nfs client` (were `pos-system-nfs-*`). Renamed the three tools (`bin/pos-share-*`), updated `# POS:` headers/usage strings, `INTERACTIVE_CMDS` (`usb-server` → `share-usb-server`), `bin/pos` usage() EXAMPLES, and the notify-scope comment in `pos-system-backup`. Docs: new `DOC/howto/share.md` (USB + NFS consolidated; `howto/usb.md` deleted, NFS sections stripped from `howto/system.md`), POS.md `### share` section (replaces `### usb`, nfs rows moved out of `### system`), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.md `INTERACTIVE_CMDS` example, AGENTS.md categories. Category is the home for future `smb`. `make gen && make check` green; `/usr/local/bin` refreshed.
|
||||
|
||||
- **2026-08-09** — Matrix/Synapse `communication` tools — `pos communication matrix sender` + `listener`, completing the second notify platform `lib/notify.sh` was designed for (`NOTIFY_PLATFORM=telegram,matrix` fan-out; the sender implements the `send <value> [--markdown]` contract via `notify_sender_name()`'s default key→tool mapping, no lib changes). **Sender** (`bin/pos-communication-matrix-sender`): `send <value> [--markdown] [--room <id|alias>]` PUTs `m.room.message` (`m.text`) to the client-server API v3 — room ids/aliases URL-encoded (`#pos:example.org` → `%23pos%3A…`), unique per-message txn id, `--markdown` sends `org.matrix.custom.html` via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); `login --user <@id>` (masked password prompt → `m.login.password` → saves `access_token`+`user_id`); `test`. Config scope `matrix` (`~/.config/linux_post_install/matrix.env`, `MATRIX_HOMESERVER`/`MATRIX_ACCESS_TOKEN`/`MATRIX_USER_ID`/`MATRIX_ROOM_ID`, secret masked) registered via `# POS_CONFIG:` → `pos config matrix` + tab-completion scope. **Listener** (`bin/pos-communication-matrix-listener`): systemd **user** daemon (`pos-matrix-listener.service`) long-polling `/sync` (30s timeout, per-sync `since` token, compact filter dropping presence/account_data/device noise, `m.room.message` only); reacts to `MATRIX_USER_ID`'s own messages (resolved via `/account/whoami` if unset), `MATRIX_ROOM_ID` restricts to one room; `/` and `!` both resolve; replies threaded `m.in_reply_to`; `@quiet` no-reply marker; `/cmd::desc=…` map descriptions; `ai …` bridge (`pos ai gemini ask`, per-room session `matrix-<room>`, `ai /reset` clears, markdown stripped); interactive editor (`--status`/`--enable`/`--disable`/`--run`), 60s command timeout, exit-code prefix, ~3800-char truncation. `communication-matrix-listener` added to `INTERACTIVE_CMDS` (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, `bin/pos` usage EXAMPLES; `make gen && make check` green. Verified against a mock homeserver: send plain/markdown/`--room`/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + `/status` reply + `/help` + `@quiet` silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of `~/.config/linux_post_install/event.env` is an independent rule: `["<msg>" if ] <check-command> <op> <threshold>` (op `> < >= <= == !=`, unit suffix ok `60c`/`80%`). The check command is run on every pass and its **first numeric output** compared float-safe; operator detected as the rightmost `op threshold` pair so checks containing their own `>`/`<` (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in `~/.local/share/linux_post_install/eventer/state/` keyed by rule-line hash (editing a rule resets its state). Subcommands: `run` (timer entrypoint), `config` (interactive add/remove/edit with validation by test-running the check), `list` (rules + live values), `enable [interval]` (systemd **user** timer `pos-event-trigger.timer` + oneshot service; `5m…weekly` or `OnCalendar=…`; graceful warnings when no user systemd manager, `loginctl enable-linger` attempt), `disable`, `status`. `--dry-run` honors the DEV.md dry-run convention. Alerts via `lib/notify.sh` (Telegram default; other platforms via `NOTIFY_PLATFORM`). New: `bin/pos-system-event-trigger`, `lib/eventer-lib.sh`, `config/event.env` template (installed no-clobber by postinstall), `lib/eventer-lib.sh` installed by install.sh, `system-event-trigger` added to `INTERACTIVE_CMDS`, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; `make gen && make check` green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.
|
||||
|
||||
- **2026-08-09** — `pos media mp3`/`mp4` hardened + smart format selection. Both tools: yt-dlp calls go through `spawn` (honor `$DRY_RUN`; `--dry-run` prints the exact command and skips dep checks), `-o/--output`, `--no-playlist`, `--cookies` (file existence check), clean ffmpeg/yt-dlp guards, `# POS_FLAGS:` for completion, full metadata (`--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites`, mp3 also `--convert-thumbnails jpg` + `--parse-metadata "%(artist,uploader)s:%(artist)s"` so the uploader fills the artist tag). mp3 gains `--by-artist` (`~/Music/<artist>/<title>.mp3`). mp4 gains `-f <id>` / `--best` / `--worst` (no prompt), conflict validation, and an interactive picker that shows a **curated** `-F` table (`[audio]`/`[video]`/`[combo]` grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); `make gen && make check` green.
|
||||
|
||||
- **2026-08-09** — Telegram `ai …` now answers about a message you reply to: the listener extracts `reply_to_message.text` (falls back to `caption`) from each update and passes it to `handle_message`; the AI bridge prefixes the prompt with `[Reply context — the message you are replying to]`. So replying to a `/status` output and asking `ai check this details` gives the model the actual output. Applies only to the AI bridge (mapped `/commands` untouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section.
|
||||
|
||||
- **2026-08-09** — `pos ai gemini` sessions + Telegram-friendly replies. `--session <name>` gives `ask`/`chat` persistent memory (`~/.local/share/linux_post_install/ai/<name>.json`, capped at 40 turns, pruning keeps the first user turn as scene); new `sessions` subcommand (list / `reset <name>`). Telegram listener now keeps one session per chat (`telegram-<chat_id>`) with `ai /reset` to clear. New `--system "<text>"` flag injects a Gemini `systemInstruction` (via `jq` merge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (`**`, `*`, backticks, `#`, links, lists, blockquotes) from replies before `sendMessage`, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting), `make gen && make check` green.
|
||||
|
||||
- **2026-08-09** — Fixed `pos config` secret-value corruption: `cfg_read_secret`'s cursor-advance `echo` went to stdout and, since the function is called via `$(...)`, a leading `\n` ended up inside every secret value → the env file got `AI_GEMINI_API_KEY="\n<key>"`, unreadable by `cfg_value`/`load_config` (menu showed `(not set)`, `pos ai gemini` demanded a key). The newline now goes to the terminal (`echo >&2`). Defense in depth: `cfg_write`/`write_config_key` strip CR and truncate multi-line pastes (warn), `cfg_value` and the ai/telegram `load_config`s strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path).
|
||||
|
||||
- **2026-08-09** — `ai` category — `pos ai gemini` (ask/chat/models) via Google Gemini REST API. `ask` prints only the answer (pipe/script/Telegram-friendly), `chat` is a multi-turn REPL (q/quit/Ctrl+C, `/reset`, empty input re-prompts), `models` lists generateContent-capable ids and flags the default; `--model` override; default `gemini-2.5-flash`. Config scope `ai` (`AI_GEMINI_API_KEY` secret + `AI_GEMINI_MODEL`) in `~/.config/linux_post_install/ai.env`, edited via `pos config ai`; `config/ai.env` template installed no-clobber by postinstall; `ai-gemini` added to `INTERACTIVE_CMDS`. Telegram listener now answers non-command messages starting with `ai ` via `pos ai gemini ask` (owner chat only; error replies carry the `pos config ai` hint) — future intents (reminders) slot in as more case arms in `handle_message`. Docs: POS.md `ai` section + listener bridge, howto/ai.md, HOWTO/README index rows, `bin/pos` usage example.
|
||||
|
||||
- **2026-08-09** — Entertainment plugins `gold` + `weather` now emit emoji-visualized Telegram messages. Gold: headline is USD/**gram** (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (`+00:00`/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode.
|
||||
|
||||
- **2026-08-09** — `pos tree`: prints the live `pos` command tree (categories → commands → subcommands) by deriving the hierarchy from `bin/pos-*` filenames + `# POS:` / `# POS_SUBCMDS:` headers, so it always matches what the dispatcher can run. Category-less like `pos-config`; `--depth N` limit; `pos help tree` works. Docs: POS.md `tree` section, `bin/pos` usage example, `make gen` regenerated the AGENT_Context tree/dispatch/filetable + `_pos_flags[tree]`.
|
||||
|
||||
- **2026-08-09** — Telegram sender `config` / `config set` removed — redundant with `pos config telegram` (same `# POS_CONFIG:` registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped `/command` values containing `|` are no longer truncated (`load_map` switched from a `|` to a `\x1f` delimiter — previously `/up=echo hi | head` silently ran `echo hi `); `pos entertainment send <plugin> [args…]` actually forwards the extra args (every arg was `shift`ed in the flag loop, so `$@` was empty) and passes `--` before the message so leading-`-` plugin output isn't parsed as an option; `write_config_key` (entertainment-lib) and `cfg_write` (config-ui) replaced unescaped `sed -i "s|^K=.*|K=\"$v\"|"` with grep-v+append so values with `&`/`|`/`\` no longer mangle (also the path all telegram config now flows through); `sync_systemd` daemon-reloads after removing timer units; `digits` config validation accepts negative group/supergroup chat ids (`-100…`).
|
||||
|
||||
- **2026-08-09** — Fixed telegram listener editor crash on remove/edit/test: `ui_pick` printed its menu listing to **stdout**, so `idx="$(ui_pick)"` captured the menu *and* the number, and `MAP_CMDS[$idx]` (arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the `::desc` work), exposed by the description column.
|
||||
|
||||
- **2026-08-09** — Telegram listener pushes its mapped `/commands` to the bot's `/` menu via `setMyCommands` (auto after every map edit, on `--enable`, and at daemon start; manual `--sync-commands` flag). Map lines may carry a menu description: `/cmd::short description=bash command` (falls back to the bash command, ~40 chars). Names are validated against Telegram's lowercase `[a-z0-9_]` rule — invalid ones are skipped from the menu with a warning but still resolve when typed; empty map clears the menu. Fixed latent bugs found by the sync work: `map_has` (awk `END{exit 1}` overrode the match), and `warn()` went to stdout so it leaked into the generated JSON (now stderr).
|
||||
|
||||
- **2026-08-09** — `pos config <TAB>` scope completion is now cached at gen time (`_pos_config_scopes` array emitted by `make gen` from the `# POS_CONFIG:` registry) instead of scanning ~40 tools per TAB — a per-keypress subshell storm that wedged interactive shells for minutes on the loaded homelab box. Two stuck `-bash` sessions (69%/38% CPU) killed. `plugin_marker`/`plugin_keys` hardened with `|| true` so `config_keys` no longer aborts mid-scan under `set -euo pipefail` on mixed lib/plugin dirs (installed layout) — fixes missing plugin keys in `pos config entertainment`.
|
||||
|
||||
- **2026-08-09** — `pos config <scope>` interactive config editor: reads the `# POS_CONFIG:` registry across tools into a single runtime config (`~/.config/linux_post_install/*.env`, one file per scope, chmod 600); secret masking with show/hide toggle, `digits:`/`num:`/`url:` validation, `-` to clear, blank keeps; `*plugins` marker expands plugin vars (entertainment) from `entertainment-lib.sh`; `desc::example` value-format hints shown in the editor; gen-docs now handles category-less tools (`pos-config`), fixed a `set -e`+`pipefail` bug that truncated the header registry.
|
||||
|
||||
- **2026-08-09** — `pos-communication-telegram` → `pos-communication-telegram-sender`: one canonical `send` (dropped the legacy `--send` flag, which duplicated the `send` subcommand in completion). `pos communication telegram <TAB>` now completes to just `sender listener`. `lib/notify.sh` maps platform `telegram` → `telegram-sender` via `notify_sender_name()`; entertainment-send + health `--send` check updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed).
|
||||
|
||||
- **2026-08-09** — Structure/convention audit fix: `--dry-run` now truly dry (`spawn()` honors `DRY_RUN`, install.sh exports it to child phases, postinstall mutations run-wrapped); `gen-docs.sh` no longer chmods regenerated files to 0600; `make check` now syntax-checks apps/entertainment/features/templates; `.gitignore` protects `config/authorized_keys` + `config/rclone.conf`; honest `--send` confirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS).
|
||||
|
||||
- **2026-08-07** — `pos system health --send` notification-only; listener `@quiet` prefix (run mapped command without replying, for commands that self-notify). `/status=@quiet pos system health --send` = exactly one digest.
|
||||
|
||||
- **2026-08-12** — `pos network download` (`bin/pos-network-download`) — aria2 JSON-RPC daemon + queue control. **Daemon:** persistent `aria2c` as a systemd **user** service (`pos-aria2.service`, `${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user`, `enable --now` + linger warning on headless boxes), `--rpc-listen-port=6800`, generated `RPC_SECRET` in `~/.config/linux_post_install/download.env` (chmod 600, env override), unit flags `--continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads`. **Commands (18):** `start`/`stop`/`status` (+ bare overview = status+list), `add <url…> [--dir --out --split --tmux]`, `torrent <file|magnet…> [--dir --seed --tmux]` (base64 `addTorrent`), `metalink <file|url> [--tmux]`, `list` (active/waiting/stopped table), `info`/`files`/`peers <gid>`, `pause|resume|remove [gid|all]` (`--force` → `force*`), `purge`, `move <gid> <pos>`, `limit [gid] <speed>` (`--upload`, 0=unlimited, `2M`/`512K`), `set <k=v…> [--gid]`, `watch [gid]` (2s live repoll; exits when that gid completes). `--tmux` opens a detached `dl-<name>` session running `watch <gid>` (name from `--out`/URL basename, sanitized, 40-char truncate, `-2` on collision; closes itself on completion). Deps: `aria2c`/`jq`/`curl` guards before `--help`; `aria2` added to preinstall PACKAGES. No stdin → not in `INTERACTIVE_CMDS`. JSON built with `jq -nc --arg` (never string interpolation — fixes JSON-quote bugs); `# POS_SUBCMDS:` (18) + `# POS_FLAGS:` → completions. Test seams `RPC_PORT`/`RPC_SECRET`/`DOWNLOAD_DIR`/`USER_SYSTEMD_DIR`/`ACTIVE_MARKER`; 76-case stub-PATH behavior suite green (unit content, secret 600, add→gid, tables, queue ops, error paths). Docs: POS.md network row+detail, howto/network.md section, HOWTO index, AGENT_Context Common Tasks row. `make gen && make check` green.
|
||||
|
||||
## Now
|
||||
|
||||
- (none — Tier 1 shipped: `pos system health`, `lib/notify.sh`, digest timer)
|
||||
|
||||
## Next
|
||||
|
||||
- Wire alerting into more tools as they are added (default: source
|
||||
- - Wire alerting into more tools as they are added (default: source
|
||||
`lib/notify.sh`, call `notify_send` on success/failure).
|
||||
|
||||
## Later — idea backlog
|
||||
## Later
|
||||
|
||||
- **Tier 2: `pos health` extras** — temperature/fan/load average thresholds,
|
||||
`ss -tln` port checks for known services, SMART status for disks.
|
||||
@@ -104,43 +40,87 @@ summary (newest last).
|
||||
- **NOT NOW:** split `lib/entertainment-lib.sh` — fine under 600 lines; revisit
|
||||
if it grows.
|
||||
|
||||
## Done (summary, newest last)
|
||||
## Done
|
||||
|
||||
- 2026-08-06: Fix entertainment timer `1h` not firing — `interval_to_oncalendar`
|
||||
- **2026-08-14** — `pos system backup` — smart USB detection: lsblk TRAN (lsusb/by-id cross-check), mount offer for plugged-in-but-unmounted sticks, sha256-verified copy (stub-suite 54/54).
|
||||
- **2026-08-05** — `pos communication telegram` — `--parse-mode` (plain/markdown/html).
|
||||
- **2026-08-05** — doc/code sync gate — `make gen` + `make check` + pre-commit hook.
|
||||
- **2026-08-05** — `pos usb server` — USB Redirector control tool (494eae2).
|
||||
- **2026-08-05** — `pos <category> --help` auto-discovery in the dispatcher.
|
||||
- **2026-08-05** — AGENTS.md with lazy-loaded DOC references.
|
||||
- **2026-08-06** — Fix entertainment timer `1h` not firing — `interval_to_oncalendar`
|
||||
emitted invalid `OnCalendar=*-*-* */N:00:00` (systemd rejects `*/N` in the hour
|
||||
field); now `*-*-* 00/N:00:00`. Dropped the cron fallback entirely: scheduling
|
||||
is systemd user timers only (`sync_cron`/`interval_to_cron`/`cron_block`
|
||||
removed), `status` simplified, `Nd` intervals rejected with a clear error.
|
||||
- 2026-08-06: Nested `pos` subcommands — `# POS_SUBCMDS:` header annotation (telegram,
|
||||
- **2026-08-06** — Nested `pos` subcommands — `# POS_SUBCMDS:` header annotation (telegram,
|
||||
docker-compose, docker-vbox) + `make gen` emits a `_pos_subcmds` completion map;
|
||||
nested tools (`telegram listener`) auto-list under their parent instead of as a
|
||||
flat sibling (`telegram-listener`) in `pos <category>` and tab-completion; generic
|
||||
tool-level completion (subcommands + flags + `--help`).
|
||||
- 2026-08-06: Telegram **listener** — `pos communication telegram listener`:
|
||||
- **2026-08-06** — Telegram **listener** — `pos communication telegram listener`:
|
||||
interactive `/command` → bash map editor + owner-only polling daemon as a
|
||||
systemd user service (map in `~/.config/linux_post_install/telegram_commands.env`,
|
||||
re-read per message; `/help`, unknown-command reply, 60s timeout, stdout reply).
|
||||
- 2026-08-06: NFS in `pos system` — `pos system nfs-server` (status/share/
|
||||
- **2026-08-06** — NFS in `pos system` — `pos system nfs-server` (status/share/
|
||||
unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic
|
||||
default with Tailscale/WireGuard/LAN examples) + `pos system nfs-client`
|
||||
(mount/unmount/list + persistent mounts as systemd `.mount` units ordered
|
||||
after network-online.target, no fstab); `nfs-kernel-server` + `nfs-common`
|
||||
added to preinstall PACKAGES.
|
||||
- 2026-08-06: `pos` HOW-TO guide set — `DOC/HOWTO.md` index + per-category
|
||||
- **2026-08-06** — `pos` HOW-TO guide set — `DOC/HOWTO.md` index + per-category
|
||||
`DOC/howto/*.md` (network, docker, media, system, ssh, usb, communication,
|
||||
entertainment) with flags, recipes, config, and troubleshooting; wired into
|
||||
DOC/README, root README, AGENTS.md.
|
||||
- 2026-08-06: Multi-platform alerting — `lib/notify.sh` routes via `NOTIFY_PLATFORM`
|
||||
- **2026-08-06** — Multi-platform alerting — `lib/notify.sh` routes via `NOTIFY_PLATFORM`
|
||||
(`notify.env`, default telegram; sender contract for Matrix/Synapse later),
|
||||
`system.env` shared config for health/backup, dynamic effective values in
|
||||
`--help`, telegram `--markdown` alias.
|
||||
- 2026-08-06: Tier 1 — `pos system health` (dashboard + `--send`), `lib/notify.sh`
|
||||
- **2026-08-06** — Tier 1 — `pos system health` (dashboard + `--send`), `lib/notify.sh`
|
||||
(wired into backup + firewall), daily digest timer via postinstall.
|
||||
- 2026-08-06: Document Map index + Entertainment section in AGENT_Context (cf36780).
|
||||
- 2026-08-06: Entertainment module — plugins (weather/joke/gold), `pos
|
||||
- **2026-08-06** — Document Map index + Entertainment section in AGENT_Context (cf36780).
|
||||
- **2026-08-06** — Entertainment module — plugins (weather/joke/gold), `pos
|
||||
entertainment config/enable/disable/send/status`, auto-trigger + Telegram send.
|
||||
- 2026-08-05: `pos communication telegram` — `--parse-mode` (plain/markdown/html).
|
||||
- 2026-08-05: doc/code sync gate — `make gen` + `make check` + pre-commit hook.
|
||||
- 2026-08-05: `pos usb server` — USB Redirector control tool (494eae2).
|
||||
- 2026-08-05: `pos <category> --help` auto-discovery in the dispatcher.
|
||||
- 2026-08-05: AGENTS.md with lazy-loaded DOC references.
|
||||
- **2026-08-07** — `pos system health --send` notification-only; listener `@quiet` prefix (run mapped command without replying, for commands that self-notify). `/status=@quiet pos system health --send` = exactly one digest.
|
||||
- **2026-08-09** — Matrix/Synapse `communication` tools — `pos communication matrix sender` + `listener`, completing the second notify platform `lib/notify.sh` was designed for (`NOTIFY_PLATFORM=telegram,matrix` fan-out; the sender implements the `send <value> [--markdown]` contract via `notify_sender_name()`'s default key→tool mapping, no lib changes). **Sender** (`bin/pos-communication-matrix-sender`): `send <value> [--markdown] [--room <id|alias>]` PUTs `m.room.message` (`m.text`) to the client-server API v3 — room ids/aliases URL-encoded (`#pos:example.org` → `%23pos%3A…`), unique per-message txn id, `--markdown` sends `org.matrix.custom.html` via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); `login --user <@id>` (masked password prompt → `m.login.password` → saves `access_token`+`user_id`); `test`. Config scope `matrix` (`~/.config/linux_post_install/matrix.env`, `MATRIX_HOMESERVER`/`MATRIX_ACCESS_TOKEN`/`MATRIX_USER_ID`/`MATRIX_ROOM_ID`, secret masked) registered via `# POS_CONFIG:` → `pos config matrix` + tab-completion scope. **Listener** (`bin/pos-communication-matrix-listener`): systemd **user** daemon (`pos-matrix-listener.service`) long-polling `/sync` (30s timeout, per-sync `since` token, compact filter dropping presence/account_data/device noise, `m.room.message` only); reacts to `MATRIX_USER_ID`'s own messages (resolved via `/account/whoami` if unset), `MATRIX_ROOM_ID` restricts to one room; `/` and `!` both resolve; replies threaded `m.in_reply_to`; `@quiet` no-reply marker; `/cmd::desc=…` map descriptions; `ai …` bridge (`pos ai gemini ask`, per-room session `matrix-<room>`, `ai /reset` clears, markdown stripped); interactive editor (`--status`/`--enable`/`--disable`/`--run`), 60s command timeout, exit-code prefix, ~3800-char truncation. `communication-matrix-listener` added to `INTERACTIVE_CMDS` (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, `bin/pos` usage EXAMPLES; `make gen && make check` green. Verified against a mock homeserver: send plain/markdown/`--room`/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + `/status` reply + `/help` + `@quiet` silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of `~/.config/linux_post_install/event.env` is an independent rule: `["<msg>" if ] <check-command> <op> <threshold>` (op `> < >= <= == !=`, unit suffix ok `60c`/`80%`). The check command is run on every pass and its **first numeric output** compared float-safe; operator detected as the rightmost `op threshold` pair so checks containing their own `>`/`<` (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in `~/.local/share/linux_post_install/eventer/state/` keyed by rule-line hash (editing a rule resets its state). Subcommands: `run` (timer entrypoint), `config` (interactive add/remove/edit with validation by test-running the check), `list` (rules + live values), `enable [interval]` (systemd **user** timer `pos-event-trigger.timer` + oneshot service; `5m…weekly` or `OnCalendar=…`; graceful warnings when no user systemd manager, `loginctl enable-linger` attempt), `disable`, `status`. `--dry-run` honors the DEV.md dry-run convention. Alerts via `lib/notify.sh` (Telegram default; other platforms via `NOTIFY_PLATFORM`). New: `bin/pos-system-event-trigger`, `lib/eventer-lib.sh`, `config/event.env` template (installed no-clobber by postinstall), `lib/eventer-lib.sh` installed by install.sh, `system-event-trigger` added to `INTERACTIVE_CMDS`, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; `make gen && make check` green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.
|
||||
- **2026-08-09** — `pos media mp3`/`mp4` hardened + smart format selection. Both tools: yt-dlp calls go through `spawn` (honor `$DRY_RUN`; `--dry-run` prints the exact command and skips dep checks), `-o/--output`, `--no-playlist`, `--cookies` (file existence check), clean ffmpeg/yt-dlp guards, `# POS_FLAGS:` for completion, full metadata (`--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites`, mp3 also `--convert-thumbnails jpg` + `--parse-metadata "%(artist,uploader)s:%(artist)s"` so the uploader fills the artist tag). mp3 gains `--by-artist` (`~/Music/<artist>/<title>.mp3`). mp4 gains `-f <id>` / `--best` / `--worst` (no prompt), conflict validation, and an interactive picker that shows a **curated** `-F` table (`[audio]`/`[video]`/`[combo]` grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); `make gen && make check` green.
|
||||
- **2026-08-09** — Telegram `ai …` now answers about a message you reply to: the listener extracts `reply_to_message.text` (falls back to `caption`) from each update and passes it to `handle_message`; the AI bridge prefixes the prompt with `[Reply context — the message you are replying to]`. So replying to a `/status` output and asking `ai check this details` gives the model the actual output. Applies only to the AI bridge (mapped `/commands` untouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section.
|
||||
- **2026-08-09** — `pos ai gemini` sessions + Telegram-friendly replies. `--session <name>` gives `ask`/`chat` persistent memory (`~/.local/share/linux_post_install/ai/<name>.json`, capped at 40 turns, pruning keeps the first user turn as scene); new `sessions` subcommand (list / `reset <name>`). Telegram listener now keeps one session per chat (`telegram-<chat_id>`) with `ai /reset` to clear. New `--system "<text>"` flag injects a Gemini `systemInstruction` (via `jq` merge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (`**`, `*`, backticks, `#`, links, lists, blockquotes) from replies before `sendMessage`, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting), `make gen && make check` green.
|
||||
- **2026-08-09** — Fixed `pos config` secret-value corruption: `cfg_read_secret`'s cursor-advance `echo` went to stdout and, since the function is called via `$(...)`, a leading `\n` ended up inside every secret value → the env file got `AI_GEMINI_API_KEY="\n<key>"`, unreadable by `cfg_value`/`load_config` (menu showed `(not set)`, `pos ai gemini` demanded a key). The newline now goes to the terminal (`echo >&2`). Defense in depth: `cfg_write`/`write_config_key` strip CR and truncate multi-line pastes (warn), `cfg_value` and the ai/telegram `load_config`s strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path).
|
||||
- **2026-08-09** — `ai` category — `pos ai gemini` (ask/chat/models) via Google Gemini REST API. `ask` prints only the answer (pipe/script/Telegram-friendly), `chat` is a multi-turn REPL (q/quit/Ctrl+C, `/reset`, empty input re-prompts), `models` lists generateContent-capable ids and flags the default; `--model` override; default `gemini-2.5-flash`. Config scope `ai` (`AI_GEMINI_API_KEY` secret + `AI_GEMINI_MODEL`) in `~/.config/linux_post_install/ai.env`, edited via `pos config ai`; `config/ai.env` template installed no-clobber by postinstall; `ai-gemini` added to `INTERACTIVE_CMDS`. Telegram listener now answers non-command messages starting with `ai ` via `pos ai gemini ask` (owner chat only; error replies carry the `pos config ai` hint) — future intents (reminders) slot in as more case arms in `handle_message`. Docs: POS.md `ai` section + listener bridge, howto/ai.md, HOWTO/README index rows, `bin/pos` usage example.
|
||||
- **2026-08-09** — Entertainment plugins `gold` + `weather` now emit emoji-visualized Telegram messages. Gold: headline is USD/**gram** (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (`+00:00`/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode.
|
||||
- **2026-08-09** — `pos tree`: prints the live `pos` command tree (categories → commands → subcommands) by deriving the hierarchy from `bin/pos-*` filenames + `# POS:` / `# POS_SUBCMDS:` headers, so it always matches what the dispatcher can run. Category-less like `pos-config`; `--depth N` limit; `pos help tree` works. Docs: POS.md `tree` section, `bin/pos` usage example, `make gen` regenerated the AGENT_Context tree/dispatch/filetable + `_pos_flags[tree]`.
|
||||
- **2026-08-09** — Telegram sender `config` / `config set` removed — redundant with `pos config telegram` (same `# POS_CONFIG:` registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped `/command` values containing `|` are no longer truncated (`load_map` switched from a `|` to a `\x1f` delimiter — previously `/up=echo hi | head` silently ran `echo hi `); `pos entertainment send <plugin> [args…]` actually forwards the extra args (every arg was `shift`ed in the flag loop, so `$@` was empty) and passes `--` before the message so leading-`-` plugin output isn't parsed as an option; `write_config_key` (entertainment-lib) and `cfg_write` (config-ui) replaced unescaped `sed -i "s|^K=.*|K=\"$v\"|"` with grep-v+append so values with `&`/`|`/`\` no longer mangle (also the path all telegram config now flows through); `sync_systemd` daemon-reloads after removing timer units; `digits` config validation accepts negative group/supergroup chat ids (`-100…`).
|
||||
- **2026-08-09** — Fixed telegram listener editor crash on remove/edit/test: `ui_pick` printed its menu listing to **stdout**, so `idx="$(ui_pick)"` captured the menu *and* the number, and `MAP_CMDS[$idx]` (arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the `::desc` work), exposed by the description column.
|
||||
- **2026-08-09** — Telegram listener pushes its mapped `/commands` to the bot's `/` menu via `setMyCommands` (auto after every map edit, on `--enable`, and at daemon start; manual `--sync-commands` flag). Map lines may carry a menu description: `/cmd::short description=bash command` (falls back to the bash command, ~40 chars). Names are validated against Telegram's lowercase `[a-z0-9_]` rule — invalid ones are skipped from the menu with a warning but still resolve when typed; empty map clears the menu. Fixed latent bugs found by the sync work: `map_has` (awk `END{exit 1}` overrode the match), and `warn()` went to stdout so it leaked into the generated JSON (now stderr).
|
||||
- **2026-08-09** — `pos config <TAB>` scope completion is now cached at gen time (`_pos_config_scopes` array emitted by `make gen` from the `# POS_CONFIG:` registry) instead of scanning ~40 tools per TAB — a per-keypress subshell storm that wedged interactive shells for minutes on the loaded homelab box. Two stuck `-bash` sessions (69%/38% CPU) killed. `plugin_marker`/`plugin_keys` hardened with `|| true` so `config_keys` no longer aborts mid-scan under `set -euo pipefail` on mixed lib/plugin dirs (installed layout) — fixes missing plugin keys in `pos config entertainment`.
|
||||
- **2026-08-09** — `pos config <scope>` interactive config editor: reads the `# POS_CONFIG:` registry across tools into a single runtime config (`~/.config/linux_post_install/*.env`, one file per scope, chmod 600); secret masking with show/hide toggle, `digits:`/`num:`/`url:` validation, `-` to clear, blank keeps; `*plugins` marker expands plugin vars (entertainment) from `entertainment-lib.sh`; `desc::example` value-format hints shown in the editor; gen-docs now handles category-less tools (`pos-config`), fixed a `set -e`+`pipefail` bug that truncated the header registry.
|
||||
- **2026-08-09** — `pos-communication-telegram` → `pos-communication-telegram-sender`: one canonical `send` (dropped the legacy `--send` flag, which duplicated the `send` subcommand in completion). `pos communication telegram <TAB>` now completes to just `sender listener`. `lib/notify.sh` maps platform `telegram` → `telegram-sender` via `notify_sender_name()`; entertainment-send + health `--send` check updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed).
|
||||
- **2026-08-09** — Structure/convention audit fix: `--dry-run` now truly dry (`spawn()` honors `DRY_RUN`, install.sh exports it to child phases, postinstall mutations run-wrapped); `gen-docs.sh` no longer chmods regenerated files to 0600; `make check` now syntax-checks apps/entertainment/features/templates; `.gitignore` protects `config/authorized_keys` + `config/rclone.conf`; honest `--send` confirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS).
|
||||
- **2026-08-11** — Docs: DEV.md / AGENTS.md / AGENT_Context improved from the SMB session's lessons. DEV.md: new "Testing tools that need root / systemd / missing deps" (env-override test seams — `FLAGS_DIR`/`SMB_CONF`/`SMB_CREDS_DIR`/`UNIT_DIR` precedents — + stub-PATH fakes + PTY prompt driving via `script`); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. the `inblock == 1` awk guard, validate-then-apply, hot reload); deps-guards-run-before-`--help` made explicit (previously only inferable by reading the NFS tools); "Update the docs" checklist completed (howto index/section, Common Tasks row, AGENTS.md Quick facts, AGENT_TODO Done move). AGENTS.md: clarified which filetable line-count rows are hand-maintained (non-`pos-*` files above the marker) + when to bump them; deps-guard clause added to Quick facts. AGENT_Context "Adding a New Tool" steps 6–7 mirror the above. `make gen && make check` green.
|
||||
- **2026-08-11** — `share` category grows SMB: `pos share smb server` (`bin/pos-share-smb-server`) + `pos share smb client` (`bin/pos-share-smb-client`), completing the share trio (usb/nfs/smb). **Server:** `status`/`share`/`unshare`/`list`/`adduser`/`deluser`/`reload`/`enable`/`disable`; idempotent marker blocks in `/etc/samba/smb.conf` (`# >>> pos-managed share: <name>` … `# <<< end pos-managed share` — hand edits outside markers survive; `inblock==1`-guarded awk so removing one block never eats another's end marker), `testparm` validation before apply + `smbcontrol smbd reload-config` hot reload; `--read-only`/`--guest`/`--users u1,u2` flags with unrestricted-share warnings; `smbpasswd` user management (prompts, requires system user first). **Client:** `mount`/`unmount`/`list`/`persist`/`unpersist`; password prompt via `/dev/tty`, throwaway chmod-600 credentials for one-shot mounts, persistent creds at `/etc/samba/credentials/<name>` (chmod 600); `persist` writes a systemd `.mount` unit (`systemd-escape`) with `x-systemd.automount` + `_netdev` — mounts on first access, never blocks boot. Both source `lib/notify.sh` for mutations; added to `INTERACTIVE_CMDS` (prompting subcommands). Deps: `samba` + `cifs-utils` added to preinstall PACKAGES. `SMB_CONF`/`SMB_CREDS_DIR`/`UNIT_DIR` env-overridable for tests (FLAGS_DIR precedent). Docs: POS.md share rows, howto/share.md SMB sections, HOWTO index row, AGENT_Context Common Tasks, AGENTS.md categories. `make gen && make check` green; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows).
|
||||
- **2026-08-11** — `pos network checkport` nmap overhaul: two-pass engine — pass 1 = fast `-Pn -T4 --max-retries 1` scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (`--versions`, opt-in) = `-sV --version-light` on open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap `-sU` requires root and quit outright); IPv6 hosts get `-6`; `no output`/filtered states set rc=1; `--timeout` scales nmap host-timeouts. New `--versions` flag in `# POS_FLAGS:` (completions regenerated) + usage text; port-metadata fallback retained. `make gen && make check` green.
|
||||
- **2026-08-11** — `pos communication matrix sender login` error reporting: captures HTTP status + Matrix `errcode`/`error` from the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends `@` when `--user` is bare (e.g. `--user alice:example.org` → `@alice:example.org`).
|
||||
- **2026-08-11** — New `share` category — `usb` and `nfs` moved out of `pos usb` / `pos system` into `pos share`: `pos share usb server` (was `pos-usb-server`), `pos share nfs server` + `pos share nfs client` (were `pos-system-nfs-*`). Renamed the three tools (`bin/pos-share-*`), updated `# POS:` headers/usage strings, `INTERACTIVE_CMDS` (`usb-server` → `share-usb-server`), `bin/pos` usage() EXAMPLES, and the notify-scope comment in `pos-system-backup`. Docs: new `DOC/howto/share.md` (USB + NFS consolidated; `howto/usb.md` deleted, NFS sections stripped from `howto/system.md`), POS.md `### share` section (replaces `### usb`, nfs rows moved out of `### system`), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.md `INTERACTIVE_CMDS` example, AGENTS.md categories. Category is the home for future `smb`. `make gen && make check` green; `/usr/local/bin` refreshed.
|
||||
- **2026-08-12** — `pos network download` (`bin/pos-network-download`) — aria2 JSON-RPC daemon + queue control. **Daemon:** persistent `aria2c` as a systemd **user** service (`pos-aria2.service`, `${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user`, `enable --now` + linger warning on headless boxes), `--rpc-listen-port=6800`, generated `RPC_SECRET` in `~/.config/linux_post_install/download.env` (chmod 600, env override), unit flags `--continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads`. **Commands (18):** `start`/`stop`/`status` (+ bare overview = status+list), `add <url…> [--dir --out --split --tmux]`, `torrent <file|magnet…> [--dir --seed --tmux]` (base64 `addTorrent`), `metalink <file|url> [--tmux]`, `list` (active/waiting/stopped table), `info`/`files`/`peers <gid>`, `pause|resume|remove [gid|all]` (`--force` → `force*`), `purge`, `move <gid> <pos>`, `limit [gid] <speed>` (`--upload`, 0=unlimited, `2M`/`512K`), `set <k=v…> [--gid]`, `watch [gid]` (2s live repoll; exits when that gid completes). `--tmux` opens a detached `dl-<name>` session running `watch <gid>` (name from `--out`/URL basename, sanitized, 40-char truncate, `-2` on collision; closes itself on completion). Deps: `aria2c`/`jq`/`curl` guards before `--help`; `aria2` added to preinstall PACKAGES. No stdin → not in `INTERACTIVE_CMDS`. JSON built with `jq -nc --arg` (never string interpolation — fixes JSON-quote bugs); `# POS_SUBCMDS:` (18) + `# POS_FLAGS:` → completions. Test seams `RPC_PORT`/`RPC_SECRET`/`DOWNLOAD_DIR`/`USER_SYSTEMD_DIR`/`ACTIVE_MARKER`; 76-case stub-PATH behavior suite green (unit content, secret 600, add→gid, tables, queue ops, error paths). Docs: POS.md network row+detail, howto/network.md section, HOWTO index, AGENT_Context Common Tasks row. `make gen && make check` green.
|
||||
- **2026-08-12** — `pos network download` grows outage resilience: `restart <gid>` (re-queue from history — torrents via rebuilt magnet `urn:btih:` + `&tr=` trackers, HTTP via original URIs with `dir`/`out` preserved, `--continue=true` resumes partials; options `--dir/--seed/--split/--tmux`), `retry <gid|all>` (smart retry — waits out internet outages via `NET_PROBE` seam, re-queues, `retry_verify` polls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in `~/.config/linux_post_install/download.retry` as `url:<uri>`/`bt:<infohash>`, `retry all` skips them, manual restart overrides; `--once`/`--quiet` timer mode; `--interval`/`--max-wait`), and the **retry healer** systemd user pair (`pos-aria2-retry.service` oneshot `retry all --once --quiet` + `pos-aria2-retry.timer` 2min, `Persistent`) that arms on download start (`add`/`torrent`/`metalink`/`restart`) and disables itself when nothing is left; `watch <gid>` now auto-restarts its download after an outage. Fixes from stub-suite review: `ensure_healer` was missing from the three submit paths; `RESTART_NAME` was lost across `do_restart`'s process-substitution subshell (now a `download_name()` helper); `restart` exited 1 because the `[ tmux -eq 1 ] && tmux_watch` test was the function's last statement. Verification: stub-based test harness (`/tmp/opencode/dl-test` — curl/systemctl stubs with tellStatus fixtures, `NET_PROBE` file-flip, unit enable/disable logging) 119/119 green, incl. new restart/retry/healer/watch-heal cases. Docs: POS.md download rows + outage-resilience paragraph, howto/network.md outage recipe, SYSTEMD.md per-user units section, AGENT_Context + completions regenerated. `make gen && make check` green.
|
||||
- **2026-08-12** — `pos network download replace <gid> <url>` + fresh-link status advisory. `status` now flags stopped errored downloads whose source is marked permanently failing in `download.retry` (`needs fresh link: <name> (<gid>) — pos network download replace … <new-url>`; one `tellStopped` RPC, id-match in jq). `replace <gid> <url>` re-queues a dead single-file HTTP/FTP download with a new URL keeping the same `dir` + file name (partial resumes via `--continue=true`), unmarks the old source (`retry_unmark`, literal `grep -vxF` — URL-safe), and reuses `retry_verify` so a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints; `--dir/--split/--tmux` supported. `retry_verify` hardened to `${quiet:-0}` so it works outside `cmd_retry`. Stub suite grew a `replace` section (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gained `uris` (real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe. `make gen && make check` green.
|
||||
- **2026-08-12** — `pos system event-trigger` (eventer) generalized into `pos system schedule` — the scheduler replaces the single-timer threshold monitor with **per-job systemd user timers** (`pos-schedule-<name>.{timer,service}`, `Persistent`, ExecStart `run <name>`, reconciled on `enable`/`disable` — orphan units + the legacy `pos-event-trigger` timer auto-removed). Each job is a chmod-600 file `~/.config/linux_post_install/schedule.d/<name>.env`: `INTERVAL` (`5m..59m`/`1h..23h`/`hourly`/`daily`/`weekly`/`OnCalendar=…`), `NOTIFY` policy, optional `MSG`, `RULE` (threshold only), and `COMMAND` = **literal remainder of the line** (pipes/quotes/`sudo` need no escaping). Policies: `always` (full output every run), `onchange` (diff vs last run, first run always sends), `onerror` (non-zero exit or empty output), `threshold` (old event-trigger behavior: first numeric vs `RULE`, alert on false→true + recovery, per-job firing state), `never` (silent side-effect jobs — no notify; run log + last-run record still kept). Per-run logs/state in `~/.local/share/linux_post_install/schedule/{logs,state}/`. Subcommands: `run [name|all]`, `list`, `config` (interactive add/edit/remove/enable/disable with validation), `enable [name|all]`, `disable [name|all]`, `status`, `migrate` (converts legacy `event.env` rules → `schedule.d/rule-N.env` threshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files: `bin/pos-system-event-trigger` → `bin/pos-system-schedule`, `lib/eventer-lib.sh` → `lib/scheduler-lib.sh` (git mv; installed by install.sh), `config/event.env` + `config/event-rules.template` → `config/schedule.d/` starter jobs (nvme-health via `sudo -n smartctl` with the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an empty `schedule.d/` (legacy `event.env` users get a migrate hint instead). `bin/pos` EXAMPLES + INTERACTIVE_CMDS (`system-schedule config`) updated. Supersedes the "Tier 2: watch plugins" backlog idea. Docs: POS.md system row rewritten, howto/event-trigger.md → howto/schedule.md (job syntax, policies, NVMe recipe, migration), HOWTO.md index row + config table + scheduling bullet, AGENT_Context lib row + Common Tasks row. `make gen && make check` green; stub-harness suite (fake `systemctl`/`sudo`/`smartctl`/`sensors`/`df` + fake telegram sender logging, env seams `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`USER_SYSTEMD_DIR`/`SCHED_LEGACY_ENV`) covers all 5 policies (threshold cross/recover/no-repeat, onchange first/diff/same, onerror, always, never-silent), COMMAND literal-pipe parsing, enable/disable/status + orphan/legacy cleanup, migrate (incl. skip-existing + dry-run), and dispatch.
|
||||
- **2026-08-13** — `pos communication scrcpy` audio control: scrcpy already forwards device audio to the desktop by default (answer: yes, default is sound-to-desktop). Added `SCRCPY_AUDIO` config key (`pos config scrcpy`, default `true`): `false`/`no`/`0` → `--no-audio`, `true`/`yes`/`1` → nothing (default), anything else → error. Docs: POS_CONFIG header, POS.md config table, howto/communication.md Mirror section, HOWTO.md env row. Verified: harness +7 tests (47/47 green — false/true/yes/0/invalid/combined-order), `bash -n`, `make gen && make check` green.
|
||||
- **2026-08-13** — `pos communication scrcpy --new-display` support: new `SCRCPY_NEW_DISPLAY` config key (`pos config scrcpy`) — `true`/`yes` → bare `--new-display` (default size/dpi), `1920x1080`, `1920x1080/420` or `/240` → `--new-display=<value>`; inline validation in `_mirror` (err runs in the main shell, not a process-substitution subshell) rejects anything else with the accepted forms. Docs: POS_CONFIG header, POS.md command+config tables, howto/communication.md Mirror section, HOWTO.md env row, usage() example. CLI pass-through `pos communication scrcpy --new-display=1920x1080` also works verbatim. Verified: harness +8 tests (40/40 green — WxH, true, WxH/DPI, /DPI, invalid-rejected, env>config, combined order), `bash -n`, `make gen && make check` green.
|
||||
- **2026-08-13** — Fix `pos communication scrcpy` mirror failure on the real box (`ERROR: Unexpected additional argument:` on every mirror, bare or with flags): `_extra_flags()` ran `printf '%s\n'` with an empty array expansion, which prints one blank line; `_mirror()`'s `while read` turned that into an empty-string arg passed to scrcpy. Fix: `_extra_flags` now returns early when `SCRCPY_EXTRA_FLAGS` is empty (and `printf '%s\n' "${extra[@]}"` when set), and `_mirror` defensively skips blank entries (`[ -n "$f" ] && cmd+=("$f")`). Rebuilt the stub-PATH suite (`/tmp/scrcpy-run-test.sh`, outside the wiped `$TEST_DIR`) — 32/32 green incl. the regression (bare mirror → zero args to scrcpy) and EXTRA_FLAGS + passthrough mixed. `bash -n`, `make gen && make check` green.
|
||||
- **2026-08-13** — Fix scrcpy apt install on the live box: preinstall `apt install` failed with `Unable to locate package scrcpy` (Debian/Ubuntu need contrib/universe for `scrcpy`, and the apt build is older anyway). Removed `scrcpy` from `preinstall.sh` PACKAGES (kept `adb`); `scrcpy` now installs via the existing optional app `apps/media/scrcpy.sh` (GitHub latest, bundles adb) — docs (POS.md, howto/communication.md) and the tool's deps-guard error reworded to lead with that path. Re-verified: `bash -n`, stub suite 21/21, `make gen && make check` green.
|
||||
- **2026-08-13** — `pos communication scrcpy` (`bin/pos-communication-scrcpy`): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: bare `scrcpy` (mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints), `devices` (`adb devices -l`), `record [file] [--headless]` (default `$SCRCPY_RECORD_DIR/<device>_<date>.mp4`, `--headless` = `--no-playback` for headless servers), `tcpip [port]` (USB→wireless switch + prints `connect` with the auto-detected device IP), `connect <ip[:port]>` (adb connect + mirror `-s`), `push` (default `/sdcard/Download` = scrcpy's own default), `pull`, `screenshot` (`adb exec-out screencap -p` → PNG in RECORD_DIR), `info` (model/android/sdk/serial via getprop). Config scope `scrcpy` (`~/.config/linux_post_install/scrcpy.env`, `pos config scrcpy`): `SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS`, env-var precedence. Deps `scrcpy` + `adb` added to preinstall PACKAGES; docs note the apt build is older and point to the existing `apps/media/scrcpy.sh` app installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions: `# POS:`/`# POS_SUBCMDS:`/`# POS_CONFIG:` headers, deps guards before `-h|--help`, no stdin → no INTERACTIVE_CMDS. Verified: `bash -n`, stub-PATH suite `/tmp/opencode/scrcpy-run-test.sh` 21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes), `make gen && make check` green, dispatch via `pos communication scrcpy --help`. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable.
|
||||
- **2026-08-13** — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to `notify_send` (platform follows `NOTIFY_PLATFORM`, default Telegram) via `lib/notify.sh` sourced by `bin/pos-entertainment-send`; a **last-run state** is recorded per plugin (`~/.local/share/linux_post_install/entertainment/last/<plugin>` — rc + timestamp) on every non-`--print` run and shown by `pos entertainment status`, which also lists installed-but-not-enabled plugins; a send that fails **while fired by a timer** (gated on `$INVOCATION_ID`) additionally notifies the configured platforms. New **message-safe plugin lib** `lib/entertainment-plugin-lib.sh` (defines only `plugin_*`, never writes stdout — the stdout contract stays "message only"): `plugin_load_config` (entertainment.env + env precedence), `plugin_have`, `plugin_require`, `plugin_err`, `plugin_http_json <url> [--key <jq>] [-H <header>]` (curl `--max-time 20 --retry 2`); `weather`/`joke`/`gold` refactored onto it. `pos entertainment config` gains `get|unset|ls|edit` (edit via the shared `pos config` UI — added to `INTERACTIVE_CMDS`). **Tier 2**: new shared lib `lib/user-timers-lib.sh` (only `ut_*`: `ut_interval_to_oncalendar`, `ut_interval_label`, `ut_unit_name`, `ut_write_unit_pair` incl. `TimeoutStopSec=5s` + `Persistent` + network-online deps, `ut_ensure_linger`, `USER_SYSTEMD_DIR`) dedupes the systemd user-timer machinery between `lib/entertainment-lib.sh` and `lib/scheduler-lib.sh` (the latter's `sched_*` duplicates deleted; both source it; collides-with-nothing). `install.sh` Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: `bash -n` everywhere; smoke-tested in an isolated `HOME=/tmp/enttest` (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); `make gen && make check` green.
|
||||
- **2026-08-13** — Fast pos-unit shutdown: every systemd unit a pos tool writes (or `systemd/` ships) now sets `TimeoutStopSec=5s` (+ `KillMode=control-group` on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: `pos-communication-telegram-listener`, `pos-communication-matrix-listener` (also gained a `trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT` in `run_daemon` so stop returns sub-second), `pos-network-download` (aria2 + retry-healer units), `lib/scheduler-lib.sh` `sched_write_units`, `lib/entertainment-lib.sh` `write_units`, and `systemd/{ssh-agent,autostart,usb-automount}.service`. Legacy-unit cleanup: the repo no longer ships `pos-health.{service,timer}` / `pos-entertainment.service` (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the `pos-health.service` section + gating special-case, added a new **Stop behavior** section), POS.md, HOWTO.md, howto/system.md (now documents the `pos system schedule` job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a **Systemd units** convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: `bash -n` on all edited scripts; `make gen && make check` green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via `pos network download start`, `pos communication telegram listener --enable`, `pos system schedule enable <job>`, `pos entertainment enable <plugin>`, then `sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload`.
|
||||
- **2026-08-13** — Bootstrap output transparency (`install.sh` / `preinstall.sh` / `postinstall.sh`): removed the redundant `apt update` (preinstall.sh owns it — install.sh previously ran it twice, showing two identical `OK apt update` lines); Phase 2 now names what it installs — libs line (`libs -> /usr/local/bin (644): common.sh flags.sh …`), plugin names in the count line, x64_bin names, per-feature `feature installed/overwritten` + `feature flag set` logs with a `N features installed: …` summary — and the misleading `"47 scripts + libs"` label is fixed to `47 scripts + 6 libs` (the 6 libs were outside the counter); preinstall prints `Installing N packages (apt install -y):` with the 40-name list wrapped at 80 cols; postinstall now logs silent skips — `config/authorized_keys is empty — nothing to add` (empty file previously looped zero times with no message), `schedule.d already exists, keeping it` (restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-service `service enabled: <name>` line. No output-layer changes (no `--verbose`, no log file — decided scope). Verified: `bash -n` + `--dry-run` smokes of phases 1/2/3 showing every new line (learned: `install.sh:19` hardcodes `export DRY_RUN=0`, so an env `DRY_RUN=1` is ignored — the flag `--dry-run` is required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168), `make gen && make check` green. usb-automount left live (user choice).
|
||||
- **2026-08-13** — `usb-automount` feature, integrated exactly like `autostart`: `features/usb-automount.sh` (root-guard re-exec via sudo; first-root-run self-install of udev rule `/etc/udev/rules.d/99-usb-automount.rules` — `ACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service"` — + `udevadm control --reload` + `trigger --subsystem-match=block`; an existing/edited rule is never overwritten; scans `lsblk -J` for unmounted removable partitions/raw whole-disk filesystems, mounts each at `/media/<label>` — vfat/exfat/ntfs world-writable via `-o umask=000`, fallback plain mount, label-collision bump `-2`/`-3`, no label → `usb-<name>`, logs `${HOME:-/root}/.usb-automount.log`) + `systemd/usb-automount.service` (`Type=oneshot`, `WantedBy=multi-user.target` — boot + hotplug + manual `systemctl start usb-automount`), gated in postinstall.sh's systemd loop exactly like autostart (`flag_is_set usb-automount` → skip with hint). Purpose: a plugged-in stick is auto-mounted world-writable, ready for `pos system backup`'s post-verify USB copy. Docs: SYSTEMD.md (service section + gating code block), SCRIPTS.md (feature section + systemd bullet + TOC), AGENT_Context tree + filetable rows (postinstall.sh count corrected 152→163 — it was already 6 lines stale), README index rows. Verified with a stub suite (`/tmp/opencode/usb-automount-test` — lsblk JSON fixtures, mount/mountpoint/udevadm/sudo stubs, `MOUNT_BASE`/`UDEV_RULES_DIR` seams, HOME isolation): 47/47 green. `make gen && make check` green. Gotcha learned: `${VAR:-{...}}` with a `{` inside the parameter-expansion default mis-parses in bash (emits a stray `}` — printf of a multi-line value showed `}}`); avoid braces in `:-` defaults.
|
||||
- **2026-08-13** — `pos system backup` copies the finished backup to a USB stick. Detection runs **after** the archive verifies (so a stick plugged in while the backup ran is found; if none is mounted, one re-scan prompt before giving up — `s` skips, EOF from cron skips silently, rc stays 0). Single stick → y/N confirm; several → numbered pick (0 = skip). Copy lands in `<usb>/backups/` (mkdir -p; `chmod 600` best-effort — vfat chmod failures warn, never fail), and the transfer is proven 100% by **sha256 source-vs-copy** before any success is announced: mismatch → warn with both hashes + `notify_send "USB copy FAILED…"` + rc=1 (the ERR trap is re-armed mid-script so a USB-phase failure no longer notifies "Backup FAILED"). Detection: `lsblk -J` → recursive jq filter (rm==true && mounted && type part|disk, space-safe via JSON) or pinned `BACKUP_USB_ROOT` seam (= `<root>/backups/`, skips detection — also the test seam). Docs: usage() Environment, POS.md backup row, howto/system.md (USB section + env table + mismatch troubleshooting), DEV.md system.env list. Verified with a stub suite (`/tmp/opencode/backup-test` — sudo/gpg/lsblk/sender stubs, HOME isolation, per-test lsblk JSON fixtures, corrupting-cp + vfat-chmod override stubs): 40/40 green (skip s/EOF, seam y/n, detect single, multi pick 2/0, re-scan after replug, corrupt copy rc=1 + honest notify, vfat tolerance). `make gen && make check` green.
|
||||
- **2026-08-13** — `pos share smb-server share` now guards the two common `NT_STATUS_ACCESS_DENIED` causes at share time (warnings only): `--users` entries missing from the Samba passdb (`pdbedit -L`, cut to user column, `grep -qxF` per user — pointer to `pos share smb-server adduser <user>`), and ancestors of the share path lacking `other:+x` traversal (sticky dirs like `/tmp` count as traversable via the `t` slot; fix hint `chmod o+x <dir>`). Both wired into the `share` case after `require_root_dir`; howto/share.md SMB section + troubleshooting updated. Rooted in `reports/bug-report-smb-server-access-denied.md` (committed as the spec). Verified with a stub-PATH suite (`/tmp/opencode/smb-test` — pdbedit/systemctl/smbcontrol/testparm/smbpasswd stubs, `SMB_CONF` seam): 16/16 green.
|
||||
- **2026-08-13** — Docs hardening from the schedule-session review (sole-developer call: terse, session-learned). DEV.md §7 env-seam registry now lists `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh`) + the scheduler's `SCHEDULE_*` seams, and documents the missing-`:-`-guard gotcha (a `VAR="${XDG…:-…}"` without leading `VAR:-` overrides the seam — stub runs then silently write to the real `$HOME`; fix: `USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-…}"`). New-tool test checklist gains an env-seam review step (grep for unguarded config writes + prove with `VAR=/tmp/x`). §7 notes stub harnesses are throwaway by design — build in `/tmp/opencode/<tool>-test/`, leave there, keep only the pattern. howto/schedule.md documents that `migrate` copies the rule LHS **verbatim** as `COMMAND` (old tool never had `disk root`/`loadavg` shorthands — rewrite those jobs with real commands). `make check` green.
|
||||
- **2026-08-14** — Gitea Actions gate is now **live and green end-to-end**: act_runner (v0.6.1, labels `ubuntu-latest`) registered on `100.100.1.2` (`~/srv/gitea/runner/`, standalone compose next to the ScaleTail gitea; `CONFIG_FILE=/config.yaml` env required or `run.sh` never reads the config; `--add-host gitea.skink-platy.ts.net:100.111.241.54` so the job container reaches gitea). First real runs **caught a deterministic gen-drift**: plain `sort` in `scripts/gen-docs.sh` is locale-dependent (category-less tool keys start with `|`, which collates after letters under the CI container's locale → `pos-config`/`pos-tree` reordered), so the `git diff --exit-code` step failed. Fixed with `export LC_ALL=C` in gen-docs.sh (byte-order sort) + regenerated `DOC/AGENT_Context_Project.md` (config/tree now sort after the letter categories); `make check` OK, `make lint` 0 FAIL / 0 WARN. Live CI verdicts: the run for `e0b5b11` (workflow commit) and the empty trigger `98a767c` both **FAILED** on the drift; the run for `9d058b7` (the fix) **SUCCEEDED** (`🏁 Job succeeded`).
|
||||
- **2026-08-14** — Gitea Actions gate added: `.gitea/workflows/lint.yml` runs `make gen` + `git diff --exit-code` (gen-drift) + `make check` + `make lint` on every push/PR. Verified locally the exact four steps pass (gen idempotent, check OK, lint 0 FAIL / 0 WARN). "no CI" lines updated in AGENTS.md (Quick facts → CI bullet, notes a registered act_runner is required) and DEV.md (stub harnesses note: CI runs static gates only, not behaviour suites). Gitea 1.26.4 confirmed reachable; runner registration completed the same day (see the entry above).
|
||||
- **2026-08-14** — Convention-drift maintenance fix session (completed the audit backlog `MAINTENANCE.md`, M-001..M-023, all VERIFIED; gate `scripts/lint-conventions.sh` + `make lint` now 0 FAIL / 0 WARN; `make gen && make check` green). P0 bugs: M-002/003/004 added `docker-compose docker-vbox network-hotspot` to `INTERACTIVE_CMDS` (stdin/log-pipe prompt swallow); M-005 `install.sh --steps` now expands documented `N-M` ranges via `normalize_steps_spec()` (dry-run verified); M-006 feature-vs-docs decision: `--send`/`--markdown` **not restored** (health is a console-only reporter by design since fe7708f; scheduler `NOTIFY=always` covers delivery) — 5 docs corrected instead; M-007 `lib/notify.sh:57` fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved **before** `-h|--help` in docker-health/docker-ps (converted to `command -v X || err`), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a `--dry-run` pre-scan preserving the documented no-deps preview); system-health documented as the sanctioned graceful-degradation no-guard pattern in DEV.md — lint refined accordingly (`first_guard_line` only matches real guards; `first_line` skips comments; precision fixes, not weakenings); M-015 system-firewall gained `usage()`+`-h|--help` (root-gated first; verified via sudo); M-016 `ffmpeg` added to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019 `chmod +x apps/media/scrcpy.sh`; M-020 `SCALE_DIR`/`CONFIG_ENV` `:-` seams in pos-docker-compose (verified via overrides; follow-on fix: `DIM` color var missing from common.sh crashed `pos docker compose config` — added it); M-021 `CONFIG_DIR` centralized as the canonical XDG-aware seam in common.sh, per-file duplicates dropped (standalone-sourced notify.sh/config-ui.sh/matrix+telegram tools keep an identical guarded copy — "no shared lib? inline fallbacks"); M-022 `plugin_*` prefix collision resolved by renaming the internal registry helpers to `ent_plugin_*` (the documented plugin-authoring API `plugin_have`/`plugin_require`/`plugin_load_config`/`plugin_http_json` kept for user plugins); M-023 six tools (pos-config, pos-tree, pos-entertainment-{config,enable,disable,status}) now filename-referenced in DOC/POS.md. Hand-maintained AGENT_Context line-count rows bumped (install.sh 223→248, preinstall 75→76, common.sh 144→151, notify.sh 76→87 stale-corrected, autostart 14→50, usb-automount 134→138); `make lint` target wired in the Makefile. `MAINTENANCE.md` kept as the working record (uncommitted by design).
|
||||
- **2026-08-14** — `pos system backup` optional encryption (`--no-encrypt` flag + `BACKUP_ENCRYPT=0` env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified `.tar.gz` with no password prompt (headless/cron safe); encrypt path unchanged (prompt → gpg AES-256 → decrypt-verify; the gpg dep-guard moved into the encrypt branch so plain backups no longer require `gnupg`). Arg parsing rewritten as a loop over `"$@"` so `pos system backup <folder> --no-encrypt` works with the flag after the folder; usage() documents all three forms + the plain artifact name; `# POS_FLAGS: --service --no-encrypt`; `config/system.env` template gains `#BACKUP_ENCRYPT=0`; POS.md row + howto/system.md section updated. Verified: stub suite +2 cases (T18 flag / T19 env: plain .tar.gz artifact, gpg never called via `$GPG_CALLED`, USB copy + sha256 of the plain archive, notify wording) — 65/65 green; `bash -n`, `make gen && make check`, `make lint` 0 FAIL / 0 WARN.
|
||||
- **2026-08-14** — CI green-check via plain git (no SSH to the runner, no API tokens — user chose "CI tags + git ls-remote" + "scripts/ci-status.sh helper"): `.gitea/workflows/lint.yml` scoped to `on: push: branches: [main]` (tag pushes no longer re-trigger it) and the gate step now reports its own outcome as a lightweight tag — `ci-ok/$GITHUB_SHA` on success / `ci-fail/$GITHUB_SHA` on failure, pushed over HTTP with the jobs automatic `GITEA_TOKEN` to `http://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git` (runner container already host-maps that hostname to 100.111.241.54); `steps.gates.conclusion` decides ok/fail, `if: always()` (guarded to `push` events) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executable `scripts/ci-status.sh [--wait] [<sha>]` reads the tags via `git ls-remote` (origin, `CI_STATUS_REMOTE` override): GREEN (0) / RED (1) / PENDING (2); `--wait` polls every 10s up to 10 min. DEV.md §CI gains a "Checking green without SSH" bullet. Verified: `bash -n`, yaml-parse OK, `make gen && make check`, `make lint` 0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret).
|
||||
- **2026-08-14** — `pos media sync` (`bin/pos-media-sync`) — incremental Music → USB sync, plus the shared USB layer it builds on. **New lib `lib/usb-lib.sh`** (194 lines, installed by install.sh): `usb_detect` (lsblk JSON, TRAN + lsusb/by-id cross-check → `USB_MOUNTED`/`USB_UNMOUNTED`), `usb_related_present`, `usb_mount_offer` (`/media/<label>` mount-offer, `usb-automount` scheme), `usb_pick_root <prefix> <subfolder> <giveup-msg>` (detect → mount-offer → single/multi picker → `USB_ROOT`); seams `USB_MOUNT_BASE`/`USB_BYID` with `BACKUP_MOUNT_BASE`/`BACKUP_USB_BYID` aliases so existing `system.env` lines keep working; TRAN-fallback warning deduped to once per scan. `pos-system-backup` refactored onto it (216 lines, was 364) — re-ran the backup stub suite: 65/65 green. **Sync tool**: add/update only, never deletes (user choice); `--mp3`/`--mp4` filter (neither = both), `--source <dir>` (default `MEDIA_SYNC_SOURCE`/`$HOME/Music`), `--dry-run` preview with counts; copies missing/changed (size/mtime) files into `<usb>/Music/` (`MEDIA_SYNC_DEST`) preserving the tree via `cp --preserve=timestamps`; result notified via `lib/notify.sh`; `media-sync` added to `INTERACTIVE_CMDS`; deps guards (`lsblk`/`jq`) before `-h|--help`. Docs: POS.md media row, howto/media.md section, SCRIPTS.md lib section + Phase-2 lib list, system.env seams, DEV.md env-seam registry, AGENT_Context Common Tasks + hand-maintained lib row (+usb-lib 194) + gen'd tree/dispatch/filetable/flags. Verified: new stub suite `/tmp/opencode/msync-run.sh` 46/46 green (fresh/no-op/update/filter/dry-run/multi-stick/mount-offer/no-USB skip/never-delete/--source/TRAN-fallback/notify) — caught and fixed an inverted `needs_copy` return; `make gen && make check`, `make lint` 0 FAIL / 0 WARN; dispatch via `pos media sync --help` + `pos media` listing.
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
|
||||
<!-- GEN:START docmap -->
|
||||
| ## 1. Project Overview | 28–43 |
|
||||
| ## 2. Directory Structure | 44–193 |
|
||||
| ## 3. Installation Flow | 194–245 |
|
||||
| ## 4. The `pos` CLI System | 246–316 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 317–348 |
|
||||
| ## 6. Docker Compose / ScaleTail | 349–391 |
|
||||
| ## 7. Optional Apps (`apps/`) | 392–421 |
|
||||
| ## 8. Entertainment Module | 422–435 |
|
||||
| ## 9. Systemd Services | 436–447 |
|
||||
| ## 10. Configuration Files | 448–474 |
|
||||
| ## 11. Coding Conventions | 475–507 |
|
||||
| ## 12. Development Workflow | 508–560 |
|
||||
| ## 13. Key File Quick Reference | 561–621 |
|
||||
| ## 14. Common Tasks for Agents | 622–652 |
|
||||
| ## 2. Directory Structure | 44–194 |
|
||||
| ## 3. Installation Flow | 195–246 |
|
||||
| ## 4. The `pos` CLI System | 247–318 |
|
||||
| ## 5. Shared Library — `lib/common.sh` | 319–350 |
|
||||
| ## 6. Docker Compose / ScaleTail | 351–393 |
|
||||
| ## 7. Optional Apps (`apps/`) | 394–423 |
|
||||
| ## 8. Entertainment Module | 424–437 |
|
||||
| ## 9. Systemd Services | 438–449 |
|
||||
| ## 10. Configuration Files | 450–476 |
|
||||
| ## 11. Coding Conventions | 477–509 |
|
||||
| ## 12. Development Workflow | 510–562 |
|
||||
| ## 13. Key File Quick Reference | 563–625 |
|
||||
| ## 14. Common Tasks for Agents | 626–657 |
|
||||
<!-- GEN:END docmap -->
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -67,7 +67,6 @@ Linux_post_install/
|
||||
│ ├── pos-communication-scrcpy # Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
|
||||
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
||||
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
|
||||
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
||||
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||
@@ -79,6 +78,7 @@ Linux_post_install/
|
||||
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||
│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select)
|
||||
│ ├── pos-media-sync # Incremental Music → USB sync (mp3/mp4, add/update only)
|
||||
│ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view
|
||||
│ ├── pos-network-download # aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
|
||||
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||
@@ -94,6 +94,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-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||
│ ├── pos-tree # Show the pos CLI command tree: categories, commands, and subcommands
|
||||
<!-- GEN:END tree -->
|
||||
│ ├── flag-reader # Inspect feature flags (list/status/--raw)
|
||||
@@ -268,7 +269,6 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
||||
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
@@ -280,6 +280,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
||||
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state |
|
||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
||||
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) |
|
||||
| media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) |
|
||||
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||
| network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
@@ -295,6 +296,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 |
|
||||
| | 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 |
|
||||
<!-- GEN:END dispatch -->
|
||||
|
||||
@@ -530,7 +532,7 @@ System-wide flag store at `/usr/local/share/linux_post_install/flags/`:
|
||||
4. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed); non-apt/manual installers → `command -v` guard in the tool instead
|
||||
5. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets); runtime tool config → `~/.config/linux_post_install/<tool>.env` (600)
|
||||
6. Update docs: `DOC/POS.md` (section table + detail — hand-written); `DOC/HOWTO.md` index row + a section in `DOC/howto/<category>.md` (recipes/troubleshooting); `DOC/AGENT_Context_Project.md` generated sections (bin tree, dispatch table, self-contained list, line-count table) and completion flags update via `make gen` — never hand-edit between `GEN:START`/`GEN:END` markers, but hand-add a row to the "Common Tasks for Agents" table; `AGENTS.md` Quick facts if a structural fact changed; root `README.md` only if the category list changes; move the task to `AGENT_TODO.md` Done (dated) in the same commit
|
||||
7. Test: `make gen && make check` — `make check` (bash -n + doc/code sync + smoke) is the definition of done; also `bin/pos help <full command> && bin/pos <category> --help`. For tools needing root/systemd/absent deps, behaviour-test via env-override paths + stub PATH (see DEV.md "Testing tools that need root / systemd / missing deps")
|
||||
7. Test: `make gen && make check && make lint` — `make check` (bash -n + doc/code sync + smoke) and `make lint` (0 FAIL / 0 WARN, `scripts/lint-conventions.sh`) together are the definition of done; also `bin/pos help <full command> && bin/pos <category> --help`. For tools needing root/systemd/absent deps, behaviour-test via env-override paths + stub PATH (see DEV.md "Testing tools that need root / systemd / missing deps"). Pushing to Gitea re-runs the same four gates on the live Actions runner (`.gitea/workflows/lint.yml`) — a red run is a merge-blocker.
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -562,58 +564,60 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `install.sh` | 223 | Main orchestrator — 4 phases with CLI flags, `--feature`, prebuilt arch bins |
|
||||
| `preinstall.sh` | 75 | System packages + hotspot deps + yt-dlp + fail2ban |
|
||||
| `install.sh` | 248 | Main orchestrator — 4 phases with CLI flags, `--feature`, prebuilt arch bins |
|
||||
| `preinstall.sh` | 76 | System packages + hotspot deps + yt-dlp + fail2ban |
|
||||
| `postinstall.sh` | 168 | fail2ban config, PATH, bash completion, systemd (flag-gated) |
|
||||
| `lib/common.sh` | 144 | Shared library (log/warn/err/run/spawn, dry-run aware, `load_system_env`) |
|
||||
| `lib/common.sh` | 151 | Shared library (log/warn/err/run/spawn, dry-run aware, `load_system_env`, CONFIG_DIR) |
|
||||
| `lib/flags.sh` | 60 | Feature flag store (set/clear/is_set/value/list/status) |
|
||||
| `lib/notify.sh` | 76 | Multi-platform alerting (`notify_send`) — opt-in source, silent-fails |
|
||||
| `lib/notify.sh` | 87 | Multi-platform alerting (`notify_send`) — opt-in source, silent-fails |
|
||||
| `lib/entertainment-lib.sh` | 311 | Entertainment module lib (ENABLED parsing, last-run state, scheduler sync via user-timers-lib) |
|
||||
| `lib/entertainment-plugin-lib.sh` | 67 | Message-safe helpers for plugins (config load, require, fetch+retry) — plugins MAY source it |
|
||||
| `lib/scheduler-lib.sh` | 760 | Scheduler lib (job parsing, notify policies, per-job user timers via user-timers-lib, legacy migrate) |
|
||||
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
|
||||
| `lib/usb-lib.sh` | 194 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`) — used by `pos system backup` + `pos media sync` |
|
||||
| `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 |
|
||||
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
||||
| `features/usb-automount.sh` | 134 | USB automount feature (udev rule + flag-gated service) |
|
||||
| `features/autostart.sh` | 50 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
||||
| `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) |
|
||||
<!-- GEN:START filetable -->
|
||||
| `bin/pos` | 292 | CLI dispatcher with smart arg matching + logging + category help |
|
||||
| `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) |
|
||||
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
|
||||
| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||
| `bin/pos-communication-scrcpy` | 239 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| `bin/pos-communication-scrcpy` | 254 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
|
||||
| `bin/pos-communication-telegram-listener` | 566 | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||
| `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| `bin/pos-docker-ps` | 128 | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
| `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||
| `bin/pos-docker-ps` | 125 | Enhanced container overview (health, IPs, ports, uptime) |
|
||||
| `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||
| `bin/pos-entertainment-config` | 143 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
|
||||
| `bin/pos-entertainment-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) |
|
||||
| `bin/pos-entertainment-enable` | 49 | Enable an auto-trigger for a plugin on a schedule |
|
||||
| `bin/pos-entertainment-send` | 95 | Run a public-API plugin and send its output via the configured notify platforms |
|
||||
| `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state |
|
||||
| `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) |
|
||||
| `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) |
|
||||
| `bin/pos-media-mp3` | 86 | Download audio as MP3 (yt-dlp) |
|
||||
| `bin/pos-media-mp4` | 132 | Download video as MP4 (smart/interactive format select) |
|
||||
| `bin/pos-media-sync` | 138 | Incremental Music → USB sync (mp3/mp4, add/update only) |
|
||||
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||
| `bin/pos-network-download` | 952 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| `bin/pos-network-download` | 951 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
|
||||
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
|
||||
| `bin/pos-network-scan` | 271 | Parallel ping sweep of CIDR |
|
||||
| `bin/pos-network-scan` | 272 | Parallel ping sweep of CIDR |
|
||||
| `bin/pos-share-nfs-client` | 138 | Mount NFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-nfs-server` | 134 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) |
|
||||
| `bin/pos-share-smb-client` | 183 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-client` | 217 | Mount SMB/CIFS shares (ephemeral or persistent systemd mount units) |
|
||||
| `bin/pos-share-smb-server` | 253 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
|
||||
| `bin/pos-share-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||
| `bin/pos-system-backup` | 220 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-firewall` | 291 | Interactive UFW management |
|
||||
| `bin/pos-system-backup` | 216 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||
| `bin/pos-system-firewall` | 308 | 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` | 81 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| `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` | 292 | Dynamic bash completion |
|
||||
| `completions/pos.bash` | 293 | Dynamic bash completion |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
@@ -643,6 +647,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| Modify NFS share logic | Edit `bin/pos-share-nfs-server` / `bin/pos-share-nfs-client` |
|
||||
| Modify SMB share logic | Edit `bin/pos-share-smb-server` / `bin/pos-share-smb-client` |
|
||||
| Modify scrcpy mirroring logic | Edit `bin/pos-communication-scrcpy` (config scope `scrcpy` via `pos config scrcpy`; `SCRCPY_*` keys in `~/.config/linux_post_install/scrcpy.env`) |
|
||||
| Modify Music→USB sync logic | Edit `bin/pos-media-sync` / shared USB layer `lib/usb-lib.sh` (seams `MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST`/`USB_MOUNT_BASE`/`USB_BYID` in `~/.config/linux_post_install/system.env`) |
|
||||
| Modify the scheduler / scheduled jobs | Edit `bin/pos-system-schedule` / `lib/scheduler-lib.sh` (jobs in `~/.config/linux_post_install/schedule.d/`) |
|
||||
| Modify AI/Gemini logic | Edit `bin/pos-ai-gemini` (config scope `ai` via `pos config ai`; `AI_GEMINI_API_KEY`/`AI_GEMINI_MODEL` in `~/.config/linux_post_install/ai.env`) |
|
||||
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
|
||||
|
||||
+107
-4
@@ -108,6 +108,7 @@ esac
|
||||
- Strict mode: `set -euo pipefail`
|
||||
- `--help` flag: accept `-h` / `--help` via `case` pattern
|
||||
- **Deps guards run before `--help`:** `command -v <bin> &>/dev/null || err "… (install <pkg>)"` lines sit at the top of the script, **before** the `-h|--help` case — so `--help` also errors when a dependency is missing. This matches every existing deps-gated tool; keep it that way.
|
||||
- Exception — tools with **no required deps** (every check degrades gracefully): `pos system health` probes binaries at runtime (`if command -v systemctl; then …`) and needs no guard. The lint (`scripts/lint-conventions.sh`) only enforces guard-before-help for lines that are actual guards (`command -v … ||`, `if ! command -v`, `command -v … \` continuation), never for graceful-degradation probes. If you add a tool like this, keep all checks optional and note it in `usage()`.
|
||||
- Shared library: always source `common.sh` for colors, logging, spinners
|
||||
- Exit codes: `0` success, `1` error
|
||||
- No shared lib? Inline fallbacks:
|
||||
@@ -153,7 +154,7 @@ Two kinds of config, don't mix them up:
|
||||
|
||||
- **Machine defaults shipped by the installer:** place the file in `config/` and add copy logic to `postinstall.sh`. If it contains secrets, add to `.gitignore` and document in `DOC/`.
|
||||
- **Runtime tool config set by the user:** `~/.config/linux_post_install/<tool>.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`), `pos-communication-telegram-sender` (`telegram.env`, edited via `pos config telegram` — token masked), and the shared ones below. Never store tokens in the repo.
|
||||
- `system.env` — shared "system" settings loaded by `pos-system-*` tools via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `HEALTH_BACKUP_MAX_AGE_DAYS`). Env already exported wins over the file.
|
||||
- `system.env` — shared "system" settings loaded via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `BACKUP_MOUNT_BASE`, `BACKUP_USB_BYID`, `HEALTH_BACKUP_MAX_AGE_DAYS`, plus `USB_MOUNT_BASE`/`USB_BYID`/`MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST` for `pos media sync`). Env already exported wins over the file.
|
||||
- `notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`), read by `lib/notify.sh`.
|
||||
|
||||
### 5. Add SSH keys (if needed)
|
||||
@@ -183,21 +184,32 @@ bin/pos help <full command> # confirm dispatch works
|
||||
bin/pos <category> --help # confirm category listing includes the new tool (first tool in a new category)
|
||||
make gen # regenerate doc tables + completion flags
|
||||
make check # full self-consistency gate (syntax, exec bits, doc/code sync, smoke)
|
||||
make lint # convention gate (scripts/lint-conventions.sh) — must end 0 FAIL, 0 WARN
|
||||
```
|
||||
|
||||
`make check` is the definition of done — the same check runs as a pre-commit hook once you've run `make hook`.
|
||||
`make check` + `make lint` (0 FAIL / 0 WARN) are the definition of done. `make check` is also run as a pre-commit hook once you've run `make hook`; `make lint` is not part of the hook — run it yourself. Pushing to Gitea re-runs all four gates on the live Actions runner (see `CI: Gitea Actions Gate` below) — a red run is a merge-blocker.
|
||||
|
||||
### Testing tools that need root / systemd / missing deps
|
||||
|
||||
`make check` only proves syntax, exec bits, doc sync and dispatch — not behaviour. For tools that need `sudo`, systemd, or binaries absent from the dev box (samba, usbsrv, …), test them end-to-end with two patterns:
|
||||
|
||||
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), and the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`). Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
|
||||
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`), and the USB layer shared by `pos system backup` + `pos media sync`: `USB_MOUNT_BASE` (default `/media`, keeps `BACKUP_MOUNT_BASE` as an alias) and `USB_BYID` (default `/dev/disk/by-id`, keeps `BACKUP_USB_BYID` as an alias) — both guarded as `VAR="${VAR:-${BACKUP_…:-default}}"` in `lib/usb-lib.sh` so existing config lines keep working. Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
|
||||
- **Stub PATH.** Create a temp dir with fake binaries, then run the tool with `PATH="$stubs:$PATH"`: fake `sudo` → `exec "$@"`; fake `systemctl`/`smbcontrol`/`mount.cifs` → echo their args; fake `testparm` → `cat` the file back (so validation passes); fake `systemd-escape` → print a fixed name. Assert on output **and** exit codes — happy path plus each failure path (`err` sets rc=1).
|
||||
- **Interactive prompts** (`read … </dev/tty`): drive them with a PTY — `printf 'answer\n' | script -qec "cmd" /dev/null` — then assert the side effect (e.g. the chmod-600 creds file lands with the right mode).
|
||||
|
||||
Example (session-learned): `PATH=/tmp/stubs:$PATH SMB_CONF=/tmp/smb.conf bin/pos-share-smb-server share /tmp/media …`.
|
||||
|
||||
Stub harnesses are **throwaway by design**: no `tests/` dir and no CI in this repo — build them outside the project (`/tmp/opencode/<tool>-test/`: `stubs/` + `run-tests.sh` with a `check "desc" "expected" "$actual"` helper and a pass/fail count), run them, then leave them in `/tmp`. Only the *pattern* above is worth keeping in the repo.
|
||||
Stub harnesses are **throwaway by design**: no `tests/` dir in this repo — build them outside the project (`/tmp/opencode/<tool>-test/`: `stubs/` + `run-tests.sh` with a `check "desc" "expected" "$actual"` helper and a pass/fail count), run them, then leave them in `/tmp`. Only the *pattern* above is worth keeping in the repo. (CI — `.gitea/workflows/lint.yml`, live act_runner — runs the *static* gates `make gen`+`git diff --exit-code`/`make check`/`make lint` on every push/PR; it does not run behaviour suites.)
|
||||
|
||||
**Stub-harness gotchas (session-learned, `pos system backup` USB-detection suite).** Each red check means exactly one assumption — in the tool *or* the harness — is wrong; keep the diagnosis cheap by copying the run's `out.log` to a per-test file and asserting on artifacts (`fake_state`, `.gpg` on disk, `sends.log`, exit code), then deciding which side lied:
|
||||
|
||||
- **Layout: the harness must live *outside* the sandbox it wipes.** If `fresh()` does `rm -rf "$TEST_DIR"`, the runner and the stubs cannot live inside `$TEST_DIR` or they get deleted mid-run. Prefer siblings: `/tmp/opencode/<tool>-run.sh` + `/tmp/opencode/<tool>-stubs/` + `/tmp/opencode/<tool>-test/` (sandbox wiped per test).
|
||||
- **Field separators: never `IFS=$'\t'` on JSON-derived data.** `read` treats IFS *whitespace* specially and collapses consecutive delimiters, so an empty JSON field (null `tran`, null `mountpoint`) shifts every following column and detection silently misfires. Emit a non-whitespace separator from jq (`… | join("\u001f")`) and read with `IFS=$'\x1f'`.
|
||||
- **`!` is not a command: `check "x" "$@"` with `! grep …` runs `!` as a binary (rc 127).** For "nothing present" prefer `test -z "$(grep … )"` — note `grep -qv` on an *empty* file still exits 1 (zero lines selected), so it fails the "nothing was written" case.
|
||||
- **`read -rp` prompts vanish when stdin is a pipe.** Bash suppresses the prompt text when stdin isn't a tty, so never assert on prompt strings in piped runs — assert on the side effect.
|
||||
- **Fakes that shell out must call the *real* binary, not the stub on PATH.** A fake `gpg` that ran `cp` picked up the corrupting fake `cp` and broke the tool's encryption step instead of the USB-copy step under test. Resolve the real one: `real() { for d in /usr/bin /bin; do [ -x "$d/$1" ] && { printf '%s' "$d/$1"; return; }; done; }` then `"$(real cp)" …`.
|
||||
- **Scope failure-injection fakes, don't make them global.** A "chmod fails" flag hit *every* `chmod` the tool runs (it chmods the archive too) and aborted the run before the section under test. Match the target instead: only fail for args under the seam path (e.g. `[[ "$target" == "$BACKUP_MOUNT_BASE/"* ]]`).
|
||||
- **Fixtures must genuinely exercise the branch.** A fixture builder that hardcodes `"tran":"usb"` means the "TRAN empty → cross-check" path never runs and its test passes for the wrong reason. Check the discriminating field actually varies (add a `_notran` builder, a card-reader `sata` fixture, etc.).
|
||||
|
||||
---
|
||||
|
||||
@@ -303,6 +315,97 @@ Place it in `apps/<category>/<name>.sh`. It auto-appears in the picker — no re
|
||||
|
||||
---
|
||||
|
||||
## Convention Lint Gate
|
||||
|
||||
`scripts/lint-conventions.sh` is the automated convention gate — it encodes the
|
||||
rules in this document so drift is caught by the machine, not the next audit.
|
||||
Run it with `make lint` (or `./scripts/lint-conventions.sh`). FAIL = definite
|
||||
violation (fix it before committing), WARN = manual review needed. Exit code is
|
||||
non-zero when any FAIL exists.
|
||||
|
||||
Check classes (all heuristic-based; heredocs, `${...}` brace-counting, `while`
|
||||
loop stdin, `/dev/tty` reads and `command -v` fallbacks are excluded):
|
||||
|
||||
- **Shebang / strict mode** (FAIL) — every shell file (`bin/*`, `install.sh`,
|
||||
`preinstall.sh`, `postinstall.sh`, `features/*`, `apps/*`, `templates/*`,
|
||||
`scripts/*`) starts with `#!/usr/bin/env bash` and has `set -euo pipefail`
|
||||
(libs are sourced, so they're exempt).
|
||||
- **Exec bits** (FAIL) — `bin/pos-*` and `entertainment/*.sh` committed as
|
||||
`100755` (`chmod +x`).
|
||||
- **`# POS:` header** (FAIL) — every `bin/pos-*` carries it with the em-dash
|
||||
separator (`# POS: <cat> <cmd> — <desc>`); a header past line ~6 is a WARN.
|
||||
- **`-h|--help`** (FAIL) — every `bin/pos-*` handles it via `case`.
|
||||
- **Deps guards before help** (FAIL) — the first real guard (`command -v X …
|
||||
|| err`, `if ! command -v X …`, multi-line `\` continuation) must sit before
|
||||
the `-h|--help` dispatch, so help errors on a box missing the dependency.
|
||||
Graceful-degradation probes (`if command -v X; then …`) are not guards.
|
||||
- **Top-level `local`** (WARN) — `local` at brace-depth 0 outside a function is
|
||||
invalid bash.
|
||||
- **stdin ⇄ `INTERACTIVE_CMDS`** (FAIL) — a tool that reads stdin must be in
|
||||
`INTERACTIVE_CMDS` in `bin/pos` (else the logging tee swallows/hangs the
|
||||
prompt); every entry must also have a matching `bin/pos-<entry>` tool.
|
||||
- **DOC/POS.md coverage** (WARN) — each `bin/pos-*` referenced in `DOC/POS.md`.
|
||||
- **Entertainment plugins** (FAIL) — must carry `# POS_PLUGIN:` and must NOT
|
||||
source `lib/common.sh` (stdout is the message).
|
||||
- **Apps** (FAIL) — each `apps/*` script has `uninstall_<name>()` and an
|
||||
`uninstall` dispatch case.
|
||||
- **Systemd units** (WARN) — `TimeoutStopSec=` and `[Install] WantedBy=`.
|
||||
- **Legacy wrappers** (FAIL/WARN) — `bin/wr-*`, `mp3`, `mp4`, `vbox`,
|
||||
`ssh-load-all` must forward to `pos` (FAIL if not); >12 lines or a `case`
|
||||
statement is a WARN (thin forwarder only).
|
||||
- **Secrets** (WARN) — literal `…TOKEN=/…SECRET=/…KEY=…` assignments are
|
||||
flagged for manual review (env guards, config reads and runtime generation
|
||||
are excluded).
|
||||
- **Env seams** (WARN) — writes to `/etc/`, `$HOME`, `/usr/local` are flagged
|
||||
unless guarded (`command -v` or `|| echo`), i.e. the write needs a
|
||||
`VAR="${VAR:-path}"` test seam.
|
||||
|
||||
If a rule is genuinely wrong for a new case (as happened with
|
||||
graceful-degradation probes in system-health), refine the heuristic — never
|
||||
weaken it — and note the change in `MAINTENANCE.md`'s lint section.
|
||||
|
||||
---
|
||||
|
||||
## CI: Gitea Actions Gate
|
||||
|
||||
`.gitea/workflows/lint.yml` re-runs the four gates on every `push` and
|
||||
`pull_request`: `make gen`, `git diff --exit-code` (gen drift), `make check`,
|
||||
`make lint`. A red run is a merge-blocker; runs are visible under Gitea →
|
||||
Actions.
|
||||
|
||||
- **Runner** — act_runner v0.6.1 (`linux-post-install`, labels `ubuntu-latest` →
|
||||
job image `node:20-bullseye`) is registered on the Gitea host and always on:
|
||||
compose project `~/srv/gitea/runner/` (`docker compose up -d`,
|
||||
`restart: unless-stopped`), standalone next to the ScaleTail gitea compose.
|
||||
- **Gotchas (session-learned):**
|
||||
- act_runner's `run.sh` `cd`s into `/data` and only reads the config when the
|
||||
`CONFIG_FILE` env var is set — the compose service must pass
|
||||
`CONFIG_FILE=/config.yaml`, not just mount the file.
|
||||
- The job container can't resolve `gitea.skink-platy.ts.net` by itself; pin it
|
||||
with `container.options: "--add-host gitea.skink-platy.ts.net:100.111.241.54"`
|
||||
in `config.yaml`.
|
||||
- Registration tokens are one-time use; the token lives in `runner/.env`
|
||||
(chmod 600) and is burned after the first registration.
|
||||
- Inspecting runs via sqlite: Gitea's status enum is runnerv1-consistent —
|
||||
**1 = success, 2 = failure** (not the old 0/1/2/3 scheme).
|
||||
- **Deterministic generators** — any script whose output is committed (gen docs,
|
||||
completions) must sort in byte order: plain `sort` collates differently per
|
||||
locale, and the CI container tripped exactly this (category-less tool keys
|
||||
like `pos-config` start with `|`, which collated after letters under that
|
||||
locale, reordering the generated tables). `scripts/gen-docs.sh` sets
|
||||
`export LC_ALL=C`; keep that in mind for any new generator.
|
||||
- **Checking green without SSH** — the workflow reports its own outcome as a
|
||||
lightweight git tag: `ci-ok/<sha>` on success, `ci-fail/<sha>` on failure
|
||||
(pushed with the job's automatic `GITEA_TOKEN`; the workflow only triggers on
|
||||
`push` to `main`, so tag pushes don't re-trigger it). Check from the dev box
|
||||
with plain git — `scripts/ci-status.sh [--wait] [<sha>]` (reads the tags via
|
||||
`git ls-remote`, exit 0/1/2 = green/red/pending). No SSH to the runner, no API
|
||||
tokens.
|
||||
- **Limits** — CI proves the *static* gates only; it never runs behaviour suites
|
||||
(stub harnesses stay throwaway in `/tmp`).
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Alerting
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ templates (without overwriting an existing file):
|
||||
|------|---------|------|
|
||||
| `telegram.env` | `pos communication telegram sender` / `listener`, everything that alerts | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` |
|
||||
| `matrix.env` | `pos communication matrix sender` / `listener` | `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID` |
|
||||
| `scrcpy.env` | `pos communication scrcpy` | `SCRCPY_SERIAL`, `SCRCPY_MAX_SIZE`, `SCRCPY_MAX_FPS`, `SCRCPY_BIT_RATE`, `SCRCPY_FULLSCREEN`, `SCRCPY_RECORD_DIR`, `SCRCPY_PUSH_TARGET`, `SCRCPY_EXTRA_FLAGS` |
|
||||
| `scrcpy.env` | `pos communication scrcpy` | `SCRCPY_SERIAL`, `SCRCPY_MAX_SIZE`, `SCRCPY_MAX_FPS`, `SCRCPY_BIT_RATE`, `SCRCPY_FULLSCREEN`, `SCRCPY_NEW_DISPLAY`, `SCRCPY_AUDIO`, `SCRCPY_RECORD_DIR`, `SCRCPY_PUSH_TARGET`, `SCRCPY_EXTRA_FLAGS` |
|
||||
| `notify.env` | `lib/notify.sh` (all alerting) | `NOTIFY_PLATFORM` (e.g. `telegram,matrix`) |
|
||||
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
||||
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
||||
@@ -71,8 +71,8 @@ Adding another platform = create `bin/pos-communication-<p>` implementing
|
||||
|
||||
### Scheduling
|
||||
|
||||
- **Daily health digest** — add a `daily` schedule job `pos system health --send --markdown`
|
||||
via `pos system schedule config` (the old `pos-health.{service,timer}` units are gone). See [system](howto/system.md).
|
||||
- **Daily health digest** — add a `daily` schedule job `COMMAND=pos system health` with
|
||||
`NOTIFY=always` via `pos system schedule config` (the old `pos-health.{service,timer}` units are gone). See [system](howto/system.md).
|
||||
- **Entertainment auto-triggers** — per-plugin `pos entertainment enable <plugin> <interval>`,
|
||||
uses systemd user timers (or cron fallback). See [entertainment](howto/entertainment.md).
|
||||
- **`pos system schedule` jobs** — run any command on a per-job timer and notify
|
||||
|
||||
+15
-8
@@ -206,18 +206,19 @@ The standalone `vbox` command still works and forwards to `pos docker vbox` (see
|
||||
|---------|------|---------|---------------|
|
||||
| `pos media mp3 <url>` | `bin/pos-media-mp3` | Download audio as MP3 via yt-dlp, with thumbnail + metadata | Output to `~/Music/%(title)s.%(ext)s`, `--audio-quality 0` |
|
||||
| `pos media mp4 <url>` | `bin/pos-media-mp4` | Download video via yt-dlp with **interactive format selection** | Lists formats (`yt-dlp -F`), asks for a format ID, saves to `~/Videos/` |
|
||||
| `pos media sync [--mp3\|--mp4]` | `bin/pos-media-sync` | Incremental Music → USB sync (add/update only — never deletes) | Copies mp3/mp4 from `$HOME/Music` (or `--source <dir>`) into `<usb>/Music/`, preserving the tree; missing or changed (size/mtime) files are copied, identical ones skipped. Same USB detection as `pos system backup` (lsblk TRAN + lsusb/by-id, mount offer for unmounted sticks, multi-stick picker). `--mp3`/`--mp4` filter by extension, neither = both; `--dry-run` previews. Config: `MEDIA_SYNC_SOURCE`, `MEDIA_SYNC_DEST`, shared `USB_MOUNT_BASE`/`USB_BYID` from `~/.config/linux_post_install/system.env`. Result notified via `lib/notify.sh` |
|
||||
|
||||
### system
|
||||
|
||||
| Command | File | Purpose | Configuration |
|
||||
|---------|------|---------|---------------|
|
||||
| `sudo pos system firewall` | `bin/pos-system-firewall` | Interactive UFW ("UFW POWER") menu: add/delete rules, status, enable/disable/reset, default policies | Must run as root. Every command is previewed and confirmed before execution; supports `--dry-run`; keeps a history of executed commands. Executed mutating changes are announced via `lib/notify.sh` |
|
||||
| `pos system backup <folder-path>` | `bin/pos-system-backup` | Create a gpg-encrypted (AES-256) `tar.gz` snapshot of a folder and verify it | Prompts twice for a password (never stored). Uses `sudo tar`; needs `gnupg` (in `preinstall.sh` PACKAGES). Artifact `<name>_<date>.tar.gz.gpg` in the current directory, `chmod 600`. Once the archive verifies, connected USB storage is offered (auto-detected removable mounts via `lsblk`, or pinned with `BACKUP_USB_ROOT`): the copy lands in `<usb>/backups/` and is proven 100% by sha256 before it is announced. Success/failure are announced via `lib/notify.sh` |
|
||||
| `pos system backup <folder-path>` | `bin/pos-system-backup` | Create a gpg-encrypted (AES-256) `tar.gz` snapshot of a folder and verify it | Prompts twice for a password (never stored). Uses `sudo tar`; needs `gnupg` (in `preinstall.sh` PACKAGES) only when encrypting. Artifact `<name>_<date>.tar.gz[.gpg]` in the current directory, `chmod 600`; `--no-encrypt` (or `BACKUP_ENCRYPT=0`) keeps a plain `.tar.gz` with no password prompt (headless/cron safe). Once the archive verifies, connected USB storage is offered (detected via `lsblk` TRAN with `lsusb`/by-id cross-check; unmounted sticks get a mount offer first — see `DOC/howto/system.md`; or pinned with `BACKUP_USB_ROOT`): the copy lands in `<usb>/backups/` and is proven 100% by sha256 before it is announced. Success/failure are announced via `lib/notify.sh` |
|
||||
| `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 [--send] [--markdown]` | `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 | `--send`/`--markdown` are notification-only: they send the summary via `lib/notify.sh` to every platform in `NOTIFY_PLATFORM` and do NOT print the dashboard (so wrappers like the Telegram listener don't echo it back — pair with the listener's `@quiet` marker). `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2) and `BACKUP_SERVICE_ROOTS` come from `~/.config/linux_post_install/system.env`; `--help` shows the effective values. Platform list from `~/.config/linux_post_install/notify.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 |
|
||||
|
||||
A scheduled job is the recommended way to run the health dashboard on a timer, e.g. a `daily` job with `COMMAND=pos system health --send` — 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).
|
||||
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).
|
||||
|
||||
### ssh
|
||||
|
||||
@@ -255,7 +256,7 @@ Subcommands that need input prompt interactively when args are omitted.
|
||||
| `pos share nfs server <cmd>` | `bin/pos-share-nfs-server` | Manage the NFS kernel server: `status`, `share <path> [client]`, `unshare <path>`, `list`, `reload`, `enable`, `disable` | Requires `nfs-kernel-server` (added to `preinstall.sh` PACKAGES). Exports live in `/etc/exports`; `share` is idempotent (replaces any existing line for the path) and runs `exportfs -ra`. Default client `*(rw,sync,no_subtree_check)` — the tool warns you to restrict it; help prints Tailscale CGNAT (`100.64.0.0/10`), WireGuard (`10.10.0.0/24`) and LAN examples. Mutating commands announce via `lib/notify.sh` |
|
||||
| `pos share nfs client <cmd>` | `bin/pos-share-nfs-client` | Mount and manage NFS shares: `mount <server:export> <local-dir>`, `unmount <local-dir>`, `list`, `persist <server:export> <local-dir>`, `unpersist <local-dir>` | Requires `nfs-common` (added to `preinstall.sh` PACKAGES). `persist` writes a systemd `.mount` unit (`systemd-escape --path --suffix=mount`) with `After=network-online.target` / `Wants=network-online.target` — mounts only once all interfaces are up, no fstab edits to break boot — then `daemon-reload` + `enable --now`. `unpersist` stops/disables/removes the unit. `mount`/`persist` announce via `lib/notify.sh` |
|
||||
| `pos share smb server <cmd>` | `bin/pos-share-smb-server` | Manage the Samba server: `status`, `share <path> [name] [--read-only|--guest|--users u1,u2]`, `unshare <name>`, `list`, `adduser <user>`, `deluser <user>`, `reload`, `enable`, `disable` | Requires `samba` (added to `preinstall.sh` PACKAGES). Shares are idempotent marker blocks (`# >>> pos-managed share: <name>` … `# <<< end pos-managed share`) in `/etc/samba/smb.conf` — hand edits outside the markers survive; `share` validates with `testparm` before applying and hot-reloads via `smbcontrol smbd reload-config`. Defaults rw + browsable; warns when unrestricted (guest or no `valid users`). `adduser`/`deluser` manage Samba accounts via `smbpasswd`. Mutating commands announce via `lib/notify.sh` |
|
||||
| `pos share smb client <cmd>` | `bin/pos-share-smb-client` | Mount and manage SMB/CIFS shares: `mount <//server/share> <local-dir> [user]`, `unmount <local-dir>`, `list`, `persist <//server/share> <local-dir> [user]`, `unpersist <local-dir>` | Requires `cifs-utils` (added to `preinstall.sh` PACKAGES). With a user you are prompted for the Samba password — one-shot mounts use a throwaway chmod-600 credentials file, `persist` keeps one at `/etc/samba/credentials/<name>` (chmod 600). `persist` writes a systemd `.mount` unit (`systemd-escape --path --suffix=mount`) with `x-systemd.automount` + `_netdev` — mounts on first access, never blocks boot — then `daemon-reload` + `enable --now`. `unpersist` stops/disables/removes the unit + credentials. `mount`/`persist` announce via `lib/notify.sh` |
|
||||
| `pos share smb client <cmd>` | `bin/pos-share-smb-client` | Mount and manage SMB/CIFS shares: `mount <//server/share> <local-dir> [user]`, `unmount <local-dir>`, `list`, `persist <//server/share> <local-dir> [user]`, `unpersist <local-dir>` | Requires `cifs-utils` (added to `preinstall.sh` PACKAGES). With a user you are prompted for the Samba password — one-shot mounts use a throwaway chmod-600 credentials file, `persist` keeps one at `/etc/samba/credentials/<name>` (chmod 600). `persist` writes systemd `.mount` **and** `.automount` units (`systemd-escape --path --suffix=mount`) with `_netdev` — the automount defers the actual mount until first access, never blocks boot — then `daemon-reload` + `enable --now` the automount. `unpersist` stops/disables/removes both units + credentials. `list` shows active mounts (`findmnt -t cifs`) **and** persistent units (as automount shares aren't mounted until first access, they'd otherwise be invisible). `mount`/`persist` announce via `lib/notify.sh` |
|
||||
|
||||
### communication
|
||||
|
||||
@@ -295,7 +296,7 @@ The bot token is a secret — it is stored only in `~/.config/linux_post_install
|
||||
| `pos communication telegram listener --sync-commands` | Push the mapped `/commands` to the bot's `/` menu (`setMyCommands`) — also run automatically after every map edit, on `--enable`, and at daemon start |
|
||||
| `pos communication telegram listener --run` | Run the polling loop in the foreground (what the service executes) |
|
||||
|
||||
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` and the answer is replied verbatim; an AI failure replies the error plus a `pos config ai` hint. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `/status=@quiet pos system health --send`), avoiding a double message. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` and the answer is replied verbatim; an AI failure replies the error plus a `pos config ai` hint. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `pos system backup` self-notifies, so `/backup=@quiet pos system backup $HOME/Documents` avoids a double message). `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
|
||||
Map entries may carry an optional **description** shown in the bot's `/` menu: `/cmd::short description=bash command` (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via `setMyCommands`, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase `[a-z0-9_]` names (1–32 chars) — commands like `/Status` or `/my-cmd` are skipped from the menu with a warning but still resolve when typed.
|
||||
|
||||
@@ -321,14 +322,14 @@ The access token is a secret — it is stored only in `~/.config/linux_post_inst
|
||||
| `pos communication matrix listener --disable` | Stops, disables, and removes the service |
|
||||
| `pos communication matrix listener --run` | Run the polling loop in the foreground (what the service executes) |
|
||||
|
||||
The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filter that drops presence/account_data/device noise and only requests `m.room.message` timeline events). It reacts only to messages **from `MATRIX_USER_ID`** (your own account — resolved via `/account/whoami` if unset); a `MATRIX_ROOM_ID` restricts it to one room, otherwise every joined room is watched. `/` and `!` prefixes both resolve (`!status` = `/status`). `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` with a per-room session (`matrix-<room>`; `ai /reset` clears it) and the answer is replied verbatim with markdown stripped. Replies are sent as `m.text` threaded with `m.in_reply_to` on your message. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`; non-zero exit is prefixed with `exit <rc>`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `/status=@quiet pos system health --send`). Map lines may carry a `/cmd::description=…` description. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filter that drops presence/account_data/device noise and only requests `m.room.message` timeline events). It reacts only to messages **from `MATRIX_USER_ID`** (your own account — resolved via `/account/whoami` if unset); a `MATRIX_ROOM_ID` restricts it to one room, otherwise every joined room is watched. `/` and `!` prefixes both resolve (`!status` = `/status`). `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` with a per-room session (`matrix-<room>`; `ai /reset` clears it) and the answer is replied verbatim with markdown stripped. Replies are sent as `m.text` threaded with `m.in_reply_to` on your message. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`; non-zero exit is prefixed with `exit <rc>`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `pos system backup` self-notifies, so `/backup=@quiet pos system backup $HOME/Documents` avoids a double message). Map lines may carry a `/cmd::description=…` description. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
|
||||
|
||||
`pos communication scrcpy` in detail:
|
||||
|
||||
| Command | Behavior |
|
||||
|---------|----------|
|
||||
| `pos communication scrcpy` | Mirror the device: opens the scrcpy window (needs a display — over ssh use `ssh -X`). Built from `scrcpy.env` defaults plus any pass-through scrcpy flags (`pos communication scrcpy --turn-screen-off --stay-awake`) |
|
||||
| `pos communication scrcpy devices` | `adb devices -l` — the source of serials for `SCRCPY_SERIAL` |
|
||||
| `pos communication scrcpy --new-display[=…]` | Mirror to a new virtual display on the phone (no need to mirror the real screen): `--new-display` (default size/dpi), `--new-display=1920x1080`, `--new-display=1920x1080/420` or `--new-display=/240`. Also settable persistently via `SCRCPY_NEW_DISPLAY` || `pos communication scrcpy devices` | `adb devices -l` — the source of serials for `SCRCPY_SERIAL` |
|
||||
| `pos communication scrcpy record [file] [--headless]` | Record a session to an mp4 — default `$SCRCPY_RECORD_DIR/<device>_<date>.mp4`; `--headless` adds `--no-playback` (no window — headless-server friendly) |
|
||||
| `pos communication scrcpy tcpip [port]` | `adb tcpip <port>` (default 5555) — switch the USB device to wireless adb, prints the reconnect command with the detected device IP |
|
||||
| `pos communication scrcpy connect <ip[:port]>` | `adb connect` then mirror over WiFi (`-s <ip:port>`) |
|
||||
@@ -348,6 +349,8 @@ A device must have **USB debugging** enabled (Developer options) and the phone's
|
||||
| `SCRCPY_MAX_FPS` | no | — | Limit frame rate, e.g. `60` (scrcpy `--max-fps`) |
|
||||
| `SCRCPY_BIT_RATE` | no | — | Video bit rate, e.g. `8M` (scrcpy `--video-bit-rate`) |
|
||||
| `SCRCPY_FULLSCREEN` | no | `false` | `true` adds `--fullscreen` |
|
||||
| `SCRCPY_NEW_DISPLAY` | no | — | New virtual display on the phone (`--new-display`): `true` (default size/dpi), `1920x1080`, `1920x1080/420` or `/240` |
|
||||
| `SCRCPY_AUDIO` | no | `true` | Forward device audio to the desktop (scrcpy default). `false` adds `--no-audio` |
|
||||
| `SCRCPY_RECORD_DIR` | no | `~/Videos/scrcpy` | Output dir for `record`/`screenshot` defaults |
|
||||
| `SCRCPY_PUSH_TARGET` | no | `/sdcard/Download` | Default `adb push` destination |
|
||||
| `SCRCPY_EXTRA_FLAGS` | no | — | Extra scrcpy flags appended to every mirror |
|
||||
@@ -356,7 +359,7 @@ Requires `scrcpy` + `adb`. `adb` is in `preinstall.sh` PACKAGES; `scrcpy` is **n
|
||||
|
||||
### entertainment
|
||||
|
||||
**File:** `bin/pos-entertainment-send`
|
||||
**File:** `bin/pos-entertainment-send` (management subcommands: `bin/pos-entertainment-config`, `bin/pos-entertainment-enable`, `bin/pos-entertainment-disable`, `bin/pos-entertainment-status`)
|
||||
**Purpose:** run a public-API plugin and send its output via `notify_send` — the platform follows `NOTIFY_PLATFORM` (default Telegram, silent-fail when none configured). Plugins are standalone scripts in `entertainment/` that fetch a public API and **print the message to stdout** — that stdout is what gets sent.
|
||||
|
||||
| Command | Behavior |
|
||||
@@ -434,6 +437,8 @@ Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libfl
|
||||
|
||||
### config
|
||||
|
||||
**File:** `bin/pos-config`
|
||||
|
||||
`pos config` is the interactive editor for the tools' runtime config (see [DEV.md](DEV.md#config-files) and §10 of AGENT_Context). Every tool exposes its configuration by declaring a `# POS_CONFIG:` header; `pos config` reads those at runtime — it knows nothing about the variables themselves. Values live in `~/.config/linux_post_install/<scope>.env` (chmod 600).
|
||||
|
||||
| Command | Purpose |
|
||||
@@ -444,6 +449,8 @@ Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libfl
|
||||
|
||||
### tree
|
||||
|
||||
**File:** `bin/pos-tree`
|
||||
|
||||
`pos tree` prints the `pos` command tree — every category, command, and subcommand the dispatcher can reach, annotated with each tool's `# POS:` description. Data is derived live from the `bin/pos-*` filenames and their `# POS_SUBCMDS:` headers, so it always matches what `pos` can actually run.
|
||||
|
||||
| Command | Purpose |
|
||||
|
||||
+8
-1
@@ -38,7 +38,7 @@ The phases:
|
||||
| # | Phase | Script/action |
|
||||
|---|-------|----------------|
|
||||
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
|
||||
| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
|
||||
| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` + `lib/usb-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
|
||||
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
|
||||
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
|
||||
| 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) |
|
||||
@@ -207,6 +207,13 @@ Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib
|
||||
|
||||
---
|
||||
|
||||
## lib/usb-lib.sh — shared USB-storage detection
|
||||
|
||||
**File:** `lib/usb-lib.sh` (installed to `/usr/local/bin/usb-lib.sh`)
|
||||
**Purpose:** the one copy of the USB-storage machinery shared by `pos system backup`'s post-verify USB copy and `pos media sync` — `usb_detect` (lsblk JSON, TRAN + lsusb/by-id cross-check → `USB_MOUNTED`/`USB_UNMOUNTED`), `usb_related_present`, `usb_mount_offer` (mount an unmounted stick at `/media/<label>`, `usb-automount` scheme), and `usb_pick_root` (detect → mount-offer → single/multi picker → `USB_ROOT`). Defines only `usb_*`; seams `USB_MOUNT_BASE` (default `/media`, alias `BACKUP_MOUNT_BASE`) and `USB_BYID` (default `/dev/disk/by-id`, alias `BACKUP_USB_BYID`) keep existing config lines working.
|
||||
|
||||
---
|
||||
|
||||
## features/autostart.sh — boot-time feature
|
||||
|
||||
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
|
||||
|
||||
@@ -80,7 +80,7 @@ pos communication telegram listener --disable # remove it
|
||||
one `/cmd=bash command` per line — re-read on every message, so edits apply
|
||||
instantly. Example:
|
||||
```
|
||||
/status=@quiet pos system health --send
|
||||
/status=pos system health
|
||||
/temp=sensors | grep -i 'Tctl\|package id 0'
|
||||
/update=cd /path/to/repo && git pull
|
||||
```
|
||||
@@ -88,7 +88,7 @@ pos communication telegram listener --disable # remove it
|
||||
(`setMyCommands`) after every map edit, on `--enable`, and at daemon start
|
||||
(force it anytime with `--sync-commands`). Add a short description with the
|
||||
`/cmd::description=bash command` syntax — e.g.
|
||||
`/backup::Encrypted nightly backup=@quiet pos system backup --send` — or it
|
||||
`/backup::Encrypted nightly backup=@quiet pos system backup $HOME/Documents` — or it
|
||||
falls back to the bash command. Telegram only registers lowercase `[a-z0-9_]`
|
||||
names (1–32 chars); `/Status` or `/my-cmd` are skipped from the menu but still
|
||||
work when typed. An empty map clears the menu.
|
||||
@@ -99,8 +99,10 @@ pos communication telegram listener --disable # remove it
|
||||
`sudo` inside a command needs a NOPASSWD rule.
|
||||
- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but
|
||||
does NOT reply — for commands that already send their own notification, so
|
||||
you don't get it twice. `/status=@quiet pos system health --send` delivers
|
||||
one digest via the notify system and nothing else.
|
||||
you don't get it twice. `pos system backup` self-notifies, so
|
||||
`/backup=@quiet pos system backup $HOME/Documents` sends the backup notification
|
||||
and nothing else. `pos system health` is console-only (it never sends), so map
|
||||
it WITHOUT `@quiet` — `/status=pos system health` replies with the dashboard.
|
||||
- **Daemon lifecycle:** the service is a systemd **user** unit; it stops at
|
||||
logout unless you enable linger: `sudo loginctl enable-linger $(whoami)`.
|
||||
`--enable` prints this warning if linger is off.
|
||||
@@ -197,7 +199,7 @@ pos communication matrix listener --disable # remove it
|
||||
one `/cmd=bash command` per line — re-read on every message, so edits apply
|
||||
instantly. Example:
|
||||
```
|
||||
/status=@quiet pos system health --send
|
||||
/status=pos system health
|
||||
/temp=sensors | grep -i 'Tctl\|package id 0'
|
||||
/update=cd /path/to/repo && git pull
|
||||
```
|
||||
@@ -212,7 +214,9 @@ pos communication matrix listener --disable # remove it
|
||||
(truncated ~3800 chars; empty → `OK`). `sudo` inside a command needs a
|
||||
NOPASSWD rule.
|
||||
- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but
|
||||
does NOT reply — for commands that already send their own notification.
|
||||
does NOT reply — for commands that already send their own notification
|
||||
(`pos system backup` self-notifies, e.g. `/backup=@quiet pos system backup $HOME/Documents`).
|
||||
`pos system health` is console-only (never sends), so map it WITHOUT `@quiet`.
|
||||
- **`ai …` bridge:** non-command messages starting with `ai ` are answered by
|
||||
`pos ai gemini` (per-room memory session; `ai /reset` clears it) — replying
|
||||
with the model's answer, markdown stripped.
|
||||
@@ -256,8 +260,17 @@ install the current GitHub release (bundles `adb`) with the optional app
|
||||
pos communication scrcpy # USB device, config defaults
|
||||
pos communication scrcpy --turn-screen-off # pass any scrcpy flag through
|
||||
pos communication scrcpy --no-audio --always-on-top
|
||||
pos communication scrcpy --new-display=1920x1080 # mirror to a NEW phone display
|
||||
```
|
||||
|
||||
`--new-display` mirrors a fresh virtual display instead of the phone's real
|
||||
screen (nice for watching the phone screen content elsewhere): no `=…` uses the
|
||||
main display's size/dpi, or force size/dpi — `1920x1080`, `1920x1080/420`,
|
||||
`/240`. Set it persistently with `SCRCPY_NEW_DISPLAY` in `pos config scrcpy`.
|
||||
|
||||
Audio is forwarded to the desktop **by default** (scrcpy default); `--no-audio`
|
||||
disables it, or set `SCRCPY_AUDIO=false` in `pos config scrcpy`.
|
||||
|
||||
The window needs a display — over ssh use `ssh -X` (and a phone already
|
||||
reachable over WiFi, see below). `scrcpy --help` lists every flag; the wrapper
|
||||
forwards flags verbatim.
|
||||
@@ -305,4 +318,4 @@ screen/file without a desktop.
|
||||
|
||||
- Reference + config file details: [DOC/POS.md → communication](../POS.md)
|
||||
- Alerting contract: [DOC/DEV.md → Alerting](../DEV.md)
|
||||
- Health digest (uses `--send --markdown`): [system.md](system.md)
|
||||
- Health dashboard + scheduled digest: [system.md](system.md)
|
||||
|
||||
+52
-1
@@ -1,11 +1,13 @@
|
||||
# How-To: `pos media`
|
||||
|
||||
Download audio and video from the web via `yt-dlp`. Tools: `mp3`, `mp4`.
|
||||
Download audio and video from the web via `yt-dlp`, and sync your library to a
|
||||
USB stick. Tools: `mp3`, `mp4`, `sync`.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `pos media mp3` | Download audio, convert to MP3 |
|
||||
| `pos media mp4` | Download video with smart/interactive format selection |
|
||||
| `pos media sync` | Incrementally copy `~/Music` onto a USB stick (mp3/mp4) |
|
||||
|
||||
Requires `yt-dlp` and `ffmpeg` (`sudo apt install yt-dlp ffmpeg`); the tools
|
||||
fail with a clean error message instead of a raw `command not found` if either
|
||||
@@ -84,6 +86,55 @@ video+audio and merges them.
|
||||
|
||||
---
|
||||
|
||||
## `pos media sync` — music onto a USB stick
|
||||
|
||||
```bash
|
||||
pos media sync # copy everything (mp3 + mp4) from ~/Music to the stick
|
||||
pos media sync --mp3 # only the .mp3 files
|
||||
pos media sync --mp4 # only the .mp4 files
|
||||
```
|
||||
|
||||
Detects connected USB storage exactly like `pos system backup` (same shared
|
||||
`lib/usb-lib.sh`): a plugged-in but unmounted stick is offered a mount first
|
||||
(`/media/<label>`, world-writable, mirrors `usb-automount`), multiple sticks
|
||||
are listed for you to pick, and if nothing is plugged in it re-scans after you
|
||||
press Enter. Files are mirrored into `<usb>/Music/` (change with
|
||||
`MEDIA_SYNC_DEST`) preserving the artist/album tree.
|
||||
|
||||
**Sync semantics — add/update only, never delete.** Files missing on the stick
|
||||
are copied; files whose size or mtime changed are overwritten; everything
|
||||
identical is skipped. Files on the stick that are no longer in the source are
|
||||
**left alone** — a playback stick can never lose files to a mirror mistake.
|
||||
Copies keep the source timestamps (`cp --preserve=timestamps`), so a re-run is
|
||||
a no-op. Preview before copying with `--dry-run`:
|
||||
|
||||
```bash
|
||||
pos media sync --mp4 --dry-run # shows "would copy" list + counts, copies nothing
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
|------|---------|
|
||||
| `--mp3` | Sync only `*.mp3` (neither flag = both) |
|
||||
| `--mp4` | Sync only `*.mp4` (neither flag = both) |
|
||||
| `--source <dir>` | Source folder (default `~/Music`) |
|
||||
| `--dry-run` | Preview what would be copied, copy nothing |
|
||||
|
||||
When it finishes it announces the result via `lib/notify.sh`
|
||||
(`Music sync completed: N added, M updated → <usb>/Music`).
|
||||
|
||||
**Recipes:**
|
||||
```bash
|
||||
pos media sync # keep the car stick up to date (both formats)
|
||||
pos media sync --mp3 --dry-run # check what a new batch will bring first
|
||||
pos media sync --source /data/Music # sync a library that lives elsewhere
|
||||
```
|
||||
|
||||
Config (all in `~/.config/linux_post_install/system.env` or exported):
|
||||
`MEDIA_SYNC_SOURCE` (default `$HOME/Music`), `MEDIA_SYNC_DEST` (default
|
||||
`Music`), plus the shared `USB_MOUNT_BASE` / `USB_BYID` seams.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Format list is empty / download fails → the site or age-gate requires
|
||||
|
||||
+8
-6
@@ -220,7 +220,7 @@ Requires `cifs-utils` (in `preinstall.sh` PACKAGES).
|
||||
pos share smb client mount //100.100.100.1/media /mnt/smb/media # guest
|
||||
pos share smb client mount //100.100.100.1/media /mnt/smb/media bob # prompts for password
|
||||
pos share smb client persist //100.100.100.1/media /mnt/smb/media bob # persistent (systemd)
|
||||
pos share smb client list # active SMB mounts
|
||||
pos share smb client list # active + persistent SMB mounts
|
||||
pos share smb client unmount /mnt/smb/media
|
||||
pos share smb client unpersist /mnt/smb/media # remove the unit
|
||||
```
|
||||
@@ -231,8 +231,9 @@ mounts use a throwaway chmod-600 credentials file, `persist` keeps one at
|
||||
`/etc/samba/credentials/<name>` (chmod 600) and references it from the unit.
|
||||
|
||||
**Persistent mounts use systemd, not fstab.** `persist` writes a
|
||||
`/etc/systemd/system/<mnt-name>.mount` unit (`systemd-escape`) with
|
||||
`x-systemd.automount` + `_netdev`: the share is mounted **on first access**
|
||||
`/etc/systemd/system/<mnt-name>.mount` unit (**and** a matching
|
||||
`<mnt-name>.automount` unit, both `systemd-escape`d) with `_netdev`: the
|
||||
automount is enabled and armed, and the share is mounted **on first access**
|
||||
instead of at boot, so an unreachable SMB server can never hang boot (with
|
||||
fstab it could). `enable --now` arms the automount immediately.
|
||||
|
||||
@@ -253,9 +254,10 @@ fstab it could). `enable --now` arms the automount immediately.
|
||||
server and re-run with the right user
|
||||
- Mount fails with `NT_STATUS_ACCESS_DENIED` on a guest mount → the server
|
||||
share has no `guest ok`; use a user or add `--guest` on the server
|
||||
- Persistent mount doesn't appear until accessed → intended (`x-systemd.automount`);
|
||||
`pos share smb client list` only shows actively mounted shares, access the
|
||||
directory to trigger the mount
|
||||
- Persistent mount doesn't appear under "Active mounts" until accessed →
|
||||
intended (`x-systemd.automount`); `pos share smb client list` now also lists
|
||||
persistent units under "Persistent (automount)", so the configured shares are
|
||||
visible even before their first access
|
||||
|
||||
---
|
||||
|
||||
|
||||
+44
-15
@@ -15,19 +15,17 @@ Host care: encrypted backups, firewall, and the health dashboard. Tools:
|
||||
|
||||
```bash
|
||||
pos system health # console report; exits 1 if any check FAILs
|
||||
pos system health --send # also send the summary via notify platforms
|
||||
pos system health --markdown # same, markdown parse mode (implies --send)
|
||||
```
|
||||
|
||||
Checks: disk per mount (>90% = FAIL), RAM/swap, failed systemd units, backup
|
||||
age, fail2ban, docker containers. Header shows hostname, uptime, load, public IP.
|
||||
|
||||
`--help` prints the **effective** config values (env > `system.env` > default),
|
||||
e.g.:
|
||||
Health is a **console-only reporter — it never sends notifications**; deliver
|
||||
its output with a wrapper or a scheduled job (below). `--help` prints the
|
||||
**effective** config values (env > `system.env` > default), e.g.:
|
||||
|
||||
```
|
||||
Environment (effective values):
|
||||
NOTIFY_PLATFORM telegram
|
||||
HEALTH_BACKUP_MAX_AGE_DAYS 2
|
||||
BACKUP_SERVICE_ROOTS /srv $HOME/srv
|
||||
```
|
||||
@@ -41,8 +39,6 @@ Environment (effective values):
|
||||
BACKUP_SERVICE_ROOTS=/srv $HOME/srv # roots for backup-age check + backup --service
|
||||
BACKUP_USB_ROOT=/mnt/usb # optional: copy finished backups to <root>/backups/ (auto-detects a mounted USB when unset)
|
||||
HEALTH_BACKUP_MAX_AGE_DAYS=3 # WARN if newest backup older (default 2)
|
||||
# ~/.config/linux_post_install/notify.env
|
||||
NOTIFY_PLATFORM=telegram
|
||||
```
|
||||
|
||||
### Daily digest (automated)
|
||||
@@ -50,14 +46,15 @@ NOTIFY_PLATFORM=telegram
|
||||
Run the health report on a timer with a scheduled job (no systemd unit needed):
|
||||
|
||||
```bash
|
||||
pos system schedule config # add a job: INTERVAL=daily,
|
||||
# COMMAND=pos system health --send --markdown
|
||||
pos system schedule config # add a job: INTERVAL=daily, NOTIFY=always,
|
||||
# COMMAND=pos system health
|
||||
systemctl --user list-timers | grep pos-schedule
|
||||
pos system schedule run <name> # run once now
|
||||
```
|
||||
|
||||
The old `pos-health.{service,timer}` systemd units are gone — a legacy install
|
||||
may still have them failed/leftover; disable and remove them:
|
||||
The `NOTIFY=always` policy sends the job's full output — i.e. the dashboard —
|
||||
as the alert. The old `pos-health.{service,timer}` systemd units are gone — a
|
||||
legacy install may still have them failed/leftover; disable and remove them:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null
|
||||
@@ -75,22 +72,28 @@ sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daem
|
||||
active; start it (`sudo systemctl enable --now fail2ban`) or ignore.
|
||||
- `[FAIL] services: nbd-server.service …` → a failed unit; inspect with
|
||||
`systemctl status <unit>`.
|
||||
- `--send` prints a warn and exits 0 when no platform is configured — by design
|
||||
(see [communication](communication.md)).
|
||||
|
||||
---
|
||||
|
||||
## `pos system backup` — encrypted folder snapshots
|
||||
|
||||
```bash
|
||||
pos system backup <folder-path> # encrypt to ./<name>_<date>.tar.gz.gpg
|
||||
pos system backup --service # pick a folder from /srv + ~/srv
|
||||
pos system backup <folder-path> # encrypt to ./<name>_<date>.tar.gz.gpg
|
||||
pos system backup <folder-path> --no-encrypt # plain ./<name>_<date>.tar.gz, no password
|
||||
pos system backup --service # pick a folder from /srv + ~/srv
|
||||
```
|
||||
|
||||
Uses `sudo tar` + gpg AES-256. The password is prompted **twice and never
|
||||
stored**; the artifact is `chmod 600`. On success (and on failure, via ERR
|
||||
trap) a `notify_send` alert is sent.
|
||||
|
||||
**Skip encryption** with `--no-encrypt` (or `BACKUP_ENCRYPT=0` in
|
||||
`system.env`): the archive stays a plain `.tar.gz`, no password is prompted,
|
||||
and the file is still `chmod 600` + USB-copy verified. This is the
|
||||
**headless/cron-safe** mode — the encrypted path prompts for a password, so
|
||||
under cron it needs `--no-encrypt` with a fixed folder
|
||||
(`pos system backup ~/Documents --no-encrypt`).
|
||||
|
||||
`--service` lists folders under the roots in `BACKUP_SERVICE_ROOTS`
|
||||
(default `/srv $HOME/srv`; override via `system.env` or env) and lets you pick.
|
||||
|
||||
@@ -111,11 +114,37 @@ pos system backup ~/Documents
|
||||
# OK Transfer verified 100% (sha256 match): .../backups/docs_2026-08-13.tar.gz.gpg
|
||||
```
|
||||
|
||||
Detection reads `lsblk` and treats a device as USB when `TRAN == usb` (the
|
||||
per-device deciding signal). Removable-but-not-USB slots (e.g. a SATA card
|
||||
reader) are skipped. If a device shows no `TRAN` at all, `lsblk`'s answer is
|
||||
cross-checked against `/dev/disk/by-id/usb-*` symlinks and `lsusb` before it is
|
||||
offered.
|
||||
|
||||
**Unmounted stick?** If the USB stick is plugged in but only shows as `sdax`
|
||||
with no mountpoint (common on CLI boxes with no automounter), you're offered a
|
||||
**mount first**, then it copies there:
|
||||
|
||||
```bash
|
||||
# [!] Found USB storage not mounted: /dev/sda1 (7.5G, DataTraveler)
|
||||
# Mount it at /media/usb-sda1 (world-writable) so the backup can go there? [y/N]
|
||||
# (y) OK Mounted /dev/sda1 at /media/usb-sda1
|
||||
# [+] Copying to /media/usb-sda1/backups/docs_2026-08-13.tar.gz.gpg ...
|
||||
# OK Transfer verified 100% (sha256 match): .../backups/docs_2026-08-13.tar.gz.gpg
|
||||
```
|
||||
|
||||
The mount mirrors `usb-automount` (`/media/<label>`, fallback
|
||||
`/media/usb-<devname>`, `-o umask=000` world-writable so the copy works without
|
||||
root). Decline it and you get the exact `sudo mkdir -p` / `sudo mount` commands
|
||||
to run yourself, then `Enter` re-checks. `s` or EOF (cron) skips silently and
|
||||
the backup stays local — it never blocks.
|
||||
|
||||
- Multiple sticks mounted → pick by number; `0` skips; `n`/EOF skips silently
|
||||
(cron runs never block).
|
||||
- Pin a fixed stick (no detection, no prompt on cron) with
|
||||
`BACKUP_USB_ROOT=/mnt/usb` in `system.env` — the copy still lands in
|
||||
`<root>/backups/` and is still sha256-verified.
|
||||
- Mount base and by-id dir are configurable: `BACKUP_MOUNT_BASE` (default
|
||||
`/media`) and `BACKUP_USB_BYID` (default `/dev/disk/by-id`).
|
||||
|
||||
### Recipes
|
||||
|
||||
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
# MAINTENANCE — Convention Audit
|
||||
|
||||
> Working report for the post-3-week-convention-drift audit. **Created first, updated
|
||||
> incrementally as findings are confirmed** — never let findings live only in agent
|
||||
> context. Keep the structure below; append findings as they land.
|
||||
>
|
||||
> Do **not** commit this file — it is a working artifact for the fix session.
|
||||
> Definition of done for the fix session: every P0/P1 resolved (or explicitly
|
||||
> marked won't-fix), `make gen && make check` green, `scripts/lint-conventions.sh`
|
||||
> green, smoke tests pass.
|
||||
|
||||
## Phase 0 — authority & conflict resolution
|
||||
|
||||
Order of authority when docs disagree:
|
||||
|
||||
1. **Templates** (`templates/pos-tool.sh`, `feature.sh`, `app.sh`) — the codified *current* convention for new files. If an old tool deviates from its template, that's drift (unless the tool is intentionally category-less/nested).
|
||||
2. **DEV.md** — full convention detail, checklists, best practices (env seams, systemd units, alerting, managed blocks).
|
||||
3. **AGENTS.md Quick facts** — operational facts (make gen/check gate, header format, INTERACTIVE_CMDS gotcha, deps-guard-before-help). If it conflicts with DEV.md, DEV.md wins on *detail*, AGENTS.md wins on *process*.
|
||||
4. **Code (`# POS:` headers, runtime behavior)** — ground truth for what a tool *does* and for all `GEN:`-derived docs (make gen regenerates from code).
|
||||
5. **POS.md / HOWTO / README / SCRIPTS / SYSTEMD / APPS** — hand-written references; drift against code = doc bug (fix the doc) unless the doc describes a feature the code never shipped (phantom feature — fix the doc too).
|
||||
6. **AGENT_Context_Project.md** — generated blocks follow code; hand-maintained rows (§14 Common Tasks, non-`pos-*` line-count rows, "How to modify" table) must match reality.
|
||||
|
||||
**Classification rule:** runtime behavior = what the tool *does*; conventions = what it *should* do. Findings are classified bug (behavior wrong), convention-violation (behavior right but against current convention), doc-drift (reference wrong), standardization (two tools same thing differently).
|
||||
|
||||
## Baseline
|
||||
|
||||
- `make gen && make check`: green (doc-sync, exec bits, syntax, dispatch smoke all pass).
|
||||
- `scripts/lint-conventions.sh` (new, this audit): automated convention gate — see results below.
|
||||
- AGENT_TODO: `Now` empty (Tier 1 shipped). Audit itself is the current work.
|
||||
- Scope order (user-confirmed): bin/ + lib/ first, then install scripts + systemd, then features/entertainment/apps, then docs. All areas audited; this is report priority.
|
||||
- Pickiness: everything flagged, tagged severity + confidence. P3 = intentional/legacy no-action list.
|
||||
- Deliverables: `MAINTENANCE.md` (uncommitted) + `scripts/lint-conventions.sh` (to be committed as the reusable gate; `make lint` target still needs wiring in the Makefile).
|
||||
|
||||
## Checklist (convention matrix — from AGENTS.md / AGENT_Context §11/§12 / DEV.md)
|
||||
|
||||
- [ ] Shebang `#!/usr/bin/env bash` + `set -euo pipefail` (libs exempt — sourced)
|
||||
- [ ] `# POS:` header right after shebang, ` — ` em-dash format; `# POS_FLAGS:` / `# POS_SUBCMDS:` / `# POS_CONFIG:` as needed
|
||||
- [ ] Dep-guards (`command -v … || err`) **before** `-h|--help`
|
||||
- [ ] stdin readers in `INTERACTIVE_CMDS` (bin/pos); no stale entries
|
||||
- [ ] No `local` outside functions
|
||||
- [ ] `run`/`spawn` respect `$DRY_RUN`; writes idempotent
|
||||
- [ ] common.sh sourced via `$(dirname "$0")/../lib/…` fallback chain (standalone from /usr/local/bin)
|
||||
- [ ] Entertainment plugins: no common.sh, `# POS_PLUGIN:`/`# POS_KEYS:`
|
||||
- [ ] Apps: `uninstall_<name>()` + `uninstall` dispatch
|
||||
- [ ] systemd units: `TimeoutStopSec=5s`, `[Install]`, `KillMode=` on daemons
|
||||
- [ ] Env seams: every system path write has `VAR="${VAR:-path}"` guard
|
||||
- [ ] No hardcoded secrets; chmod 600 on creds/tokens
|
||||
- [ ] Docs: POS.md / README / HOWTO / AGENT_Context hand-maintained spots match behavior
|
||||
|
||||
## Lint results (`scripts/lint-conventions.sh`)
|
||||
|
||||
_Automated gate — FAIL = definite violation, WARN = manual review needed. All 18 FAIL/WARN classes below are **verified real** against source (heuristics were iterated until zero false positives; heredocs, `${...}` brace-counting, `while`-loop stdin, `/dev/tty` reads, `command -v` fallbacks and env-guard secret patterns are all excluded)._
|
||||
|
||||
### FAIL — 12 (maps to tickets M-001, M-003..M-015) — ALL RESOLVED in the fix session (see ticket statuses)
|
||||
- `features/autostart.sh` — missing `set -euo pipefail` (→ M-001)
|
||||
- `bin/pos-system-firewall` — missing `-h|--help` handling (→ M-015)
|
||||
- `bin/pos-docker-compose` — reads stdin (read `-rp` :204, `confirm` :210) not in `INTERACTIVE_CMDS` (→ M-002)
|
||||
- `bin/pos-docker-vbox` — reads stdin (`confirm` :105) not in `INTERACTIVE_CMDS` (→ M-003)
|
||||
- `bin/pos-network-hotspot` — reads stdin (`read -rp` :53) not in `INTERACTIVE_CMDS` (→ M-004)
|
||||
- `bin/pos-docker-health` (:21/:24), `bin/pos-docker-ps` (:17/:20), `bin/pos-media-mp3` (:38/:58), `bin/pos-media-mp4` (:43/:71), `bin/pos-network-scan` (:28/:66), `bin/pos-share-usb-server` (:191/:199), `bin/pos-system-health` (:39/:119) — `-h|--help` dispatched before deps guards (→ M-008..M-014)
|
||||
|
||||
### WARN — 6 (manual review done, all real docs-coverage gaps → M-023) — ALL RESOLVED
|
||||
- `bin/pos-config`, `bin/pos-tree`, `bin/pos-entertainment-config`, `-enable`, `-disable`, `-status` — not referenced anywhere in DOC/POS.md
|
||||
|
||||
_Fix-session note: lint now reports 0 FAIL / 0 WARN. Two precision fixes were made to the lint itself (documented, not weakenings): `first_guard_line()` only treats real deps guards (`command -v … ||`, `if ! command -v`, `command -v … \`) as guards so graceful-degradation probes (system-health) don't trip the guard-before-help rule; `first_line()` skips comment lines so a comment containing `-h|--help` isn't mistaken for the dispatch._
|
||||
|
||||
_Heuristics that passed clean (reviewed, no findings): no secrets committed (all `TOKEN/SECRET/RPC_SECRET` assignments are env guards / config reads / runtime generation), all libs' system writes carry `command -v` fallbacks, legacy wrappers are thin, no plugin sources common.sh, systemd units carry `TimeoutStopSec=`/`WantedBy=`._
|
||||
|
||||
## Findings
|
||||
|
||||
Findings are tickets. Every ticket gets a unique `M-###` ID; the fix session works
|
||||
through them in order (P0/HIGH first). Keep this exact field set:
|
||||
|
||||
```
|
||||
### M-000
|
||||
Status: OPEN
|
||||
Severity: HIGH | MEDIUM | LOW
|
||||
Category: bug | convention | dead-code | docs | UX | standardization | security
|
||||
Files: <path:line …>
|
||||
Evidence: <what was observed, quote the actual code/output>
|
||||
Expected: <what the convention/runtime says should happen>
|
||||
Recommended fix: <one-line, actionable>
|
||||
Verification: <how the fix session proves it fixed>
|
||||
```
|
||||
|
||||
### P0 — bugs / security
|
||||
|
||||
### M-002
|
||||
Status: VERIFIED
|
||||
Severity: HIGH
|
||||
Category: bug
|
||||
Files: bin/pos-docker-compose:204,210; bin/pos:259
|
||||
Evidence: `pos docker compose config`/startup runs `read -rp "Enter your Tailscale auth key…"` (:204) and `confirm "Edit .env before starting?"` (:210) — both read stdin. `bin/pos` logging tee would swallow/hang these prompts because `docker-compose` is not in `INTERACTIVE_CMDS` (line 259). Same class of bug as the just-fixed smb-client prompt swallow.
|
||||
Expected: every tool that reads stdin is in `INTERACTIVE_CMDS`.
|
||||
Recommended fix: add `docker-compose` to `INTERACTIVE_CMDS`.
|
||||
Verification: `pos docker compose config` via `pos` (logged) still prompts.
|
||||
Fix (2026-08-14): added `docker-compose` to `INTERACTIVE_CMDS` in bin/pos:259. Verified: lint stdin-reader FAIL cleared for all three tools (M-002..M-004).
|
||||
|
||||
### M-003
|
||||
Status: VERIFIED
|
||||
Severity: HIGH
|
||||
Category: bug
|
||||
Files: bin/pos-docker-vbox:105; bin/pos:259
|
||||
Evidence: `pos docker vbox start` calls `confirm "Enter now?"` (stdin read via lib/common.sh `confirm()` → `read -rp`) at line 105. Not in `INTERACTIVE_CMDS` → prompt swallow/hang under the logging pipe.
|
||||
Expected: every stdin reader in `INTERACTIVE_CMDS`.
|
||||
Recommended fix: add `docker-vbox` to `INTERACTIVE_CMDS`.
|
||||
Verification: `pos docker vbox start` prompts correctly through the dispatcher.
|
||||
Fix (2026-08-14): added `docker-vbox` to `INTERACTIVE_CMDS` in bin/pos:259; lint stdin FAIL cleared.
|
||||
|
||||
### M-004
|
||||
Status: VERIFIED
|
||||
Severity: HIGH
|
||||
Category: bug
|
||||
Files: bin/pos-network-hotspot:53; bin/pos:259
|
||||
Evidence: `read -rp "Run in background? [y/N]: " bg` at line 53 (non-`--foreground` path). Not in `INTERACTIVE_CMDS` → prompt swallow/hang.
|
||||
Expected: every stdin reader in `INTERACTIVE_CMDS`.
|
||||
Recommended fix: add `network-hotspot` to `INTERACTIVE_CMDS`.
|
||||
Verification: `pos network hotspot start <dev> <iface>` prompts through the dispatcher.
|
||||
Fix (2026-08-14): added `network-hotspot` to `INTERACTIVE_CMDS` in bin/pos:259; lint stdin FAIL cleared.
|
||||
|
||||
### M-005
|
||||
Status: VERIFIED
|
||||
Severity: HIGH
|
||||
Category: bug
|
||||
Files: install.sh:38,84-99
|
||||
Evidence: `install.sh --steps` documents format "`1,3,4 or 1-3`" (line 38) but the filter is `[[ ",$STEPS_SPEC," != *",$phase_num,"* ]]` (line 86) — only comma-separated matches. `--steps 1-3` matches nothing → **zero phases run**, silently.
|
||||
Expected: both documented syntaxes work (comma list and `N-M`/`N-M,K` range).
|
||||
Recommended fix: expand the range spec into the explicit phase set before filtering (e.g. `1-3` → `1,2,3`).
|
||||
Verification: `install.sh --dry-run --steps 1-3` runs phases 1,2,3; `--steps 1,3` runs 1,3.
|
||||
Fix (2026-08-14): added `normalize_steps_spec()` to install.sh (placed after variable init, before arg loop) — expands `1-3` → `1,2,3`, validates single/range/list syntax, errors on garbage and end<start. Verified dry-run: `--steps 1-3`→1,2,3; `--steps 3`→3; `--steps 2-2`→2; `--steps 1,3`→1,3; `--steps 1-2-3` and `--steps 3-1` error.
|
||||
|
||||
### M-006
|
||||
Status: VERIFIED
|
||||
Severity: HIGH
|
||||
Category: docs
|
||||
Files: DOC/POS.md:217,220,298,324; DOC/howto/communication.md:83,91,102,200,317; DOC/howto/system.md:18,19,54,78; DOC/HOWTO.md:74; (code: bin/pos-system-health, bin/pos-system-backup)
|
||||
Evidence: `pos system health [--send] [--markdown]` and `pos system backup --send` are documented in 5 docs, but commit fe7708f deleted the flag handling from `bin/pos-system-health` (82 lines removed) — the tools now hit `err "Unknown option '--send'"`. Not a flag our tools accept → documented feature would fail at runtime.
|
||||
Expected: docs match code. Either restore `--send`/`--markdown` (notify-path) or strip every reference and rework the howto examples that use it (`@quiet pos system health --send` maps).
|
||||
Recommended fix: decide feature vs docs: restore the notify send flags in system-health/backup (they were deleted alongside the old pos-health units) OR remove all `--send`/`--markdown` references. Update HOWTO examples accordingly.
|
||||
Verification: no `--send`/`--markdown` mention in docs that isn't in code (or flags work).
|
||||
Fix (2026-08-14): decision — do NOT restore the flags. fe7708f deliberately made health a console-only reporter ("health itself never sends notifications. Sending is the wrapper's job"); backup never had a `--send` flag; the scheduler's `NOTIFY=always` already delivers full output. Docs corrected instead: DOC/POS.md:217,220,298,324; DOC/howto/communication.md:83,91,102,200,317; DOC/howto/system.md (17-19, schedule example, env sample); DOC/HOWTO.md:74. Grep-verified: no stale `system health --send`/`--markdown` refs remain (only legit `--markdown` for matrix sender / entertainment send).
|
||||
|
||||
### M-007
|
||||
Status: VERIFIED
|
||||
Severity: MEDIUM
|
||||
Category: bug
|
||||
Files: lib/notify.sh:57,73,79,83
|
||||
Evidence: `warn "…" 2>/dev/null || echo "…"` — when sourced standalone (notify.sh is designed to be sourceable without common.sh, line 3) and `warn` is absent, the `|| echo` fallback writes the message to **stdout**. notify.sh is meant to be a silent helper (per-file note line 3: "so it can be sourced by tools that define their own log/warn/err"); stdout here contaminates wrappers/plugins (e.g. entertainment send).
|
||||
Expected: notify.sh must never write to stdout.
|
||||
Recommended fix: route fallbacks to stderr (`warn() { printf … >&2; }` or `>&2` on the echoes).
|
||||
Verification: `lib/notify.sh` sourced alone prints nothing to stdout on a failed send.
|
||||
Fix (2026-08-14): only line 57 actually leaked stdout (`|| echo "…"`); lines 73/79/83 already end in `|| true` (silent). Routed line 57's fallback to stderr (`>…` on the echo). Verified: standalone `source lib/notify.sh; notify_send ""` → stdout empty, message on stderr.
|
||||
|
||||
### P1 — clear convention violations
|
||||
|
||||
### M-001
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: convention
|
||||
Files: features/autostart.sh
|
||||
Evidence: Lines 1-2: shebang then `LOG="${HOME:-/root}/.autostart.log"` directly — no `set -euo pipefail`. Template `templates/feature.sh:2` requires it; sibling `features/usb-automount.sh:2` has it.
|
||||
Expected: `set -euo pipefail` as line 2 (all scripts, libs exempt).
|
||||
Recommended fix: insert `set -euo pipefail` after the shebang (script already defensively uses `|| true`).
|
||||
Verification: `./scripts/lint-conventions.sh` clean; `bash -n features/autostart.sh`.
|
||||
Fix (2026-08-14): rewrote autostart.sh with the full feature-template preamble (`set -euo pipefail`, flags.sh load, usage()/`-h|--help` — folded into M-017). Verified: `bash -n` clean; `-h` prints usage; a real run appends the 3 log lines; lint clean.
|
||||
|
||||
### M-008..M-014 — `-h|--help` before deps guards (standardization)
|
||||
Status: VERIFIED
|
||||
Severity: MEDIUM
|
||||
Category: standardization
|
||||
Files: bin/pos-docker-health:21/24, bin/pos-docker-ps:17/20, bin/pos-media-mp3:38/58, bin/pos-media-mp4:43/71, bin/pos-network-scan:28/66, bin/pos-share-usb-server:191/199, bin/pos-system-health:39/119
|
||||
Evidence: AGENTS.md/DEV.md convention — `command -v` deps guards sit **before** the `-h|--help` dispatch so help also errors on a box missing the dependency. These 7 tools dispatch help first. For docker-health/docker-ps the guard is only ~3 lines late (near-miss). For system-health there is **no top-level guard at all** (all `command -v` are per-check runtime probes at 119/166/182) — it degrades gracefully instead.
|
||||
Expected: uniform guard-before-help, or an explicitly documented exception for graceful-degradation tools.
|
||||
Recommended fix: move guards above help in the 6 hard-dep tools; for system-health either add a minimal guard (docker/fail2ban/systemctl are optional by design) or document it as the sanctioned no-guard pattern in DEV.md.
|
||||
Verification: `make check` green; `./scripts/lint-conventions.sh` shows no dep-guard FAILs; each tool's `--help` still works without deps installed.
|
||||
Fix (2026-08-14): 6 hard-dep tools now guard before help — docker-health, docker-ps (converted to documented `command -v X || err` form), network-scan (moved up, kept its echo/exit style), share-usb-server (moved up), media-mp3/mp4 (guards moved before help with a `--dry-run` pre-scan preserving the documented dry-run-without-deps behavior; duplicate `DRY_RUN=0` and post-loop guard blocks removed). system-health documented as the sanctioned no-guard pattern in DEV.md (graceful degradation). Lint refined: `first_guard_line()` only matches real guards (`command -v … ||`, `if ! command -v`, `command -v … \`) and `first_line()` skips comment lines. Verified: lint shows no dep-guard FAILs; restricted-PATH tests — `pos media mp3 -h` errors without deps, `--dry-run` still previews; `pos share usb-server -h` errors (usbsrv absent); mp4 mutual-exclusion checks intact.
|
||||
|
||||
### M-015
|
||||
Status: VERIFIED
|
||||
Severity: MEDIUM
|
||||
Category: convention
|
||||
Files: bin/pos-system-firewall
|
||||
Evidence: `pos system firewall` has no `usage()` and no `-h|--help` case at all — first thing is a root check, then `read -rp "Execute this command?…"`. Violates the universal tool contract (templates/pos-tool.sh). Lint FAIL confirms.
|
||||
Expected: `-h|--help` shows a usage synopsis.
|
||||
Recommended fix: add `usage()` + `-h|--help` case (per templates/pos-tool.sh), keeping the root/deps checks ahead of it.
|
||||
Verification: `pos system firewall --help` prints usage without prompting.
|
||||
Fix (2026-08-14): added `usage()` + `-h|--help` case after the root check and notify.sh source (root check stays first — tool is root-only by design). Verified as root: `sudo pos-system-firewall -h` prints usage, exit 0.
|
||||
|
||||
### M-016
|
||||
Status: VERIFIED
|
||||
Severity: MEDIUM
|
||||
Category: convention
|
||||
Files: preinstall.sh:28-41 (PACKAGES); bin/pos-media-mp3:59, bin/pos-media-mp4:72
|
||||
Evidence: both media tools `command -v ffmpeg || err …` — ffmpeg is an apt package, so per DEV.md "apt packages → PACKAGES array in preinstall.sh" it belongs there. It's absent (only yt-dlp is handled, via manual curl).
|
||||
Expected: ffmpeg in `PACKAGES`; tools keep the guard as a safety net for non-install.sh installs.
|
||||
Recommended fix: add `ffmpeg` to PACKAGES.
|
||||
Verification: `install.sh` installs ffmpeg; mp3/mp4 guards remain as fallback.
|
||||
Fix (2026-08-14): added `ffmpeg` to the PACKAGES array in preinstall.sh. `bash -n` clean; mp3/mp4 `command -v ffmpeg || err` guards untouched.
|
||||
|
||||
### M-017
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: convention
|
||||
Files: features/autostart.sh
|
||||
Evidence: template `templates/feature.sh:24-26` requires sourcing `lib/flags.sh` (`# POS_FLAGS:` support), and the tool contract requires `-h|--help`. autostart.sh has neither — it's a bare boot script. Also missing `set -euo pipefail` (M-001).
|
||||
Expected: feature template contract (set -euo pipefail, flags.sh load, -h|--help).
|
||||
Recommended fix: add the template preamble (flags.sh load + usage/-h); no functional change to boot logic.
|
||||
Verification: `bash -n` clean; `-h` prints usage; lint clean.
|
||||
Fix (2026-08-14): full template preamble added (flags.sh load via the repo/usr-local fallback chain, FEATURE_NAME, usage() with Flag/Log lines, `-h|--help` case). Boot logic unchanged. Verified: `bash -n`, `-h` usage, live run logs 3 lines, lint clean.
|
||||
|
||||
### M-018
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: convention
|
||||
Files: features/usb-automount.sh
|
||||
Evidence: same as M-017 — feature template `flags.sh` load (templates/feature.sh:24-26) absent. (Has `set -euo pipefail`, so only flags.sh + usage are missing.)
|
||||
Recommended fix: add flags.sh load + usage/-h per template.
|
||||
Verification: lint clean; `-h` works.
|
||||
Fix (2026-08-14): added the flags.sh load (repo/usr-local fallback) right after `set -euo pipefail`. usage()/`-h|--help` already present. Verified: `bash -n`, lint clean.
|
||||
|
||||
### M-019
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: standardization
|
||||
Files: apps/media/scrcpy.sh
|
||||
Evidence: 0644 (`-rw-rw-r--`) while every other `apps/*/*.sh` is 0755. Consistent exec-bit convention violated.
|
||||
Recommended fix: `chmod +x apps/media/scrcpy.sh`.
|
||||
Verification: `ls -l apps/media/scrcpy.sh` shows 0755; `make check` exec-bit gate green.
|
||||
Fix (2026-08-14): `chmod +x apps/media/scrcpy.sh` → 0755. `make check` exec-bit gate will confirm at final run.
|
||||
|
||||
### M-020
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: standardization
|
||||
Files: bin/pos-docker-compose:8,9
|
||||
Evidence: `SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"` and `CONFIG_ENV="${HOME}/.config/linux_post_install/compose.env"` hardcoded — no `${VAR:-…}` env seam, unlike the documented `SERVICES_BASE` default (/srv). DEV.md seam rule says every system path write has a `VAR="${VAR:-path}"` guard.
|
||||
Expected: `SCALE_DIR="${SCALE_DIR:-/usr/local/share/…}"` etc.
|
||||
Recommended fix: add `:-` seams for SCALE_DIR and CONFIG_ENV.
|
||||
Verification: env override respected in a dry-run.
|
||||
Fix (2026-08-14): added `:-` seams for both. Verified: `CONFIG_ENV=/tmp/... config` prints the override; `SCALE_DIR=/tmp/... ls` errors citing the override.
|
||||
Follow-on found while verifying: `pos docker compose config` crashed with `DIM: unbound variable` (common.sh color block had no DIM; pre-existing on HEAD). Fixed by adding `DIM=$(tput dim)` / empty-fallback to the common.sh color block. `config` now prints correctly.
|
||||
|
||||
### P2 — doc drift / consistency / style
|
||||
|
||||
### M-021
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: standardization
|
||||
Files: lib/notify.sh:27, lib/entertainment-lib.sh:5, lib/config-ui.sh:22, bin/pos-network-download:16, bin/pos-communication-{matrix,telegram}-{listener,sender}
|
||||
Evidence: `CONFIG_DIR` redefined per-file with inconsistent semantics: notify.sh is XDG-aware (`${XDG_CONFIG_HOME:-$HOME/.config}`), the rest use plain `$HOME/.config/linux_post_install`, network-download uses the `${CONFIG_DIR:-…}` seam form. Same global name, three conventions.
|
||||
Expected: one definition (in lib/common.sh) + seam form everywhere.
|
||||
Recommended fix: define `CONFIG_DIR` once in common.sh; drop per-file definitions (keep the `:-` seam in network-download).
|
||||
Verification: all tools still resolve config after refactor.
|
||||
Fix (2026-08-14): canonical `CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"` defined in common.sh; dropped the duplicate in entertainment-lib.sh (loads common.sh) and network-download (loads common.sh). Standalone-sourced files (notify.sh, config-ui.sh, the matrix/telegram listener/sender — none source common.sh) keep an identical guarded seam line, mirroring the "no shared lib? inline fallbacks" convention; noted in common.sh. Verified: notify.sh standalone resolves default/XDG/CONFIG_DIR overrides correctly; pos-config (common.sh + config-ui) works; all touched files bash -n clean.
|
||||
|
||||
### M-022
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: standardization
|
||||
Files: lib/entertainment-lib.sh (plugin_dir/plugin_exists/plugin_keys/plugin_marker), lib/entertainment-plugin-lib.sh (plugin_config_file/plugin_err/plugin_have/plugin_http_json/plugin_load_config/plugin_require)
|
||||
Evidence: two libs share the `plugin_*` prefix for unrelated jobs (marker/registry helpers vs plugin runtime helpers). Collision-prone and unclear at call sites.
|
||||
Expected: distinct prefixes per concern.
|
||||
Recommended fix: rename the plugin-lib helpers (e.g. `plt_*`/`plugin_rt_*`) or the marker helpers; keep one prefix per lib.
|
||||
Verification: grep for `plugin_` shows no cross-lib ambiguity; `make gen && make check` green.
|
||||
Fix (2026-08-14): kept `plugin_*` for the documented plugin-authoring API (`plugin_load_config`/`plugin_have`/`plugin_require`/`plugin_http_json`/`plugin_err`/`plugin_config_file` — referenced in DOC/DEV.md and DOC/POS.md; user plugins depend on it). Renamed the internal registry helpers in lib/entertainment-lib.sh to `ent_plugin_dir`/`ent_plugin_marker`/`ent_plugin_exists`/`ent_plugin_keys` and updated callers (pos-entertainment-{status,enable,config,send}, lib/config-ui.sh). Verified: no bare `plugin_` registry helpers remain; plugin runtime helpers untouched; entertainment tools + pos-config smoke-tested; bash -n clean.
|
||||
|
||||
### M-023
|
||||
Status: VERIFIED
|
||||
Severity: LOW
|
||||
Category: docs
|
||||
Files: DOC/POS.md; bin/pos-config, bin/pos-tree, bin/pos-entertainment-{config,enable,disable,status}
|
||||
Evidence: 6 shipped tools are absent from the POS.md command reference (31 `bin/pos-*` refs exist but not these). Lint WARN confirms.
|
||||
Expected: every tool documented in POS.md.
|
||||
Recommended fix: add rows to the POS.md command table (and cross-check HOWTO for the entertainment group).
|
||||
Verification: lint WARNs gone; `grep` shows each tool in POS.md.
|
||||
Fix (2026-08-14): the tools were documented by command name but not by filename (the lint references basenames). Added `**File:** bin/pos-config` (config section), `**File:** bin/pos-tree` (tree section), and a file list on the entertainment section header covering `bin/pos-entertainment-{config,enable,disable,status}`. Verified: lint 0 WARN. HOWTO already covers the entertainment group via `pos entertainment *` command forms.
|
||||
|
||||
### P3 — intentional / legacy (no action)
|
||||
- install.sh:123,135,155,185 — installer writes to /usr/local/bin are its purpose; no seam needed (lint excludes install scripts).
|
||||
- network-download RPC_SECRET at :150 — generated at runtime (`/dev/urandom`), not a committed secret.
|
||||
- system-health graceful degradation — candidate for the sanctioned no-guard pattern (see M-014); decide in fix session whether it becomes a P1 or a P3. Commit fe7708f's removal of the old `pos-health.{service,timer}` units is correct (superseded by scheduler); the only leftover is the stale docs (M-006).
|
||||
|
||||
## Semantic deep-dive notes (the detective pass)
|
||||
|
||||
- `lib/notify.sh` — stdout-leak via `warn … || echo` fallbacks (M-007); rest of helper logic (platform routing, silent-fail contract) correct.
|
||||
- `lib/common.sh` — `confirm()` reads stdin via `read -rp` → it makes any caller an INTERACTIVE_CMDS candidate (this is how M-003/M-002 were caught). Deps guards, `run`/`spawn`, `$DRY_RUN` semantics all match convention.
|
||||
- `bin/pos` — INTERACTIVE_CMDS list (259) was complete except the 3 new stdin readers (M-002..M-004); no stale entries (lint reverse-check green). Dispatcher longest-prefix logic unchanged from prior fix session.
|
||||
- `bin/pos-system-firewall` — entirely interactive; root-check-first is correct, but no usage/-h at all (M-015).
|
||||
- `bin/pos-system-health` — pure passive reporter; per-check `command -v` probes are correct for graceful degradation, but leave the no-top-level-guard pattern undocumented (M-014). Prints to stdout by design (it IS the report) — the notify path is what the docs claim (M-006) but was removed in fe7708f.
|
||||
- `install.sh` — `--steps` comma-only parsing (M-005); the phase functions themselves and step numbering are consistent with the header. Legacy `x-systemd.automount`-style remnants: none — the fstab/automount handling in share tools uses correct systemd units now (verified in prior fix session, commits 1724096/93fb6b6).
|
||||
- `pos-docker-compose` — hardcoded template dir + config path (M-020); `SERVICES_BASE` documented in `# POS_CONFIG:` and used at runtime for deployments, but not for SCALE_DIR.
|
||||
- No `x-systemd.automount` in any unit `Options=` (grep clean). No `interact`/`getconf`-style stale-format headers anywhere in `bin/`.
|
||||
- Entertainment plugins: none source common.sh; all carry `# POS_PLUGIN:` + `# POS_KEYS:` (lint green).
|
||||
- systemd units: `TimeoutStopSec=` + `[Install] WantedBy=` present everywhere (lint green); no leftover `pos-health.{service,timer}` (removed in fe7708f, correct).
|
||||
|
||||
## Next-session brief
|
||||
|
||||
1. Fix P0 → P1 → P2 in order (HIGH first: M-002..M-006, then M-007, then the rest).
|
||||
2. M-006 needs a product decision first: restore `--send`/`--markdown` (notify path) or strip the docs.
|
||||
3. Re-run `make gen && make check && make lint` + smoke each changed tool.
|
||||
4. ✅ DONE — `scripts/lint-conventions.sh` committed and `make lint` wired into the Makefile (5ef38dc).
|
||||
5. Move audit tasks to AGENT_TODO Done (dated) on completion.
|
||||
|
||||
## Checked & clean
|
||||
|
||||
- lib/common.sh (deps/run/spawn/confirm — semantics verified)
|
||||
- lib/notify.sh (logic — only the stdout-fallback leak, M-007)
|
||||
- legacy wrappers bin/wr-*, bin/mp3, bin/mp4, bin/vbox, bin/ssh-load-all (thin, forward to pos)
|
||||
- systemd/*.service (TimeoutStopSec + WantedBy everywhere)
|
||||
- entertainment/*.sh (no common.sh, POS_PLUGIN/POS_KEYS present)
|
||||
- secrets: no committed credentials anywhere (grep for TOKEN/SECRET/AUTHKEY/RPC_SECRET literals clean — all guards/config-reads/runtime-gen)
|
||||
- stale-format conventions: no `x-systemd.automount`, no old header formats, no leftover pos-health units
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: check gen hook
|
||||
.PHONY: check gen hook lint
|
||||
|
||||
## check — verify repo self-consistency (syntax, exec bits, doc/code sync, smoke)
|
||||
check:
|
||||
@@ -8,6 +8,10 @@ check:
|
||||
gen:
|
||||
./scripts/gen-docs.sh
|
||||
|
||||
## lint — convention gate (shebang/pipefail, headers, deps-guard ordering, stdin, secrets, docs)
|
||||
lint:
|
||||
./scripts/lint-conventions.sh
|
||||
|
||||
## hook — install the opt-in pre-commit hook (runs `make check`)
|
||||
hook:
|
||||
./scripts/install-hooks.sh
|
||||
|
||||
Regular → Executable
@@ -256,7 +256,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="system-firewall media-mp4 system-backup share-usb-server share-smb-server share-smb-client communication-telegram-listener communication-matrix-listener ai-gemini system-schedule entertainment-config config"
|
||||
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 communication-telegram-listener communication-matrix-listener ai-gemini system-schedule entertainment-config config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
# POS: communication matrix-listener — Matrix listener: map /command → bash, run them on room messages
|
||||
# POS_FLAGS: --enable --disable --status --run
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/matrix.env"
|
||||
MAP_FILE="$CONFIG_DIR/matrix_commands.env"
|
||||
SERVICE="pos-matrix-listener.service"
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
# POS_SUBCMDS: send test login
|
||||
# POS_CONFIG: matrix | matrix.env | MATRIX_HOMESERVER=:Homeserver URL (https://matrix.example.org)::https://matrix.example.org | MATRIX_ACCESS_TOKEN=secret:Access token (from 'pos communication matrix sender login' or a Matrix client) | MATRIX_USER_ID=:Your Matrix user id (set by login)::@you:example.org | MATRIX_ROOM_ID=:Room id or alias::#pos:example.org
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/matrix.env"
|
||||
|
||||
usage() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
# POS: communication scrcpy — Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
|
||||
# POS_SUBCMDS: devices record tcpip connect push pull screenshot info
|
||||
# POS_CONFIG: scrcpy | scrcpy.env | SCRCPY_SERIAL=:Default adb serial (from 'pos communication scrcpy devices') | SCRCPY_MAX_SIZE=:Limit video size (e.g. 1920) | SCRCPY_MAX_FPS=:Limit frame rate (e.g. 60) | SCRCPY_BIT_RATE=:Video bit rate (e.g. 8M) | SCRCPY_FULLSCREEN=:Start fullscreen (true/false) | SCRCPY_RECORD_DIR=:Record output dir (default ~/Videos/scrcpy) | SCRCPY_PUSH_TARGET=:adb push destination (default /sdcard/Download) | SCRCPY_EXTRA_FLAGS=:Extra scrcpy flags appended to every mirror
|
||||
# POS_CONFIG: scrcpy | scrcpy.env | SCRCPY_SERIAL=:Default adb serial (from 'pos communication scrcpy devices') | SCRCPY_MAX_SIZE=:Limit video size (e.g. 1920) | SCRCPY_MAX_FPS=:Limit frame rate (e.g. 60) | SCRCPY_BIT_RATE=:Video bit rate (e.g. 8M) | SCRCPY_FULLSCREEN=:Start fullscreen (true/false) | SCRCPY_NEW_DISPLAY=:New virtual display (--new-display): true, WxH, WxH/DPI or /DPI | SCRCPY_AUDIO=:Forward device audio to the desktop (true/false, default true) | SCRCPY_RECORD_DIR=:Record output dir (default ~/Videos/scrcpy) | SCRCPY_PUSH_TARGET=:adb push destination (default /sdcard/Download) | SCRCPY_EXTRA_FLAGS=:Extra scrcpy flags appended to every mirror
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
@@ -36,6 +36,8 @@ SCRCPY_FULLSCREEN="${SCRCPY_FULLSCREEN:-false}"
|
||||
SCRCPY_RECORD_DIR="${SCRCPY_RECORD_DIR:-$HOME/Videos/scrcpy}"
|
||||
SCRCPY_PUSH_TARGET="${SCRCPY_PUSH_TARGET:-/sdcard/Download}"
|
||||
SCRCPY_EXTRA_FLAGS="${SCRCPY_EXTRA_FLAGS:-}"
|
||||
SCRCPY_NEW_DISPLAY="${SCRCPY_NEW_DISPLAY:-}"
|
||||
SCRCPY_AUDIO="${SCRCPY_AUDIO:-true}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -70,6 +72,7 @@ Precedence: CLI flags > environment > config file.
|
||||
Examples:
|
||||
pos communication scrcpy
|
||||
pos communication scrcpy --no-audio --always-on-top
|
||||
pos communication scrcpy --new-display=1920x1080
|
||||
pos communication scrcpy devices
|
||||
pos communication scrcpy record --headless
|
||||
pos communication scrcpy tcpip 5555
|
||||
@@ -109,9 +112,10 @@ _require_device() {
|
||||
_dev_name() { printf '%s' "${SCRCPY_SERIAL:-device}" | tr ':' '-'; }
|
||||
|
||||
_extra_flags() {
|
||||
[ -n "$SCRCPY_EXTRA_FLAGS" ] || return 0
|
||||
local -a extra
|
||||
[ -n "$SCRCPY_EXTRA_FLAGS" ] && read -r -a extra <<< "$SCRCPY_EXTRA_FLAGS"
|
||||
printf '%s\n' "${extra[@]+"${extra[@]}"}"
|
||||
read -r -a extra <<< "$SCRCPY_EXTRA_FLAGS"
|
||||
printf '%s\n' "${extra[@]}"
|
||||
}
|
||||
|
||||
# Build the scrcpy command from config defaults + pass-through args.
|
||||
@@ -123,7 +127,18 @@ _mirror() { # $@ = pass-through scrcpy flags
|
||||
[ -n "$SCRCPY_MAX_FPS" ] && cmd+=(--max-fps "$SCRCPY_MAX_FPS")
|
||||
[ -n "$SCRCPY_BIT_RATE" ] && cmd+=(--video-bit-rate "$SCRCPY_BIT_RATE")
|
||||
[ "$SCRCPY_FULLSCREEN" = "true" ] && cmd+=(--fullscreen)
|
||||
while IFS= read -r f; do cmd+=("$f"); done < <(_extra_flags)
|
||||
case "${SCRCPY_NEW_DISPLAY:-}" in
|
||||
true|yes) cmd+=(--new-display) ;;
|
||||
[0-9]*x[0-9]*|/[0-9]*) cmd+=(--new-display="$SCRCPY_NEW_DISPLAY") ;;
|
||||
"") : ;;
|
||||
*) err "invalid SCRCPY_NEW_DISPLAY '$SCRCPY_NEW_DISPLAY' — use true, <W>x<H>, <W>x<H>/<DPI> or /<DPI> (e.g. 1920x1080, 1920x1080/420, /240)" ;;
|
||||
esac
|
||||
case "${SCRCPY_AUDIO:-true}" in
|
||||
true|yes|1) : ;;
|
||||
false|no|0) cmd+=(--no-audio) ;;
|
||||
*) err "invalid SCRCPY_AUDIO '$SCRCPY_AUDIO' — use true or false" ;;
|
||||
esac
|
||||
while IFS= read -r f; do [ -n "$f" ] && cmd+=("$f"); done < <(_extra_flags)
|
||||
cmd+=("$@")
|
||||
exec "${cmd[@]}"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
# POS: communication telegram-listener — Telegram bot listener: map /command → bash, run them on chat messages
|
||||
# POS_FLAGS: --enable --disable --status --sync-commands --run
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
MAP_FILE="$CONFIG_DIR/telegram_commands.env"
|
||||
API="https://api.telegram.org"
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
# 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
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||
API="https://api.telegram.org"
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ set -euo pipefail
|
||||
# POS_CONFIG: compose | compose.env | TS_AUTHKEY=secret:Tailscale auth key for the sidecar | TZ=:Service timezone (default Europe/Amsterdam) | DNS_SERVER=:Custom DNS server (default 9.9.9.9) | SERVICES_BASE=:Deployment root (default /srv)
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"
|
||||
CONFIG_ENV="${HOME}/.config/linux_post_install/compose.env"
|
||||
SCALE_DIR="${SCALE_DIR:-/usr/local/share/linux_post_install/scale-tail/services}"
|
||||
CONFIG_ENV="${CONFIG_ENV:-${HOME}/.config/linux_post_install/compose.env}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
|
||||
@@ -17,15 +17,12 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
command -v docker &>/dev/null || err "docker not found — install it with: sudo apt install docker.io"
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "docker not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! container_ids=$(docker ps -a -q 2>/dev/null) || [[ -z "$container_ids" ]]; then
|
||||
echo "No containers."
|
||||
exit 0
|
||||
|
||||
+2
-5
@@ -13,15 +13,12 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
command -v docker &>/dev/null || err "docker not found — install it with: sudo apt install docker.io"
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "docker not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! container_ids=$(docker ps -q 2>/dev/null) || [[ -z "$container_ids" ]]; then
|
||||
echo "No running containers."
|
||||
exit 0
|
||||
|
||||
@@ -43,7 +43,7 @@ After 'set'/'unset'/'edit', auto-trigger timers are re-synced from ENABLED.
|
||||
Config file: $CONFIG_FILE
|
||||
|
||||
Keys (declared by the installed plugins' # POS_KEYS: headers):
|
||||
$(keys_section "$(plugin_dir)")
|
||||
$(keys_section "$(ent_plugin_dir)")
|
||||
|
||||
Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...
|
||||
|
||||
@@ -60,7 +60,7 @@ EOF
|
||||
}
|
||||
|
||||
key_known() {
|
||||
config_key_known "$(plugin_dir)" "$1"
|
||||
config_key_known "$(ent_plugin_dir)" "$1"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
@@ -84,14 +84,14 @@ case "${1:-}" in
|
||||
while IFS= read -r line; do
|
||||
IFS='|' read -r _plugin key _desc _req <<<"$line"
|
||||
[ ${#key} -gt "$maxw" ] && maxw=${#key}
|
||||
done <<< "$(config_keys "$(plugin_dir)")"
|
||||
done <<< "$(config_keys "$(ent_plugin_dir)")"
|
||||
printf ' %-*s %s\n' "$maxw" "ENABLED" "\"$(config_value ENABLED)\""
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
IFS='|' read -r plugin key desc req <<<"$line"
|
||||
v="$(config_value "$key")"
|
||||
printf ' %-*s %s [%s] %s (%s)\n' "$maxw" "$key" "${v:-<not set>}" "$plugin" "$desc" "$req"
|
||||
done <<< "$(config_keys "$(plugin_dir)")"
|
||||
done <<< "$(config_keys "$(ent_plugin_dir)")"
|
||||
;;
|
||||
edit)
|
||||
shift
|
||||
@@ -138,6 +138,6 @@ case "${1:-}" in
|
||||
echo "Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=..."
|
||||
echo
|
||||
echo "Plugin keys (declared in each plugin's # POS_KEYS: header):"
|
||||
keys_section "$(plugin_dir)"
|
||||
keys_section "$(ent_plugin_dir)"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -15,7 +15,7 @@ and schedules 'pos entertainment send <plugin>' to run as a systemd user
|
||||
timer. Re-running without an interval keeps the plugin's current one.
|
||||
|
||||
Available plugins:
|
||||
$(list_plugins "$(plugin_dir)" | sed 's/^/ /')
|
||||
$(list_plugins "$(ent_plugin_dir)" | sed 's/^/ /')
|
||||
|
||||
Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...
|
||||
Default: $DEFAULT_INTERVAL
|
||||
@@ -38,7 +38,7 @@ esac
|
||||
plugin="$1"
|
||||
interval="${2:-}"
|
||||
|
||||
resolve_plugin "$(plugin_dir)" "$plugin" >/dev/null
|
||||
resolve_plugin "$(ent_plugin_dir)" "$plugin" >/dev/null
|
||||
if [ -n "$interval" ]; then
|
||||
ut_interval_to_oncalendar "$interval" >/dev/null || \
|
||||
err "Invalid interval '$interval' (allowed: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...)"
|
||||
|
||||
@@ -9,7 +9,7 @@ source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")
|
||||
|
||||
usage() {
|
||||
local dir plugins
|
||||
dir="$(plugin_dir 2>/dev/null)" || dir=""
|
||||
dir="$(ent_plugin_dir 2>/dev/null)" || dir=""
|
||||
if [ -n "$dir" ]; then
|
||||
plugins="$(list_plugins "$dir" | sed 's/^/ /')"
|
||||
else
|
||||
@@ -67,7 +67,7 @@ plugin="${plugin_args[0]}"
|
||||
unset 'plugin_args[0]'
|
||||
|
||||
# ── Run the plugin ──────────────────────────────────────────────
|
||||
dir="$(plugin_dir)"
|
||||
dir="$(ent_plugin_dir)"
|
||||
script="$(resolve_plugin "$dir" "$plugin")"
|
||||
|
||||
rc=0
|
||||
|
||||
@@ -36,7 +36,7 @@ for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||
done
|
||||
|
||||
not_enabled=()
|
||||
for name in $(list_plugins "$(plugin_dir)"); do
|
||||
for name in $(list_plugins "$(ent_plugin_dir)"); do
|
||||
found=0
|
||||
for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||
[ "${entry%%,*}" = "$name" ] && found=1 && break
|
||||
|
||||
+13
-7
@@ -5,6 +5,19 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# --dry-run is parsed before the deps guards (below): it previews the yt-dlp
|
||||
# command without requiring yt-dlp/ffmpeg to be installed.
|
||||
DRY_RUN=0
|
||||
for arg in "$@"; do
|
||||
[ "$arg" = "--dry-run" ] && DRY_RUN=1
|
||||
done
|
||||
|
||||
# Deps guards sit before -h|--help (help also errors on a box missing the deps).
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp"
|
||||
command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP3 conversion) — install it with: sudo apt install ffmpeg"
|
||||
fi
|
||||
|
||||
OUT_DIR="$HOME/Music"
|
||||
BY_ARTIST=0
|
||||
EXTRA_ARGS=()
|
||||
@@ -31,7 +44,6 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
DRY_RUN=0
|
||||
URL=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -53,12 +65,6 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
[ -n "$URL" ] || usage
|
||||
|
||||
# Dependency checks (skipped under --dry-run: the command is only previewed).
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp"
|
||||
command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP3 conversion) — install it with: sudo apt install ffmpeg"
|
||||
fi
|
||||
|
||||
if [ "$BY_ARTIST" -eq 1 ]; then
|
||||
OUT_TEMPLATE="$OUT_DIR/%(artist,uploader)s/%(title)s.%(ext)s"
|
||||
else
|
||||
|
||||
+13
-7
@@ -5,6 +5,19 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
# --dry-run is parsed before the deps guards (below): it previews the yt-dlp
|
||||
# command without requiring yt-dlp/ffmpeg to be installed.
|
||||
DRY_RUN=0
|
||||
for arg in "$@"; do
|
||||
[ "$arg" = "--dry-run" ] && DRY_RUN=1
|
||||
done
|
||||
|
||||
# Deps guards sit before -h|--help (help also errors on a box missing the deps).
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp"
|
||||
command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP4 merge) — install it with: sudo apt install ffmpeg"
|
||||
fi
|
||||
|
||||
OUT_DIR="$HOME/Videos"
|
||||
FORMAT=""
|
||||
BEST=0
|
||||
@@ -36,7 +49,6 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
DRY_RUN=0
|
||||
URL=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -66,12 +78,6 @@ done
|
||||
&& err "--format and --best/--worst are mutually exclusive"
|
||||
[ "$BEST" -eq 1 ] && [ "$WORST" -eq 1 ] && err "--best and --worst are mutually exclusive"
|
||||
|
||||
# Dependency checks (skipped under --dry-run: the command is only previewed).
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp"
|
||||
command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP4 merge) — install it with: sudo apt install ffmpeg"
|
||||
fi
|
||||
|
||||
# Resolve the format selector: explicit flag → smart preset → interactive.
|
||||
# The curated table goes to stderr; stdout carries ONLY the chosen format id
|
||||
# (same ui_pick lesson — a $(...) capture must not swallow the menu).
|
||||
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: media sync — Incremental Music → USB sync (mp3/mp4, add/update only)
|
||||
# POS_FLAGS: --mp3 --mp4 --source --dry-run
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
||||
|
||||
load_system_env
|
||||
|
||||
# Deps guards sit before -h|--help (help also errors on a box missing the deps).
|
||||
command -v lsblk &>/dev/null || err "lsblk not found (util-linux) — needed to detect USB storage"
|
||||
command -v jq &>/dev/null || err "jq not found — needed to detect USB storage (sudo apt install jq)"
|
||||
|
||||
SRC="${MEDIA_SYNC_SOURCE:-$HOME/Music}"
|
||||
DEST_DIR="${MEDIA_SYNC_DEST:-Music}"
|
||||
MP3=0
|
||||
MP4=0
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos media sync [options]
|
||||
|
||||
Incrementally copy your music onto a USB stick. Add/update only — files on
|
||||
the stick that are no longer in the source are left alone, never deleted.
|
||||
USB storage is detected like \`pos system backup\` (lsblk TRAN + lsusb/by-id
|
||||
corroboration; unmounted sticks get a mount offer first) and you pick which
|
||||
one to sync to. The source tree is mirrored under <usb>/$DEST_DIR.
|
||||
|
||||
Options:
|
||||
--mp3 Sync only *.mp3 files
|
||||
--mp4 Sync only *.mp4 files
|
||||
--source <dir> Source folder (default: $SRC)
|
||||
--dry-run Preview what would be copied (copies nothing)
|
||||
-h, --help This help
|
||||
|
||||
Examples:
|
||||
pos media sync
|
||||
pos media sync --mp3
|
||||
pos media sync --mp4 --dry-run
|
||||
pos media sync --source /data/Music
|
||||
|
||||
Environment:
|
||||
MEDIA_SYNC_SOURCE Source folder (default: \$HOME/Music)
|
||||
MEDIA_SYNC_DEST Subfolder on the USB stick (default: Music)
|
||||
USB_MOUNT_BASE Where to mount an unmounted stick (default: /media)
|
||||
USB_BYID by-id dir used to corroborate USB detection
|
||||
(default: /dev/disk/by-id)
|
||||
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--mp3) MP3=1; shift ;;
|
||||
--mp4) MP4=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--source)
|
||||
[ $# -ge 2 ] || err "--source needs a value: pos media sync --source <dir>"
|
||||
SRC="$2"; shift 2 ;;
|
||||
--source=*) SRC="${1#--source=}"; shift ;;
|
||||
-*) err "Unknown option: $1 (see --help)" ;;
|
||||
*) err "Unexpected argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
[ "$MP3" -eq 1 ] || [ "$MP4" -eq 1 ] || { MP3=1; MP4=1; }
|
||||
|
||||
[ -d "$SRC" ] || err "Source not found: $SRC"
|
||||
|
||||
trap 'notify_send "Music sync FAILED"' ERR
|
||||
|
||||
section "Music sync"
|
||||
echo "Source : $SRC"
|
||||
if [ "$MP3" -eq 1 ] && [ "$MP4" -eq 1 ]; then
|
||||
echo "Filter : mp3 + mp4"
|
||||
find_expr=(-type f \( -iname '*.mp3' -o -iname '*.mp4' \))
|
||||
elif [ "$MP3" -eq 1 ]; then
|
||||
echo "Filter : mp3 only"
|
||||
find_expr=(-type f -iname '*.mp3')
|
||||
else
|
||||
echo "Filter : mp4 only"
|
||||
find_expr=(-type f -iname '*.mp4')
|
||||
fi
|
||||
|
||||
usb_pick_root "Sync to" "$DEST_DIR" "no sync performed" || {
|
||||
log "Skipped — no sync performed"
|
||||
exit 0
|
||||
}
|
||||
dest_root="${USB_ROOT%/}/$DEST_DIR"
|
||||
echo "Target : $dest_root"
|
||||
|
||||
# Does the destination need this file copied? Missing, or size/mtime differs.
|
||||
needs_copy() {
|
||||
local src="$1" dst="$2" ss="" ds="" sm="" dm=""
|
||||
[ -f "$dst" ] || return 0
|
||||
ss="$(stat -c %s "$src" 2>/dev/null || printf 0)"
|
||||
ds="$(stat -c %s "$dst" 2>/dev/null || printf 0)"
|
||||
sm="$(stat -c %Y "$src" 2>/dev/null || printf 0)"
|
||||
dm="$(stat -c %Y "$dst" 2>/dev/null || printf 0)"
|
||||
[ "$ss" = "$ds" ] && [ "$sm" -le "$dm" ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
added=0
|
||||
updated=0
|
||||
unchanged=0
|
||||
while IFS= read -r f; do
|
||||
rel="${f#"$SRC/"}"
|
||||
dest="$dest_root/$rel"
|
||||
|
||||
if needs_copy "$f" "$dest"; then
|
||||
if [ -f "$dest" ]; then
|
||||
updated=$((updated + 1))
|
||||
else
|
||||
added=$((added + 1))
|
||||
fi
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "would copy $rel"
|
||||
else
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp --preserve=timestamps "$f" "$dest"
|
||||
echo " + $rel"
|
||||
fi
|
||||
else
|
||||
unchanged=$((unchanged + 1))
|
||||
fi
|
||||
done < <(find "$SRC" "${find_expr[@]}" | sort)
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "DRY RUN — nothing copied. Would sync: ${added} new, ${updated} updated, ${unchanged} unchanged → $dest_root"
|
||||
else
|
||||
ok "Sync complete: ${added} added, ${updated} updated, ${unchanged} unchanged → $dest_root"
|
||||
notify_send "Music sync completed: ${added} added, ${updated} updated → $dest_root"
|
||||
fi
|
||||
@@ -13,7 +13,6 @@ command -v curl &>/dev/null || err "curl not found (install curl)"
|
||||
# ── Config / seams (env overrides for tests) ───────────────────
|
||||
RPC_PORT="${RPC_PORT:-6800}"
|
||||
RPC_URL="http://127.0.0.1:$RPC_PORT/jsonrpc"
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.config/linux_post_install}"
|
||||
CONFIG_FILE="$CONFIG_DIR/download.env"
|
||||
DOWNLOAD_DIR="${DOWNLOAD_DIR:-$HOME/Downloads}"
|
||||
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"
|
||||
|
||||
@@ -24,6 +24,12 @@ EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Deps guard before -h|--help (help also errors without nmap).
|
||||
if ! command -v nmap &>/dev/null; then
|
||||
echo "ERROR: nmap is required. Install with: sudo apt install nmap"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help|"") usage ;;
|
||||
esac
|
||||
@@ -63,11 +69,6 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v nmap &>/dev/null; then
|
||||
echo "ERROR: nmap is required. Install with: sudo apt install nmap"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Estimate host count ────────────────────────────────────────
|
||||
cidr_bits="${net##*/}"
|
||||
if [[ "$cidr_bits" -ge 24 ]]; then
|
||||
|
||||
+48
-14
@@ -19,10 +19,10 @@ Mount and manage SMB/CIFS shares from remote servers (cifs-utils).
|
||||
Commands:
|
||||
mount <//server/share> <local-dir> [user] One-shot mount (creates local-dir if needed)
|
||||
unmount <local-dir> Unmount the share
|
||||
list Show active SMB mounts
|
||||
persist <//server/share> <local-dir> [user] Persistent mount via a systemd .mount unit
|
||||
(automount — never blocks boot)
|
||||
unpersist <local-dir> Stop, disable and remove the mount unit
|
||||
list Show active + persistent SMB mounts
|
||||
persist <//server/share> <local-dir> [user] Persistent mount via systemd .mount + .automount
|
||||
units (mounts on first access — never blocks boot)
|
||||
unpersist <local-dir> Stop, disable and remove the mount/automount units
|
||||
|
||||
With no user, guest access is attempted. With a user, you are prompted for
|
||||
the Samba password — one-shot mounts use a throwaway chmod-600 credentials
|
||||
@@ -108,11 +108,30 @@ case "$cmd" in
|
||||
;;
|
||||
|
||||
list)
|
||||
if findmnt -t cifs >/dev/null 2>&1; then
|
||||
findmnt -t cifs
|
||||
else
|
||||
echo "No SMB mounts"
|
||||
found=0
|
||||
active_mounts="$(findmnt -t cifs 2>/dev/null || true)"
|
||||
if [ -n "$active_mounts" ]; then
|
||||
printf 'Active mounts:\n'
|
||||
printf '%s\n' "$active_mounts"
|
||||
found=1
|
||||
fi
|
||||
persistent=()
|
||||
for unit in "${UNIT_DIR}"/*.mount; do
|
||||
[ -e "$unit" ] || continue
|
||||
grep -q '^Type=cifs$' "$unit" || continue
|
||||
what="$(sed -n 's/^What=//p' "$unit")"
|
||||
where="$(sed -n 's/^Where=//p' "$unit")"
|
||||
[ -n "$what" ] && [ -n "$where" ] || continue
|
||||
persistent+=("$where|$what")
|
||||
done
|
||||
if [ "${#persistent[@]}" -gt 0 ]; then
|
||||
found=1
|
||||
printf 'Persistent (automount):\n'
|
||||
for entry in "${persistent[@]}"; do
|
||||
printf ' %-44s %s\n' "${entry%%|*}" "${entry#*|}"
|
||||
done
|
||||
fi
|
||||
[ "$found" -eq 1 ] || echo "No SMB mounts"
|
||||
;;
|
||||
|
||||
persist)
|
||||
@@ -124,8 +143,10 @@ case "$cmd" in
|
||||
validate_dir "$where"
|
||||
|
||||
unit="$(systemd-escape --path --suffix=mount "$where")"
|
||||
auto_unit="${unit%.mount}.automount"
|
||||
unit_file="${UNIT_DIR}/${unit}"
|
||||
opts="$(mount_opts "$user"),_netdev,noexec,x-systemd.automount"
|
||||
auto_file="${UNIT_DIR}/${auto_unit}"
|
||||
opts="$(mount_opts "$user"),_netdev,noexec"
|
||||
if [ -n "$user" ]; then
|
||||
creds_file="$SMB_CREDS_DIR/$(basename "$where")"
|
||||
sudo mkdir -p "$SMB_CREDS_DIR"
|
||||
@@ -150,10 +171,20 @@ What=${what}
|
||||
Where=${where}
|
||||
Type=cifs
|
||||
Options=${opts}
|
||||
UNIT
|
||||
cat <<UNIT | sudo tee "$auto_file" >/dev/null
|
||||
[Unit]
|
||||
Description=Automount of SMB share ${what} at ${where}
|
||||
|
||||
[Automount]
|
||||
Where=${where}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now "$unit"
|
||||
log "Persistent SMB mount: ${what} → ${where} (${unit})"
|
||||
sudo systemctl enable --now "$auto_unit"
|
||||
log "Persistent SMB mount (automount): ${what} → ${where} (${auto_unit})"
|
||||
notify_send "SMB mount persisted: ${what} → ${where}"
|
||||
;;
|
||||
|
||||
@@ -163,17 +194,20 @@ UNIT
|
||||
validate_dir "$where"
|
||||
|
||||
unit="$(systemd-escape --path --suffix=mount "$where")"
|
||||
auto_unit="${unit%.mount}.automount"
|
||||
unit_file="${UNIT_DIR}/${unit}"
|
||||
auto_file="${UNIT_DIR}/${auto_unit}"
|
||||
|
||||
if [ ! -f "$unit_file" ]; then
|
||||
if [ ! -f "$unit_file" ] && [ ! -f "$auto_file" ]; then
|
||||
warn "No systemd mount unit for $where (${unit})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sudo systemctl disable "$auto_unit" 2>/dev/null || true
|
||||
sudo systemctl stop "$auto_unit" 2>/dev/null || true
|
||||
sudo systemctl disable "$unit" 2>/dev/null || true
|
||||
sudo systemctl stop "$unit" 2>/dev/null || true
|
||||
sudo systemctl stop "${unit%.mount}.automount" 2>/dev/null || true
|
||||
sudo rm -f "$unit_file"
|
||||
sudo rm -f "$unit_file" "$auto_file"
|
||||
sudo rm -f "$SMB_CREDS_DIR/$(basename "$where")"
|
||||
sudo rmdir "$SMB_CREDS_DIR" 2>/dev/null || true
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
@@ -185,6 +185,9 @@ cmd_port() {
|
||||
warn "Restart your server for port changes to take effect."
|
||||
}
|
||||
|
||||
command -v usbsrv &>/dev/null \
|
||||
|| err "usbsrv not found — install the USB Redirector server (https://www.incentivespro.com/usb-server.html)"
|
||||
|
||||
cmd="${1:-}"
|
||||
|
||||
case "$cmd" in
|
||||
@@ -196,9 +199,6 @@ case "$cmd" in
|
||||
*) err "Unknown flag '$cmd'" ;;
|
||||
esac
|
||||
|
||||
command -v usbsrv &>/dev/null \
|
||||
|| err "usbsrv not found — install the USB Redirector server (https://www.incentivespro.com/usb-server.html)"
|
||||
|
||||
case "$cmd" in
|
||||
--ls) cmd_ls ;;
|
||||
--ls-shared) cmd_ls_shared ;;
|
||||
|
||||
+67
-71
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: system backup — Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||
# POS_FLAGS: --service
|
||||
# POS_FLAGS: --service --no-encrypt
|
||||
# POS_CONFIG: notify | notify.env | NOTIFY_PLATFORM=:Comma-separated notify platforms (default telegram) — shared by backup, firewall, share nfs client/server
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
||||
|
||||
load_system_env
|
||||
EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}"
|
||||
@@ -15,91 +16,71 @@ trap 'notify_send "Backup FAILED: ${FOLDER:-unknown}"' ERR
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos system backup <folder-path>
|
||||
pos system backup <folder-path> --no-encrypt
|
||||
pos system backup --service
|
||||
|
||||
Create a gpg-encrypted (AES-256) tar.gz snapshot of a folder and verify it.
|
||||
The archive password is prompted twice and never stored.
|
||||
The archive password is prompted twice and never stored. With --no-encrypt
|
||||
(or BACKUP_ENCRYPT=0) the backup is kept as a plain .tar.gz — no password,
|
||||
headless/cron safe.
|
||||
|
||||
Modes:
|
||||
<folder-path> Back up that folder directly.
|
||||
--no-encrypt Skip encryption (no password prompt, artifact stays .tar.gz).
|
||||
--service List folders under /srv and ~/srv, pick one, back it up.
|
||||
|
||||
The final artifact <name>_<date>.tar.gz.gpg is written to the current directory.
|
||||
The final artifact <name>_<date>.tar.gz[.gpg] is written to the current directory.
|
||||
After it verifies, connected USB storage is offered: the copy lands in
|
||||
<usb>/backups/ and is sha256-verified 100% before it is announced.
|
||||
<usb>/backups/ and is sha256-verified 100% before it is announced. A stick
|
||||
that is plugged in but not mounted is offered a mount first (sudo, mirrors
|
||||
the usb-automount scheme) before the copy.
|
||||
|
||||
Environment:
|
||||
BACKUP_ENCRYPT Set to 0 to skip encryption (same as --no-encrypt)
|
||||
(default: 1)
|
||||
BACKUP_SERVICE_ROOTS Space-separated roots for --service
|
||||
(effective: ${EFF_ROOTS})
|
||||
BACKUP_USB_ROOT USB root to copy finished backups to
|
||||
(default: auto-detect mounted removable storage)
|
||||
(default: auto-detect mounted USB storage)
|
||||
BACKUP_MOUNT_BASE Where to mount an unmounted USB stick offered
|
||||
during the copy (default: /media; alias of
|
||||
USB_MOUNT_BASE)
|
||||
BACKUP_USB_BYID by-id dir used to corroborate USB detection
|
||||
(default: /dev/disk/by-id; alias of USB_BYID)
|
||||
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
command -v tar &>/dev/null || err "tar not found"
|
||||
command -v gpg &>/dev/null || err "gpg not found (install gnupg)"
|
||||
|
||||
# ── USB copy (optional post-backup step) ─────────────────────────
|
||||
# Detection runs AFTER the backup finished, so a stick plugged in while
|
||||
# the archive was being made is found. The copy lands in <usb>/backups/
|
||||
# and the transfer is proven 100% (sha256 source vs copy) before any
|
||||
# success is announced. BACKUP_USB_ROOT pins the root and skips
|
||||
# detection; otherwise mounted removable storage is auto-detected.
|
||||
# detection; otherwise usb_pick_root (lib/usb-lib.sh) auto-detects,
|
||||
# offers to mount unmounted sticks, and picks the target.
|
||||
usb_copy_offer() {
|
||||
local archive="$1" pass=0 resp="" i=0 root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
local -a roots=()
|
||||
local archive="$1" root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
|
||||
command -v lsblk &>/dev/null || { warn "lsblk not found — USB copy skipped"; return 0; }
|
||||
command -v jq &>/dev/null || { warn "jq not found — USB copy skipped"; return 0; }
|
||||
|
||||
section "USB copy"
|
||||
|
||||
while :; do
|
||||
pass=$((pass + 1))
|
||||
roots=()
|
||||
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
roots+=("$BACKUP_USB_ROOT")
|
||||
else
|
||||
while IFS= read -r mp; do
|
||||
[ -n "$mp" ] && roots+=("$mp")
|
||||
done < <(lsblk -J -o MOUNTPOINT,RM,TYPE 2>/dev/null \
|
||||
| jq -r '.. | objects | select(.rm == true and .mountpoint != null and (.type == "part" or .type == "disk")) | .mountpoint')
|
||||
fi
|
||||
|
||||
[ ${#roots[@]} -gt 0 ] && break
|
||||
|
||||
# None found — offer one re-scan before giving up.
|
||||
if [ "$pass" -ge 2 ]; then
|
||||
warn "Still no USB storage detected — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
warn "No USB storage detected"
|
||||
read -rp "Plug a USB drive in now and press Enter to re-check (or 's' to skip): " resp || return 0
|
||||
case "$resp" in
|
||||
s|S) log "Skipped — backup stays local: $archive"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#roots[@]} -eq 1 ]; then
|
||||
root="${roots[0]}"
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
root="$BACKUP_USB_ROOT"
|
||||
if ! confirm "Copy backup to ${root%/}/backups/?" n; then
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "Multiple USB storages found:"
|
||||
for i in "${!roots[@]}"; do
|
||||
printf "%2d) %s\n" "$((i + 1))" "${roots[$i]}"
|
||||
done
|
||||
read -rp "Copy backup to which one? [1-${#roots[@]}] (0 = skip): " resp || return 0
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#roots[@]} )); then
|
||||
usb_pick_root "Copy backup to" "backups" "backup stays local: $archive" || {
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
root="${roots[$((resp - 1))]}"
|
||||
}
|
||||
root="$USB_ROOT"
|
||||
fi
|
||||
|
||||
dest_dir="${root%/}/backups"
|
||||
@@ -125,13 +106,21 @@ usb_copy_offer() {
|
||||
notify_send "Backup copied to USB: $dest (sha256 verified)"
|
||||
}
|
||||
|
||||
# ── USB detection / mount offer / pick flow ─────────────────────
|
||||
# Shared with pos-media-sync: lib/usb-lib.sh (usb_detect /
|
||||
# usb_related_present / usb_mount_offer / usb_pick_root).
|
||||
SERVICE=0
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
--service) SERVICE=1 ;;
|
||||
"") err "Missing folder path (or use --service)" ;;
|
||||
*) FOLDER="$1" ;;
|
||||
esac
|
||||
ENCRYPT=1
|
||||
[ "${BACKUP_ENCRYPT:-1}" = "0" ] && ENCRYPT=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-h|--help) usage ;;
|
||||
--service) SERVICE=1 ;;
|
||||
--no-encrypt) ENCRYPT=0 ;;
|
||||
*) FOLDER="$arg" ;;
|
||||
esac
|
||||
done
|
||||
{ [ "$SERVICE" -eq 1 ] || [ -n "${FOLDER:-}" ]; } || err "Missing folder path (or use --service)"
|
||||
|
||||
if [ "$SERVICE" -eq 1 ]; then
|
||||
if [ -n "${BACKUP_SERVICE_ROOTS:-}" ]; then
|
||||
@@ -186,29 +175,36 @@ log "Verifying archive..."
|
||||
tar -tzf "$ARCHIVE" > /dev/null
|
||||
log "Archive verified"
|
||||
|
||||
while true; do
|
||||
read -s -rp "Enter backup password: " PASS
|
||||
echo
|
||||
read -s -rp "Confirm backup password: " CONFIRM
|
||||
echo
|
||||
if [ -n "$PASS" ] && [ "$PASS" = "$CONFIRM" ]; then
|
||||
break
|
||||
fi
|
||||
warn "Passwords are empty or do not match — try again"
|
||||
done
|
||||
unset CONFIRM
|
||||
if [ "$ENCRYPT" -eq 1 ]; then
|
||||
command -v gpg &>/dev/null || err "gpg not found (install gnupg)"
|
||||
|
||||
log "Encrypting backup..."
|
||||
gpg --batch --yes --passphrase "$PASS" --symmetric --cipher-algo AES256 "$ARCHIVE"
|
||||
while true; do
|
||||
read -s -rp "Enter backup password: " PASS
|
||||
echo
|
||||
read -s -rp "Confirm backup password: " CONFIRM
|
||||
echo
|
||||
if [ -n "$PASS" ] && [ "$PASS" = "$CONFIRM" ]; then
|
||||
break
|
||||
fi
|
||||
warn "Passwords are empty or do not match — try again"
|
||||
done
|
||||
unset CONFIRM
|
||||
|
||||
rm -f "$ARCHIVE"
|
||||
ARCHIVE="${ARCHIVE}.gpg"
|
||||
chmod 600 "$ARCHIVE"
|
||||
log "Encrypting backup..."
|
||||
gpg --batch --yes --passphrase "$PASS" --symmetric --cipher-algo AES256 "$ARCHIVE"
|
||||
|
||||
log "Verifying encrypted backup..."
|
||||
gpg --batch --quiet --passphrase "$PASS" --decrypt "$ARCHIVE" | tar -tzf - > /dev/null
|
||||
rm -f "$ARCHIVE"
|
||||
ARCHIVE="${ARCHIVE}.gpg"
|
||||
chmod 600 "$ARCHIVE"
|
||||
|
||||
unset PASS
|
||||
log "Verifying encrypted backup..."
|
||||
gpg --batch --quiet --passphrase "$PASS" --decrypt "$ARCHIVE" | tar -tzf - > /dev/null
|
||||
|
||||
unset PASS
|
||||
else
|
||||
chmod 600 "$ARCHIVE"
|
||||
log "No encryption requested — keeping $ARCHIVE"
|
||||
fi
|
||||
echo
|
||||
log "Backup completed: $ARCHIVE"
|
||||
notify_send "Backup completed: $ARCHIVE"
|
||||
|
||||
@@ -14,6 +14,23 @@ source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")
|
||||
HISTORY=()
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: sudo pos system firewall [--dry-run]
|
||||
|
||||
Interactive UFW management menu.
|
||||
|
||||
Options:
|
||||
--dry-run Preview commands without executing them
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
DRY_RUN=1
|
||||
fi
|
||||
|
||||
@@ -10,11 +10,12 @@ _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-pr
|
||||
_pos_flags[entertainment-send]="--print --markdown"
|
||||
_pos_flags[media-mp3]="--output --no-playlist --cookies --by-artist --dry-run"
|
||||
_pos_flags[media-mp4]="--format --best --worst --output --no-playlist --cookies --dry-run"
|
||||
_pos_flags[media-sync]="--mp3 --mp4 --source --dry-run"
|
||||
_pos_flags[network-checkport]="--tcp --udp --ping --no-banner --versions --timeout"
|
||||
_pos_flags[network-download]="--dir --out --split --seed --force --upload --gid --tmux"
|
||||
_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"
|
||||
_pos_flags[system-backup]="--service"
|
||||
_pos_flags[system-backup]="--service --no-encrypt"
|
||||
_pos_flags[system-schedule]="--dry-run"
|
||||
_pos_flags[tree]="--depth"
|
||||
# GEN:END posflags
|
||||
|
||||
+27
-3
@@ -1,12 +1,36 @@
|
||||
# ~/.config/linux_post_install/system.env — shared "system" tool config
|
||||
#
|
||||
# Loaded by `pos system health` and `pos system backup` via load_system_env()
|
||||
# with precedence: already-exported environment > this file > defaults.
|
||||
# Commented lines are defaults — uncomment to override.
|
||||
# Loaded by `pos system health`, `pos system backup` and `pos media sync` via
|
||||
# load_system_env() with precedence: already-exported environment > this file
|
||||
# > defaults. Commented lines are defaults — uncomment to override.
|
||||
|
||||
# Roots scanned by `pos system backup --service` and by the backup-age check
|
||||
# in `pos system health`. Default: /srv $HOME/srv
|
||||
#BACKUP_SERVICE_ROOTS=/srv /home/you/srv
|
||||
|
||||
# Skip encryption in `pos system backup` (plain .tar.gz, no password prompt —
|
||||
# headless/cron safe). Equivalent to the --no-encrypt flag. Default: 1
|
||||
#BACKUP_ENCRYPT=0
|
||||
|
||||
# Max backup age in days before `pos system health` raises a WARN. Default: 2
|
||||
#HEALTH_BACKUP_MAX_AGE_DAYS=3
|
||||
|
||||
# Pin a fixed USB stick for `pos system backup` (no detection, no prompt on
|
||||
# cron). The copy lands in <root>/backups/ and is still sha256-verified.
|
||||
#BACKUP_USB_ROOT=/mnt/usb
|
||||
|
||||
# Where `pos system backup` / `pos media sync` mount an unmounted stick they
|
||||
# found. Default: /media (BACKUP_MOUNT_BASE is kept as an alias)
|
||||
#USB_MOUNT_BASE=/media
|
||||
|
||||
# Dir holding by-id symlinks used to cross-check devices that show no TRAN in
|
||||
# lsblk. Shared by `pos system backup` and `pos media sync`. Default:
|
||||
# /dev/disk/by-id (BACKUP_USB_BYID is kept as an alias)
|
||||
#USB_BYID=/dev/disk/by-id
|
||||
|
||||
# Source folder synced by `pos media sync`. Default: $HOME/Music
|
||||
#MEDIA_SYNC_SOURCE=/home/you/Music
|
||||
|
||||
# Subfolder on the USB stick that `pos media sync` mirrors the source into.
|
||||
# Default: Music
|
||||
#MEDIA_SYNC_DEST=Music
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# autostart feature — tiny boot-time log marker.
|
||||
#
|
||||
# Installed on demand with: ./install.sh --feature
|
||||
# → copied to /usr/local/bin/autostart.sh (chmod 755)
|
||||
# → flag "autostart" is set
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
|
||||
# Robust flags.sh load — works from the repo checkout AND from
|
||||
# /usr/local/bin after install.sh (which copies lib/flags.sh there).
|
||||
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||
|
||||
# Self-name → matches the flag install.sh sets for this feature.
|
||||
FEATURE_NAME="$(basename "$0")"
|
||||
FEATURE_NAME="${FEATURE_NAME%.sh}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: autostart.sh [options]
|
||||
|
||||
Boot-time connectivity marker — appends a timestamped Network:
|
||||
online/offline line to ~/.autostart.log (idempotent, safe to run
|
||||
repeatedly).
|
||||
|
||||
Installed via: ./install.sh --feature
|
||||
Flag: ${FEATURE_NAME}
|
||||
Log: ${HOME:-/root}/.autostart.log
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
LOG="${HOME:-/root}/.autostart.log"
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Robust flags.sh load — works from the repo checkout AND from
|
||||
# /usr/local/bin after install.sh (which copies lib/flags.sh there).
|
||||
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# USB automount feature — auto-mounts removable USB storage.
|
||||
#
|
||||
|
||||
+27
-2
@@ -22,6 +22,31 @@ RUN_FEATURES=0
|
||||
SKIP_PHASES=""
|
||||
STEPS_SPEC=""
|
||||
|
||||
# Expand range syntax in a --steps spec ("1-3" → "1,2,3"; also "1,3-4,6").
|
||||
normalize_steps_spec() {
|
||||
local spec="$1" out="" part from to p
|
||||
local oldIFS="$IFS"
|
||||
IFS=','
|
||||
for part in $spec; do
|
||||
if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
||||
from="${BASH_REMATCH[1]}"
|
||||
to="${BASH_REMATCH[2]}"
|
||||
if [ "$to" -lt "$from" ]; then
|
||||
err "Invalid --steps range '$part' (end < start)"
|
||||
fi
|
||||
for ((p = from; p <= to; p++)); do
|
||||
out="${out:+$out,}$p"
|
||||
done
|
||||
elif [[ "$part" =~ ^[0-9]+$ ]]; then
|
||||
out="${out:+$out,}$part"
|
||||
else
|
||||
err "Invalid --steps value '$part' (expected 1-4, comma list, or N-M range)"
|
||||
fi
|
||||
done
|
||||
IFS="$oldIFS"
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ./install.sh [OPTIONS]
|
||||
@@ -61,7 +86,7 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
--steps)
|
||||
[ -z "${2:-}" ] && err "Missing value for --steps"
|
||||
STEPS_SPEC="$2"
|
||||
STEPS_SPEC="$(normalize_steps_spec "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--no-color) shift ;;
|
||||
@@ -115,7 +140,7 @@ if should_run 2 scripts; then
|
||||
done
|
||||
lib_count=0
|
||||
lib_names=""
|
||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh; do
|
||||
for lf in 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; do
|
||||
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
|
||||
lib_count=$((lib_count + 1))
|
||||
lib_names+="$lf "
|
||||
|
||||
+8
-1
@@ -6,11 +6,18 @@ if [ -t 1 ]; then
|
||||
RED=$(tput setaf 1)
|
||||
BLUE=$(tput setaf 4)
|
||||
BOLD=$(tput bold)
|
||||
DIM=$(tput dim)
|
||||
RESET=$(tput sgr0)
|
||||
else
|
||||
CYAN=""; GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET=""
|
||||
CYAN=""; GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; DIM=""; RESET=""
|
||||
fi
|
||||
|
||||
# ── Config dir (env seam, XDG-aware) ───────────────────────────
|
||||
# Canonical definition. Standalone-sourced files (notify.sh,
|
||||
# config-ui.sh, the matrix/telegram tools) keep an identical guarded
|
||||
# copy — see DEV.md "no shared lib? inline fallbacks".
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
|
||||
# ── Core helpers ───────────────────────────────────────────────
|
||||
log() { echo "${GREEN}[+]${RESET} $*"; }
|
||||
warn() { echo "${YELLOW}[!]${RESET} $*"; }
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
# cfg_scopes # list all declared scopes (deduped, sorted)
|
||||
# cfg_ui <scope> # interactive numbered-menu editor for one scope
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
|
||||
# common.sh helpers (guarded so the lib is safe if common.sh wasn't loaded)
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
@@ -124,7 +124,7 @@ _cfg_plugin_keys() {
|
||||
}
|
||||
declare -F config_keys >/dev/null 2>&1 || return 0
|
||||
local pdir line plugin key desc req
|
||||
pdir="$(plugin_dir)"
|
||||
pdir="$(ent_plugin_dir)"
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
IFS='|' read -r plugin key desc req <<<"$line"
|
||||
|
||||
+15
-15
@@ -1,8 +1,8 @@
|
||||
# Shared library for the entertainment module (pos entertainment *).
|
||||
# Sourced by the bin/pos-entertainment-* tools AFTER lib/common.sh.
|
||||
# NOTE: plugins themselves must NOT source this — their stdout is the message.
|
||||
# Sourced by the bin/pos-entertainment-* tools AFTER lib/common.sh
|
||||
# (which defines CONFIG_DIR). NOTE: plugins themselves must NOT source
|
||||
# this — their stdout is the message.
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_FILE="$CONFIG_DIR/entertainment.env"
|
||||
TIMER_PREFIX="pos-entertainment"
|
||||
DEFAULT_INTERVAL="daily"
|
||||
@@ -54,7 +54,7 @@ write_config_key() {
|
||||
}
|
||||
|
||||
# ── Plugin lookup ──────────────────────────────────────────────────
|
||||
plugin_dir() {
|
||||
ent_plugin_dir() {
|
||||
if [ -n "${ENTERTAINMENT_DIR:-}" ]; then
|
||||
echo "$ENTERTAINMENT_DIR"
|
||||
elif [ -d "$(dirname "$0")/../entertainment" ]; then
|
||||
@@ -64,7 +64,7 @@ plugin_dir() {
|
||||
fi
|
||||
}
|
||||
|
||||
plugin_marker() {
|
||||
ent_plugin_marker() {
|
||||
grep -m1 '^# POS_PLUGIN:' "$1" 2>/dev/null | sed 's/^# POS_PLUGIN:[[:space:]]*//;s/[[:space:]]*$//' || true
|
||||
}
|
||||
|
||||
@@ -72,15 +72,15 @@ list_plugins() {
|
||||
local dir="$1" f name
|
||||
for f in "$dir"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
name="$(plugin_marker "$f")"
|
||||
name="$(ent_plugin_marker "$f")"
|
||||
[ -n "$name" ] && echo "$name"
|
||||
done
|
||||
}
|
||||
|
||||
plugin_exists() {
|
||||
ent_plugin_exists() {
|
||||
local dir="$1" name="$2" f
|
||||
for f in "$dir/$name" "$dir/$name.sh"; do
|
||||
[ -f "$f" ] && [ -x "$f" ] && [ -n "$(plugin_marker "$f")" ] && return 0
|
||||
[ -f "$f" ] && [ -x "$f" ] && [ -n "$(ent_plugin_marker "$f")" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
@@ -90,7 +90,7 @@ resolve_plugin() {
|
||||
[ -n "$name" ] || err "No plugin given"
|
||||
for f in "$dir/$name" "$dir/$name.sh"; do
|
||||
if [ -f "$f" ] && [ -x "$f" ]; then
|
||||
[ -n "$(plugin_marker "$f")" ] || err "'$f' is not an entertainment plugin (missing '# POS_PLUGIN:' header)"
|
||||
[ -n "$(ent_plugin_marker "$f")" ] || err "'$f' is not an entertainment plugin (missing '# POS_PLUGIN:' header)"
|
||||
echo "$f"
|
||||
return 0
|
||||
fi
|
||||
@@ -105,8 +105,8 @@ resolve_plugin() {
|
||||
# config' prints them in its Keys section; config set warns when a key is
|
||||
# not declared by any installed plugin.
|
||||
|
||||
plugin_keys() {
|
||||
# Usage: plugin_keys <plugin-file> → "KEY|description|required|optional"
|
||||
ent_plugin_keys() {
|
||||
# Usage: ent_plugin_keys <plugin-file> → "KEY|description|required|optional"
|
||||
local file="$1" line key desc req
|
||||
grep '^# POS_KEYS:' "$file" 2>/dev/null | sed 's/^# POS_KEYS:[[:space:]]*//' | while IFS= read -r line; do
|
||||
key="${line%% *}"
|
||||
@@ -125,12 +125,12 @@ config_keys() {
|
||||
local dir="$1" f name line
|
||||
for f in "$dir"/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
name="$(plugin_marker "$f")"
|
||||
name="$(ent_plugin_marker "$f")"
|
||||
[ -n "$name" ] || continue
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
printf '%s|%s\n' "$name" "$line"
|
||||
done <<< "$(plugin_keys "$f")"
|
||||
done <<< "$(ent_plugin_keys "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ parse_enabled() {
|
||||
IFS=' ' read -ra toks <<<"$(printf '%s' "$raw" | tr ',' ' ')"
|
||||
for token in "${toks[@]}"; do
|
||||
[ -n "$token" ] || continue
|
||||
if plugin_exists "$(plugin_dir)" "$token"; then
|
||||
if ent_plugin_exists "$(ent_plugin_dir)" "$token"; then
|
||||
[ -n "$cur" ] && ENABLED_ENTRIES+=("$cur")
|
||||
cur="$token"
|
||||
elif [ -n "$cur" ]; then
|
||||
@@ -240,7 +240,7 @@ sync_systemd() {
|
||||
for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||
plugin="${entry%%,*}"; interval="${entry##*,}"
|
||||
[ "$interval" = "$plugin" ] && interval="$DEFAULT_INTERVAL"
|
||||
if ! plugin_exists "$(plugin_dir)" "$plugin"; then
|
||||
if ! ent_plugin_exists "$(ent_plugin_dir)" "$plugin"; then
|
||||
warn "plugin '$plugin' not installed — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
# Silent-fails per platform: a missing sender or a failed send only warns and
|
||||
# never changes the caller's exit code.
|
||||
|
||||
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install"
|
||||
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
|
||||
|
||||
# Platform key → sender tool name (bin/pos-communication-<name>).
|
||||
notify_sender_name() {
|
||||
@@ -54,7 +54,7 @@ notify_send() {
|
||||
done
|
||||
|
||||
if [ -z "$msg" ]; then
|
||||
warn "notify_send: empty message, skipped" 2>/dev/null || echo "notify_send: empty message, skipped"
|
||||
warn "notify_send: empty message, skipped" 2>/dev/null || echo "notify_send: empty message, skipped" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
# lib/usb-lib.sh — shared USB-storage detection + pick flow. Used by
|
||||
# pos-system-backup (post-verify USB copy) and pos-media-sync (Music sync).
|
||||
# Requires common.sh helpers: log/warn/ok/section/confirm/run. Defines ONLY
|
||||
# usb_* functions plus the USB_MOUNTED / USB_UNMOUNTED / USB_ROOT globals.
|
||||
#
|
||||
# Seams (env overrides; the BACKUP_* names are kept as aliases so existing
|
||||
# system.env lines keep working):
|
||||
# USB_MOUNT_BASE where an unmounted stick is offered to mount (default /media)
|
||||
# USB_BYID /dev/disk/by-id dir consulted by usb_related_present
|
||||
#
|
||||
# API:
|
||||
# usb_detect fill USB_MOUNTED (mountpoints, one per
|
||||
# removable USB storage) and USB_UNMOUNTED
|
||||
# ("path|label|size|model" entries)
|
||||
# usb_related_present whole-system "is any USB storage attached?"
|
||||
# usb_mount_offer <devs...> offer to mount detected-but-unmounted sticks
|
||||
# (sudo, mirrors usb-automount: /media/<label>,
|
||||
# fallback /media/usb-<devname>, umask=000);
|
||||
# returns 0 = mounted (or user will do it
|
||||
# manually), 1 = skipped/EOF
|
||||
# usb_pick_root <confirm-prefix> <subfolder> <giveup-msg>
|
||||
# full detect → mount-offer → pick loop; returns
|
||||
# 0 with USB_ROOT set to the chosen mountpoint,
|
||||
# 1 = skipped. Single-stick: confirm
|
||||
# "<confirm-prefix> <root>/<subfolder>?" first.
|
||||
|
||||
USB_MOUNT_BASE="${USB_MOUNT_BASE:-${BACKUP_MOUNT_BASE:-/media}}"
|
||||
USB_BYID="${USB_BYID:-${BACKUP_USB_BYID:-/dev/disk/by-id}}"
|
||||
|
||||
# ── USB detection ────────────────────────────────────────────────
|
||||
# TRAN=="usb" is the per-device deciding signal; when TRAN is empty,
|
||||
# usb_related_present() corroborates. Non-USB removables are skipped. The
|
||||
# "TRAN unavailable" warning fires once per scan, not per device.
|
||||
usb_detect() {
|
||||
local out path="" mp="" label="" size="" model="" tran="" type="" children=""
|
||||
local warned=0
|
||||
USB_MOUNTED=()
|
||||
USB_UNMOUNTED=()
|
||||
out="$(lsblk -J -o NAME,PATH,LABEL,MOUNTPOINT,RM,TYPE,TRAN,SIZE,MODEL 2>/dev/null)" || return 0
|
||||
|
||||
while IFS=$'\x1f' read -r path mp label size model tran type children; do
|
||||
[ -n "$path" ] || continue
|
||||
if [ "$tran" = "usb" ]; then
|
||||
:
|
||||
elif [ -z "$tran" ] && usb_related_present; then
|
||||
if [ "$warned" -eq 0 ]; then
|
||||
warn "TRAN unavailable — assuming USB (lsusb/by-id corroboration)"
|
||||
warned=1
|
||||
fi
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ -n "$mp" ]; then
|
||||
USB_MOUNTED+=("$mp")
|
||||
elif [ "$children" = "0" ]; then
|
||||
USB_UNMOUNTED+=("$path|$label|$size|$model")
|
||||
fi
|
||||
done < <(printf '%s' "$out" | jq -r '
|
||||
.. | objects
|
||||
| select(.rm == true and (.type == "part" or .type == "disk"))
|
||||
| [.path, (.mountpoint // ""), (.label // ""), (.size // ""),
|
||||
(.model // ""), (.tran // ""), (.type // ""),
|
||||
((.children // []) | length)]
|
||||
| join("\u001f")')
|
||||
}
|
||||
|
||||
# Whole-system "is any USB storage attached?" — udev by-id usb-* symlinks
|
||||
# are definitive; lsusb text is a secondary hint for storage-ish devices.
|
||||
usb_related_present() {
|
||||
[ -n "$(ls "${USB_BYID}"/usb-* 2>/dev/null)" ] && return 0
|
||||
command -v lsusb &>/dev/null \
|
||||
&& lsusb 2>/dev/null | grep -qiE 'mass storage|card reader|flash disk|usb.*(disk|drive|storage)|reader|external'
|
||||
}
|
||||
|
||||
# ── Mount an unmounted USB stick (CLI-box case) ──────────────────
|
||||
# Returns 0 → caller re-scans (mounted now or the user will mount manually);
|
||||
# 1 → skipped/EOF.
|
||||
usb_mount_offer() {
|
||||
local -a devs=("$@")
|
||||
local i=0 dev="" label="" size="" model="" mp="" resp="" n=""
|
||||
local -a chosen=()
|
||||
|
||||
if [ ${#devs[@]} -gt 1 ]; then
|
||||
echo "Multiple unmounted USB storages found:"
|
||||
for i in "${!devs[@]}"; do
|
||||
IFS='|' read -r dev label size model <<< "${devs[$i]}"
|
||||
printf "%2d) %s (%s, %s)\n" "$((i + 1))" "$dev" "$size" "${model:-no label}"
|
||||
done
|
||||
read -rp "Use which one? [1-${#devs[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#devs[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
chosen=("${devs[$((resp - 1))]}")
|
||||
else
|
||||
chosen=("${devs[0]}")
|
||||
fi
|
||||
|
||||
IFS='|' read -r dev label size model <<< "${chosen[0]}"
|
||||
label="${label//\//_}"
|
||||
mp="${USB_MOUNT_BASE}/${label:-usb-$(basename "$dev")}"
|
||||
if [ -d "$mp" ] && mountpoint -q "$mp" 2>/dev/null; then
|
||||
n=2
|
||||
while [ -d "${mp}-${n}" ] && mountpoint -q "${mp}-${n}" 2>/dev/null; do
|
||||
n=$((n + 1))
|
||||
done
|
||||
mp="${mp}-${n}"
|
||||
fi
|
||||
|
||||
echo "Found USB storage not mounted: $dev ($size, ${model:-no label})"
|
||||
if confirm "Mount it at $mp (world-writable) so we can use it?" n; then
|
||||
run sudo mkdir -p "$mp"
|
||||
if ! run sudo mount -o umask=000 "$dev" "$mp"; then
|
||||
run sudo mount "$dev" "$mp" || true
|
||||
fi
|
||||
if [ "${DRY_RUN:-0}" -eq 1 ] || mountpoint -q "$mp" 2>/dev/null; then
|
||||
ok "Mounted $dev at $mp"
|
||||
return 0
|
||||
fi
|
||||
warn "Mount failed — do it manually:"
|
||||
log " sudo mkdir -p $mp && sudo mount $dev $mp"
|
||||
else
|
||||
warn "Mount it manually, then we'll continue:"
|
||||
log " sudo mkdir -p $mp"
|
||||
log " sudo mount $dev $mp"
|
||||
fi
|
||||
|
||||
read -rp "Press Enter once mounted (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Full pick flow ───────────────────────────────────────────────
|
||||
# Loops detect → mount-offer (unmounted sticks) → "plug one in" re-scan until a
|
||||
# mounted USB root is chosen or the user gives up. USB_ROOT is set on success.
|
||||
usb_pick_root() {
|
||||
local confirm_prefix="${1:-Use this storage for}"
|
||||
local subfolder="${2:-}"
|
||||
local giveup="${3:-skipping}"
|
||||
local pass=0 resp="" i=0 root=""
|
||||
local -a roots=()
|
||||
local offered=0
|
||||
|
||||
while :; do
|
||||
pass=$((pass + 1))
|
||||
roots=()
|
||||
|
||||
usb_detect
|
||||
roots=("${USB_MOUNTED[@]}")
|
||||
|
||||
if [ ${#roots[@]} -eq 0 ] && [ ${#USB_UNMOUNTED[@]} -gt 0 ]; then
|
||||
if [ "$offered" -ge 1 ]; then
|
||||
warn "USB stick still not mounted — ${giveup}"
|
||||
return 1
|
||||
fi
|
||||
offered=1
|
||||
usb_mount_offer "${USB_UNMOUNTED[@]}" || return 1
|
||||
continue
|
||||
fi
|
||||
|
||||
[ ${#roots[@]} -gt 0 ] && break
|
||||
|
||||
if [ "$pass" -ge 2 ]; then
|
||||
warn "Still no USB storage detected — ${giveup}"
|
||||
return 1
|
||||
fi
|
||||
warn "No USB storage detected"
|
||||
read -rp "Plug a USB drive in now and press Enter to re-check (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#roots[@]} -eq 1 ]; then
|
||||
root="${roots[0]}"
|
||||
if ! confirm "${confirm_prefix} ${root%/}/${subfolder}?" n; then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "Multiple USB storages found:"
|
||||
for i in "${!roots[@]}"; do
|
||||
printf "%2d) %s\n" "$((i + 1))" "${roots[$i]}"
|
||||
done
|
||||
read -rp "Use which one? [1-${#roots[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#roots[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
root="${roots[$((resp - 1))]}"
|
||||
fi
|
||||
|
||||
USB_ROOT="$root"
|
||||
return 0
|
||||
}
|
||||
@@ -37,6 +37,7 @@ PACKAGES=(
|
||||
lm-sensors smartmontools nvme-cli hdparm
|
||||
sysstat iotop atop vnstat
|
||||
python3 python3-pip rclone
|
||||
ffmpeg
|
||||
libqrencode4 libgtk-3-0 adb
|
||||
)
|
||||
|
||||
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Report whether the `gates` workflow has finished for a commit, using only
|
||||
# git against the remote (no SSH to the runner box, no API tokens).
|
||||
#
|
||||
# `.gitea/workflows/lint.yml` pushes a lightweight tag `ci-ok/<sha>` when the
|
||||
# gates pass and `ci-fail/<sha>` when they fail. This script reads those tags
|
||||
# with `git ls-remote`.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/ci-status.sh [<sha>] # one-shot check (default: current HEAD)
|
||||
# scripts/ci-status.sh --wait [<sha>] # poll until done (max 10 min)
|
||||
#
|
||||
# Exit codes: 0 = green, 1 = red, 2 = still running / not reported.
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE="${CI_STATUS_REMOTE:-origin}"
|
||||
INTERVAL=10
|
||||
TIMEOUT=600
|
||||
|
||||
sha="${1:-}"
|
||||
if [ "$sha" = "--wait" ]; then
|
||||
wait=1
|
||||
sha="${2:-}"
|
||||
fi
|
||||
[ -n "$sha" ] || sha="$(git rev-parse HEAD)"
|
||||
|
||||
check() {
|
||||
local ok="" fail=""
|
||||
ok="$(git ls-remote "$REMOTE" "refs/tags/ci-ok/$sha" | awk '{print $1}')"
|
||||
fail="$(git ls-remote "$REMOTE" "refs/tags/ci-fail/$sha" | awk '{print $1}')"
|
||||
if [ -n "$ok" ]; then
|
||||
echo "GREEN — gates passed for $sha ($ok)"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$fail" ]; then
|
||||
echo "RED — gates failed for $sha ($fail)"
|
||||
return 1
|
||||
fi
|
||||
echo "PENDING — no gate result yet for $sha"
|
||||
return 2
|
||||
}
|
||||
|
||||
if [ "${wait:-0}" -ne 1 ]; then
|
||||
if check; then
|
||||
exit 0
|
||||
else
|
||||
rc=$?
|
||||
exit "$rc"
|
||||
fi
|
||||
fi
|
||||
|
||||
deadline=$((SECONDS + TIMEOUT))
|
||||
while :; do
|
||||
out="$(check 2>&1)" && { echo "$out"; exit 0; }
|
||||
rc=$?
|
||||
if [ "$rc" -eq 1 ]; then
|
||||
echo "$out"
|
||||
exit 1
|
||||
fi
|
||||
if (( SECONDS >= deadline )); then
|
||||
echo "TIMEOUT after ${TIMEOUT}s — no gate result for $sha"
|
||||
exit 2
|
||||
fi
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
@@ -15,6 +15,10 @@ root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
mode="write"
|
||||
[ "${1:-}" = "--check" ] && mode="check"
|
||||
|
||||
# Deterministic byte-order sorting regardless of host/CI locale (category-less
|
||||
# tool keys start with "|", which collates differently per-locale).
|
||||
export LC_ALL=C
|
||||
|
||||
ctx="$root/DOC/AGENT_Context_Project.md"
|
||||
comp="$root/completions/pos.bash"
|
||||
|
||||
|
||||
Executable
+261
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
fails=0
|
||||
warns=0
|
||||
|
||||
fail() { fails=$((fails + 1)); printf 'FAIL %s\n' "$1"; }
|
||||
warn_() { warns=$((warns + 1)); printf 'WARN %s\n' "$1"; }
|
||||
|
||||
shell_files() {
|
||||
printf '%s\n' bin/pos bin/pos-* lib/*.sh features/*.sh entertainment/*.sh \
|
||||
apps/*/*.sh templates/*.sh scripts/*.sh install.sh preinstall.sh postinstall.sh \
|
||||
2>/dev/null
|
||||
}
|
||||
|
||||
executable_files() {
|
||||
printf '%s\n' bin/pos-* entertainment/*.sh 2>/dev/null
|
||||
}
|
||||
|
||||
has_regex() {
|
||||
local file="$1" re="$2"
|
||||
grep -qE "$re" "$file" && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
first_line() {
|
||||
local file="$1" re="$2"
|
||||
grep -nE "$re" "$file" 2>/dev/null | while IFS=: read -r ln rest; do
|
||||
[ -z "$ln" ] && continue
|
||||
[[ "$rest" =~ ^[[:space:]]*# ]] && continue
|
||||
printf '%s' "$ln"
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
last_line() {
|
||||
local file="$1" re="$2"
|
||||
grep -nE "$re" "$file" 2>/dev/null | tail -1 | cut -d: -f1 || true
|
||||
}
|
||||
|
||||
# First deps guard: a `command -v` line that hard-fails the tool when the binary
|
||||
# is absent — i.e. `command -v X … || err`, `if ! command -v X …`, or a
|
||||
# `command -v X … \` multi-line continuation. Runtime capability probes like
|
||||
# `if command -v X; then` (graceful degradation, e.g. system-health) are NOT
|
||||
# guards and must not trigger the guard-before-help rule.
|
||||
first_guard_line() {
|
||||
local file="$1" ln=0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
ln=$((ln + 1))
|
||||
[[ "$line" == *"command -v"* ]] || continue
|
||||
[[ "$line" == *"||"* || "$line" == *"if !"* || "$line" == *\\ ]] && { printf '%s' "$ln"; return; }
|
||||
done < "$file"
|
||||
printf '%s' ""
|
||||
}
|
||||
|
||||
uses_stdin() {
|
||||
local file="$1" line heredoc=""
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$heredoc" ]; then
|
||||
[ "$line" = "$heredoc" ] && heredoc=""
|
||||
continue
|
||||
fi
|
||||
local delim
|
||||
delim="$(printf '%s\n' "$line" | sed -nE 's/.*<<-?[[:space:]]*([A-Za-z0-9_]+).*/\1/p' | tail -1)"
|
||||
[ -n "$delim" ] && { heredoc="$delim"; continue; }
|
||||
case "$line" in
|
||||
*'read -'*|*'read '*|*'select '*|*'confirm '*|*'confirm('*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ "$line" == *"/dev/tty"* ]] && continue
|
||||
[[ "$line" =~ (while|until)[[:space:]].*read ]] && continue
|
||||
[[ "$line" =~ [[:space:]]\< ]] && continue
|
||||
return 0
|
||||
done < "$file"
|
||||
return 1
|
||||
}
|
||||
|
||||
INTERACTIVE_CMDS=""
|
||||
if [ -f bin/pos ]; then
|
||||
INTERACTIVE_CMDS="$(sed -n 's/^INTERACTIVE_CMDS="\(.*\)"$/\1/p' bin/pos | head -1)"
|
||||
fi
|
||||
|
||||
for f in $(shell_files); do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in
|
||||
lib/*.sh) continue ;; # libraries are sourced, never executed
|
||||
esac
|
||||
if ! head -1 "$f" | grep -q '^#!/usr/bin/env bash'; then
|
||||
fail "$f: missing '#!/usr/bin/env bash' shebang"
|
||||
fi
|
||||
if ! has_regex "$f" '^set -euo pipefail'; then
|
||||
fail "$f: missing 'set -euo pipefail'"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in $(executable_files); do
|
||||
[ -f "$f" ] || continue
|
||||
if [ ! -x "$f" ]; then
|
||||
fail "$f: not executable (needs chmod +x, committed as 100755)"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in bin/pos-*; do
|
||||
[ -f "$f" ] || continue
|
||||
|
||||
headline="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f" 2>/dev/null)"
|
||||
if [ -z "$headline" ]; then
|
||||
fail "$f: missing '# POS:' header"
|
||||
continue
|
||||
fi
|
||||
if ! grep -q ' — ' <<<"$headline"; then
|
||||
fail "$f: '# POS:' header missing em-dash ' — ' (format: '# POS: <cat> <cmd> — <desc>')"
|
||||
fi
|
||||
posline="$(grep -nE '^# POS: ' "$f" | head -1 | cut -d: -f1 || true)"
|
||||
if [ "${posline:-99}" -gt 6 ]; then
|
||||
warn_ "$f: '# POS:' header on line $posline (convention: right after shebang/strict-mode)"
|
||||
fi
|
||||
|
||||
if ! has_regex "$f" '\-h\|\-\-help'; then
|
||||
fail "$f: missing -h|--help handling"
|
||||
fi
|
||||
|
||||
guard="$(first_guard_line "$f")"
|
||||
help_line="$(first_line "$f" '\-h\|\-\-help')"
|
||||
if [ -n "$guard" ] && [ -n "$help_line" ] && [ "$help_line" -lt "$guard" ]; then
|
||||
fail "$f: -h|--help (line $help_line) dispatched before deps guards (line $guard) — help must error on missing deps"
|
||||
fi
|
||||
|
||||
local_line=""
|
||||
lineno=0
|
||||
depth=0
|
||||
heredoc=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
lineno=$((lineno + 1))
|
||||
if [ -n "$heredoc" ]; then
|
||||
[ "$line" = "$heredoc" ] && heredoc=""
|
||||
continue
|
||||
fi
|
||||
delim="$(printf '%s\n' "$line" | sed -nE 's/.*<<-?[[:space:]]*([A-Za-z0-9_]+).*/\1/p' | tail -1)"
|
||||
if [ -n "$delim" ]; then
|
||||
heredoc="$delim"
|
||||
continue
|
||||
fi
|
||||
if [[ "$line" =~ ^[[:space:]]*local[[:space:]] ]]; then
|
||||
if [ "$depth" -eq 0 ]; then
|
||||
[ -n "$local_line" ] || local_line="$lineno"
|
||||
fi
|
||||
fi
|
||||
opens="${line//[^{]/}"
|
||||
closes="${line//[^\}]/}"
|
||||
depth=$((depth + ${#opens} - ${#closes}))
|
||||
[ "$depth" -lt 0 ] && depth=0
|
||||
done < "$f"
|
||||
if [ -n "$local_line" ]; then
|
||||
warn_ "$f: '$local_line': 'local' at top-level brace depth (invalid in bash outside a function)"
|
||||
fi
|
||||
|
||||
if uses_stdin "$f"; then
|
||||
name="${f#bin/pos-}"
|
||||
if ! [[ " $INTERACTIVE_CMDS " == *" $name "* ]]; then
|
||||
fail "$f: reads stdin but NOT in INTERACTIVE_CMDS in bin/pos (log tee will swallow/hang prompts)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "$(basename "$f")" DOC/POS.md 2>/dev/null; then
|
||||
warn_ "$f: file not referenced in DOC/POS.md"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$INTERACTIVE_CMDS" ]; then
|
||||
for entry in $INTERACTIVE_CMDS; do
|
||||
if [ ! -x "bin/pos-$entry" ]; then
|
||||
fail "bin/pos: INTERACTIVE_CMDS entry '$entry' has no matching bin/pos-$entry tool"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for f in entertainment/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
if has_regex "$f" 'common\.sh'; then
|
||||
fail "$f: entertainment plugin must NOT source lib/common.sh (stdout is the Telegram message)"
|
||||
fi
|
||||
if ! has_regex "$f" '^# POS_PLUGIN: '; then
|
||||
fail "$f: missing '# POS_PLUGIN:' marker"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in apps/*/*.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
app="${f##*/}"
|
||||
app="${app%.sh}"
|
||||
if ! has_regex "$f" "uninstall_${app//-/_}\(\).*\{"; then
|
||||
if ! has_regex "$f" "uninstall_${app}\(\).*\{"; then
|
||||
fail "$f: missing 'uninstall_${app}()' function"
|
||||
fi
|
||||
fi
|
||||
if ! has_regex "$f" 'uninstall\)'; then
|
||||
fail "$f: missing 'uninstall' dispatch case"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in systemd/*.service; do
|
||||
[ -f "$f" ] || continue
|
||||
if ! has_regex "$f" '^TimeoutStopSec='; then
|
||||
warn_ "$f: missing 'TimeoutStopSec=5s' (convention: a stuck process must not stall reboot 90s)"
|
||||
fi
|
||||
if ! has_regex "$f" '^WantedBy='; then
|
||||
warn_ "$f: missing '[Install] WantedBy='"
|
||||
fi
|
||||
done
|
||||
|
||||
for f in bin/wr-* bin/mp3 bin/mp4 bin/vbox bin/ssh-load-all; do
|
||||
[ -f "$f" ] || continue
|
||||
if ! has_regex "$f" '\bpos\b'; then
|
||||
fail "$f: legacy wrapper does not forward to 'pos'"
|
||||
fi
|
||||
if [ "$(wc -l < "$f")" -gt 12 ]; then
|
||||
warn_ "$f: legacy wrapper has $(wc -l < "$f") lines (convention: thin forwarder only)"
|
||||
fi
|
||||
if has_regex "$f" '^[[:space:]]*case '; then
|
||||
warn_ "$f: legacy wrapper contains a case statement (should be a thin forwarder)"
|
||||
fi
|
||||
done
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[0-9]+: ]] || continue
|
||||
num="${line%%:*}"
|
||||
body="${line#*:}"
|
||||
if grep -qE '(TOKEN|PASSWORD|PASSWD|SECRET|API[_-]?KEY|ACCESS[_-]?TOKEN|AUTH[_-]?KEY)=' <<<"$body"; then
|
||||
val="${body#*=}"
|
||||
case "$val" in
|
||||
""|*'$'*) ;;
|
||||
*) warn_ "$f:$num: secret-like literal assignment (manual review for hardcoded credentials)" ;;
|
||||
esac
|
||||
fi
|
||||
done < <(grep -nE '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*(TOKEN|PASSWORD|PASSWD|SECRET|API[_-]?KEY|ACCESS[_-]?TOKEN|AUTH[_-]?KEY)=' "$f" 2>/dev/null || true)
|
||||
done < <(printf '%s\n' bin/pos bin/pos-* lib/*.sh features/*.sh entertainment/*.sh install.sh preinstall.sh postinstall.sh)
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^[0-9]+: ]] || continue
|
||||
num="${line%%:*}"
|
||||
body="${line#*:}"
|
||||
if grep -qE '(>|>>|tee )' <<<"$body" && grep -qE '(/etc/|\$HOME|/usr/local)' <<<"$body"; then
|
||||
case "$body" in
|
||||
*'command -v'*|*'|| echo'*) ;;
|
||||
*) warn_ "$f:$num: writes to a system path (verify a VAR=\"\${VAR:-path}\" test seam exists)" ;;
|
||||
esac
|
||||
fi
|
||||
done < <(grep -nE '(\btee\b|>>?)[^#]*?(/etc/|\$HOME|/usr/local)' "$f" 2>/dev/null || true)
|
||||
done < <(printf '%s\n' bin/pos-* lib/*.sh features/*.sh entertainment/*.sh)
|
||||
|
||||
printf '\n%d FAIL, %d WARN (convention lint)\n' "$fails" "$warns"
|
||||
[ "$fails" -eq 0 ] || exit 1
|
||||
Reference in New Issue
Block a user