Compare commits

77 Commits
Author SHA1 Message Date
bchanot 6b634e2c77 chore(memory): journal — tmux drag fix merged 2026-10-06 18:05:20 +02:00
bchanot 22452477d5 Merge bugfix/tmux-drag-syntax-error into develop 2026-10-06 18:05:11 +02:00
bchanot 72d38b68df chore(memory): journal — tmux drag syntax error 2026-10-06 18:02:54 +02:00
bchanot 31f8400852 fix(tmux): drag in a mouse app (claude, vim) died with "syntax error"
The MouseDrag1Pane binding escaped the inner quotes as \" inside a
single-quoted string. Since tmux 3.0 the backslashes are literal there,
so the branch taken when the pane has mouse tracking on failed to parse:
no selection, "syntax error" in the status line. Plain-shell panes never
hit that branch, which is why only claude/vim panes were affected.
2026-10-06 18:02:47 +02:00
bchanot 773e1c5fa6 chore(memory): journal — tmux ctrl nav in copy-mode 2026-10-06 17:16:20 +02:00
bchanot 8b6ed6b983 Merge bugfix/tmux-copy-mode-ctrl-nav into develop 2026-10-06 17:16:06 +02:00
bchanot dda78bb9bb fix(tmux): ctrl+u/h/j/k switch panes in copy-mode too
Stock copy-mode-vi table binds C-h cursor-left, C-u halfpage-up and C-j
copy, so the pane moves turned into text navigation once scrolled. Arrows
keep that job; the four keys now select-pane in copy-mode like at root.
2026-10-06 16:58:19 +02:00
bchanot cbdbe58491 chore(memory): journal — tmux wheel halved, dtach mac/termux 2026-10-06 16:51:39 +02:00
bchanot ec9d0ed934 Merge bugfix/tmux-wheel-half into develop 2026-10-06 16:51:28 +02:00
bchanot 0a34c528ca fix(tmux): wheel scrolls 2 lines per tick instead of 5 2026-10-06 16:49:32 +02:00
bchanot 7f63f4b0e2 chore(memory): journal — identity asked at install 2026-10-06 15:35:22 +02:00
bchanot 0687f6a4fe Merge feature/identity-prompt into develop 2026-10-06 15:35:14 +02:00
bchanot 8f6969001f feat(identity): ask name/email at install, no hardcoded USER/EMAIL in the repo
- rc templates carry @USER@/@EMAIL@ placeholders; install.sh renders them
- resolve_identity: an export already in ~/.bashrc or ~/.zshrc is reused silently,
  else IDENTITY_USER/IDENTITY_EMAIL, else a prompt, else login name + empty email
- render_gitconfig generalised to render_identity_template (gitconfig unchanged)
- vimrc reads g:_author/g:_email from $USER/$EMAIL instead of hardcoded values
- README + CLAUDE.md
2026-10-06 15:34:11 +02:00
bchanot f6a039fc80 chore(memory): journal — tmux click exit, alt copy/paste 2026-10-06 15:22:23 +02:00
bchanot 8b5e9367d4 Merge bugfix/tmux-click-exit into develop 2026-10-06 15:22:14 +02:00
bchanot 1f5e8990c8 feat(tmux): alt+c copies and leaves copy-mode, alt+v pastes (cmd-less keyboards) 2026-10-06 15:19:47 +02:00
bchanot 51aed0e24d fix(tmux): return to the prompt on click release, not on press
MouseDown cancelled copy-mode before a drag could start, so selecting text
after scrolling up jumped back to the bottom. MouseUp1Pane (sent only when
no drag happened) now cancels; MouseDown clears the selection as before.
2026-10-06 15:17:21 +02:00
bchanot 8ab681627c fix(tmux): a click in copy-mode returns to the prompt
MouseDown1Pane in copy-mode-vi now cancels the mode instead of only clearing
the selection. Drag-select (root MouseDrag1Pane re-enters copy-mode) and
click-to-switch-pane are unchanged.
2026-10-06 15:13:00 +02:00
bchanot ef418fea04 chore(memory): journal — tmux selection, keep tmux 2026-10-06 15:05:00 +02:00
bchanot b73ee086fd Merge bugfix/tmux-selection into develop 2026-10-06 15:04:48 +02:00
bchanot b900c6d3a3 fix(tmux): visible selection colour, mouse drag copies on release
- mode-style bg=yellow,fg=black (black on a dark terminal was invisible)
- MouseDragEnd1Pane copies (pbcopy on macOS, tmux buffer elsewhere) without leaving copy-mode
2026-10-06 15:02:42 +02:00
bchanot ec9187c70f chore(memory): BDR-017 tmux ctrl+uhjk, BDR-018 Linux clipboard OSC 52 2026-10-06 14:28:35 +02:00
bchanot 7abef66f50 chore(memory): journal — tmux keys + Linux deploy 2026-10-06 14:26:39 +02:00
bchanot e3be25f94c Merge bugfix/tmux-macos-keys into develop 2026-10-06 14:26:29 +02:00
bchanot d2821fa462 feat(tmux): deploy the config on Linux too; prefix i / - splits
- install.sh: deploy_tmux_config runs on both OSes after the bashrc (gated on tmux)
- tmux.conf: clipboard via if-shell (pbcopy on macOS, tmux buffer + OSC 52 elsewhere)
- bashrc-linux exports XDG_CACHE_HOME like the macOS rc files
- prefix i = side-by-side split, prefix - = stacked split (h/j/k/l kept)
- verified in an Ubuntu 24.04 container (tmux 3.4): deploy, bindings, split, plugins, libtmux
2026-10-06 13:09:34 +02:00
bchanot ca8b1c19c9 feat(tmux): pane moves on ctrl+u/h/j/k (arrow layout, AZERTY/QWERTY invariant)
Replaces ctrl+h/j/k/l and the option+arrow bindings. Resize mirrors it with prefix.
C-l is free again, so it clears the shell screen.
2026-10-06 13:05:25 +02:00
bchanot b2244fc7ba feat(tmux): option+arrow moves between panes without prefix 2026-10-06 13:02:37 +02:00
bchanot d639f22e08 fix(tmux): macOS clipboard, C-a passthrough, vim detection
- y/p use pbcopy/pbpaste (xsel is X11, absent on macOS); xsel kept as the Linux variant in a comment
- bind C-a send-prefix: C-a C-a sends a literal C-a to the shell
- is_vim reads #{pane_current_command}; the @tmux_vim_<pane> variable was never set, so C-h/j/k/l never reached vim
2026-10-06 12:41:18 +02:00
bchanot 7adcef4f94 Merge feature/tmux-config into develop 2026-10-06 12:27:34 +02:00
bchanot 825abe842a feat(macos): deploy tmux.conf + tpm plugins; cloudpex becomes an install-time offer
- tmux.conf (vi keys, C-a prefix, resurrect/continuum, window-name) → ~/.config/tmux/tmux.conf
  on macOS; tpm cloned, plugins fetched headlessly, libtmux installed non-fatally
- bashrc-osx / zshrc-osx export XDG_CACHE_HOME (session save dir of the config)
- Linux: install_cloudpex → offer_cloudpex ([y/N] with the /tmp and SSH offers)
- README + CLAUDE.md updated
2026-10-06 12:27:22 +02:00
bchanot 1fbbffcedc chore(memory): journal — v1.0.0 release 2026-10-06 11:53:26 +02:00
bchanot dda3fabcf7 Merge release/1.0.0 into develop 2026-10-06 11:41:37 +02:00
bchanot caf388ce0b chore(release): 1.0.0 — version.txt + CHANGELOG 2026-10-06 11:39:40 +02:00
bchanot dd36bc6f18 chore(githooks): refresh post-commit/post-merge from the lib (hook label) 2026-10-06 11:38:56 +02:00
Bastien CHANOT 7acdcd4128 Merge feature/gitconfig-user-scope into develop 2026-10-06 11:25:48 +02:00
Bastien CHANOT d293943106 feat(install): add git-delta to the apt and brew packages
gitconfig sets core.pager = delta; without the binary git cannot page.
2026-10-06 10:55:24 +02:00
Bastien CHANOT 51521e7ca4 feat(git): deploy user-scope ~/.gitconfig from the repo template
Git never expands $VARS in its config, so gitconfig carries @USER@ and
@EMAIL@ placeholders that install.sh fills from the deployed bashrc's
USER/EMAIL exports. A differing ~/.gitconfig is kept as
~/.gitconfig.backup-<date>; an identical one is left alone. A repo's
.git/config still overrides it. excludesfile uses ~ (git expands it,
not $HOME).

The rc files now override $USER, so the installer takes the login name
from id -un for dscl and the code-server unit.
2026-10-06 10:55:24 +02:00
Bastien CHANOT 3b53213af4 feat(shell): export identity as USER/EMAIL instead of VIUSER/VIMAIL
Same names in bashrc-linux, bashrc-osx and zshrc-osx, so the vim header
and the rendered ~/.gitconfig share one identity.
2026-10-06 10:55:14 +02:00
Bastien CHANOT f053164cf7 chore(memory): BDR-015/016 macOS + zshrc backup, LRN-014 BSD traps, BLK-007 master URL, journal 2026-10-05 17:32:33 +02:00
Bastien CHANOT 7d5dabda36 Merge feature/macos-support into develop 2026-10-05 17:28:53 +02:00
Bastien CHANOT 7edde9d0f3 chore(todo): macOS support + zsh choice plans 2026-10-05 17:28:47 +02:00
Bastien CHANOT cebc1f055a docs: macOS install, shell choice and zsh files 2026-10-05 17:28:47 +02:00
Bastien CHANOT 093d21f8a1 feat(install): macOS via Homebrew, colima, brew services, bash/zsh login shell choice, gaps report; cp -Rpv for BSD cp 2026-10-05 17:28:47 +02:00
Bastien CHANOT 6b60b64c8f feat(zsh): macOS zshrc (oh-my-zsh) + bchanot theme porting the bash prompt 2026-10-05 17:28:47 +02:00
Bastien CHANOT f09151d1f3 feat(bashrc-osx): mirror bashrc-linux with macOS deltas (brew env, ls -G, EPOCHREALTIME timer, cc without systemd-run) 2026-10-05 17:28:46 +02:00
Bastien CHANOT 72648597ee fix(dt): macOS portability (lsof cwd, BSD date start time, bash 3.2 tilde, sed -E help) 2026-10-05 17:28:39 +02:00
bastien 1a4f8d4715 Merge feature/apt-packages into develop 2026-09-28 21:57:04 +02:00
bastien ba1817a4e9 chore(memory): BDR-014 apt mirror choices, LRN-013 gitleaks version probe 2026-09-28 21:57:03 +02:00
bastien 3c5b1ece6e chore(memory): journal — apt packages feature, gitleaks hook fix 2026-09-28 21:41:27 +02:00
bastien 9e49b9d92c chore(githooks): refresh pre-commit (gitleaks protect fallback) + track reference-transaction 2026-09-28 21:41:06 +02:00
bastien 0bc9e3f467 feat(install): mirror this machine's apt packages
gitleaks, web stack (mariadb-server, imagemagick, unversioned php-* modules),
ubuntu-desktop-minimal before the RDP setup, and a lspci-gated
install_nvidia_driver() that runs ubuntu-drivers install. README + TODO updated.
2026-09-28 21:41:05 +02:00
bastien 58adb28ec2 chore(memory): journal, branch cleanup + gitea-deploy split out 2026-09-22 18:03:46 +02:00
bastien a42e8f6024 chore(githooks): refresh pre-commit/post-commit/post-merge from the lib (gitleaks backstop, autopush) 2026-09-22 17:55:41 +02:00
bastien cfd144d158 chore(memory): journal, security baseline merged 2026-09-22 17:53:53 +02:00
bastien a6c416e1cf Merge feature/security-baseline into develop 2026-09-22 17:53:46 +02:00
bastien 26c8e345c5 chore(memory): BDR-013 security baseline, LRN-012 fail2ban port vs allports, journal 2026-09-22 17:47:35 +02:00
bastien 55ede6f08c feat(install): security baseline: fail2ban, unattended-upgrades, sshd hardening
Always applied in the Linux block, no prompt, idempotent:
- install_fail2ban: fail2ban + nftables, etc/fail2ban/jail.d/local.conf.
  sshd jail reads the journal (backend systemd, works with or without
  auth.log) and bans the offender on every port, so the SSH port is
  irrelevant: the previous server's jail banned 22 while sshd listened
  on 337. 5 failures / 10 min / 1 h. Loopback + RFC1918 never banned.
- install_unattended_upgrades: package + 20auto-upgrades (the file
  dpkg-reconfigure writes, without the prompt).
- harden_sshd: sshd_config.d/20-hardening.conf (PermitRootLogin no,
  MaxAuthTries 3, LoginGraceTime 20), sshd -t gated: a rejected file is
  removed and the install continues with a warning. Auth methods, port
  and user lists untouched.
Docs: README table + step 13 + packages, CLAUDE.md layout.
2026-09-22 17:47:34 +02:00
bastien 0d2e96819a chore(memory): journal 2026-09-22, round-2 TODO closed 2026-09-22 17:35:05 +02:00
bastien 836bb675bf Merge feature/tmp-disk-ssh-oom-cloudpex into develop 2026-09-22 17:34:31 +02:00
bastien 6e38c3b6cf Merge main into develop 2026-09-22 17:34:30 +02:00
bastien a9f7b6437f chore(memory): capitalize /tmp on disk, SSH guard, cloudpex conf (BDR-010..012, LRN-009..011, BLK-005/006, EVAL-002) 2026-09-22 17:34:18 +02:00
bastien 4f8bb61458 feat(cloudpex): site values out of the script, prompted at install into /etc/cloudpex.conf
cloudpex/cloudpex no longer carries the NAS host, share name, SMB user,
mount point or SMB version. It reads /etc/cloudpex.conf (root:root 0600,
KEY=value) line by line, never sources it, and dies with a hint when the
file is missing, incomplete or has a relative mount point.

cloudpex/install.sh prompts for the five values (regex-validated, re-asked
on bad input so the main installer never aborts), shows and keeps an
existing config unless answered n, and skips the config when no terminal
is attached. README (FR) + root README + CLAUDE.md updated.
2026-09-22 17:34:18 +02:00
bastien 872079bafb feat(install): offer /tmp on disk + SSH memory guard, deploy cloudpex helper
/tmp is a RAM-backed tmpfs on Ubuntu (50% of RAM). Agent runs fill it: half
the RAM goes, then every temp-file creation fails with ENOSPC and shells
break. Swap does not lift the cap, so the fix is /tmp on disk.

End-of-install offers (Linux, [y/N], skipped without a terminal, idempotent):
- offer_tmp_on_disk: mask tmp.mount + etc/tmpfiles.d/tmp.conf (wipe at
  boot, 10-day purge, /var/tmp rule kept). Effective at next reboot.
- offer_ssh_memory_guard: the previous server's rules. ssh.service drop-in
  (OOMScoreAdjust=-1000, MemoryMin=256M) + earlyoom with --avoid sshd and
  --prefer node/java. MemoryMin covers sshd only; earlyoom is the real guard.

install_cloudpex deploys the NAS mount helper in the Linux block.
Docs: README steps 12-14 + table, CLAUDE.md layout + lint command.
2026-09-22 16:57:08 +02:00
bastien 9dacef3823 feat(cloudpex): track the on-demand SMB mount helper, add installer + README
cloudpex/cloudpex mounts //192.168.1.111/CloudPex on /mnt/cloudpex on demand
(password prompted, nothing stored, noexec/nosuid/nodev, dir_mode 0750).
cloudpex/install.sh reproduces the live deployment: /usr/local/bin/cloudpex
root:root 0755, /mnt/cloudpex, cifs-utils if mount.cifs is missing.
README (FR) explains why on-demand and not fstab (RECOVERY doc 04).
2026-09-22 16:57:08 +02:00
bastien a210d0173b added correct dtach 2026-09-22 02:07:49 +00:00
Bastien Chanot 5a8b575f1a added the gh package instalation in install script 2026-07-01 21:25:14 +02:00
Bastien Chanot bd593e007d Merge feature/doc-sync into develop 2026-07-01 14:31:36 +02:00
Bastien ChanotandClaude Opus 4.8 bfd9f31ff0 docs: install.sh OS auto-detect
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNaYKPEkjH1jbgoX1TwKMX
2026-07-01 14:31:36 +02:00
Bastien Chanot b866004456 Merge chore/reconcile-memory into develop 2026-07-01 14:27:48 +02:00
Bastien ChanotandClaude Opus 4.8 c4586599d1 chore(memory): reconcile TODO + registries (BDR/LRN/BLK supersessions, LICENSE done)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNaYKPEkjH1jbgoX1TwKMX
2026-07-01 00:20:23 +02:00
Bastien Chanot 8fd32631fe chore: adopt gitflow socle + pre-commit hook 2026-06-29 02:27:57 +02:00
Bastien ChanotandClaude Opus 4.8 ea33e231ef docs(memory): add BDR-009 + LRN-008 (dtach menu -> ~/.bashrc, VS Code non-login)
- BDR-009: reverses BDR-007; menu moved to ~/.bashrc because VS Code
  Remote-SSH terminals are non-login; per-tab firing accepted over a
  once-per-connection sentinel.
- LRN-008: VS Code Remote-SSH (Linux) terminals are non-login -> skip
  ~/.profile; hook ~/.bashrc for "run once at session start"; diagnose via
  VSCODE_IPC_HOOK_CLI + no login-bash ancestry + shopt -q login_shell.
- journal: 2026-06-25 session entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CN1KSmsuLG6TxSeN5m8xvM
2026-06-26 00:09:16 +02:00
Bastien ChanotandClaude Opus 4.8 e37eb77ed5 fix(dtach): wire resume menu into ~/.bashrc for non-login VS Code shells
VS Code Remote-SSH integrated terminals are non-login interactive shells:
they source ~/.bashrc but never ~/.profile, where the resume menu was wired
(login-scope, BDR-007). The auto-check therefore never fired in the user's
actual environment, even after install.sh.

Source dtach-router from bashrc-linux (every interactive shell) instead, and
turn install.sh's wire_dtach_profile() into unwire_dtach_profile(): it now
strips any stale ~/.profile block so a plain SSH login (which reads ~/.bashrc
via ~/.profile) does not prompt twice. README updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CN1KSmsuLG6TxSeN5m8xvM
2026-06-26 00:09:16 +02:00
Bastien ChanotandClaude Opus 4.8 4f03d87a86 docs(memory): add LRN-007 (doc-drift) + BDR-008 (GPL-3.0)
- LRN-007: doc-drift detection by file mtime misses partial doc updates;
  cross-reference feature commits, not the doc timestamp.
- BDR-008: repo licensed GPL-3.0-or-later (copyleft), with rejected
  alternatives and reversibility note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8QfqokDoggsPVhEp7FnCa
2026-06-25 11:19:21 +02:00
Bastien ChanotandClaude Opus 4.8 00d88f723f docs(readme): document code-server + RDP, add License section
The feature batch in 0bd936b shipped code-server and gnome-remote-desktop
RDP login, but the later README edit (46512ee) only fixed the package list,
leaving both features undocumented.

- Add install steps for code-server (browser VS Code) and gnome-remote-desktop
  RDP remote login.
- Add a "Remote access" bullet to the apt package list (gnome-remote-desktop,
  openssl, code-server).
