feat: public dotfiles tier — no credential, no identity, one installer
Fresh history. This is the repo a throwaway VM clones anonymously: it brings a
machine to a working baseline and carries nothing that makes it mine.
56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation,
1 is the manifest, and a 15-file test harness stays behind in .tests/.
What did not travel, and why:
encrypted_private_bws-token.age a real credential; age is dropped entirely
.chezmoidata/bws.toml env-var -> secret-id map; belongs with the
tier that can use it
SECRETS.md documentation of the rules, not config
finish-setup.sh.tmpl superseded by dotup
nvim/init.lua.backup dead file
dot_claude/**, dot_codex/**, 120 files of agent config, private tier
dot_pi/**
De-identified rather than dropped:
.gitconfig [user], the GitHub ssh rewrite and both Gitea host rewrites are
identity, not configuration. They move behind an [include] of
~/.config/git/config.local, which the private tier writes. Git
treats a missing include as a no-op, so a public-only machine
reads the file and stops.
.zshrc the two gitea aliases carried a personal domain and a LAN IP.
They move behind a guarded source of ~/.config/zsh/local.zsh,
the sibling of the secrets.zsh seam phase 2 established.
nvim a commented-out LM Studio endpoint naming a LAN address.
ghostty a stale auto-generated header naming an absolute home directory.
Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The
former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup
printed an error -- the same shape as the unguarded oh-my-zsh source phase 2
fixed. It is guarded now.
.chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k,
zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere,
which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke
the shell it configures. The theme and both plugins nest under
.oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to.
dotup gains an install engine. It resolves each selected package to a channel
(apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one
function every consumer reads, probes apt-cache before batching so a name apt
does not know moves to brew instead of failing all thirty, and retries
individually if a batch still fails -- which earned its keep on the first real
container run, where mermaid-cli's puppeteer dependency failed and the other
twelve npm packages installed anyway. --unattended computes safe defaults fresh
from the manifest rather than inheriting a state file, and refuses private and
invasive rows outright even when a stale state file ticks them.
The manifest gains @spec, a second directive kind alongside @needs, carrying the
argument a channel needs but a package name cannot supply -- the scoped npm
name, the flatpak app id, the .deb source. The TSV stays five columns wide.
Three bugs the container runs found, all fixed here:
* `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen
npm packages failed on a fresh box. The manifest asks apt for both names.
* A tool installed a moment ago is not on this process's PATH -- uv lands in
~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a
non-login PATH. Resolved by looking in the places we just wrote to, never by
exporting a modified PATH.
* `A || { B && C; }` is one || list, so when `command -v sudo` failed the list
failed and `set -e` killed dotup at load. On a non-root machine with no
sudo it died before printing anything. There is a regression test.
.zshenv and .p10k.zsh are marked private_. Both are shell code the login shell
executes and both applied at 664, group-writable. Third occurrence of the class
of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the
first one found on purpose rather than by accident.
Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04
and debian:12. The installer is driven against a directory of fake package
managers that record what they were asked to do and install nothing, so the
engine is exercised end to end without a package landing on the test machine.
`gitleaks detect` over the full history and the working tree: no leaks found,
with no allowlist and no .gitleaks.toml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
# sysjournal — shared system-knowledge journal for the Obsidian vault.
|
||||
#
|
||||
# One tool, used by every coding agent (Claude Code, Codex, Pi), so host/
|
||||
# environment changes get recorded in ONE consistent, greppable place with
|
||||
# the frontmatter schema that "Tech/Infrastructure/System Log MOC.md" indexes
|
||||
# via Dataview (date, machine, type, status, tags, review-by).
|
||||
#
|
||||
# Journal HOST/ENVIRONMENT changes — system config, services, networking,
|
||||
# VMs, drivers, build toolchains, host-wiring of an app. NOT ordinary work
|
||||
# inside a code repo. See `sysjournal help`.
|
||||
#
|
||||
# Portable bash (invoked by agents in varied environments), not zsh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VAULT="${SYSJOURNAL_VAULT:-$HOME/Documents/Obsidian25}"
|
||||
SUBDIR="${SYSJOURNAL_SUBDIR:-Tech/Infrastructure}"
|
||||
INFRA="$VAULT/$SUBDIR"
|
||||
|
||||
die() { printf 'sysjournal: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
ensure_dir() {
|
||||
[ -d "$VAULT" ] || die "vault not found: $VAULT (set SYSJOURNAL_VAULT)"
|
||||
mkdir -p "$INFRA"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
sysjournal — record & recall host/environment changes in the Obsidian vault.
|
||||
|
||||
USAGE
|
||||
sysjournal search <keywords...> Recall: grep the journal first (case-insensitive)
|
||||
sysjournal new "<Title>" [options] Scaffold a new note, print its path
|
||||
sysjournal list [N] List the N most-recent notes (default 20)
|
||||
sysjournal path Print the infrastructure folder path
|
||||
sysjournal help This help
|
||||
|
||||
`new` OPTIONS
|
||||
--type <t> change | setup | debug | fix | incident | note (default: change)
|
||||
--status <s> deployed | resolved | unresolved | workaround | planned (default: deployed)
|
||||
--tags a,b,c comma-separated tags
|
||||
--machine <m> host the change was made on (default: `hostname -s`)
|
||||
--review-by <YYYY-MM-DD> optional follow-up/expiry date
|
||||
--summary "<one line>" optional lead line
|
||||
|
||||
WHAT TO JOURNAL
|
||||
YES system config, services/daemons (systemd/launchd/cron), networking/DNS/
|
||||
VPN/firewall, VMs & host containers, drivers/kernel/boot, disks/mounts,
|
||||
build toolchains & global package installs, wiring an app into the host.
|
||||
NO feature work, bug fixes, refactors, tests INSIDE a project repo.
|
||||
Discriminator: does it change state outside the repo, on the host? If no, skip.
|
||||
Straddle (build an app AND install it as a service): journal only the
|
||||
host-wiring part (the unit/cron/port), not the app code.
|
||||
|
||||
EXAMPLES
|
||||
sysjournal search systemd relay port
|
||||
sysjournal new "WireGuard VPN to homelab" --type setup --tags wireguard,vpn,network
|
||||
sysjournal new "DNS resolution flaky after netplan change" --type debug --status unresolved
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_search() {
|
||||
ensure_dir
|
||||
[ "$#" -ge 1 ] || die "search needs at least one keyword"
|
||||
# OR-match the keywords so a few loosely-related terms still surface notes.
|
||||
local pattern
|
||||
pattern=$(printf '%s|' "$@"); pattern="${pattern%|}"
|
||||
echo "# Journal matches in $SUBDIR for: $*"
|
||||
echo
|
||||
if ! rg -i --no-heading -n -C1 --color never -e "$pattern" "$INFRA" 2>/dev/null; then
|
||||
echo "(no matches — nothing journaled on this yet)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
ensure_dir
|
||||
local n="${1:-20}"
|
||||
# Newest first by mtime; strip the vault prefix for readability.
|
||||
find "$INFRA" -maxdepth 1 -name '*.md' -printf '%T@ %p\n' 2>/dev/null \
|
||||
| sort -rn | head -n "$n" | sed "s#[0-9.]* $INFRA/##"
|
||||
}
|
||||
|
||||
cmd_path() { echo "$INFRA"; }
|
||||
|
||||
cmd_new() {
|
||||
ensure_dir
|
||||
local title="" type="change" status="deployed" tags="" machine review_by="" summary=""
|
||||
machine="$(hostname -s 2>/dev/null || hostname)"
|
||||
|
||||
# First non-flag arg is the title.
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--type) type="${2:?--type needs a value}"; shift 2;;
|
||||
--status) status="${2:?--status needs a value}"; shift 2;;
|
||||
--tags) tags="${2:?--tags needs a value}"; shift 2;;
|
||||
--machine) machine="${2:?--machine needs a value}"; shift 2;;
|
||||
--review-by) review_by="${2:?--review-by needs a value}"; shift 2;;
|
||||
--summary) summary="${2:?--summary needs a value}"; shift 2;;
|
||||
--*) die "unknown option: $1";;
|
||||
*) [ -z "$title" ] && title="$1" || die "unexpected arg: $1"; shift;;
|
||||
esac
|
||||
done
|
||||
[ -n "$title" ] || die 'new needs a "<Title>"'
|
||||
|
||||
# Filename: keep the human title (Obsidian-friendly), drop only path-illegal chars.
|
||||
local fname; fname=$(printf '%s' "$title" | tr '/\\' '--' | sed 's/[[:cntrl:]]//g; s/ */ /g; s/^ *//; s/ *$//')
|
||||
local file="$INFRA/$fname.md"
|
||||
if [ -e "$file" ]; then
|
||||
echo "$file" # already exists — recall, don't clobber; edit/append this note.
|
||||
echo "sysjournal: note already exists — edit it instead of creating a duplicate." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# YAML frontmatter — inline tag list, matching the MOC's own `tags: [moc]` style.
|
||||
local yaml_tags="[]"
|
||||
if [ -n "$tags" ]; then
|
||||
yaml_tags="[$(printf '%s' "$tags" | sed 's/ *, */, /g')]"
|
||||
fi
|
||||
local date_today; date_today="$(date +%F)"
|
||||
|
||||
{
|
||||
echo "---"
|
||||
echo "date: $date_today"
|
||||
echo "machine: $machine"
|
||||
echo "type: $type"
|
||||
echo "status: $status"
|
||||
echo "tags: $yaml_tags"
|
||||
[ -n "$review_by" ] && echo "review-by: $review_by"
|
||||
echo "---"
|
||||
echo
|
||||
echo "# $title"
|
||||
echo
|
||||
[ -n "$summary" ] && { echo "$summary"; echo; }
|
||||
echo "## Why"
|
||||
echo
|
||||
echo "## What changed"
|
||||
echo
|
||||
echo "## Design decisions / gotchas"
|
||||
echo
|
||||
echo "## Verify"
|
||||
echo
|
||||
echo '```'
|
||||
echo '# command run + observed result'
|
||||
echo '```'
|
||||
echo
|
||||
echo "## Status / follow-ups"
|
||||
echo
|
||||
echo "Related: "
|
||||
} > "$file"
|
||||
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
main() {
|
||||
local sub="${1:-help}"; shift || true
|
||||
case "$sub" in
|
||||
search|recall|grep) cmd_search "$@";;
|
||||
new|add) cmd_new "$@";;
|
||||
list|ls) cmd_list "$@";;
|
||||
path|dir) cmd_path;;
|
||||
help|-h|--help) usage;;
|
||||
*) usage; die "unknown command: $sub";;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user