- Broaden the install.sh summary row in "What's inside".
- Replace the Lint section with a License section (GPL-3.0-or-later).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8QfqokDoggsPVhEp7FnCa
2026-06-25 11:19:21 +02:00
Bastien ChanotandClaude Opus 4.8 40c6524348 chore: add GPL-3.0 license
Add verbatim GPL-3.0 text (copied from /usr/share/common-licenses/GPL-3).
Repo is now licensed GPL-3.0-or-later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8QfqokDoggsPVhEp7FnCa
2026-06-25 11:19:21 +02:00
Bastien ChanotandClaude Opus 4.8 46512ee2de docs(readme): fix stale wkhtmltopdf -> weasyprint in package list
install.sh installs weasyprint (HTML/CSS->PDF), not wkhtmltopdf; the apt package list in README was stale. Mirror the installer verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CN1KSmsuLG6TxSeN5m8xvM
2026-06-24 18:02:23 +02:00
34 changed files with 2671 additions and 84 deletions
+17 -1
View File
@@ -21,7 +21,7 @@ resource-DB tool, no such systemd service). Returns non-zero → under `set -euo
whole installer. Also `apt-get install xrdp` missing `-y` → hangs non-interactive run. Root cause:
one-letter typo `xrdp`→`xrdb` + missing -y. Fix: idempotent `install_xrdp()` (apt -y, adduser xrdp
ssl-cert, polkit .rules, conditional ufw 3389, enable+restart). shellcheck + bash -n CLEAN.
Status: resolved in repo. Not run live / RDP connection not tested.
Status: SUPERSEDED by BLK-004 (xrdp dropped for gnome-remote-desktop) — was resolved in repo, never run live.
## BLK-004 — RDP Win→Linux 0x904/0x7: empty gate creds on g-r-d --system — RESOLVED
2026-06-23. After xrdp dropped for gnome-remote-desktop (Wayland), mstsc fails `0x904 / 0x7`
@@ -31,3 +31,19 @@ despite: daemon LISTEN *:3389, ufw inactive, TLS cert readable, service active.
PAM login at GDM. Empty gate creds → RDP nego refused before GDM → 0x904. Fix: set-credentials,
connect (gate creds → GDM `bchanot`). Connection CONFIRMED live. Automated in install.sh via
ensure_rdp_credentials (prompt, TTY-guarded, idempotent). Supersedes BLK-003 (xrdp). Status: resolved.
## BLK-005 — secrets still on NAS share: /mnt/cloudpex/transfert/root/ — OPEN (user action)
2026-09-22. RECOVERY checklist 06 + doc 04: delete `CloudPex/transfert/root/` (root ssh keys, .smbcredentials,
.acme.sh copied 21/09 01:41) then regenerate. Still present 2026-09-22. Claude never deletes on the NAS
(destructive-tools rule). User: delete on NAS, rotate root/bchanot SSH keys, SMB password, acme account.
## BLK-006 — permission layer denies read-only diagnostics (`systemctl cat/is-enabled`, `sudo -n`, /tmp globs) — OPEN
2026-09-22. Compound Bash calls holding `systemctl cat tmp.mount`, `systemctl is-enabled`, `sudo -n du`,
`du /tmp/*`, `find -exec` were denied ("Permission to use Bash ... denied"), even read-only. Cause not
identified (guard-bash hook vs auto-mode classifier). Workaround: read unit files under /usr/lib/systemd +
/etc/systemd directly, `ls`/`du` on literal paths, no `find -exec`, no `sudo`. Cost ≈ 5 retries. Candidate fix:
allowlist `systemctl {cat,show,is-enabled,is-active,status}` wherever the denial comes from.
## BLK-007 — remote-install.sh + README one-liner point at branch `master` — OPEN
2026-10-05. `BRANCH="${BRANCH:-master}"` + README raw URLs `/branch/master/`, but repo = main/develop (gitflow) →
curl|bash one-liner 404 / clone fails. Fix: default `main`, update URLs. Not fixed (out of macOS scope).
+94 -1
View File
@@ -49,5 +49,98 @@ idempotent: awk strips prior block (marker-delimited managed block `# >>> claude
`DT=$(dt ls)…fi` execute block) then re-appends marker block. cc/d aliases live in bashrc-linux (sourced by
.profile BEFORE the router runs → available). Alts rejected: (a) source from `.bashrc` (router's own header
suggests it) — fires too often for login-only intent; (b) keep execute + string-parse — broke the return-based
guard (LRN-006) + fragile parse. Supersedes the old execute+string-parse block. Status: done in repo; live
guard (LRN-006) + fragile parse. Supersedes the old execute+string-parse block. Status: SUPERSEDED by BDR-009 — done in repo; live
~/.profile re-migrated this session.
## BDR-008 — config repo licensed GPL-3.0-or-later (copyleft)
2026-06-25. Added LICENSE (verbatim GPLv3 copied from `/usr/share/common-licenses/GPL-3`) + README
`## License` (`GPL-3.0-or-later — see LICENSE`, © 2026 Bastien Chanot). User said "full opensource" → read
as strong COPYLEFT (code + all derivatives stay open), not permissive. SPDX: GPL-3.0-or-later; "or-later"
grant asserted in README per FSF convention, LICENSE holds plain GPLv3 text. Alts rejected: MIT / Apache-2.0
(permissive — allow CLOSED derivatives, weaker open guarantee); Unlicense (public domain, no copyleft).
Repo private (CLAUDE.md Public=no) so license optional, but user wanted one set. Reversible: swap LICENSE +
README line if "full opensource" meant permissive. Status: done in repo (committed: LICENSE 40c6524, README License 00d88f7).
## BDR-009 — dtach resume menu moved ~/.profile → ~/.bashrc (every interactive shell)
2026-06-25. Reversed BDR-007. Root cause: user works in VS Code Remote-SSH; its Linux integrated terminals are
NON-login interactive shells → read `~/.bashrc`, never `~/.profile` → login-scoped wiring (BDR-007) silently
never fired (LRN-008). Now source dtach-router from `bashrc-linux` via `case $- in *i*) … . dtach-router`;
fires in EVERY interactive shell (covers VS Code, plain SSH via `~/.profile`→`~/.bashrc`, tmux, new tabs).
install.sh `wire_dtach_profile()` → `unwire_dtach_profile()`: strips any stale `~/.profile` block (marker +
legacy) so plain SSH login (sources `~/.bashrc` via `~/.profile`) doesn't prompt twice. Trade-off ACCEPTED
(user chose "simplest"): menu shows in each new terminal tab when sessions exist, not once-per-connection —
the exact noise BDR-007 avoided, now tolerated for VS Code reliability. Alts rejected: (a) once-per-connection
sentinel keyed to `SSH_CONNECTION`/`VSCODE_IPC_HOOK_CLI` in `$XDG_RUNTIME_DIR` — more code, user declined;
(b) VS Code `terminal.integrated` `args:["-l"]` — not carried by dotfiles, same per-tab firing. Supersedes
BDR-007. Status: done in repo; live needs `./install.sh` re-run.
## BDR-010 — /tmp on disk (mask tmp.mount), swap rejected
2026-09-22. Ubuntu 26.04 mounts /tmp tmpfs size=50% RAM (7.4G of 14G here). Agents fill it → RAM halved +
ENOSPC → shells break. Chose `systemctl mask tmp.mount` + `/etc/tmpfiles.d/tmp.conf` (`D /tmp 10d`, `/var/tmp`
line kept). Offered [y/N] end of install.sh (`offer_tmp_on_disk`), TTY-guarded, idempotent, effective next
reboot (never umount live). Alts rejected: (a) add/grow swap — cap + ENOSPC stay, thrash instead of OOM;
(b) bigger tmpfs `size=` — still RAM; (c) `TMPDIR=/var/tmp` in bashrc — leaky (services, IDE spawns, cron).
Status: done in repo, live apply = user (EVAL-002).
## BDR-011 — SSH memory guard = old-server rules (ssh drop-in + earlyoom), systemd-oomd untouched
2026-09-22. Restored from NAS `RECOVERY/40-systeme/etc`: `ssh.service.d/override.conf` (MemoryMin=256M,
OOMScoreAdjust=-1000) + earlyoom `-r 60 -m 10 -s 10 --avoid '^(sshd|systemd|systemd-logind|dbus-daemon|containerd)$'
--prefer '^(java|node|pnpm|esbuild)$'`. MemoryMin covers sshd cgroup only (logind puts sessions in user.slice)
→ real guard = OOMScoreAdjust + earlyoom (kills ONE largest proc, shell survives). systemd-oomd (Ubuntu default
`ManagedOOMMemoryPressure=kill` 50% on user@.service, kills WHOLE session cgroup) left as-is: zero kills in
journal (fresh install), unproven as shell-killer. `offer_ssh_memory_guard`, [y/N], idempotent, ssh restart keeps
sessions (KillMode=process). Alt rejected: drop-in only — kernel/oomd may still kill whole session. Status: done
in repo, live apply = user.
## BDR-012 — cloudpex site values in /etc/cloudpex.conf, prompted by installer
2026-09-22. User: no IP/user in script. Chose key=value `/etc/cloudpex.conf` root:root 0600 written by
`cloudpex/install.sh` prompts (HOST, SHARE, SMB_USER, MNT, SMB_VERS; regex-validated, re-ask on bad input so
main install.sh never aborts; keep-existing [Y/n]; skipped without TTY). Script parses lines
(`sed -n s/^KEY=//p`), never sources → no code exec as root from config. Alt rejected: sed placeholders into
deployed script — config + code mixed, every re-run overwrites values. Status: done in repo.
## BDR-013 — security baseline always-on in install.sh: fail2ban (all-ports), unattended-upgrades, sshd limits
2026-09-22. User: "fail2ban and the like, systematically". Chose no-prompt Linux-block steps: (1) fail2ban sshd
jail `backend = systemd` + `banaction = %(banaction_allports)s` → SSH port irrelevant (old server banned 22 while
sshd on 337, LRN-012); `ignoreip` = loopback + RFC1918 static (no LAN detection; trade-off: compromised LAN host
never banned); 5/10m/1h from RECOVERY doc 01. (2) `20auto-upgrades` file instead of interactive dpkg-reconfigure.
(3) sshd drop-in limited to PermitRootLogin/MaxAuthTries/LoginGraceTime, `sshd -t` gated, rejected file removed +
install continues. Declined by user: auditd rules, ufw whitelist (site-specific ports, lockout risk → would be an
offer, not systematic). Not included by design: PasswordAuthentication no / AllowUsers / X11Forwarding no
(lockout or workflow risk). Status: done in repo (feature/security-baseline), live apply = user.
## BDR-014 — install.sh mirrors machine apt set: GNOME + LAMP unconditional, NVIDIA via ubuntu-drivers
2026-09-28. Source: `apt-mark showmanual` + /var/log/apt/history.log diffed vs script. Added gitleaks, web stack
(mariadb-server imagemagick php-* unversioned → follows distro PHP), ubuntu-desktop-minimal before RDP setup
(gnome-remote-desktop needs GDM, bare server had none), `install_nvidia_driver()` = `lspci -d 10de:` gate +
`ubuntu-drivers install` (distro-recommended, 595-open today). Alternatives rejected: pin nvidia-driver-595-open
(ages, hardware-bound), LAMP behind confirm() offer (user: base list), GNOME left implicit (RDP fails silently).
Status: merged to develop. Live rerun of install.sh = user.
## BDR-015 — macOS: Homebrew replaces apt, Docker via colima, login shell bash 5 OR zsh (user choice)
2026-10-05. install.sh Darwin branch: ensure_homebrew (official script if missing) → brew update/upgrade → apt list
mapped to formulae. Docker = colima + docker/compose/buildx CLI (`cliPluginsExtraDirs` written only if
~/.docker/config.json absent). colima/code-server/mariadb = `brew services` (skip if started). Shell asked first
(`MACOS_SHELL` presets, no TTY → bash): bash → brew bash 5 in /etc/shells + chsh (macOS bash 3.2 too old: no
EPOCHREALTIME, HISTSIZE=-1); zsh → oh-my-zsh unattended + zsh/zshrc-osx + bchanot.zsh-theme (bash prompt port), chsh
/bin/zsh. End: print_macos_gaps lists Linux-only items skipped. Alts rejected: Docker Desktop (GUI, licence), no
Docker; staying on zsh w/o config. Status: merged develop 7d5dabd.
## BDR-016 — ~/.zshrc backed up to ~/.zshrc.backup-<date>, not ~/Oldconfig
2026-10-05. ~/Oldconfig is `rm -rf` at every run → 2nd run destroys 1st-run backup of user's real zshrc (nvm, bun
lines). deploy_zsh_config: if ~/.zshrc differs from repo copy (cmp -s) → timestamped mv in $HOME; identical → no
backup (rerun no dup). Machine-specific lines → ~/.zshrc.local (sourced). Same flaw still on .bashrc/.vim (open,
not fixed). Status: done.
## BDR-017 — tmux pane moves on ctrl+u/h/j/k, not hjkl nor option+arrows
2026-10-06. u/h/j/k laid out as arrows (u up, h left, j down, k right): same key positions on AZERTY and QWERTY US,
control exists on every keyboard (option does not, cmd never reaches tmux). Alternatives rejected: ctrl+i/j/k/m
(C-i = Tab, C-m = Enter, same bytes, would break completion); option+arrows (needs iTerm2 "Left Option = Esc+",
no option key on some keyboards). Cost: shell loses C-u (readline clear-line). Gain: C-l free, clears screen again.
Resize mirrors it with prefix. Status: done (tmux.conf).
## BDR-018 — Linux tmux clipboard = tmux buffer + OSC 52, no xsel/xclip
2026-10-06. tmux.conf clipboard via `if-shell 'command -v pbcopy'`: macOS → pbcopy/pbpaste; else `y` =
copy-selection-and-cancel, `p` = paste-buffer, tmux hands the buffer to the terminal through OSC 52 (set-clipboard),
reaches the local clipboard over ssh from iTerm2. xsel/xclip rejected: headless servers have no X display (xsel
errors), xclip keeps STDOUT open and hangs tmux. Status: done.
+7
View File
@@ -7,3 +7,10 @@ Quality check of Claude output. Caveman + English.
Not runtime-tested (would mutate ~/.vim, ~/.bashrc on this machine). Logic traced by hand:
SCRIPT_DIR resolution, idempotent clones, target case map all correct. Anomaly: none.
Action: safe to commit. Full runtime test deferred to next clean VM.
## EVAL-002 — install.sh offers (tmp on disk, ssh guard) — stub-verified, live pending
2026-09-22. Method: shellcheck + bash -n CLEAN (install.sh, cloudpex/install.sh); stub harness (LRN-011) ran both
offers through 9 scenarios, emitted sudo calls match design; `systemd-tmpfiles --dry-run` accepts tmp.conf;
`sh -n` on earlyoom env file. NOT run live (sudo). Anomaly: none. Action: user applies runbook, then checks
`findmnt -T /tmp` (no tmpfs), `systemctl status earlyoom`, `systemctl show ssh -p OOMScoreAdjust -p MemoryMin`,
`cloudpex -s` → close this EVAL.
+70
View File
@@ -41,3 +41,73 @@ SOURCE `case $- in *i*) ... . dtach-router` via idempotent wire_dtach_profile()
marker block, re-appends marker block). Added cc (create) / d (re-summon) aliases to bashrc-linux.
shellcheck + bash -n CLEAN; migration simulated on real .profile copy. LRN-006 + BDR-007. README synced.
Not committed; live ~/.profile not yet re-migrated.
## 2026-06-25 — dtach menu: ~/.profile → ~/.bashrc (VS Code non-login fix)
User: dtach resume menu never fires at session start, even post-install. Root cause: user runs VS Code
Remote-SSH → its Linux terminals are NON-login → skip ~/.profile (where BDR-007 wired it). Proven by process
tree (VSCODE_IPC_HOOK_CLI, no sshd/login-bash) + provably-correct ~/.profile wiring + existing session yet zero
menu. Fix: source dtach-router from bashrc-linux (every interactive shell); install.sh wire_dtach_profile() →
unwire_dtach_profile() strips stale ~/.profile block (avoids double-prompt on plain SSH). User chose simplest
(per-tab) over once-per-connection sentinel. shellcheck install.sh CLEAN, bash -n OK, strip proven idempotent
on .profile copy. BDR-009 (supersedes BDR-007) + LRN-008. Live needs ./install.sh re-run.
## 2026-09-22 — /tmp on disk + SSH OOM guard + cloudpex conf
User: swap for /tmp? keep RAM for ssh, old-server rules, cloudpex README+installer. Found /tmp = tmpfs 50% RAM
→ swap rejected, mask tmp.mount offer (BDR-010). Old rules in NAS RECOVERY/40-systeme: ssh drop-in + earlyoom →
end-of-install offers (BDR-011). cloudpex tracked; site values → /etc/cloudpex.conf prompted by installer
(BDR-012). shellcheck/bash -n CLEAN, stub harnesses (LRN-011; EVAL-002 open until live apply). Reconciled
main→develop (a210d01 dtach was main-only), feature finished via lib → develop 836bb67. Not applied live.
Flagged: secrets in NAS transfert/root (BLK-005), remote-install.sh BRANCH=master stale vs main, gitea-deploy/
untracked, remote feature branch left on origin (lib deletes local only).
Later same day: security baseline always-on in install.sh (fail2ban all-ports + RFC1918 ignore, unattended-
upgrades file, sshd limits drop-in sshd -t gated) on feature/security-baseline (BDR-013, LRN-012: old jail
banned 22 not 337). auditd + ufw declined. shellcheck/bash -n CLEAN, stub harness incl. sshd -t reject path,
configparser + apt-config checks. Branch pushed, NOT finished (no merge signal). Live apply = user runbook.
Update: user said merge → feature/security-baseline finished via lib, develop a6c416e pushed.
Cleanup: user asked all-in-develop + delete branches. Hooks refresh committed (a42e8f6). All 3 remote feature
branches verified merged; `git push --delete` DENIED by permission layer → user runs it. gitea-deploy/ (untracked
Gitea server deploy project, 31 files, no secrets) moved to ~/Documents/gitea-deploy, own repo via gitflow init,
pushed main+develop to git.bchanot.fr (push-to-create worked). deploy.conf gitignored.
## 2026-09-28
- feature/apt-packages (0bc9e3f, unmerged): install.sh mirrors machine apt set. Diff `apt-mark showmanual` +
apt history vs script → added gitleaks, web stack (mariadb-server imagemagick php-* unversioned),
ubuntu-desktop-minimal before RDP, install_nvidia_driver() (lspci 10de gate, `ubuntu-drivers install`, no pin).
User approved 3 choices (GNOME in, ubuntu-drivers, LAMP unconditional). shellcheck + bash -n + stub run OK.
- Blocked mid-commit: lib pre-commit ran `gitleaks git --staged`, Ubuntu apt gitleaks = 8.16 (no `git` subcmd,
exit 1 read as leak). Fixed in claude-config bugfix/gitleaks-protect-fallback (347073a, unmerged): probe
`gitleaks git --help`, fallback `protect --staged`; T16c symlink-farm PATH. make test 0. Hooks refreshed here (9e49b9d).
- Note: `gh` in install.sh list but not installed on this box (script not rerun since added).
## 2026-10-05 — macOS support + zsh/bash choice
Local main 15 commits behind develop → worked off develop. install.sh Darwin branch (Homebrew, colima, brew services,
gaps report), bashrc-osx = bashrc-linux + macOS deltas, dt portable, zsh option (oh-my-zsh + bchanot theme). Tested:
shellcheck, bash 3.2/5 + zsh -n, bashrc/zshrc/theme live in shells, dt with real dtach session, stub harness both
choices. Full install.sh not run on this Mac. BDR-015/016, LRN-014, BLK-007. Merged develop 7d5dabd, not pushed.
## 2026-10-06
- Cut v1.0.0: first tagged release, develop → main via /release-candidate. CHANGELOG.md bootstrapped from 31 develop commits, version.txt created. Tag pushed. Pre-existing shellcheck SC2148 on bashrc files untouched.
## 2026-10-06 (pm) — macOS tmux config + cloudpex offer
Done: tmux.conf (sohorx vi-style, tpm) deployed on macOS → ~/.config/tmux, tpm cloned + plugins fetched headless, libtmux via pip --user (PEP 668 fallback). XDG_CACHE_HOME exported in osx rc files (config needs it, else resurrect dir = "/tmux/"). cloudpex: unconditional install → [y/N] offer with the other Linux extras. Verified: shellcheck, temp-HOME run x3, tmux parses config. Branch feature/tmux-config → develop.
## 2026-10-06 (pm, 2) — tmux keys + Linux
Done: tmux.conf fixes (pbcopy, C-a passthrough, is_vim via pane_current_command), pane moves ctrl+u/h/j/k (AZERTY/QWERTY invariant; C-i/C-m = Tab/Enter, rejected), splits prefix i / -, deploy on Linux too (clipboard if-shell: pbcopy else tmux buffer + OSC 52). Verified in Ubuntu 24.04 container. Merged bugfix/tmux-macos-keys → develop (e3be25f).
## 2026-10-06 (pm, 3) — tmux selection, keep tmux over iTerm2 splits
Done: mode-style yellow/black (black bg invisible on dark iTerm2), mouse drag release copies (pbcopy / tmux buffer) without leaving copy-mode. Merged bugfix/tmux-selection → develop (b73ee08). Decided (chat, AskUserQuestion): keep tmux, not iTerm2/Terminator/WezTerm splits — ssh persistence, GNOME Terminal has no splits. BDR offered, not written yet. Wheel issue was a stale iTerm2 session: restart fixed it.
## 2026-10-06 (pm, 4) — tmux click exit + alt copy/paste
Done: plain click leaves copy-mode (MouseUp1Pane cancel; MouseDown cancel was wrong, killed drag after scroll), alt+c = y, alt+v = paste, per OS. Merged bugfix/tmux-click-exit → develop (8b5e936). Open: iTerm2 "Left Option = Esc+" is manual (US Intl PC layout coming); dynamic-profile deploy offered, not asked. GNOME Terminal OSC 52 support unverified.
## 2026-10-06 (pm, 5) — identity asked at install
Done: USER/EMAIL no longer hardcoded in tracked rc files (@USER@/@EMAIL@ placeholders, rendered by install.sh; existing rc export reused, else IDENTITY_* env, else prompt). vimrc reads $USER/$EMAIL. Merged feature/identity-prompt → develop (0687f6a). Open: old values remain in git history (rewrite not done, destructive); deployed ~/.bashrc/.zshrc on this Mac still hold bchanot@gmail.fr.
## 2026-10-06 — tmux wheel halved, dtach mac/termux checked
Done: copy-mode-vi WheelUp/DownPane rebound `-N 2` (stock 5), live reload OK. Merged bugfix/tmux-wheel-half → develop (ec9d0ed). Checked: dtach master on macOS reparents to launchd, survives parent SIGKILL + SIGHUP (sessions outlive shell crash). Termux: dtach 0.9 in official repo, scripts need `pkg install dtach fzf procps`; install.sh NOT Termux-safe (apt-get path); app kill = sessions lost, needs termux-wake-lock. Untested on device.
## 2026-10-06 (pm, 7) — tmux ctrl+h/j/k/u in copy-mode
Done: copy-mode-vi table had stock C-h cursor-left, C-u halfpage-up, C-j copy, so no-prefix pane moves became text nav once scrolled. Four keys rebound select-pane in copy-mode-vi, live reload OK. Merged bugfix/tmux-copy-mode-ctrl-nav → develop (8b6ed6b). Open: deployed ~/.config/tmux/tmux.conf on this Mac predates alt+v/wheel/this fix, `./install.sh` not re-run.
## 2026-10-06 (pm, 8) — tmux drag "syntax error" in claude panes
Done: MouseDrag1Pane binding had `\"` inside '...' (literal since tmux 3.0); branch for mouse-tracking panes (claude fullscreen, vim mouse=a) failed to parse → "syntax error", no selection. Backslashes dropped, pitfall noted in conf. Deployed to ~/.config/tmux/tmux.conf + source-file, live binding verified (`forwarded-to-app`). Merged bugfix/tmux-drag-syntax-error → develop (2245247). Closes the "deployed conf stale" open item.
+62 -1
View File
@@ -18,7 +18,7 @@ files already cover before adding external fetch.
(else recurring "Authentication required to create a color managed device" popups). Polkit format
version-gated: v>=0.106 → `/etc/polkit-1/rules.d/*.rules` (JS); older → `.pkla`. Verified live polkit
127 → `.rules` only (`.pkla` backend dropped). Open RDP 3389 only if firewall active. Restart xrdp
after group add so daemon reloads ssl-cert membership.
after group add so daemon reloads ssl-cert membership. SUPERSEDED by LRN-004 for Wayland-only GNOME (xrdp recipe N/A there).
## LRN-004 — gnome-remote-desktop --system: remote desktop on Wayland-only GNOME
2026-06-23. xrdp does NOT work on Wayland-only GNOME (Shell asserts XDG_SESSION_TYPE=wayland, Xorg
@@ -47,3 +47,64 @@ runs fzf + `dt at … >/dev/tty` → `/dev/tty: No such device or address` in EV
(`[ "$(dt ls)" != "Aucune session dtach." ]`) — fragile (couples to exact string) AND redundant
(`dtach-router` already returns on empty `dt --raw`). Let the script self-guard. Bonus gotcha: `~/.profile`
is NOT read by bash if `~/.bash_profile` or `~/.bash_login` exists.
## LRN-007 — Doc-drift by file mtime misses partial doc updates
2026-06-25. `git log -1 --format=%aI -- README.md` reports "fresh" → false negative for staleness.
README touched in 46512ee (`docs(readme): fix wkhtmltopdf→weasyprint`, PACKAGE LIST ONLY) AFTER feature
commit 0bd936b (`gnome-remote-desktop + code-server`), so the mtime-based "commits since doc edit" scan
returned EMPTY while code-server + RDP stayed undocumented. A partial doc commit resets the clock and
hides earlier feature drift. Fix: drift-detect against FEATURE commits (scan `git log` for feat/* touching
source since the doc's last SUBSTANTIVE edit, OR cross-ref each entry-point / install-step in code vs doc
text) — never trust doc timestamp alone. Surfaced by /doc clean this session.
## LRN-008 — VS Code Remote-SSH terminals are non-login → skip ~/.profile
2026-06-25. VS Code Remote-SSH (Linux) integrated terminals = NON-login interactive bash → source `~/.bashrc`,
NEVER `~/.profile`. Any login-scoped startup wiring (`~/.profile`, `~/.bash_profile`) silently never runs there.
Diagnose via process tree: `VSCODE_IPC_HOOK_CLI` env + `.vscode-server/.../remote-cli` in PATH, no `sshd` /
no `bash -l` ancestry (the launching shell reparents to systemd); confirm with `shopt -q login_shell`. For
"run once at session start" that must work under VS Code, hook `~/.bashrc` (universal: sourced by login shells
via `~/.profile` AND directly by non-login interactive shells), NOT `~/.profile`. Extends/corrects LRN-006
(its `~/.profile` fix is valid only for real login shells, not IDE remotes). Deductive tell that pinned it:
wiring proven correct + target resource (session) proven present, yet menu never fires at startup → the startup
file is not being sourced → non-login shell. See BDR-009.
## LRN-009 — tmpfs /tmp + agents: two symptoms, one cause; swap is not the fix
2026-09-22. "RAM overloaded" + "No space left on device" in shells = same root: /tmp tmpfs (RAM). Diagnose
`findmnt -T /tmp` (FSTYPE tmpfs, SIZE=50% RAM). Swap only pages tmpfs out, cap unchanged. Fix = /tmp on disk
(mask tmp.mount; it is wanted from `/usr/lib/systemd/system/local-fs.target.wants/`). Gotcha:
`/etc/tmpfiles.d/X.conf` REPLACES `/usr/lib/tmpfiles.d/X.conf` wholesale → copy the other lines
(`q /var/tmp 30d`) or they vanish. Validate: `systemd-tmpfiles --dry-run --create <file>`.
## LRN-010 — systemd `$VAR` in ExecStart honours quotes inside EnvironmentFile values
2026-09-22. `EARLYOOM_ARGS="-m 10 --avoid '^(a|b)$'"` + `ExecStart=… $EARLYOOM_ARGS`: bare `$VAR` = split on
whitespace, quotes respected then stripped → regex arrives as ONE arg. `${VAR}` = whole value as one arg (wrong
here). Old-server earlyoom file valid as-is. Env-file syntax check: `sh -n`.
## LRN-011 — verify sudo-bound installer functions with a stub harness
2026-09-22. Can't run sudo/systemctl here (security rule; permission layer even denied `systemctl is-enabled`).
Extract functions (`sed -n '/^fn()/,/^}/p'`) into scratch, define `sudo(){ echo "SUDO: $*"; }` + `systemctl`
+ `findmnt` stubs, override `confirm` per scenario, `</dev/null` for no-TTY. Covers every branch, prints exact
sudo calls, `set -e` behaviour included. Gotcha: `unset -f` on an overridden fn removes it entirely.
## LRN-012 — fail2ban jail must match the real sshd port, or ban all ports
2026-09-22. Old server: sshd `Port 337`, fail2ban sshd jail with default `port = ssh` (=22) → bans hit port 22
only, SSH on 337 stayed open to the banned IP. Silent, no error. Fix options: `port = 337` (needs detection /
templating, drifts if port changes) or `banaction = %(banaction_allports)s` (offender blocked everywhere, port
irrelevant) — chose allports. Offline checks without fail2ban installed: python `configparser` with
BasicInterpolation resolves `%(x)s` refs and proves the file parses; apt.conf → `apt-config
--config-file=<f> dump APT::Periodic`. sshd drop-ins can't be `sshd -t`-tested without root (host keys).
## LRN-013 — distro package lags upstream: probe subcommand before calling it
2026-09-28. Ubuntu apt gitleaks = 8.16; lib pre-commit hook written for >= 8.19 (`gitleaks git --staged`).
"unknown command" exit 1 read as a leak → every commit blocked, silently (stderr swallowed). Script calling a
subcommand born in version N must probe `tool sub --help` and fall back (`protect --staged`). Test faking
"binary absent" via shorter PATH (`/usr/bin:/bin`) breaks once the binary lives in /usr/bin: symlink farm of
/usr/bin minus the binary instead. Apply: any tool install.sh pulls from apt while ~/.claude scripts assume
the upstream release. Fix: claude-config bugfix/gitleaks-protect-fallback.
## LRN-014 — macOS (BSD/bash 3.2) traps for Linux shell scripts
2026-10-05. `cp -u` absent on BSD cp → `cp -Rp`. BSD `date` no `%N` → bash 5 `$EPOCHREALTIME` (strip `[.,]`, fr
locale = comma); no `date -d` → `date -j -f '%a %b %e %T %Y'` with `LC_ALL=C ps -o lstart=`. No /proc → cwd via
`lsof -a -p PID -d cwd -Fn`. bash 3.2 `${x/#$HOME/\~}` keeps backslash → use var `tilde='~'`. BSD sed no `\?` in BRE
→ `sed -E`. Terminal.app = LOGIN shells → read ~/.bash_profile only → must source ~/.bashrc. `ls --color` → `ls -G`.
zsh: `status` is read-only special var, don't name locals that. Verify installer fns via stub harness (LRN-011).
+101 -3
View File
@@ -13,15 +13,113 @@
## P2 — Moyenne
- [ ] [P2] [/hotfix] — vim/vimrc GenerateClassC: bare `name` → `a:name` (fixes :ClassC E121)
Files: vim/vimrc (~line 75-95)
Files: vim/vimrc (GenerateClassC, ~line 80-104)
Source: .claude/memory/blockers.md BLK-001
## P3 — Basse
- [ ] [P3] [/code-clean] — bashrc-* legacy backticks → $(...) (SC2006), arithmetic SC2004
Files: bash/bashrc-linux, bash/bashrc-server, bash/bashrc-osx
Files: bash/bashrc-linux, bash/bashrc-osx
Note: cosmetic only, no behavior change
## Post-MVP (optional, backlog)
- [ ] Runtime-test install.sh on a clean VM (all 4 targets) — not safe on dev machine
- [ ] Consider an `uninstall.sh` (restore from ~/Oldconfig)
- [ ] LICENSE if repo ever goes public
- [x] LICENSE if repo ever goes public — done (GPL-3.0, BDR-008, 40c6524)
## Feature — /tmp on disk + SSH OOM guard + cloudpex installer (2026-09-22)
Branch: feature/tmp-disk-ssh-oom-cloudpex (off develop). Design approved in chat (bounded).
Root cause: /tmp is tmpfs (50% RAM) → agents fill it → RAM halved + ENOSPC breaks shells. Swap rejected.
- [x] etc/tmpfiles.d/tmp.conf (D /tmp 10d + q /var/tmp 30d — keep both upstream lines)
- [x] etc/systemd/ssh.service.d/override.conf (MemoryMin=256M, OOMScoreAdjust=-1000 — old server)
- [x] etc/default/earlyoom (old server args: -r 60 -m 10 -s 10 --avoid sshd… --prefer node…)
- [x] install.sh: confirm() TTY-guarded prompt helper
- [x] install.sh: offer_tmp_on_disk() — mask tmp.mount + tmpfiles rule, reboot notice, idempotent
- [x] install.sh: offer_ssh_memory_guard() — drop-in + daemon-reload/restart ssh + earlyoom, idempotent
- [x] install.sh: install_cloudpex() in Linux block; offers at end of script (Linux-gated)
- [x] cloudpex/install.sh — /usr/local/bin/cloudpex root 0755, /mnt/cloudpex, cifs-utils if missing
- [x] cloudpex/README.md (FR) — purpose, why on-demand not fstab, usage, install
- [x] README.md steps 12-14 + table rows; CLAUDE.md layout
- [x] shellcheck + bash -n (install.sh, cloudpex/install.sh); stub-sudo dry run of the offers
- [x] commit on feature branch (no gitea-deploy/, no .githooks changes)
## Round 2 — cloudpex config out of script, reconcile main/develop, capitalize, merge (2026-09-22)
- [x] cloudpex/cloudpex: constants → /etc/cloudpex.conf parsed line by line (never sourced), die if missing
- [x] cloudpex/install.sh: prompt host/share/user/mnt/vers (regex-validated), keep-existing [Y/n], no-TTY skip
- [x] cloudpex/README.md + README.md + CLAUDE.md: no site values, describe prompts + conf file
- [x] registries: BDR-010/011/012, LRN-009/010/011, BLK-005/006, EVAL-002, journal
- [x] reconcile: merge main (a210d01 dtach) into develop via lib helper
- [x] gitflow finish feature → develop (explicit user signal: "puis merge")
- [x] runbook for live apply on this machine
## Feature — security baseline in install.sh (2026-09-22)
Branch: feature/security-baseline (off develop). Scope approved: fail2ban, unattended-upgrades, sshd hardening. auditd + ufw declined.
- [x] etc/fail2ban/jail.d/local.conf — sshd jail, backend systemd, allports ban, RFC1918 ignoreip
- [x] etc/apt/apt.conf.d/20auto-upgrades — Periodic Update-Package-Lists + Unattended-Upgrade = 1
- [x] etc/ssh/sshd_config.d/20-hardening.conf — PermitRootLogin no, MaxAuthTries 3, LoginGraceTime 20
- [x] install.sh: install_fail2ban / install_unattended_upgrades / harden_sshd (sshd -t gated), called in Linux block
- [x] README.md (table, step 13, packages) + CLAUDE.md layout
- [x] shellcheck + bash -n; stub harness harden_sshd (accept / reject paths); configparser check of jail file
- [x] commit; registries (BDR-013, LRN-012); runbook. No finish without explicit signal.
## Feature — install.sh mirrors this machine's apt packages (2026-09-28)
Branch: feature/apt-packages (off develop). Source: apt-mark showmanual + /var/log/apt/history.log diffed against install.sh.
- [x] gitleaks in the base list (backs the pre-commit hook)
- [x] web stack group: mariadb-server imagemagick + unversioned php-* modules (approved: base list, not an offer)
- [x] ubuntu-desktop-minimal before setup_remote_desktop (approved)
- [x] install_nvidia_driver(): lspci vendor 10de gate + ubuntu-drivers install (approved: no version pin)
- [x] README steps 11 + packages; shellcheck + bash -n; stub run of the NVIDIA helper
- [x] commit on the feature branch. No finish without explicit signal.
## Feature — macOS support, parity with Linux minus apt (2026-10-05)
Branch: feature/macos-support (off develop). User choices: Docker = colima + CLI; login shell → brew bash 5.
- [x] install.sh: Darwin block — ensure Homebrew, brew update/upgrade, brew formula list mirroring apt list
- [x] install.sh: colima + docker CLI (compose/buildx plugin dir), code-server + mariadb via brew services
- [x] install.sh: brew bash → /etc/shells + chsh; ~/.bash_profile sources ~/.bashrc (Terminal = login shell)
- [x] install.sh: `cp -rupv` → `cp -Rpv` (BSD cp has no -u; target dir is fresh anyway)
- [x] install.sh: end-of-run report of Linux items not installed on macOS
- [x] bash/bashrc-osx: mirror bashrc-linux (ls -G, brew shellenv, EPOCHREALTIME timer, dtach_claude w/o systemd-run)
- [x] bin/dt: portable _cwd_of (lsof) + _starttime_of (BSD date), help sed -E
- [x] README + CLAUDE.md macOS section
- [x] shellcheck + bash -n; runtime test bashrc-osx + dt on this Mac; stub run of Darwin block
## Feature — macOS: choose zsh (oh-my-zsh) or bash as login shell (2026-10-05)
Same branch. Prompt at start of Darwin block (MACOS_SHELL=bash|zsh env overrides, no TTY → bash).
- [x] zsh/zshrc-osx: brew env, PATH, history, GCC_COLORS, VIUSER, cc/d + dtach-router, oh-my-zsh, ~/.zshrc.local hook
- [x] zsh/bchanot.zsh-theme: same prompt as bashrc (✔/✘ + timer, user [ cwd ], git [branch -*+], root red)
- [x] install.sh: choose_macos_shell, install_oh_my_zsh (unattended, keep zshrc), deploy_zsh_config (backup → Oldconfig)
- [x] install.sh: use_brew_bash_login_shell → set_login_shell <path>, called at end with chosen shell
- [x] README + CLAUDE.md
- [x] shellcheck/bash -n/zsh -n; runtime: theme in zsh (prompt render, timer, git bits), dtach-router sourced in zsh; harness both choices
## Feature — user-scope ~/.gitconfig from repo template, VIUSER/VIMAIL → USER/EMAIL (2026-10-06)
- [x] rc files (bashrc-linux, bashrc-osx, zshrc-osx): `VIUSER`/`VIMAIL` → `USER`/`EMAIL`
- [x] `gitconfig` template: git never expands `$VAR` → `@USER@`/`@EMAIL@` placeholders, `excludesfile = ~/.gitignore`
- [x] install.sh `deploy_gitconfig`: values read from deployed bashrc, rendered → ~/.gitconfig, differing old one → ~/.gitconfig.backup-<date>
- [x] install.sh: `$USER` → `$(id -un)` (dscl, code-server unit): rc now overrides USER with identity
- [x] README + CLAUDE.md layout
- [x] Verify: shellcheck, bash -n, render to temp HOME, `git config --file` reads values
- [x] `git-delta` added to apt + brew lists (gitconfig pager = delta)
## Feature — macOS tmux config (tmux.conf + tpm) ; cloudpex becomes an offer (2026-10-06)
Branch: feature/tmux-config (off develop, "met ca dans develop" = finish into develop). tmux already in the brew list.
- [x] install.sh `deploy_tmux_config` (Darwin): tmux.conf → ~/.config/tmux/tmux.conf (differing one → .backup-<date>, stray ~/.tmux.conf moved aside since tmux reads it first), clone tpm, headless `install_plugins`, libtmux for tmux-window-name (non-fatal)
- [x] bashrc-osx + zshrc-osx: export XDG_CACHE_HOME (tmux.conf resurrect dir needs it, else "/tmux/")
- [x] README macOS step + CLAUDE.md layout
- [x] shellcheck, bash -n, zsh -n; run deploy_tmux_config against a temp HOME; tmux parses the config
- [x] cloudpex: `install_cloudpex` → `offer_cloudpex` ([y/N] via confirm, with the other Linux offers); README/CLAUDE.md wording
## tmux follow-ups (2026-10-06, branch bugfix/tmux-macos-keys)
- [x] tmux.conf: pbcopy/pbpaste, `bind C-a send-prefix`, is_vim via pane_current_command
- [x] pane moves ctrl+u/h/j/k (arrow layout, AZERTY/QWERTY invariant), resize mirrors with prefix; C-l free
- [x] splits: prefix i (side by side) and prefix - (stacked), h/j/k/l kept
- [x] Linux: deploy_tmux_config on both OSes (gated on tmux), clipboard if-shell (pbcopy else tmux buffer + OSC 52), XDG_CACHE_HOME in bashrc-linux
- [x] Verified: shellcheck/bash -n, macOS test server, Ubuntu 24.04 container (tmux 3.4) full deploy + bindings + split + plugins + libtmux
## Feature — identity asked at install, no hardcoded USER/EMAIL in tracked rc files (2026-10-06)
Branch: feature/identity-prompt (off develop). Values were visible to anyone reading the repo.
- [x] rc templates (bashrc-linux, bashrc-osx, zshrc-osx): `export USER="@USER@"` / `export EMAIL="@EMAIL@"`
- [x] install.sh resolve_identity: existing ~/.bashrc / ~/.zshrc export wins silently → IDENTITY_USER/IDENTITY_EMAIL env → prompt (TTY) → default (id -un, empty email)
- [x] install.sh: render_identity_template (generic, replaces render_gitconfig); bashrc + zshrc rendered, gitconfig unchanged (reads the rendered bashrc)
- [x] vim/vimrc: g:_author / g:_email from $USER / $EMAIL
- [x] README, CLAUDE.md (CHANGELOG left to the release step, like the tmux work)
- [x] Verify: shellcheck, bash -n, zsh -n; temp-HOME render (env preset, existing rc reuse); vim reads the env
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# gitflow post-commit — generated by gitflow_init. Do not hand-edit.
hook=post-commit
# Pushes every commit as it lands (BDR-095): a remote only backs up what it
# holds. Never fails the commit: no origin / offline / refused → warning only.
# Opt out for one command with GITFLOW_NO_PUSH=1 (throwaway repos, tests).
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && exit 0
# Per-repo opt-out (no push rights on a foreign clone): git config gitflow.autopush false
[ "$(git config --bool --default true gitflow.autopush)" = false ] && exit 0
git remote get-url origin >/dev/null 2>&1 || exit 0
br=$(git symbolic-ref --short -q HEAD 2>/dev/null) || exit 0 # detached HEAD — nothing to track
if command -v timeout >/dev/null 2>&1; then t="timeout ${GITFLOW_PUSH_TIMEOUT:-30}"; else t=""; fi
if $t git push -q -u --follow-tags origin "$br" >/dev/null 2>&1; then exit 0; fi
echo "gitflow $hook: push of '$br' FAILED — this commit exists only on this disk." >&2
echo " Push by hand: git push -u origin $br (rejected as non-fast-forward? never force-push; ask first)" >&2
exit 0
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# gitflow post-merge — generated by gitflow_init. Do not hand-edit.
hook=post-merge
# Pushes every commit as it lands (BDR-095): a remote only backs up what it
# holds. Never fails the commit: no origin / offline / refused → warning only.
# Opt out for one command with GITFLOW_NO_PUSH=1 (throwaway repos, tests).
[ "${GITFLOW_NO_PUSH:-0}" = 1 ] && exit 0
# Per-repo opt-out (no push rights on a foreign clone): git config gitflow.autopush false
[ "$(git config --bool --default true gitflow.autopush)" = false ] && exit 0
git remote get-url origin >/dev/null 2>&1 || exit 0
br=$(git symbolic-ref --short -q HEAD 2>/dev/null) || exit 0 # detached HEAD — nothing to track
if command -v timeout >/dev/null 2>&1; then t="timeout ${GITFLOW_PUSH_TIMEOUT:-30}"; else t=""; fi
if $t git push -q -u --follow-tags origin "$br" >/dev/null 2>&1; then exit 0; fi
echo "gitflow $hook: push of '$br' FAILED — this commit exists only on this disk." >&2
echo " Push by hand: git push -u origin $br (rejected as non-fast-forward? never force-push; ask first)" >&2
exit 0
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
# gitflow pre-commit — generated by gitflow_init. Do not hand-edit.
# Mirrors gitflow_protected_base (lib/gitflow.sh). Drift caught by T10.
gd=$(git rev-parse --git-dir)
br=$(git symbolic-ref --short -q HEAD 2>/dev/null)
git rev-parse --verify -q HEAD >/dev/null 2>&1 || exit 0 # root commit — allow
[ -f "$gd/MERGE_HEAD" ] && exit 0 # merge in progress — allow
# Secret backstop (job7) — any branch, not just protected ones. Non-blocking
# if gitleaks isn't installed; auto-discovers ./.gitleaks.toml (repo root).
# gitleaks >= 8.19 scans the index with `git --staged`; older builds (Ubuntu's
# 8.16 package) only know `protect --staged`, and `git` exits 1 there as an
# unknown command — which would block every commit. Probe the subcommand first.
if command -v gitleaks >/dev/null 2>&1; then
gl_sub=git
gitleaks git --help >/dev/null 2>&1 || gl_sub=protect
if ! gitleaks "$gl_sub" --staged --no-banner >/dev/null 2>&1; then
echo "gitflow pre-commit: BLOCKED — gitleaks found a secret in staged changes." >&2
echo " Details: gitleaks $gl_sub --staged --no-banner" >&2
echo " Genuine false-positive? add an allowlist rule to .gitleaks.toml — never bypass with --no-verify." >&2
exit 1
fi
else
echo "gitflow pre-commit: gitleaks not installed — secret scan skipped (https://github.com/gitleaks/gitleaks)." >&2
fi
# Per-repo opt-out of the branch model (a clone of a foreign project):
# git config gitflow.protect false
[ "$(git config --bool --default true gitflow.protect)" = false ] && exit 0
case "$br" in
main|develop) ;; # protected — keep checking
*) exit 0 ;; # working branch — allow
esac
# whitelist: all-staged-under-.claude/ (memory/doc/deploy helpers) or
# .githooks/ (the hooks themselves, refreshed by the lib) — allow
if [ -z "$(git diff --cached --name-only | grep -vE '^\.(claude|githooks)/' | head -1)" ]; then
exit 0
fi
echo "gitflow pre-commit: BLOCKED — direct commit on '$br'." >&2
echo " Branch from the right base (feature/bugfix->develop, hotfix->main), or merge." >&2
echo " (.claude/** and .githooks/** commits are exempt; foreign clone? git config gitflow.protect false)" >&2
exit 1
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# gitflow reference-transaction — generated by gitflow_init. Do not hand-edit.
# Refuses deleting (or renaming) main / develop, whatever the
# command. Mirrors gitflow_protected_base (lib/gitflow.sh).
[ "$1" = prepared ] || exit 0
while read -r _old new ref; do
case "$ref" in refs/heads/main|refs/heads/develop) ;; *) continue ;; esac
case "$new" in *[!0]*) continue ;; esac # new value not all-zeros → an update, not a deletion
# Per-repo opt-out (a foreign clone): git config gitflow.protect false
[ "$(git config --bool --default true gitflow.protect)" = false ] && exit 0
echo "gitflow reference-transaction: BLOCKED — deleting '$ref', a protected base." >&2
echo " main and develop are never deleted or renamed. A merged working branch: gitflow.sh delete <branch>" >&2
exit 1
done
exit 0
+18
View File
@@ -15,3 +15,21 @@ Oldconfig/
# Claude local settings (machine-specific)
.claude/settings.local.json
# ── gitflow standard socle (added by gitflow_init; additive, safe to edit) ──
.env
.env.*
!.env.example
.vscode/
.idea/
*.log
.claude/*
!.claude/memory/
!.claude/tasks/
!.claude/audits/
!.claude/settings.json
!.claude/deploy/
.claude/agent-memory/
.claude/gstack/
.claude/deploy/PENDING.json
.claude/deploy/NEXT.sh
+39
View File
@@ -0,0 +1,39 @@
# Changelog
All notable changes to this project are documented here.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: semver.
## [Unreleased]
## [1.0.0] — 2026-10-06
### Added
- macOS support in `install.sh`: Homebrew instead of apt, colima for Docker,
brew services for code-server/mariadb, bash or zsh login shell choice
(`MACOS_SHELL`), report of the Linux-only items skipped.
- `bash/bashrc-osx`: `bashrc-linux` mirrored with the macOS deltas (brew env,
`ls -G`, `EPOCHREALTIME` timer, `cc` without systemd-run).
- `zsh/zshrc-osx` + `zsh/bchanot.zsh-theme`: oh-my-zsh config and theme
porting the bash prompt.
- `gitconfig` template deployed as user-scope `~/.gitconfig`, `@USER@` /
`@EMAIL@` filled at install; a differing file is kept as
`~/.gitconfig.backup-<date>`.
- `git-delta` in the apt and brew package lists (the gitconfig pager).
- Security baseline on Linux, always applied and idempotent: fail2ban sshd
jail (journal backend, all-ports ban, LAN ignored), unattended security
upgrades, sshd hardening drop-in gated by `sshd -t`.
- End-of-install offers on Linux: `/tmp` on disk (mask `tmp.mount` +
tmpfiles rules) and SSH memory guard (sshd OOM-exempt drop-in + earlyoom).
- `cloudpex/`: on-demand SMB mount helper, installer and FR README; site
values prompted at install into `/etc/cloudpex.conf`, never in the script.
- Apt package list mirroring the reference machine: gitleaks, web stack,
`ubuntu-desktop-minimal`, lspci-gated NVIDIA driver install, `gh`.
### Changed
- Shell identity exported as `USER` / `EMAIL` instead of `VIUSER` / `VIMAIL`
in every rc file; the installer takes the login name from `id -un`.
- `install.sh` uses `cp -Rpv` (BSD-compatible).
### Fixed
- `bin/dt` macOS portability: `lsof` cwd, BSD `date` start time, bash 3.2
tilde expansion, `sed -E` help.
+25 -4
View File
@@ -10,7 +10,7 @@ Produces vim + bash configuration deployed by `install.sh`. Private/personal aud
- Public: no
- Database: none
- Stack: POSIX/bash shell scripts + vimscript
- Distribution: `git clone` + `./install.sh <target>`
- Distribution: `git clone` + `./install.sh` (OS auto-detected)
## Layout
@@ -20,12 +20,33 @@ install.sh one-shot installer (OS auto-detected)
vim/vimrc vim config (pathogen, molokai, syntastic, NERDTree)
vim/autoload/ pathogen loader (committed)
vim/colors/ molokai colorscheme (committed)
bash/bashrc-{linux,osx} OS-detected bashrc
bash/bashrc-{linux,osx} OS-detected bashrc; USER/EMAIL identity = @USER@/@EMAIL@ placeholders,
asked at install (reused from an existing rc), never tracked
gitconfig user-scope ~/.gitconfig template, @USER@/@EMAIL@ filled at install
tmux.conf tmux config (vi keys, tpm plugins) → ~/.config/tmux/tmux.conf, both OSes (tmux ≥ 3.1)
zsh/{zshrc-osx,bchanot.zsh-theme} macOS zsh option: oh-my-zsh zshrc + theme porting the bash prompt
bin/{dt,dtach-router,claude-provider} CLI scripts deployed to ~/.local/bin
etc/profile.d/disk-usage-warning.sh login-time low-disk warning → /etc/profile.d (Linux only)
etc/tmpfiles.d/tmp.conf disk-backed /tmp cleanup rules (offer: /tmp on disk)
etc/systemd/ssh.service.d/override.conf sshd OOM-exempt drop-in (offer: SSH memory guard)
etc/default/earlyoom earlyoom args, spare sshd / kill node first (same offer)
etc/fail2ban/jail.d/local.conf sshd jail: journal backend, all-ports ban, LAN ignored (always)
etc/apt/apt.conf.d/20auto-upgrades unattended security upgrades on (always)
etc/ssh/sshd_config.d/20-hardening.conf sshd limits that cannot lock out, sshd -t gated (always)
cloudpex/{cloudpex,install.sh,README.md} on-demand SMB mount helper → /usr/local/bin, offered [y/N]; site values
prompted at install → /etc/cloudpex.conf, never in the script (FR docs)
.claude/{tasks,memory,audits}/ Claude working state
```
`/tmp` is a RAM-backed tmpfs on Ubuntu (50% of RAM): agent runs fill it, which is why
install.sh offers to mask `tmp.mount`. Swap is not the fix (the cap and ENOSPC stay).
macOS: install.sh swaps apt-get for Homebrew (colima for Docker, brew services for
code-server/mariadb, `~/.bash_profile` → `~/.bashrc`), asks bash or zsh (`MACOS_SHELL`
presets it; zsh = oh-my-zsh + `zsh/` files, bash = brew bash 5) and prints the Linux-only
items it skipped. Keep `bashrc-osx` = `bashrc-linux` + macOS deltas only, and the zsh
files in step with them (same env, aliases, prompt).
`pymupdf`/`markdown_py` are NOT tracked — they are pipx entry-point shims,
recreated by `pipx install PyMuPDF Markdown` in install.sh.
`claude-provider` reads `$OPENROUTER_API_KEY` from the env — never hardcode it (the
@@ -35,8 +56,8 @@ original had a live key; it was scrubbed — see decisions/blockers).
| Task | Command |
| ----- | ---------------------------------------- |
| Lint | `shellcheck *.sh bash/bashrc-*` |
| Syntax check | `bash -n install.sh remote-install.sh` |
| Lint | `shellcheck *.sh cloudpex/install.sh cloudpex/cloudpex bash/bashrc-*` |
| Syntax check | `bash -n install.sh remote-install.sh cloudpex/install.sh` |
| Install | `./install.sh` (OS auto-detected) |
| Remote install | `curl -fsSL <raw>/remote-install.sh \| bash` |
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+52 -16
View File
@@ -16,14 +16,25 @@ curl -fsSL https://git.bchanot.fr/bchanot/config/raw/branch/master/remote-instal
| Path | Purpose |
| -------------------- | -------------------------------------------------------------- |
| `install.sh` | Installs apt packages + Docker, backs up old config, deploys vim + bashrc (OS-detected), installs CLI scripts. |
| `install.sh` | Linux: installs apt packages + Docker + code-server + RDP (gnome-remote-desktop), backs up old config, deploys vim + bashrc (OS-detected), installs CLI scripts, pipx tools and a low-disk login warning; ends by offering three extras (`/tmp` on disk, SSH memory guard, the `cloudpex` NAS mount helper). macOS: same tooling through Homebrew (see [macOS](#macos)), deploys the tmux config, then prints what was not installed compared with Linux. |
| `tmux.conf` | tmux config (vi keys, mouse, status bar, tpm plugins: resurrect + continuum session restore, window-name). Deployed on both OSes to `~/.config/tmux/tmux.conf` (tmux ≥ 3.1). |
| `cloudpex/` | On-demand SMB mount of a NAS share (`cloudpex` command + its installer, offered `[y/N]` at the end of a Linux install). Site values (host, share, SMB user, mount point, SMB version) are prompted at install and stored in `/etc/cloudpex.conf`, never in the script. French README inside. |
| `etc/tmpfiles.d/tmp.conf` | Cleanup rules for a disk-backed `/tmp` (wiped at boot, 10-day purge). Deployed by the `/tmp` on disk offer. |
| `etc/systemd/ssh.service.d/override.conf` | `ssh.service` drop-in: sshd exempt from the OOM killer + memory reclaim protection. Deployed by the SSH memory guard offer. |
| `etc/default/earlyoom` | earlyoom arguments: spare sshd/systemd, kill node/java first. Deployed by the SSH memory guard offer. |
| `etc/fail2ban/jail.d/local.conf` | fail2ban sshd jail: journal backend, all-ports ban, 5 tries / 10 min / 1 h, private LAN never banned. Deployed on every Linux install. |
| `etc/apt/apt.conf.d/20auto-upgrades` | Enables unattended security upgrades (what `dpkg-reconfigure` writes). Deployed on every Linux install. |
| `etc/ssh/sshd_config.d/20-hardening.conf` | sshd limits that cannot lock you out: `PermitRootLogin no`, `MaxAuthTries 3`, `LoginGraceTime 20`. Deployed on every Linux install after `sshd -t`. |
| `vim/vimrc` | Vim config: pathogen, molokai, syntastic (C with `-Wall -Werror -Wextra`), NERDTree, 42-style canonical class generators (`:ClassH`, `:ClassC`). |
| `vim/autoload/` | `pathogen.vim` plugin loader (committed). |
| `vim/colors/` | `molokai.vim` colorscheme (committed). |
| `gitconfig` | Template of the user-scope `~/.gitconfig`. `@USER@` and `@EMAIL@` are filled at install with the `USER` and `EMAIL` exported by the bashrc (git never expands `$VARS` itself). |
| `bash/bashrc-linux` | bashrc for desktop Linux (git-aware prompt + command timer). |
| `bash/bashrc-osx` | bashrc for macOS. |
| `bash/bashrc-osx` | bashrc for macOS: `bashrc-linux` adapted (Homebrew on `PATH`, BSD `ls -G`, bash 5 clock for the timer, `cc` without `systemd-run`). |
| `zsh/zshrc-osx` | zshrc for macOS when zsh is chosen: oh-my-zsh + the same env, aliases and dtach menu as `bashrc-osx`. Loads `~/.zshrc.local` for machine-specific lines. |
| `zsh/bchanot.zsh-theme` | oh-my-zsh theme reproducing the bash prompt: `✔ (12ms) user [ ~/dir ] [branch -*+] >`. |
| `bin/dt` | dtach session manager for claude-in-dtach sessions. |
| `bin/dtach-router` | SSH-login dashboard to resume dtach sessions (wired into `~/.profile` by the installer). |
| `bin/dtach-router` | Dashboard to resume dtach sessions, shown at the start of every interactive shell (wired into `~/.bashrc` by the installer). |
| `bin/claude-provider`| Switch Claude Code between Anthropic and OpenRouter. |
| `etc/profile.d/disk-usage-warning.sh` | Login-time warning (bold red) when `/` or `/home` cross 85% usage. Deployed to `/etc/profile.d/` on Linux. |
@@ -50,36 +61,61 @@ No argument — the OS is auto-detected.
What it does:
1. On Debian/Ubuntu, installs a set of CLI/dev packages via `apt-get` (see below). Skipped automatically where `apt-get` is absent (macOS).
1. On Debian/Ubuntu, installs a set of CLI/dev packages via `apt-get` (see below). On macOS, Homebrew does it instead: see [macOS](#macos).
2. Sets up Docker's official apt repo (Ubuntu) and installs the engine + compose plugin — skipped if `docker` is already present.
3. Moves any existing `~/.vim`, `~/.vimrc`, `~/.bashrc`, `~/.Sublivim` to `~/Oldconfig`.
4. Clones the `syntastic` and `nerdtree` vim plugins into `~/.vim/bundle/`.
5. Copies the tracked vim files into `~/.vim` and symlinks `~/.vimrc`.
6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Copies it to `~/.bashrc`.
6. Picks the bashrc by OS: macOS → `bashrc-osx` (falls back to `bashrc-linux` if missing), everything else → `bashrc-linux`. Renders it into `~/.bashrc` with the identity asked at the very start: a name and an email for git commits and vim headers. An `export USER=` / `export EMAIL=` already present in `~/.bashrc` or `~/.zshrc` is reused without asking, so a re-run never prompts twice; `IDENTITY_USER` / `IDENTITY_EMAIL` preset them; with no terminal attached it falls back to the login name and an empty email. The values live only in the deployed files, never in the repo. Then renders `gitconfig` into `~/.gitconfig` with the same `USER` / `EMAIL`. A different existing `~/.gitconfig` is saved as `~/.gitconfig.backup-<date>`; an identical one is left alone. It is the global level only: a repo's own `.git/config` still overrides it. `core.excludesfile` points at `~/.gitignore`, ignored by git when the file does not exist. Then deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (both OSes, tmux ≥ 3.1: Ubuntu 22.04+, brew), clones [tpm](https://github.com/tmux-plugins/tpm) and fetches the listed plugins headlessly; details under [macOS](#macos) step 6, the step is the same. On Linux, `y` copies into tmux's buffer and the terminal clipboard through OSC 52; macOS uses `pbcopy`/`pbpaste`.
7. Installs Python CLIs via `pipx` (`PyMuPDF` → `pymupdf`, `Markdown` → `markdown_py`) — skipped if `pipx` is absent.
8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin` and wires the dtach session-resume menu into `~/.profile` (idempotent — sourced only at interactive login, and replaces any prior block).
8. Copies the `bin/` scripts (`dt`, `dtach-router`, `claude-provider`) into `~/.local/bin`. The dtach session-resume menu ships in the deployed bashrc (both OSes), so every interactive shell offers it — including VS Code Remote-SSH terminals, which are non-login and never read `~/.profile`. The installer also strips any older dtach block left in `~/.profile` so a plain SSH login doesn't prompt twice.
9. On Linux, installs `etc/profile.d/disk-usage-warning.sh` to `/etc/profile.d/` (needs `sudo`) so each login warns when `/` or `/home` cross 85% usage.
10. On Linux, installs **code-server** (VS Code in the browser) via its vendor script — skipped if already present — and enables the `code-server@<login>` systemd service (login from `id -un`: the bashrc overrides `$USER`).
11. On Linux, installs **`ubuntu-desktop-minimal`** (GDM + GNOME Shell, ~1.5 GB): the RDP remote login below hands out a GNOME session, which a bare server install does not have. Then sets up **RDP remote login** via `gnome-remote-desktop` (Wayland-native): installs the daemon + `openssl`, generates a self-signed TLS cert once, and prompts interactively for shared "gate" credentials (skipped when no terminal is attached, or already set). Disables `xrdp` if present; opens UFW port `3389` only when UFW is already active. Finally, when `lspci` sees an NVIDIA GPU, runs `ubuntu-drivers install` to put on the driver the distro recommends for the card (no version pinned; loads at the next reboot). Skipped on machines without an NVIDIA GPU.
12. On Linux, at the very end, **offers** (`[y/N]`) to install the **`cloudpex`** NAS mount helper to `/usr/local/bin` via `cloudpex/install.sh`, which prompts for the NAS host, share name, SMB user, mount point and SMB version and writes them to `/etc/cloudpex.conf` (root, `0600`; an existing config is shown and kept unless you say `n`; skipped when no terminal is attached). Nothing is mounted, no password stored, see [`cloudpex/README.md`](cloudpex/README.md).
13. On Linux, installs the **security baseline**, always, no prompt: **fail2ban** (+ `nftables`) with `etc/fail2ban/jail.d/local.conf` (sshd jail reading the journal, bans the offending IP on every port so the SSH port does not matter, 5 failures in 10 min → 1 h ban, loopback and private LAN ranges never banned); **unattended-upgrades** enabled through `etc/apt/apt.conf.d/20auto-upgrades`; and the **sshd drop-in** `etc/ssh/sshd_config.d/20-hardening.conf` (`PermitRootLogin no`, `MaxAuthTries 3`, `LoginGraceTime 20`), checked with `sshd -t` and removed again if sshd rejects it, then `reload ssh`. Authentication methods, port and user lists are left as they are.
14. On Linux, at the very end, **offers** (`[y/N]`, skipped when no terminal is attached) to move **`/tmp` to disk**: Ubuntu mounts `/tmp` as a RAM-backed tmpfs capped at 50% of RAM, which agent runs fill, halving the RAM and breaking every shell with "No space left on device". Accepting masks `tmp.mount` and installs `etc/tmpfiles.d/tmp.conf` (wipe at boot, 10-day purge). Effective at the next reboot.
15. On Linux, at the very end, **offers** to keep **SSH reachable under memory pressure**: installs the `ssh.service` drop-in (`OOMScoreAdjust=-1000`, `MemoryMin=256M`) and `earlyoom` with `etc/default/earlyoom` (kills the largest process, `node`/`java` first and never `sshd`, once free RAM and swap both drop under 10%). Restarting `ssh` keeps open sessions. Note: `MemoryMin` protects the sshd daemon only; login sessions live in `user.slice`, so no setting can reserve RAM for a future shell. earlyoom acting in time is the real protection.
### Packages installed (apt)
- **Build / VCS / C dev**: `vim git git-lfs git-filter-repo gcc make pkg-config dkms valgrind shellcheck`
- **Build / VCS / C dev**: `vim git git-lfs git-filter-repo gitleaks gcc make pkg-config dkms valgrind shellcheck gh git-delta` (`git-delta` = `delta`, the pager set in `gitconfig`)
- **Net / security / transport**: `curl gnupg ca-certificates apt-transport-https net-tools openssh-server cifs-utils lftp ftp`
- **Shell tooling**: `unzip tree tmux fzf dtach`
- **Runtimes**: `nodejs python3-pip pipx php-cli`
- **Media / doc CLI**: `ffmpeg wkhtmltopdf poppler-utils qpdf webp libavif-bin`
- **Web stack (local WordPress/LAMP)**: `mariadb-server imagemagick php-mysql php-gd php-imagick php-mbstring php-xml php-intl php-curl` (unversioned `php-*` metapackages, so they follow the distro's PHP)
- **Media / doc CLI**: `ffmpeg weasyprint poppler-utils qpdf webp libavif-bin`
- **Docker**: `docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` (via Docker's repo)
- **Desktop / GPU**: `ubuntu-desktop-minimal` (always, Linux) + the distro-recommended NVIDIA driver via `ubuntu-drivers install` (only when an NVIDIA GPU is detected)
- **Remote access**: `gnome-remote-desktop openssl` (apt) + `code-server` (via its vendor install script, not apt) — RDP remote login + browser VS Code
- **pipx**: `PyMuPDF` (`pymupdf`), `Markdown` (`markdown_py`)
- **Security baseline (Linux, always)**: `fail2ban nftables unattended-upgrades`
- **Optional (end-of-install offer, Linux)**: `earlyoom`
The script is re-runnable: each run re-backs up to `~/Oldconfig` (overwriting the previous backup), re-clones plugins, skips Docker if already installed, and re-deploys the `bin/` scripts.
> Notes: the package list is Debian/Ubuntu-specific, and the Docker repo step assumes **Ubuntu**. On macOS the whole `apt-get` block is skipped — install `vim`/`git`/toolchain via Homebrew yourself.
> Note: the Docker repo step assumes **Ubuntu**.
### macOS
The same `./install.sh` detects macOS and replaces `apt-get` with Homebrew. It first asks which login shell you want, **bash** or **zsh** (`[bash]` by default; answer in advance with `MACOS_SHELL=zsh ./install.sh`, and with no terminal attached it picks bash):
1. Installs Homebrew with its official script when `brew` is missing (this also pulls the Xcode Command Line Tools: clang, make, git), then `brew update` + `brew upgrade`.
2. Installs the apt list mapped to formulae: `vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta curl gnupg lftp inetutils unzip tree tmux fzf dtach node python pipx php mariadb imagemagick ffmpeg weasyprint poppler qpdf webp libavif bash`. Brew's `php` already ships gd, mbstring, xml, intl, curl and mysql.
3. Docker: `colima` (the Linux VM) + `docker docker-compose docker-buildx`. Writes `~/.docker/config.json` with `cliPluginsExtraDirs` so `docker compose` works, only when that file does not exist yet (otherwise prints the line to add).
4. Starts `colima`, `code-server` and `mariadb` as `brew services` (the `systemctl enable --now` equivalent), skipping any already started.
5. Deploys `bashrc-osx`, then appends one line to `~/.bash_profile` that sources `~/.bashrc`: macOS terminals open login shells, which never read `~/.bashrc` on their own. Done for both choices, so `bash` stays usable.
6. Deploys `tmux.conf` to `~/.config/tmux/tmux.conf` (same step on Linux), clones [tpm](https://github.com/tmux-plugins/tpm) into `~/.config/tmux/plugins/tpm` and fetches the listed plugins headlessly (resurrect + continuum restore your sessions, window-name renames windows after what runs in them). A `~/.tmux.conf` would be read instead, so one found is moved to `~/.tmux.conf.backup-<date>`; a differing `~/.config/tmux/tmux.conf` becomes `tmux.conf.backup-<date>`. `libtmux` (needed by window-name) goes in the python user site; brew's python blocks that by default, so the install retries with `--break-system-packages`. Both plugin steps only warn on failure: `prefix` + `I` inside tmux fetches the plugins. Prefix is `C-a`.
7. **bash** chosen: makes brew's bash 5 the login shell (adds it to `/etc/shells` with `sudo`, then `chsh`, which asks for your password). macOS ships bash 3.2, too old for the bashrc.
**zsh** chosen: installs oh-my-zsh with its official script (unattended, skipped if `~/.oh-my-zsh` exists), renders `zsh/zshrc-osx` into `~/.zshrc` (same identity as the bashrc) and deploys the `bchanot` theme to `~/.oh-my-zsh/custom/themes/`, then makes `/bin/zsh` the login shell. An existing `~/.zshrc` that differs from the repo's is saved as `~/.zshrc.backup-<date>` (outside `~/Oldconfig`, which every run wipes). Move your machine-specific lines (nvm, bun, tokens) into `~/.zshrc.local`: the deployed zshrc loads it.
8. Ends with the list of what the Linux install has and this one does not: `gcc` (Apple clang answers to `gcc`), `valgrind`, `dkms`, `net-tools`, `openssh-server` and the RDP desktop (both built into macOS, switched on in System Settings > Sharing), `cifs-utils`, `php-imagick`, the NVIDIA driver, the disk-usage warning, `cloudpex`, the security baseline and the two end-of-install offers.
### CLI scripts (`bin/`)
Deployed to `~/.local/bin` (the deployed bashrc adds this dir to `PATH`):
- **`dt`** — manage claude-in-dtach sessions (`dt ls|at|kill`). Needs `dtach` + `fzf`.
- **`dtach-router`** — session dashboard on SSH login. The installer wires it into `~/.profile`, where it is **sourced** (not executed) at interactive login and is a silent no-op when no session exists. Create a session with `cc [name]`, re-open the menu anytime with `d` (both aliases from the bashrc). Needs `dt`, `dtach`, `fzf`.
- **`dtach-router`** — session dashboard shown at shell startup. It ships in the deployed bashrc and is **sourced** (not executed) in every interactive shell, so it also fires in VS Code Remote-SSH terminals (non-login shells that skip `~/.profile`). Silent no-op when no session exists. Create a session with `cc [name]`, re-open the menu anytime with `d` (both aliases from the bashrc). Needs `dt`, `dtach`, `fzf`.
- **`claude-provider`** — switch Claude Code between Anthropic and OpenRouter.
OpenRouter mode reads the key from **`$OPENROUTER_API_KEY`** (never hardcoded). Export it from a private, untracked file, e.g. `~/.bashrc.local`:
```sh
@@ -89,11 +125,11 @@ Deployed to `~/.local/bin` (the deployed bashrc adds this dir to `PATH`):
## Requirements
- `bash`, `git`
- Debian/Ubuntu `apt-get` for the package step (optional elsewhere)
- A `bash` login shell (zsh users: switch to bash for these prompts to apply)
- Debian/Ubuntu `apt-get`, or macOS (Homebrew is installed if missing)
- A `bash` login shell on Linux (zsh users switch to bash for these prompts to apply). On macOS the installer sets bash or zsh, your choice
## Lint
## License
```sh
shellcheck install.sh bash/bashrc-*
```
GPL-3.0-or-later — see [LICENSE](LICENSE).
Copyright (C) 2026 Bastien Chanot.
+20 -5
View File
@@ -7,6 +7,9 @@ alias grep='grep --color=auto'
# colored GCC warnings and errors
export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
# XDG cache dir: tmux.conf saves resurrect/continuum sessions under it
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
# Ensure ~/.local/bin is on PATH (pipx CLIs + personal scripts from bin/)
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
@@ -25,9 +28,10 @@ fi
#export LANG=en_US.UTF-8
# Used for vim header
export VIUSER=bchanot
export VIMAIL=bchanot@gmail.fr
# Identity for git commits and vim headers. Filled by install.sh (asked once,
# reused from an existing ~/.bashrc / ~/.zshrc on re-runs), never stored here.
export USER="@USER@"
export EMAIL="@EMAIL@"
## Activate and custom bash completion
#bind 'TAB:menu-complete'
@@ -114,8 +118,19 @@ PROMPT_COMMAND='set_prompt'
# claude-dans-dtach : creer une session (claude tournant dans dtach, detache via Ctrl-\).
# Usage : cd ~/projets/seo && cc seo -> session nommee "seo".
dtach_claude() { dtach -c "$HOME/.dtach/${1:-claude-$(date +%H%M%S)}" -e '^\' claude; }
dtach_claude() {
local name="${1:-claude-$(date +%H%M%S)}"
systemd-run --user --quiet --collect --scope --slice=dev.slice \
--unit="cc-${name}" \
dtach -c "$HOME/.dtach/${name}" -e '^\' \
bash -c 'set -a; [ -f "$HOME/.claude/.env" ] && . "$HOME/.claude/.env"; set +a; exec claude'
}
alias cc='dtach_claude'
# Rappeler a la demande le menu de reprise (sinon il s'affiche seul au login SSH).
# Rappeler a la demande le menu de reprise.
alias d='source ~/.local/bin/dtach-router'
# Au demarrage d'un shell interactif, proposer de reprendre une session dtach existante
# (silencieux si aucune). Place ici plutot que dans ~/.profile car les terminaux VS Code
# Remote-SSH sont non-login et ne lisent pas ~/.profile ; ~/.bashrc, lui, est toujours lu.
case $- in *i*) [ -x "$HOME/.local/bin/dtach-router" ] && . "$HOME/.local/bin/dtach-router" ;; esac
+91 -16
View File
@@ -1,32 +1,47 @@
# Homebrew on PATH (Apple Silicon: /opt/homebrew, Intel: /usr/local).
# Must come first: brew's bash, vim, git, fzf and dtach shadow the system ones.
for brew_bin in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$brew_bin" ]; then
eval "$("$brew_bin" shellenv)"
break
fi
done
unset brew_bin
## Enable color support of ls and also add handy aliases
# Some colors
# Some colors (BSD ls: -G instead of GNU --color)
alias ls='ls -G'
alias grep='grep --color=auto'
# Some utils aliases
alias ll='ls -l'
alias la='ls -A'
alias rmrf='rm -rf'
alias gcl='git clone'
alias vim='vim -p'
# Aliases for executing ~/.script scripts
alias gitan='sh ~/.script/gitan.sh'
alias clean='sh ~/.script/clean.sh'
## Some export
# colored GCC warnings and errors
export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
# XDG cache dir: tmux.conf saves resurrect/continuum sessions under it
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
# Ensure ~/.local/bin is on PATH (pipx CLIs + personal scripts from bin/)
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
# Used for vim header
export VIUSER=xuser
export VIMAIL=xuser@student.42.fr
# Set history size to unlimited
if [[ $EUID == 0 ]] ; then
export HISTSIZE=0
export HISTFILESIZE=0
else
export HISTSIZE=-1
export HISTFILESIZE=-1
fi
#export LANG=en_US.UTF-8
# Identity for git commits and vim headers. Filled by install.sh (asked once,
# reused from an existing ~/.bashrc / ~/.zshrc on re-runs), never stored here.
export USER="@USER@"
export EMAIL="@EMAIL@"
## Activate and custom bash completion
#bind 'TAB:menu-complete'
@@ -44,7 +59,38 @@ function parse_git_branch() {
echo ""
fi
}
# BSD date has no %N: use bash 5's microsecond clock, scaled to nanoseconds.
# Falls back to whole seconds under the system bash 3.2.
function timer_now {
if [ -n "${EPOCHREALTIME:-}" ]; then
echo "${EPOCHREALTIME/[.,]/}000"
else
echo "$(date +%s)000000000"
fi
}
function timer_start {
timer_start=${timer_start:-$(timer_now)}
}
function timer_stop {
local delta_us=$((($(timer_now) - $timer_start) / 1000))
local us=$((delta_us % 1000))
local ms=$(((delta_us / 1000) % 1000))
local s=$(((delta_us / 1000000) % 60))
local m=$(((delta_us / 60000000) % 60))
local h=$((delta_us / 3600000000))
# Goal: always show around 3 digits of accuracy
if ((h > 0)); then timer_show=${h}h${m}m
elif ((m > 0)); then timer_show=${m}m${s}s
elif ((s >= 10)); then timer_show=${s}.$((ms / 100))s
elif ((s > 0)); then timer_show=${s}.$(printf %03d $ms)s
elif ((ms >= 100)); then timer_show=${ms}ms
elif ((ms > 0)); then timer_show=${ms}.$((us / 100))ms
else timer_show=${us}us
fi
unset timer_start
}
# get current status of git repo
function parse_git_dirty {
status=`git status 2>&1 | tee`
@@ -72,6 +118,35 @@ function parse_git_dirty {
fi
}
export PS1='`if [ $? = 0 ]; then echo "\[\033[01;36m\]✔"; else echo "\[\033[01;31m\]✘"; fi` \[\033[32m\]\w\[\033[34m\]$(parse_git_branch " %s") \[\033[0m\]>\[\033[00m\] '
function set_prompt {
timer_stop
if [[ $EUID == 0 ]] ; then
export PS1='`if [ $? = 0 ]; then echo "\[\033[01;36m\]✔"; else echo "\[\033[01;31m\]✘"; fi` ($timer_show) \[\033[01;31m\]\u [\[\033[00;0m\] \w \[\033[01;31m\]]\[\033[01;34m\]$(parse_git_branch " %s") \[\033[00;00m\]> '
else
export PS1='`if [ $? = 0 ]; then echo "\[\033[01;36m\]✔"; else echo "\[\033[01;31m\]✘"; fi` ($timer_show) \[\033[01;32m\]\u [\[\033[00;0m\] \w \[\033[01;32m\]]\[\033[01;34m\]$(parse_git_branch " %s") \[\033[00;00m\]> '
fi
}
trap 'timer_start' DEBUG
PROMPT_COMMAND='set_prompt'
## Lancement des commandes au demarrages
# claude-dans-dtach : creer une session (claude tournant dans dtach, detache via Ctrl-\).
# Usage : cd ~/projets/seo && cc seo -> session nommee "seo".
# Pas de systemd sur macOS : dtach est lance directement (pas de slice cgroup).
dtach_claude() {
local name="${1:-claude-$(date +%H%M%S)}"
mkdir -p "$HOME/.dtach"
dtach -c "$HOME/.dtach/${name}" -e '^\' \
bash -c 'set -a; [ -f "$HOME/.claude/.env" ] && . "$HOME/.claude/.env"; set +a; exec claude'
}
alias cc='dtach_claude'
# Rappeler a la demande le menu de reprise.
alias d='source ~/.local/bin/dtach-router'
# Au demarrage d'un shell interactif, proposer de reprendre une session dtach existante
# (silencieux si aucune). Place ici plutot que dans ~/.profile car les terminaux VS Code
# Remote-SSH sont non-login et ne lisent pas ~/.profile ; ~/.bashrc, lui, est toujours lu.
case $- in *i*) [ -x "$HOME/.local/bin/dtach-router" ] && . "$HOME/.local/bin/dtach-router" ;; esac
+18 -6
View File
@@ -28,12 +28,22 @@ _pids_for_socket() {
pgrep -f -- "dtach -[cnAN] $sock" 2>/dev/null | sort -u
}
_cwd_of() { readlink -f "/proc/$1/cwd" 2>/dev/null; }
# Dossier courant du process : /proc sur Linux, lsof sur macOS (pas de /proc).
_cwd_of() {
if [ -d /proc ]; then
readlink -f "/proc/$1/cwd" 2>/dev/null
else
lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p'
fi
}
# Heure de demarrage REELLE du process (fiable, immuable), en epoch.
# date -d = GNU (Linux), date -j -f = BSD (macOS). Locale C : format lstart stable.
_starttime_of() {
local ls; ls=$(ps -o lstart= -p "$1" 2>/dev/null) || return
[ -n "$ls" ] && date -d "$ls" +%s 2>/dev/null
local ls; ls=$(LC_ALL=C ps -o lstart= -p "$1" 2>/dev/null) || return
[ -n "$ls" ] || return
LC_ALL=C date -d "$ls" +%s 2>/dev/null \
|| LC_ALL=C date -j -f '%a %b %e %T %Y' "$ls" +%s 2>/dev/null
}
_age() {
@@ -52,6 +62,8 @@ _age() {
emit_raw() {
shopt -s nullglob
local sock name pids master cwd start
# Tilde via variable : "\~" en remplacement reste "\~" sous bash 3.2 (macOS).
local tilde='~'
for sock in "$DTDIR"/*; do
[ -S "$sock" ] || continue
name=$(basename "$sock")
@@ -61,7 +73,7 @@ emit_raw() {
cwd=$(_cwd_of "$master"); cwd="${cwd:-?}"
start=$(_starttime_of "$master"); start="${start:-$now}"
# colonnes : NOM(cache pour fzf) TAB DOSSIER TAB AGE
printf '%s\t%s\t%s\n' "$name" "${cwd/#$HOME/\~}" "$(_age "$start")"
printf '%s\t%s\t%s\n' "$name" "${cwd/#$HOME/$tilde}" "$(_age "$start")"
done
}
@@ -99,6 +111,6 @@ case "${1:-}" in
kill) shift; cmd_kill "$@" ;;
sock) shift; cmd_sock "$@" ;;
--raw) emit_raw ;;
""|-h|--help) sed -n '2,18p' "$0" | sed 's/^# \?//' ;;
*) echo "Commande inconnue: $1"; sed -n '2,18p' "$0" | sed 's/^# \?//'; exit 1 ;;
""|-h|--help) sed -n '2,18p' "$0" | sed -E 's/^# ?//' ;;
*) echo "Commande inconnue: $1"; sed -n '2,18p' "$0" | sed -E 's/^# ?//'; exit 1 ;;
esac
+84
View File
@@ -0,0 +1,84 @@
# cloudpex : montage à la demande d'un partage SMB (NAS)
`cloudpex` monte et démonte un partage SMB du NAS sur un point de montage local.
Le mot de passe SMB est demandé à chaque montage. Rien n'est écrit sur disque,
rien ne passe en argument (le mot de passe est transmis à `mount.cifs` par la
variable d'environnement `PASSWD`, invisible dans `ps`).
Les valeurs propres au site (hôte du NAS, nom du partage, utilisateur SMB, point
de montage, version SMB) ne sont pas dans le script. Elles sont demandées à
l'installation et écrites dans `/etc/cloudpex.conf`, lisible par root seulement.
## Pourquoi à la demande, et pas dans fstab
Sur l'ancien serveur le partage était monté en permanence, en écriture, avec
`uid=1000` forcé. Tout processus de l'utilisateur pouvait donc tout effacer, agents
Claude compris. C'est ce qui a rendu l'incident du 21/09 total (voir
`RECOVERY/01-prochain-systeme/04-NAS-cloudpex-sauvegardes.md` sur le partage).
Ce script applique la règle 2 de ce document :
- montage à la demande, par un humain, jamais automatique au boot ;
- aucun identifiant stocké (`/root/.smbcredentials` n'existe plus) ;
- `noexec,nosuid,nodev` : rien ne s'exécute depuis le partage ;
- `dir_mode=0750`, propriétaire = l'utilisateur qui a lancé `sudo` : les autres
comptes (dont les comptes agents) ne voient pas le contenu.
Démonte quand tu as fini (`cloudpex -u`). Un partage monté reste effaçable par
tes propres processus.
## Usage
```sh
cloudpex # monte (demande le mot de passe SMB)
cloudpex -s # état
cloudpex -u # démonte (alias : -d, dc, disconnect, disable)
cloudpex -h # aide
```
Le script se relance lui-même via `sudo` : pas besoin de le préfixer.
## Installation
```sh
./cloudpex/install.sh
```
Ce que ça fait, à l'identique de cette machine :
| Cible | Détail |
| --- | --- |
| `/usr/local/bin/cloudpex` | copie du script, `root:root`, `0755` |
| `/etc/cloudpex.conf` | les cinq valeurs du site, demandées au clavier, `root:root`, `0600` |
| point de montage | créé vide (`/mnt/cloudpex` par défaut) |
| `cifs-utils` | installé via `apt-get` seulement si `mount.cifs` manque |
Questions posées (défaut entre crochets) :
```
Hôte du NAS (IP ou nom) :
Nom du partage SMB :
Utilisateur SMB :
Point de montage [/mnt/cloudpex] :
Version SMB [3.0] :
```
Réexécutable : si `/etc/cloudpex.conf` existe, il est affiché et gardé sauf
réponse `n`. Sans terminal (`curl | bash`), le script est réinstallé mais la
config n'est ni créée ni modifiée. `../install.sh` appelle cet installeur sur
Linux. Rien n'est monté à l'installation.
## Changer de NAS, de partage ou de compte
Relance `./cloudpex/install.sh` et réponds `n` à « La garder ? », ou édite
`/etc/cloudpex.conf` en root (format `CLÉ=valeur`, une par ligne : `HOST`,
`SHARE`, `SMB_USER`, `MNT`, `SMB_VERS`). Le script lit ce fichier ligne à ligne,
il ne l'exécute jamais.
## Dépannage
- `config absente` : lance `./cloudpex/install.sh` depuis un terminal.
- Échec du montage : `dmesg | tail` (mot de passe, réseau, ou version SMB
refusée par le NAS : essayer `SMB_VERS=3.1.1` dans la config).
- Démontage refusé (fichiers ouverts) : `lsof +D <point de montage>`, fermer,
réessayer.
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# cloudpex — monte / démonte le partage SMB du NAS déclaré dans /etc/cloudpex.conf
# Usage : cloudpex -> monte (demande le mot de passe)
# cloudpex -u -> démonte
# cloudpex -s -> état
# Aucun credential n'est écrit sur disque ni passé en argument (invisible dans ps).
set -euo pipefail
CONF="/etc/cloudpex.conf"
# Re-lance le script en root si nécessaire (avant toute saisie du mot de passe,
# et avant de lire la config, lisible par root seulement)
if [[ $EUID -ne 0 ]]; then
exec sudo -- "$0" "$@"
fi
# Propriétaire des fichiers montés : l'utilisateur qui a lancé sudo, sinon 1000
OWNER_UID="${SUDO_UID:-1000}"
OWNER_GID="${SUDO_GID:-1000}"
die() { echo "Erreur : $*" >&2; exit 1; }
# Valeurs propres au site (hôte, partage, utilisateur, point de montage, version),
# écrites par cloudpex/install.sh au format CLÉ=valeur. Lues ligne à ligne,
# jamais sourcées : le fichier de config n'exécute rien.
conf_get() { sed -n "s/^$1=//p" "$CONF" | head -n 1; }
[[ -r $CONF ]] || die "config absente : $CONF (lance cloudpex/install.sh)"
HOST="$(conf_get HOST)"
SHARE_NAME="$(conf_get SHARE)"
SMB_USER="$(conf_get SMB_USER)"
MNT="$(conf_get MNT)"
SMB_VERS="$(conf_get SMB_VERS)"
[[ -n $HOST && -n $SHARE_NAME && -n $SMB_USER && -n $MNT && -n $SMB_VERS ]] \
|| die "config incomplète : $CONF (relance cloudpex/install.sh)"
[[ $MNT == /* ]] || die "MNT doit être un chemin absolu ($CONF)"
SHARE="//${HOST}/${SHARE_NAME}"
is_mounted() { mountpoint -q "$MNT"; }
do_status() {
if is_mounted; then
echo "Monté : $SHARE -> $MNT"
df -h "$MNT" | tail -n 1
else
echo "Non monté."
fi
}
do_umount() {
is_mounted || { echo "Déjà démonté."; return 0; }
umount "$MNT" || die "démontage impossible (fichiers ouverts ? voir : lsof +D $MNT)"
echo "Démonté : $MNT"
}
do_mount() {
command -v mount.cifs >/dev/null || die "cifs-utils absent (sudo apt install cifs-utils)"
is_mounted && { echo "Déjà monté : $MNT"; return 0; }
mkdir -p "$MNT"
local pass
read -rsp "Mot de passe SMB pour ${SMB_USER}@${SHARE} : " pass
echo
[[ -n "$pass" ]] || die "mot de passe vide"
# mount.cifs lit PASSWD dans l'environnement : pas d'exposition dans ps ni sur disque
if PASSWD="$pass" mount -t cifs "$SHARE" "$MNT" \
-o "username=${SMB_USER},uid=${OWNER_UID},gid=${OWNER_GID},iocharset=utf8,vers=${SMB_VERS},file_mode=0640,dir_mode=0750,nosuid,nodev,noexec"; then
unset pass
echo "Monté : $SHARE -> $MNT"
else
unset pass
die "échec du montage (mot de passe, réseau ou version SMB ; voir : dmesg | tail)"
fi
}
case "${1:-}" in
"") do_mount ;;
-u|-d|--umount|dc|disconnect|disable) do_umount ;;
-s|--status) do_status ;;
-h|--help) sed -n '2,6p' "$0" ;;
*) die "option inconnue : $1 (voir -h)" ;;
esac
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# cloudpex/install.sh — installe la commande `cloudpex` (montage à la demande d'un
# partage SMB, voir README.md) telle qu'elle est déployée ici :
# /usr/local/bin/cloudpex le script, root:root 0755
# /etc/cloudpex.conf hôte, partage, utilisateur SMB, point de montage,
# version SMB : demandés ici, root:root 0600
# cifs-utils installé si mount.cifs manque (apt-get)
# Réexécutable : réinstalle le script en place et propose de garder la config
# existante. Sans terminal (curl | bash), la config n'est ni créée ni modifiée.
# Ne monte rien, ne stocke aucun mot de passe.
# Usage : ./cloudpex/install.sh (appelé aussi par ../install.sh sur Linux)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TARGET=/usr/local/bin/cloudpex
CONF=/etc/cloudpex.conf
die() { echo "Erreur : $*" >&2; exit 1; }
# Saisie validée : ask VAR "libellé" "défaut" "regex autorisée". Redemande tant
# que la valeur ne correspond pas ; la valeur vide prend le défaut.
ask() {
local value
while :; do
read -rp "$2${3:+ [$3]} : " value || die "saisie interrompue"
value="${value:-$3}"
[[ $value =~ ^$4$ ]] && break
echo " valeur invalide, format attendu : $4" >&2
done
printf -v "$1" '%s' "$value"
}
# Demande les cinq valeurs propres au site et les écrit dans $CONF (root, 0600).
# Les formats refusent ce qui casserait la ligne d'options de mount.cifs
# (virgule, espace, guillemet) ; seul le nom de partage admet des espaces.
write_conf() {
local host share user mnt vers tmp
ask host "Hôte du NAS (IP ou nom)" "" '[A-Za-z0-9.-]+'
ask share "Nom du partage SMB" "" '[A-Za-z0-9._ -]+'
ask user "Utilisateur SMB" "" '[A-Za-z0-9._-]+'
ask mnt "Point de montage" "/mnt/cloudpex" '/[A-Za-z0-9._/-]+'
ask vers "Version SMB" "3.0" '[0-9]+(\.[0-9]+)*'
tmp="$(mktemp)"
printf 'HOST=%s\nSHARE=%s\nSMB_USER=%s\nMNT=%s\nSMB_VERS=%s\n' \
"$host" "$share" "$user" "$mnt" "$vers" > "$tmp"
sudo install -m 0600 -o root -g root "$tmp" "$CONF"
rm -f "$tmp"
}
# Config : créée au clavier, ou gardée si elle existe déjà (répondre n pour la
# refaire). Sans terminal, rien n'est demandé.
configure() {
local keep=""
if [ ! -t 0 ]; then
[ -f "$CONF" ] || echo "Pas de terminal : $CONF non créé, relance ./cloudpex/install.sh depuis un terminal" >&2
return 0
fi
if [ -f "$CONF" ]; then
echo "Configuration existante ($CONF) :"
sudo sed 's/^/ /' "$CONF"
read -rp "La garder ? [Y/n] " keep || true
case "$keep" in
[nN]*) write_conf ;;
esac
else
write_conf
fi
}
# Le point de montage déclaré dans la config, créé vide s'il manque.
ensure_mountpoint() {
local mnt
[ -f "$CONF" ] || return 0
mnt="$(sudo sed -n 's/^MNT=//p' "$CONF" | head -n 1)"
[ -n "$mnt" ] && sudo install -d -m 0755 "$mnt"
}
if ! command -v mount.cifs >/dev/null 2>&1; then
command -v apt-get >/dev/null 2>&1 || die "mount.cifs absent et apt-get introuvable : installe cifs-utils à la main"
sudo apt-get install -y cifs-utils
fi
sudo install -m 0755 -o root -g root "$SCRIPT_DIR/cloudpex" "$TARGET"
configure
ensure_mountpoint
echo "cloudpex installé : $TARGET (monter : cloudpex · état : cloudpex -s · démonter : cloudpex -u)"
+4
View File
@@ -0,0 +1,4 @@
// Automatic security updates, deployed by install.sh. Same content as
// `dpkg-reconfigure -plow unattended-upgrades` writes, without the prompt.
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
+8
View File
@@ -0,0 +1,8 @@
# earlyoom settings, sourced by earlyoom.service (rules of the previous server).
# -r 60 memory report in the journal every minute
# -m 10 act when available RAM drops under 10% ...
# -s 10 ... and free swap under 10% (both conditions)
# --avoid never kill sshd, systemd, logind, dbus, containerd
# --prefer kill the agent runtimes first: java, node, pnpm, esbuild
# Quotes inside the value are honoured by systemd's $VAR word splitting.
EARLYOOM_ARGS="-r 60 -m 10 -s 10 --avoid '^(sshd|systemd|systemd-logind|dbus-daemon|containerd)$' --prefer '^(java|node|pnpm|esbuild)$'"
+20
View File
@@ -0,0 +1,20 @@
# fail2ban local settings, deployed by install.sh. Debian's defaults-debian.conf
# enables the sshd jail; this file decides how it bans.
[DEFAULT]
# Never ban loopback or the private LAN ranges: five typos from the LAN must not
# lock the admin out for an hour. Trade-off: a compromised LAN host is never banned.
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
bantime = 1h
findtime = 10m
maxretry = 5
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
enabled = true
# Read the journal directly: works with or without /var/log/auth.log (rsyslog).
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd
# Ban the offender on every port, so the port sshd listens on is irrelevant. The
# previous server's jail banned port 22 only while sshd listened on 337.
banaction = %(banaction_allports)s
+5
View File
@@ -0,0 +1,5 @@
# sshd hardening, deployed by install.sh. Only settings that cannot lock anyone
# out: authentication methods, ports and user lists are left to the host.
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 20
+8
View File
@@ -0,0 +1,8 @@
# ssh.service drop-in: keep sshd alive when RAM runs out (rules of the previous server).
# OOMScoreAdjust=-1000 the kernel OOM killer never selects sshd.
# MemoryMin=256M reclaim protection for the daemon's own cgroup. Login sessions
# live in user.slice (logind), so this cannot reserve RAM for an
# interactive shell — earlyoom is what frees memory in time.
[Service]
MemoryMin=256M
OOMScoreAdjust=-1000
+5
View File
@@ -0,0 +1,5 @@
# /tmp on disk (install.sh masks tmp.mount): keep the tmpfs semantics — wipe /tmp
# at boot (D) and purge entries untouched for 10 days. Same file name as
# /usr/lib/tmpfiles.d/tmp.conf, so this REPLACES it: the /var/tmp rule must stay.
D /tmp 1777 root root 10d
q /var/tmp 1777 root root 30d
+31
View File
@@ -0,0 +1,31 @@
# Template for the user-scope ~/.gitconfig, rendered by install.sh.
# Git never expands $VARS: @USER@ and @EMAIL@ are replaced at install
# time with the USER and EMAIL exported by the deployed bashrc.
# A repo .git/config still overrides these values for that repo.
[user]
name = @USER@
email = @EMAIL@
[push]
default = current
[color]
ui = auto
[pull]
rebase = true
[core]
editor = vim
pager = delta
excludesfile = ~/.gitignore
[advice]
detachedHead = false
[merge]
tool = vimdiff
conflictStyle = zdiff3
[pager]
branch = false
[url "git@salsa.debian.org:installer-team/"]
pushInsteadOf = https://salsa.debian.org/installer-team/
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true # use n and N to move between diff sections
dark = true # or light = true, or omit for auto-detection
+532 -28
View File
@@ -6,6 +6,9 @@ set -euo pipefail
# Resolve the repo root so the script works from any working directory.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Helpers are defined below; the identity itself is resolved right before the
# OS-specific steps, so every prompt comes before the long package installs.
# Set up Docker's official Ubuntu apt repo, then install the engine + compose plugin.
# Idempotent: skips entirely if docker is already on PATH. Ubuntu-only (uses the ubuntu repo).
install_docker() {
@@ -27,6 +30,25 @@ install_docker() {
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
}
# NVIDIA driver: only when an NVIDIA GPU is on the PCI bus (vendor id 10de), so the
# script stays hardware-agnostic elsewhere. ubuntu-drivers picks the driver the distro
# recommends for the card (595-open on the RTX 3060 Ti this was written on) instead of
# pinning a version that ages. Idempotent: a no-op when the recommended driver is in.
# The driver loads at the next reboot. Ubuntu-only (ubuntu-drivers-common).
install_nvidia_driver() {
if ! command -v ubuntu-drivers >/dev/null 2>&1; then
echo "ubuntu-drivers not found — skipping NVIDIA driver" >&2
return 0
fi
if [ -z "$(lspci -d 10de: 2>/dev/null)" ]; then
echo "No NVIDIA GPU detected — skipping NVIDIA driver"
return 0
fi
echo "NVIDIA GPU detected — installing the recommended driver"
sudo ubuntu-drivers install
echo "NVIDIA driver loads at the next reboot."
}
# RDP "gate" credentials: a shared username/password that unlocks the GDM
# login screen (each user then logs into GDM with his own account). Required —
# without it the RDP server rejects every connection (mstsc error 0x904). It is
@@ -113,16 +135,16 @@ install_disk_warning() {
/etc/profile.d/disk-usage-warning.sh
}
# Wire the dtach session-resume menu into ~/.profile. At interactive login we SOURCE
# dtach-router rather than execute it: the script hands control back with `return` and
# attaches to the host TTY, so running it as a command makes its interactivity guard
# fail and errors on /dev/tty whenever the login shell is non-interactive (bash -lc,
# cron, scp). Sourced, the guard works and it is a silent no-op when no session exists.
# Idempotent: strips any prior block first — the marker-delimited managed one AND the
# legacy execute-based one (DT=$(dt ls) ... fi) from earlier installs. User scope, no sudo.
wire_dtach_profile() {
# The dtach session-resume menu now ships in ~/.bashrc (deployed above): every interactive
# shell sources it, including VS Code Remote-SSH terminals, which are non-login and therefore
# never read ~/.profile. This strips any dtach block a previous install left in ~/.profile —
# the marker-delimited managed one AND the legacy execute-based one (DT=$(dt ls) ... fi) — so
# the menu does not also fire from there (a plain SSH login sources ~/.bashrc via ~/.profile,
# which would otherwise prompt twice). No-op when absent. User scope, no sudo.
unwire_dtach_profile() {
local profile="$HOME/.profile"
touch "$profile"
[ -f "$profile" ] || return 0
grep -qF 'dtach-router' "$profile" || return 0
awk '
$0 == "# >>> claude-dtach >>>" { drop = 1; next }
@@ -131,29 +153,455 @@ wire_dtach_profile() {
$0 == "DT=$(dt ls)", $0 == "fi" { next }
{ print }
' "$profile" > "$profile.tmp" && mv "$profile.tmp" "$profile"
}
cat >> "$profile" << 'EOF'
# NAS helper, on request: deploys the on-demand CloudPex SMB mount command to
# /usr/local/bin (see cloudpex/README.md). Nothing is mounted and no credential is
# stored. Linux-only (cifs-utils); the helper's own installer is idempotent.
offer_cloudpex() {
confirm "Install the cloudpex NAS mount helper (on-demand SMB mount)?" || return 0
bash "$SCRIPT_DIR/cloudpex/install.sh"
}
# >>> claude-dtach >>>
# At interactive login, offer to resume a claude-in-dtach session (no-op if none).
case $- in *i*) [ -x "$HOME/.local/bin/dtach-router" ] && . "$HOME/.local/bin/dtach-router" ;; esac
# <<< claude-dtach <<<
# fail2ban: bans an IP on every port after repeated SSH failures. The sshd jail
# reads the journal (works with or without /var/log/auth.log) and bans all ports,
# so the port sshd listens on does not matter — the previous server's jail only
# banned port 22 while sshd listened on 337. Private LAN ranges are never banned.
# nftables is the ban backend. Idempotent: config overwritten, service restarted.
install_fail2ban() {
echo "Installing fail2ban (sshd jail, all-ports ban)"
sudo apt-get install -y fail2ban nftables
sudo install -D -m 0644 "$SCRIPT_DIR/etc/fail2ban/jail.d/local.conf" \
/etc/fail2ban/jail.d/local.conf
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
}
# Automatic security updates: the file dpkg-reconfigure would write, deployed
# directly so the install stays non-interactive. Idempotent.
install_unattended_upgrades() {
echo "Enabling unattended security upgrades"
sudo apt-get install -y unattended-upgrades
sudo install -D -m 0644 "$SCRIPT_DIR/etc/apt/apt.conf.d/20auto-upgrades" \
/etc/apt/apt.conf.d/20auto-upgrades
}
# sshd hardening drop-in (PermitRootLogin, MaxAuthTries, LoginGraceTime): only
# settings that cannot lock anyone out; authentication methods stay untouched.
# Validated with sshd -t before the reload. A rejected file is removed rather than
# left in place, so sshd keeps starting on the next boot; the install goes on and
# the warning tells you.
harden_sshd() {
echo "Deploying sshd hardening drop-in"
sudo install -D -m 0644 "$SCRIPT_DIR/etc/ssh/sshd_config.d/20-hardening.conf" \
/etc/ssh/sshd_config.d/20-hardening.conf
if ! sudo sshd -t; then
sudo rm -f /etc/ssh/sshd_config.d/20-hardening.conf
echo "sshd rejected 20-hardening.conf — removed, sshd config unchanged" >&2
return 0
fi
sudo systemctl reload ssh
}
# Yes/no prompt for the optional system changes offered at the end of the install.
# Declines (returns 1) when no terminal is attached (curl | bash), so an offer is
# skipped with a hint instead of blocking; re-run ./install.sh from a terminal to
# get it offered again.
confirm() {
local answer=""
if [ ! -t 0 ]; then
echo "Skipped (no terminal attached): $1" >&2
return 1
fi
read -rp "$1 [y/N] " answer || true
case "$answer" in
[yY]|[yY][eE][sS]) return 0 ;;
*) return 1 ;;
esac
}
# /tmp on disk instead of the tmpfs Ubuntu mounts by default (RAM-backed, capped at
# 50% of RAM). Agent runs fill it: that eats half the RAM and, once the cap is hit,
# every temp-file creation fails with ENOSPC — which is what breaks shells. Masking
# tmp.mount leaves /tmp on the root filesystem; the tmpfiles rule keeps the tmpfs
# semantics (wiped at boot, entries older than 10 days purged). Takes effect at the
# next reboot: a busy /tmp is never unmounted live. Idempotent.
offer_tmp_on_disk() {
if [ "$(systemctl is-enabled tmp.mount 2>/dev/null)" = "masked" ]; then
echo "/tmp already on disk (tmp.mount masked) — skipping"
return 0
fi
if [ "$(findmnt -n -o FSTYPE -T /tmp)" != "tmpfs" ]; then
echo "/tmp is not a tmpfs — nothing to do"
return 0
fi
confirm "Move /tmp from RAM (tmpfs) to disk? Agents fill it and break shells" || return 0
sudo systemctl mask tmp.mount
sudo install -D -m 0644 "$SCRIPT_DIR/etc/tmpfiles.d/tmp.conf" /etc/tmpfiles.d/tmp.conf
echo "/tmp moves to disk at the next reboot."
}
# Keep SSH reachable when RAM runs out — the two rules the previous server ran:
# - ssh.service drop-in: OOMScoreAdjust=-1000 (the kernel OOM killer never picks
# sshd) + MemoryMin=256M (reclaim protection for the daemon's cgroup);
# - earlyoom: kills the single largest process (node preferred, sshd/systemd spared)
# once free RAM and free swap both drop under 10%, before the box thrashes.
# MemoryMin covers sshd only: logind puts login sessions in user.slice, so nothing can
# reserve RAM for a future shell — earlyoom acting in time is the real protection.
# Idempotent: each piece is skipped when already in place. Restarting ssh keeps the
# current sessions alive (KillMode=process).
offer_ssh_memory_guard() {
local dropin="/etc/systemd/system/ssh.service.d/override.conf"
local ssh_done=0 oom_done=0
cmp -s "$SCRIPT_DIR/etc/systemd/ssh.service.d/override.conf" "$dropin" && ssh_done=1
if cmp -s "$SCRIPT_DIR/etc/default/earlyoom" /etc/default/earlyoom \
&& [ "$(systemctl is-enabled earlyoom 2>/dev/null)" = "enabled" ]; then
oom_done=1
fi
if [ "$ssh_done" = 1 ] && [ "$oom_done" = 1 ]; then
echo "SSH memory guard already in place — skipping"
return 0
fi
confirm "Protect SSH under memory pressure (sshd OOM-exempt + earlyoom)?" || return 0
if [ "$ssh_done" = 0 ]; then
sudo install -D -m 0644 "$SCRIPT_DIR/etc/systemd/ssh.service.d/override.conf" "$dropin"
sudo systemctl daemon-reload
sudo systemctl restart ssh
fi
if [ "$oom_done" = 0 ]; then
sudo apt-get install -y earlyoom
sudo install -m 0644 "$SCRIPT_DIR/etc/default/earlyoom" /etc/default/earlyoom
sudo systemctl enable earlyoom
sudo systemctl restart earlyoom
fi
}
# Put brew on this script's PATH (Apple Silicon: /opt/homebrew, Intel: /usr/local).
# Returns 1 when Homebrew is not installed.
load_brew_env() {
local brew_bin
for brew_bin in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$brew_bin" ]; then
eval "$("$brew_bin" shellenv)"
return 0
fi
done
return 1
}
# Homebrew is the macOS stand-in for apt-get. Installed with its official script
# (asks for the sudo password, pulls the Xcode Command Line Tools: clang, make, git).
# Idempotent: a no-op when brew is already there.
ensure_homebrew() {
load_brew_env && return 0
echo "Installing Homebrew"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
load_brew_env
}
# The apt-get package list, mapped to Homebrew formulae. Linux-only packages are
# left out and listed by print_macos_gaps. php ships gd, mbstring, xml, intl, curl
# and mysql built in; bash is the bash 5 the bashrc needs (macOS ships 3.2).
install_brew_packages() {
brew update
brew upgrade
brew install \
vim git git-lfs git-filter-repo gitleaks pkgconf shellcheck gh git-delta \
curl gnupg lftp inetutils \
unzip tree tmux fzf dtach \
node python pipx php \
mariadb imagemagick \
ffmpeg weasyprint poppler qpdf webp libavif \
bash
}
# Start a Homebrew service at login, like systemctl enable --now. Skipped when
# already started, so a re-run never restarts a running database or VM.
start_brew_service() {
local status
status="$(brew services list | awk -v name="$1" '$1 == name { print $2 }')"
if [ "$status" = "started" ]; then
echo "$1 service already started — skipping"
return 0
fi
brew services start "$1"
}
# Docker on macOS: the docker CLI talks to a Linux VM run by colima (free, no GUI,
# no Docker Desktop licence). compose and buildx are CLI plugins that brew installs
# outside Docker's search path, hence cliPluginsExtraDirs. An existing
# ~/.docker/config.json is never rewritten: a hint is printed instead.
install_colima_docker() {
local config="$HOME/.docker/config.json"
local plugins
plugins="$(brew --prefix)/lib/docker/cli-plugins"
brew install colima docker docker-compose docker-buildx
if [ ! -f "$config" ]; then
mkdir -p "$HOME/.docker"
printf '{\n "cliPluginsExtraDirs": ["%s"]\n}\n' "$plugins" > "$config"
elif ! grep -qF "$plugins" "$config"; then
echo "Add \"cliPluginsExtraDirs\": [\"$plugins\"] to $config for 'docker compose'" >&2
fi
start_brew_service colima
}
# macOS login shell: bash (brew's bash 5 + bashrc-osx) or zsh (oh-my-zsh +
# zshrc-osx). MACOS_SHELL=bash|zsh answers in advance; with no terminal attached
# and no answer, bash. Prints the choice on stdout (the question goes to stderr).
choose_macos_shell() {
local answer="${MACOS_SHELL:-}"
if [ -z "$answer" ] && [ -t 0 ]; then
read -rp "Login shell on macOS: bash or zsh (oh-my-zsh)? [bash] " answer || true
fi
case "$answer" in
zsh) echo zsh ;;
bash|"") echo bash ;;
*) echo "Unknown shell '$answer' — using bash" >&2; echo bash ;;
esac
}
# Make $1 the login shell. /etc/shells must list it before chsh accepts it.
# chsh asks for the account password; a failure only prints a hint. Idempotent.
set_login_shell() {
local target="$1" current
if ! grep -qxF "$target" /etc/shells; then
echo "$target" | sudo tee -a /etc/shells >/dev/null
fi
# id -un, not $USER: the deployed rc sets USER to the git/vim identity.
current="$(dscl . -read "/Users/$(id -un)" UserShell | awk '{ print $2 }')"
if [ "$current" = "$target" ]; then
echo "Login shell already $target — skipping"
return 0
fi
chsh -s "$target" || echo "chsh failed — run: chsh -s $target" >&2
}
# oh-my-zsh with its official script, unattended: no chsh (set_login_shell does
# it), no zsh launched mid-install, ~/.zshrc left alone (deploy_zsh_config owns
# it). Idempotent: skipped when ~/.oh-my-zsh exists.
install_oh_my_zsh() {
if [ -d "$HOME/.oh-my-zsh" ]; then
echo "oh-my-zsh already installed — skipping"
return 0
fi
echo "Installing oh-my-zsh"
KEEP_ZSHRC=yes sh -c \
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \
"" --unattended
}
# Deploy zshrc-osx to ~/.zshrc and the bchanot prompt theme into oh-my-zsh's
# custom themes. A ~/.zshrc that differs from the repo's is kept as
# ~/.zshrc.backup-<date>, not in ~/Oldconfig: that dir is wiped on every run, so
# a second run would destroy the original (and its nvm/bun lines).
deploy_zsh_config() {
local name="$1" email="$2" rendered backup
local themes="$HOME/.oh-my-zsh/custom/themes"
backup="$HOME/.zshrc.backup-$(date +%Y%m%d-%H%M%S)"
rendered="$(render_identity_template "$SCRIPT_DIR/zsh/zshrc-osx" "$name" "$email")"
if [ -e "$HOME/.zshrc" ] && ! printf '%s\n' "$rendered" | cmp -s - "$HOME/.zshrc"; then
echo "Saving the current ~/.zshrc to $backup"
mv "$HOME/.zshrc" "$backup"
fi
echo "Deploying zsh/zshrc-osx + bchanot theme ($name <$email>)"
printf '%s\n' "$rendered" > "$HOME/.zshrc"
mkdir -p "$themes"
cp "$SCRIPT_DIR/zsh/bchanot.zsh-theme" "$themes/"
}
# The chosen macOS shell ($1): zsh gets oh-my-zsh + its config rendered with the
# identity ($2 name, $3 email); bash needs brew's bash 5 (macOS ships 3.2, too
# old for the bashrc). Either way it becomes the login shell, so new terminals
# load the matching config.
setup_macos_shell() {
if [ "$1" = zsh ]; then
install_oh_my_zsh
deploy_zsh_config "$2" "$3"
set_login_shell /bin/zsh
else
set_login_shell "$(brew --prefix)/bin/bash"
fi
}
# tmux, both OSes: tmux.conf goes to ~/.config/tmux/tmux.conf (read there since
# tmux 3.1: Ubuntu 22.04+, brew) with tpm, which the config runs, in
# ~/.config/tmux/plugins/tpm. tmux reads a ~/.tmux.conf first, so one found is
# moved aside; a differing config is kept as tmux.conf.backup-<date>, outside
# ~/Oldconfig which every run wipes. Idempotent.
deploy_tmux_config() {
local dir="$HOME/.config/tmux" stamp
stamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$dir"
if [ -e "$HOME/.tmux.conf" ]; then
echo "Moving ~/.tmux.conf to ~/.tmux.conf.backup-$stamp (it would shadow $dir/tmux.conf)"
mv "$HOME/.tmux.conf" "$HOME/.tmux.conf.backup-$stamp"
fi
if [ -e "$dir/tmux.conf" ] && ! cmp -s "$dir/tmux.conf" "$SCRIPT_DIR/tmux.conf"; then
echo "Saving the current tmux.conf to $dir/tmux.conf.backup-$stamp"
mv "$dir/tmux.conf" "$dir/tmux.conf.backup-$stamp"
fi
echo "Deploying tmux.conf to $dir"
cp "$SCRIPT_DIR/tmux.conf" "$dir/tmux.conf"
install_tmux_plugins "$dir/plugins/tpm"
}
# tpm plus the plugins tmux.conf lists, fetched now so the first tmux start is
# complete. tpm's script starts a tmux server to read @tpm_plugins, and the config
# expects XDG_CACHE_HOME (exported by the rc files, not loaded by this script).
# Failures only warn: prefix+I fetches the plugins from inside tmux.
install_tmux_plugins() {
local tpm="$1"
if [ ! -d "$tpm" ]; then
echo "Cloning tpm"
git clone --quiet https://github.com/tmux-plugins/tpm "$tpm"
fi
echo "Installing tmux plugins"
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" "$tpm/bin/install_plugins" \
|| echo "tmux plugins not installed — press prefix+I inside tmux" >&2
install_libtmux
}
# tmux-window-name (a listed plugin) is a python script importing libtmux. Brew's
# python and Ubuntu 23.04+ refuse pip installs outside a venv (PEP 668), so the
# user-site install is retried with that guard lifted. Non-fatal: without it,
# windows keep tmux's own automatic-rename.
install_libtmux() {
python3 -c 'import libtmux' 2>/dev/null && return 0
echo "Installing libtmux (tmux-window-name plugin)"
python3 -m pip install --quiet --user libtmux 2>/dev/null \
|| python3 -m pip install --quiet --user --break-system-packages libtmux \
|| echo "libtmux not installed — tmux-window-name stays inactive" >&2
}
# macOS terminals open LOGIN shells, which read ~/.bash_profile and never ~/.bashrc.
# Appends one line sourcing ~/.bashrc. Idempotent: skipped when already present.
wire_bash_profile() {
local profile="$HOME/.bash_profile"
# shellcheck disable=SC2016 # $HOME must expand when the profile runs, not now.
local line='[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"'
grep -qxF "$line" "$profile" 2>/dev/null && return 0
echo "Wiring ~/.bash_profile to source ~/.bashrc"
printf '\n# Load the interactive bash config (deployed by install.sh).\n%s\n' \
"$line" >> "$profile"
}
# Value of `export NAME=value` ($1) in the rc file $2, quotes stripped. The last
# match wins, as when the shell sources it. Empty when absent.
rc_export_value() {
sed -n "s/^export $1=//p" "$2" | tail -n 1 | tr -d "\"'"
}
# Print the template $1 with @USER@ and @EMAIL@ replaced by $2 and $3. Bash
# substitution, so the values need no sed escaping.
render_identity_template() {
local file="$1" name="$2" email="$3" line
while IFS= read -r line || [ -n "$line" ]; do
line="${line//@USER@/$name}"
line="${line//@EMAIL@/$email}"
printf '%s\n' "$line"
done < "$file"
}
# One identity value (USER or EMAIL, $1) for git commits, vim headers and the rc
# exports. Never stored in the repo. An export already in ~/.bashrc or ~/.zshrc
# wins silently (a re-run never asks twice), else IDENTITY_<VAR> from the
# environment, else a prompt ($2 label, $3 default) when a terminal is attached,
# else the default. Prints the value.
resolve_identity() {
local var="$1" label="$2" default="$3" value="" rc envvar="IDENTITY_$1"
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
[ -f "$rc" ] && value="$(rc_export_value "$var" "$rc")"
if [ -n "$value" ] && [ "$value" != "@$var@" ]; then
printf '%s\n' "$value"
return 0
fi
done
value="${!envvar:-}"
if [ -z "$value" ] && [ -t 0 ]; then
read -rp "$label [$default]: " value || true
fi
printf '%s\n' "${value:-$default}"
}
# Install the user-scope ~/.gitconfig (a repo's own .git/config still wins).
# The identity is read from the USER/EMAIL exports of the deployed rc ($1), so
# git and the shell agree. A ~/.gitconfig that differs is kept as
# ~/.gitconfig.backup-<date>, outside ~/Oldconfig which every run wipes.
# Idempotent: an identical ~/.gitconfig is left alone.
deploy_gitconfig() {
local rc="$1" name email rendered backup
name="$(rc_export_value USER "$rc")"
email="$(rc_export_value EMAIL "$rc")"
if [ -z "$name" ] || [ -z "$email" ]; then
echo "USER/EMAIL not exported by $rc — skipping ~/.gitconfig" >&2
return 0
fi
rendered="$(render_identity_template "$SCRIPT_DIR/gitconfig" "$name" "$email")"
if printf '%s\n' "$rendered" | cmp -s - "$HOME/.gitconfig"; then
echo "$HOME/.gitconfig already up to date — skipping"
return 0
fi
if [ -e "$HOME/.gitconfig" ]; then
backup="$HOME/.gitconfig.backup-$(date +%Y%m%d-%H%M%S)"
echo "Saving the current ~/.gitconfig to $backup"
mv "$HOME/.gitconfig" "$backup"
fi
echo "Deploying gitconfig to ~/.gitconfig ($name <$email>)"
printf '%s\n' "$rendered" > "$HOME/.gitconfig"
}
# What the Linux install sets up that this macOS run did not, and why.
print_macos_gaps() {
cat <<'EOF'
Not installed on macOS (compared with the Linux install):
- gcc, make Apple clang + make come with the Xcode Command Line Tools
(`gcc` runs clang). Real GCC: brew install gcc (gcc-15).
- valgrind not supported on macOS arm64. Use `leaks` or -fsanitize=address.
- dkms Linux kernel modules, no macOS equivalent.
- net-tools ifconfig / netstat / route are built into macOS.
- openssh-server built in, off by default: System Settings > General >
Sharing > Remote Login.
- cifs-utils SMB mounts are built in: mount_smbfs, or Finder Cmd-K.
- ca-certificates, apt-transport-https apt plumbing, not needed.
- php-imagick not bundled with brew php: pecl install imagick.
- ubuntu-desktop-minimal + RDP (gnome-remote-desktop) use Screen Sharing:
System Settings > General > Sharing > Screen Sharing.
- NVIDIA driver no NVIDIA GPU support on macOS.
- disk-usage login warning (/etc/profile.d) Linux-only (GNU df).
- cloudpex NAS mount helper Linux-only (cifs-utils).
- fail2ban, unattended-upgrades, sshd hardening drop-in Linux security baseline.
macOS: enable automatic updates in System Settings >
General > Software Update.
- /tmp on disk + SSH memory guard offers systemd-only.
Replaced: Docker engine -> colima VM + docker CLI; code-server and mariadb run
as brew services instead of systemd units.
EOF
}
# System packages: Debian/Ubuntu only. Skipped where apt-get is absent (e.g. macOS).
# Identity for git, vim and the rc exports: reused from an existing rc, else asked.
identity_name="$(resolve_identity USER "Name for git commits and vim headers" "$(id -un)")"
identity_email="$(resolve_identity EMAIL "Email for git commits and vim headers" "")"
echo "Identity: $identity_name <${identity_email:-no email}>"
# System packages: apt-get on Debian/Ubuntu, Homebrew on macOS.
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get upgrade -y
# Build + version control + C dev tooling.
# Build + version control + C dev tooling (gitleaks backs the pre-commit hook,
# git-delta provides `delta`, the pager set in gitconfig).
# Web stack: MariaDB + PHP modules for local WordPress/LAMP work; the php-* metapackages
# follow the distro's PHP version instead of pinning php8.x-*.
sudo apt-get install -y \
vim git git-lfs git-filter-repo gcc make pkg-config dkms valgrind shellcheck \
vim git git-lfs git-filter-repo gitleaks gcc make pkg-config dkms valgrind shellcheck git-delta \
curl gnupg ca-certificates apt-transport-https \
unzip tree tmux fzf dtach net-tools \
openssh-server cifs-utils lftp ftp \
nodejs python3-pip pipx php-cli \
ffmpeg weasyprint poppler-utils qpdf webp libavif-bin
ffmpeg weasyprint poppler-utils qpdf webp libavif-bin gh \
mariadb-server imagemagick \
php-mysql php-gd php-imagick php-mbstring php-xml php-intl php-curl
# Docker (separate repo).
install_docker
@@ -162,15 +610,41 @@ if command -v apt-get >/dev/null 2>&1; then
if ! command -v code-server >/dev/null 2>&1; then
curl -fsSL https://code-server.dev/install.sh | sh
fi
sudo systemctl enable --now "code-server@$USER"
# id -un, not $USER: the deployed bashrc sets USER to the git/vim identity.
sudo systemctl enable --now "code-server@$(id -un)"
# GNOME desktop (GDM + Shell): the RDP remote login below needs a GNOME session
# to hand out; a bare server install has none. Ubuntu-only metapackage.
sudo apt-get install -y ubuntu-desktop-minimal
# Remote desktop (gnome-remote-desktop — see the function header for why not xrdp).
setup_remote_desktop
# NVIDIA driver, only when an NVIDIA GPU is present.
install_nvidia_driver
# Low-disk login warning (system-wide profile.d snippet).
install_disk_warning
# Security baseline: brute-force bans, automatic security updates, sshd limits.
install_fail2ban
install_unattended_upgrades
harden_sshd
elif [ "$(uname -s)" = "Darwin" ]; then
# Asked first, so the long brew steps below can run unattended.
macos_shell="$(choose_macos_shell)"
echo "macOS login shell: $macos_shell"
ensure_homebrew
install_brew_packages
# Docker (colima VM), then code-server and MariaDB as login services.
install_colima_docker
brew install code-server
start_brew_service code-server
start_brew_service mariadb
else
echo "apt-get not found — skipping system packages (install vim/git manually)."
echo "Neither apt-get nor macOS — skipping system packages (install vim/git manually)."
fi
# Back up any existing config before overwriting (re-runnable).
@@ -196,7 +670,7 @@ git clone --quiet https://github.com/preservim/nerdtree "$HOME/.vim/bundle/nerdt
# Deploy tracked vim files: vimrc, pathogen loader, molokai colorscheme.
echo "Deploying vim config"
cp -rupv "$SCRIPT_DIR"/vim/* "$HOME/.vim/"
cp -Rpv "$SCRIPT_DIR"/vim/* "$HOME/.vim/"
ln -sf "$HOME/.vim/vimrc" "$HOME/.vimrc"
# Deploy the bashrc matching the detected OS.
@@ -206,8 +680,16 @@ if [ "$(uname -s)" = "Darwin" ] && [ -f "$SCRIPT_DIR/bash/bashrc-osx" ]; then
else
bashrc="bash/bashrc-linux"
fi
echo "Deploying $bashrc"
cp "$SCRIPT_DIR/$bashrc" "$HOME/.bashrc"
echo "Deploying $bashrc ($identity_name <$identity_email>)"
render_identity_template "$SCRIPT_DIR/$bashrc" "$identity_name" "$identity_email" > "$HOME/.bashrc"
# User-scope git config, identity taken from the bashrc just deployed.
deploy_gitconfig "$SCRIPT_DIR/$bashrc"
# tmux config + plugins (tmux comes from the apt or brew list above).
if command -v tmux >/dev/null 2>&1; then
deploy_tmux_config
fi
# Python CLIs via pipx (run as the user, never sudo). Skipped if pipx is absent.
if command -v pipx >/dev/null 2>&1; then
@@ -224,9 +706,31 @@ cp "$SCRIPT_DIR"/bin/* "$HOME/.local/bin/"
chmod +x "$HOME"/.local/bin/dt "$HOME"/.local/bin/dtach-router "$HOME"/.local/bin/claude-provider
# Wire the dtach session-resume menu into ~/.profile (idempotent; migrates any prior block).
wire_dtach_profile
# Remove any stale dtach wiring from ~/.profile (the menu now ships in ~/.bashrc; see above).
unwire_dtach_profile
echo "Done. Restart your shell or run: source ~/.bashrc"
echo "If you use zsh, switch to bash to enjoy these settings =)"
echo "Note: the deployed bashrc puts ~/.local/bin on PATH — re-login or run: source ~/.bashrc"
# Optional pieces, offered last so the base install is complete even when
# declined. Linux only. Each prompts [y/N] on a terminal, is skipped otherwise.
if command -v apt-get >/dev/null 2>&1; then
offer_tmp_on_disk
offer_ssh_memory_guard
offer_cloudpex
fi
# macOS: login shells skip ~/.bashrc unless ~/.bash_profile sources it (kept even
# with zsh, so `bash` stays usable); set up the chosen shell, then report what the
# Linux install has that this one does not.
if [ "$(uname -s)" = "Darwin" ]; then
wire_bash_profile
setup_macos_shell "${macos_shell:-bash}" "$identity_name" "$identity_email"
print_macos_gaps
fi
if [ "${macos_shell:-bash}" = zsh ]; then
echo "Done. Open a new terminal or run: exec zsh"
echo "Machine-specific zsh lines (nvm, bun...) go in ~/.zshrc.local"
else
echo "Done. Restart your shell or run: source ~/.bashrc"
echo "If you use zsh, switch to bash to enjoy these settings =)"
echo "Note: the deployed bashrc puts ~/.local/bin on PATH — re-login or run: source ~/.bashrc"
fi
+290
View File
@@ -0,0 +1,290 @@
# --------------------------------------------------------------------------- #
# #
# Module: tmux.conf By: sohorx <banlin.adrien@gmail.com> #
# #
# Created: 2015/04/01 18:17:25 Updated: 2016/04/01 18:17:25 #
# #
# --------------------------------------------------------------------------- #
# Note: {{{ #
# minimalist tmux configuration which behave a bit like vi #
# }}} #
# --------------------------------------------------------------------------- #
# Global options: {{{
# --------------
set -g bell-action none # make it Shut up!
set-option -g default-shell $SHELL # set shell
set -g default-terminal "screen-256color" # term colors
#set -ga terminal-overrides ",*256col*:Tc" # term cap only
set -ga terminal-overrides '*:Ss=\E[%p1%d q:Se=\E[ q,*256col*:Tc' # cursor control
setw -g mode-keys vi # vi mode
set -s escape-time 1 # Fix terminal name issues
set -g set-titles on # Turn on window titles
setw -g automatic-rename on # Automatically set window title
set -g mouse on # mouse cap
set-window-option -g clock-mode-colour brightwhite # set colour clock
set -g focus-events on # for vim (?)
set-option -g automatic-rename on # auto rename session
set-option -g status-interval 5 # rename session interval
set -g status on # enable status bar at start
# https://github.com/3rd/image.nvim/?tab=readme-ov-file#tmux
set -gq allow-passthrough on # image.nvim requirements
set -g visual-activity off # image.nvim requirements
# ------------------------------------------------------------------------- }}}
# Change default strings: {{{
# ----------------------
# Set window title string
set -g set-titles-string '#S :: #W :: #F#P #{pane_current_command}'
#set-option -g automatic-rename-format "#{b:pane_current_path}"
# ------------------------------------------------------------------------- }}}
# Variables: {{{
# ---------
is_vim='echo "#{pane_current_command}" | grep -iqE "(^|\/)g?(view|n?vim?)(diff)?$"'
# is_vim Note: {{{
# The pane's foreground command, as tmux sees it. The older variant read a
# "@tmux_vim_<pane>" variable that a vim plugin had to set; nothing sets it
# here, so C-h/j/k/l never reached vim.
# }}}
resurrect_root_dir="$XDG_CACHE_HOME/tmux/resurect"
# chunk of status bar variables and coloration
#sep_st='#[fg=brightwhite,bg=brightblue]'
#sep_c='#[fg=black]'
#
#sep_d='#[fg=brightwhite,bold]'
#sep_e='#[default,fg=brightblack]#[bg=brightblack]#[fg=brightwhite]'
#sep_f='#[fg=black]'
#
#sep_g='#[fg=blue,bold]'
#sep_h='#[default,fg=brightwhite]#[fg=blue,bg=brightwhite,bold]'
#sep_i='#[default,bg=brightwhite,fg=black]'
#
#sep_b='#[fg=brightyellow]#[default, bg=brightyellow, fg=black]'
#sep_a='#[fg=yellow]#[bg=yellow, fg=black]'
#
#sep_off="#[fg=brightred]#[bg=brightred, fg=brightwhite, bold] OFF"
#sep_on="#[fg=green]#[bg=green, fg=brightwhite, bold] ON"
#
#brightwhite_fg="#[fg=brightwhite]"
##
sep_st='#[fg=brightwhite,bg=brightblue]'
sep_c='#[fg=black] '
sep_d='#[fg=brightwhite,bold] '
sep_e='#[default,fg=brightblack] #[bg=brightblack]#[fg=brightwhite]'
sep_f='#[fg=black]'
sep_g='#[fg=brightblue,bold] '
sep_h='#[default,fg=brightwhite] #[fg=black,bg=brightwhite,bold]'
sep_i='#[default,bg=brightwhite,fg=black]'
sep_b='#[fg=brightyellow] #[default, bg=brightyellow, fg=black]'
sep_a='#[fg=yellow] #[bg=yellow, fg=black]'
sep_off="#[fg=brightred] #[bg=brightred, fg=brightwhite, bold] OFF"
sep_on="#[fg=green]#[bg=green, fg=brightwhite, bold] ON"
brightwhite_fg="#[fg=brightwhite]"
# ------------------------------------------------------------------------- }}}
# Key bindings: {{{
# ------------
# Unbindings
unbind [
unbind C-b # (prefix)
unbind C-n # (nav)
unbind l
unbind C-p
unbind v # (selection)
unbind y # (copy)
unbind p # (pasting)
unbind % # (sp)
unbind '"' # (vs)
unbind-key -T copy-mode-vi MouseDragEnd1Pane # (mouse setting) {{{
# stock binding = "release the drag: copy AND leave copy-mode", which loses
# the selection on screen. Rebound below (clipboard section) as copy without
# leaving, so a drag is enough and y stays available.
# ref: https://github.com/tmux/tmux/issues/140
# }}}
set -g prefix C-a # bind screen like prefix
bind C-a send-prefix # C-a C-a = literal C-a (shell home)
# specific commands
bind-key C-o command-prompt "new-session -s %1 -c ~"
bind R source-file ~/.config/tmux/tmux.conf \; display-message "Config reloaded..."
# Resizing key bindings (same arrow layout as the moves below, with prefix)
bind-key -r C-u resize-pane -U 5
bind-key -r C-h resize-pane -L 5
bind-key -r C-j resize-pane -D 5
bind-key -r C-k resize-pane -R 5
bind-key -r C-n next-window \; set status on
bind-key -r C-p previous-window \; set status on
# Moving across panes (and vim), no prefix: ctrl + u/h/j/k laid out as arrows
# (u up, h left, j down, k right). These keys sit at the same place on AZERTY
# and QWERTY US, and need no option key. Each one still reaches vim when vim
# runs in the pane. C-l is free, so it clears the shell screen again.
# Shell cost: C-u (readline "clear the line") is taken.
bind -n C-u if-shell "$is_vim" "send-keys C-u" "select-pane -U"
bind -n C-h if-shell "$is_vim" "send-keys C-h" "select-pane -L"
bind -n C-j if-shell "$is_vim" "send-keys C-j" "select-pane -D"
bind -n C-k if-shell "$is_vim" "send-keys C-k" "select-pane -R"
# Copy-mode has its own key table, where stock tmux turns these keys into text
# navigation (C-h cursor-left, C-u halfpage-up, C-j copy). Arrows do that job;
# the four keys keep switching panes there too.
bind -T copy-mode-vi C-u select-pane -U
bind -T copy-mode-vi C-h select-pane -L
bind -T copy-mode-vi C-j select-pane -D
bind -T copy-mode-vi C-k select-pane -R
bind Escape copy-mode # bind vi-like 'normal' mode
bind -Tcopy-mode-vi 'v' send -X begin-selection # bind vi-like visual mode
# Clipboard: pbcopy/pbpaste on macOS. Elsewhere tmux's own buffer, which tmux
# also hands to the terminal through OSC 52 (set-clipboard), so it reaches the
# local clipboard over ssh. xsel/xclip dropped: headless servers have no X.
# Releasing a mouse drag copies too (selection kept on screen, copy-mode stays).
# alt+c (in a selection) = y, alt+v (anywhere, no prefix) = prefix p: cmd-less
# keyboards. GNOME Terminal: keep "Enable mnemonics" off or alt+v opens View.
if-shell 'command -v pbcopy >/dev/null' \
'bind -Tcopy-mode-vi y send -X copy-pipe-and-cancel pbcopy ; bind -Tcopy-mode-vi M-c send -X copy-pipe-and-cancel pbcopy ; bind -Tcopy-mode-vi MouseDragEnd1Pane send -X copy-pipe-no-clear pbcopy ; bind p run "pbpaste | tmux load-buffer - ; tmux paste-buffer" ; bind -n M-v run "pbpaste | tmux load-buffer - ; tmux paste-buffer"' \
'bind -Tcopy-mode-vi y send -X copy-selection-and-cancel ; bind -Tcopy-mode-vi M-c send -X copy-selection-and-cancel ; bind -Tcopy-mode-vi MouseDragEnd1Pane send -X copy-selection-no-clear ; bind p paste-buffer ; bind -n M-v paste-buffer'
bind-key -Tcopy-mode-vi Escape send -X cancel # cancel v mode
bind-key -Tcopy-mode-vi ^v send -X rectangle-toggle # cancel v mode
# Wheel: stock tmux scrolls 5 lines per tick, too fast on a trackpad (one flick =
# dozens of ticks). Halved to 2; applies to history scrolling (copy-mode) only.
bind -T copy-mode-vi WheelUpPane select-pane \; send-keys -X -N 2 scroll-up
bind -T copy-mode-vi WheelDownPane select-pane \; send-keys -X -N 2 scroll-down
bind-key -T copy-mode-vi MouseDown1Pane select-pane -t = \; send-keys -X clear-selection
bind-key -T copy-mode-vi MouseUp1Pane select-pane -t = \; send-keys -X cancel
bind -n MouseDrag1Pane if -Ft= '#{mouse_any_flag}' 'if -Ft= "#{pane_in_mode}" "copy-mode -eM" "send-keys -M"' 'copy-mode -eM'
# (note) {{{
# A plain click (MouseDown then MouseUp, no drag) leaves copy-mode: back to
# the prompt, selection dropped. A drag sends MouseDrag/MouseDragEnd instead
# of MouseUp, so it keeps the scroll position and selects as usual. A click
# on another pane still switches to it.
# The `bind -n MouseDrag1Pane if -Ft= ...` changes the default binding of
# MouseDrag1Pane, the only difference is that we use `copy-mode -eM` instead
# of `copy-mode -M`, so that WheelDownPane can trigger copy-mode to exit when
# copy-mode is entered by MouseDrag1Pane
# Inner quotes stay plain: `\"` is literal inside '...' since tmux 3.0, and the
# mouse-app branch (claude, vim) then dies with "syntax error" on every drag.
# }}}
bind-key k "split-window -v -b -c '#{pane_current_path}'"
bind-key h "split-window -h -b -c '#{pane_current_path}'"
bind-key j "split-window -v -c '#{pane_current_path}'"
bind-key l "split-window -h -c '#{pane_current_path}'"
bind-key i "split-window -h -c '#{pane_current_path}'" # i: side by side (vertical bar)
bind-key - "split-window -v -c '#{pane_current_path}'" # -: stacked (horizontal bar)
bind q kill-window # Kill window
bind Q kill-session # Kill the session (not server)
bind C-c neww \; set status on # enable status bar
bind c neww \; set status on # disable status bar
# Enable disable bindings and change status bar with it {{{
bind -T root F10 set status off
bind -T root F9 set status on
bind -T root F12 \
set prefix None \; \
set key-table off \;\
set focus-events off \; \
set status-right '#{brightwhite_fg} #h #{sep_b} %a %d/%m/%Y #{sep_a} %H:%M #{sep_off} : #{continuum_status} ' \;\
set status on\; \
refresh-client -S\;
bind -T off F11 \
set -u prefix \;\
set -u key-table \;\
set -u focus-events \;\
set status-right '#{brightwhite_fg} #h #{sep_b} %a %d/%m/%Y #{sep_a} %H:%M #{sep_on} : #{continuum_status} ' \;\
set status on\;\
refresh-client -S\;
# }}}
# ------------------------------------------------------------------------- }}}
# Plugins: {{{
# -------
# To make plugins works, the run tpm must be done by the main file.
#
# run '~/.config/tmux/plugins/tpm/tpm'
##
set -g @tpm_plugins ' \
tmux-plugins/tpm \
ofirgall/tmux-window-name \
tmux-plugins/tmux-sensible \
tmux-plugins/tmux-resurrect \
tmux-plugins/tmux-continuum \
'
# plugins options
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-capture-pane-contents 'on'
set -g @resurrect-processes ' \
irb \
ipython \
~ranger \
pry \
ri \
"~bundle exec ri" \
"~bundle exec pry" \
"~pipenv shell" \
"~pipenv run" \
glow \
"~vaas-infra ssh" \
"~vaas-cli -s vaas-infra.dev.alphalink.fr ssh" \
ssh \
mosh \
tig \
'
set -g @resurrect-strategy-session 'always'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15' # save every minutes
set -g @resurrect-dir "$XDG_CACHE_HOME/tmux/$TMUX_SOCKET_NAME"
# ------------------------------------------------------------------------- }}}
# Status Bar: {{{
# ----------
set -wg mode-style bg=yellow,fg=black # selection: same accent as the status bar
set-window-option -g window-status-separator ''
set -g status-position bottom
set -g status-left-length 42
set -g status-right-length 80
set -g status-justify left
#setw -g status-style bg=black
setw -g status-style bg=default
set -g status-left '#{sep_st} λ #S #{sep_c}'
# setw -g window-status-format '#{sep_d} #I #{sep_e} #W #{sep_f}'
# setw -g window-status-current-format '#{sep_g} #I #{sep_h} #W #{sep_i}'
# set -g status-right '#{brightwhite_fg} #h #{sep_b} %a %d/%m/%Y #{sep_a} %H:%M #{sep_on} : #{continuum_status} '
setw -g window-status-format '#{sep_e} #W #{sep_f}'
setw -g window-status-current-format '#{sep_h} #W #{sep_i}'
set -g status-right '#{sep_b} #h #{sep_e}#{sep_on} : #{continuum_status} '
# set Panel borders
set -g pane-active-border-style fg=white,bg=white
set -g pane-border-style fg=white,bg=default
# set Message text
set-option -g message-style bg=default,fg=red
set-option -g message-command-style bg=default,fg=brightwhite
# ------------------------------------------------------------------------- }}}
run '~/.config/tmux/plugins/tpm/tpm'
+1
View File
@@ -0,0 +1 @@
1.0.0
+3 -3
View File
@@ -1,9 +1,9 @@
" Uset compatibility with Vii
set nocompatible
" add user info variable
let g:_author = "Bastien Chanot"
let g:_email = "chanot.bastien@gmail.com"
" user info from the shell identity (USER / EMAIL exported by the deployed rc)
let g:_author = $USER
let g:_email = exists('$EMAIL') ? $EMAIL : ''
" Enable pathogen for plugins
execute pathogen#infect()
+57
View File
@@ -0,0 +1,57 @@
# bchanot.zsh-theme — oh-my-zsh port of the bashrc prompt:
# ✔ (12ms) user [ ~/dir ] [branch -*+] >
# Mark + timer cyan (red ✘ on failure), user and brackets green (red for root),
# git segment blue. Deployed to ~/.oh-my-zsh/custom/themes by install.sh.
zmodload zsh/datetime
autoload -Uz add-zsh-hook
# Git segment, same rules as parse_git_branch/parse_git_dirty in the bashrc:
# " [branch bits]" with + (new, modified, renamed or untracked files),
# * (ahead of upstream) and - (deleted files). Empty outside a repo.
bchanot_git_info() {
local lines branch bits=''
lines="$(git status --porcelain --branch 2>/dev/null)" || return 0
branch="$(git symbolic-ref --short HEAD 2>/dev/null)" || branch='(detached)'
if print -r -- "$lines" | grep -qE '^(\?\?|[MAR].|.M)'; then
bits='+'
fi
if print -r -- "${lines%%$'\n'*}" | grep -q '\[ahead '; then
bits="*$bits"
fi
if print -r -- "$lines" | grep -qE '^(D.|.D)'; then
bits="-$bits"
fi
print -r -- " [${branch}${bits:+ $bits}]"
}
# Command timer: preexec stamps the start, precmd formats the elapsed time with
# the bashrc's rules (about 3 significant digits, us up to h).
bchanot_timer_start() {
_bchanot_cmd_start=$EPOCHREALTIME
}
bchanot_timer_stop() {
integer delta_us us ms s m h
(( delta_us = (EPOCHREALTIME - ${_bchanot_cmd_start:-$EPOCHREALTIME}) * 1000000 ))
(( us = delta_us % 1000, ms = (delta_us / 1000) % 1000 ))
(( s = (delta_us / 1000000) % 60, m = (delta_us / 60000000) % 60 ))
(( h = delta_us / 3600000000 ))
if ((h > 0)); then _bchanot_timer_show=${h}h${m}m
elif ((m > 0)); then _bchanot_timer_show=${m}m${s}s
elif ((s >= 10)); then _bchanot_timer_show=${s}.$((ms / 100))s
elif ((s > 0)); then _bchanot_timer_show=${s}.$(printf %03d $ms)s
elif ((ms >= 100)); then _bchanot_timer_show=${ms}ms
elif ((ms > 0)); then _bchanot_timer_show=${ms}.$((us / 100))ms
else _bchanot_timer_show=${us}us
fi
unset _bchanot_cmd_start
}
add-zsh-hook preexec bchanot_timer_start
add-zsh-hook precmd bchanot_timer_stop
PROMPT='%B%(?.%F{cyan}✔.%F{red}✘) (${_bchanot_timer_show}) '
PROMPT+='%(!.%F{red}.%F{green})%n [%f%b %~ %B%(!.%F{red}.%F{green})]'
PROMPT+='%F{blue}$(bchanot_git_info) %f%b> '
RPROMPT=''
+71
View File
@@ -0,0 +1,71 @@
# zshrc for macOS: zsh + oh-my-zsh counterpart of bash/bashrc-osx (same env,
# aliases and dtach menu; the prompt lives in bchanot.zsh-theme).
# Machine-specific lines (nvm, bun, tokens...) go in ~/.zshrc.local, never here.
# Homebrew on PATH (Apple Silicon: /opt/homebrew, Intel: /usr/local).
for brew_bin in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$brew_bin" ]; then
eval "$("$brew_bin" shellenv)"
break
fi
done
unset brew_bin
## oh-my-zsh (loaded first: the aliases below override its defaults, e.g. `d`)
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="bchanot"
plugins=(git)
source "$ZSH/oh-my-zsh.sh"
## Enable color support of ls and also add handy aliases
# Some colors (BSD ls: -G instead of GNU --color)
alias ls='ls -G'
alias grep='grep --color=auto'
## Some export
# colored GCC warnings and errors
export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
# XDG cache dir: tmux.conf saves resurrect/continuum sessions under it
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
# Ensure ~/.local/bin is on PATH (pipx CLIs + personal scripts from bin/)
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
# History: none for root, near-unlimited otherwise (zsh has no -1 = unlimited).
if [[ $EUID == 0 ]] ; then
HISTSIZE=0
SAVEHIST=0
else
HISTSIZE=10000000
SAVEHIST=10000000
fi
# Identity for git commits and vim headers. Filled by install.sh (asked once,
# reused from an existing ~/.bashrc / ~/.zshrc on re-runs), never stored here.
export USER="@USER@"
export EMAIL="@EMAIL@"
# claude-dans-dtach : creer une session (claude tournant dans dtach, detache via Ctrl-\).
# Usage : cd ~/projets/seo && cc seo -> session nommee "seo".
# Pas de systemd sur macOS : dtach est lance directement (pas de slice cgroup).
dtach_claude() {
local name="${1:-claude-$(date +%H%M%S)}"
mkdir -p "$HOME/.dtach"
dtach -c "$HOME/.dtach/${name}" -e '^\' \
bash -c 'set -a; [ -f "$HOME/.claude/.env" ] && . "$HOME/.claude/.env"; set +a; exec claude'
}
alias cc='dtach_claude'
# Rappeler a la demande le menu de reprise.
alias d='source ~/.local/bin/dtach-router'
# Reglages propres a cette machine (non versionnes).
[ -f "$HOME/.zshrc.local" ] && source "$HOME/.zshrc.local"
# Au demarrage d'un shell interactif, proposer de reprendre une session dtach existante
# (silencieux si aucune).
case $- in *i*) [ -x "$HOME/.local/bin/dtach-router" ] && . "$HOME/.local/bin/dtach-router" ;; esac