@@ -21,6 +21,12 @@ TypeScript, installed as executables on `PATH`.
2121| [ ` vid ` ] ( #vid ) | Inspect, thumbnail, clip and shrink video, through ffmpeg |
2222| [ ` codeburn ` ] ( #codeburn ) | See where your AI spend goes, by task, tool, model and project |
2323
24+ One thing here is not a ` PATH ` command and does not need Node:
25+
26+ | Script | What it does |
27+ | --- | --- |
28+ | [ ` root-ubuntu.sh ` ] ( #root-ubuntush ) | Provision an Ubuntu/Debian server: dev environment, accounts, web, TLS |
29+
2430## Requirements
2531
2632- ** Node 20+**
@@ -683,6 +689,83 @@ before every launch, which is fine for a one-shot and wrong for a dashboard you
683689open twenty times a day. Upstream wants ** Node 22.13+** ; on an older one it says
684690so and tries anyway, since that floor is theirs to move.
685691
692+ ### ` root-ubuntu.sh `
693+
694+ Sets up a server the way we like them, and keeps it that way. It is the odd one
695+ out in this repository: a single bash script rather than a TypeScript command,
696+ because it has to run on a machine where nothing is installed yet — including
697+ Node. Nothing links it onto ` PATH ` ; you curl it onto the box.
698+
699+ ``` sh
700+ # on the server, as root
701+ curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh \
702+ | bash -s -- --refresh
703+ ```
704+
705+ ** ` bash ` , not ` sh ` .** ` /bin/sh ` on Ubuntu is dash and this script is bash
706+ throughout. Piping it into ` sh ` stops with one sentence telling you so rather
707+ than a syntax error on a line you never typed.
708+
709+ A pipe has no terminal on stdin, so that form is always non-interactive: it
710+ takes defaults instead of reading answers out of its own source. To be asked the
711+ questions, download it first:
712+
713+ ``` sh
714+ curl -fsSLO https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh
715+ chmod +x root-ubuntu.sh
716+ ./root-ubuntu.sh # as root
717+ ./root-ubuntu.sh alice bob # ...and provision two accounts
718+ ./root-ubuntu.sh alice --groups sudo,docker
719+ ```
720+
721+ What a run does:
722+
723+ - apt update/upgrade, base packages, unattended security updates
724+ - ` ufw ` , with ssh opened * before* the firewall is enabled
725+ - accounts and groups — created, or refreshed if an earlier run made them
726+ - zsh + oh-my-zsh, oh-my-tmux, mise, moshcode, chawan
727+ - nginx: ` ~/public_html ` at ` /~user ` and ` user.$WEB_DOMAIN ` , plus per-user dev
728+ apps at ` <app>.<user>.$WEB_DOMAIN ` , static or reverse-proxied
729+ - Let's Encrypt, wildcard via DNS-01 where credentials allow it
730+
731+ ** Re-running is the update path.** Every step converges rather than assuming a
732+ blank machine: files it owns are rewritten only when the content really changed,
733+ so nginx is not reloaded for nothing; files a user has since edited are never
734+ clobbered, and the new version is left beside them as ` .new ` ; and a lock file
735+ makes two concurrent runs impossible. On a settled box a re-run reports that
736+ nothing changed, which is the point.
737+
738+ #### Configuring it
739+
740+ Read from the environment first, then ` $SERVER_CONFIG ` , then
741+ ` /etc/cli-tools/server.conf ` . Copy [ ` server.conf.example ` ] ( server.conf.example ) ,
742+ which documents every value:
743+
744+ ``` sh
745+ install -d -m 0755 /etc/cli-tools
746+ install -m 0600 server.conf.example /etc/cli-tools/server.conf
747+ ```
748+
749+ ` KEY=value ` , one per line, ` # ` for comments. The file is ** read, not sourced** :
750+ nothing in it executes, so ` $(…) ` in a config file stays literal text instead of
751+ running as root, and the environment still wins over the file. It is not JSON
752+ either — the script runs before apt has put ` jq ` on the box, and a bootstrap
753+ that cannot read its own config until it has installed a parser has a hole in
754+ it.
755+
756+ ** Dotfiles are optional and are not in this repository.** They cannot be: a
757+ dotfiles tree carries ssh config, ` known_hosts ` and sometimes keys, and this
758+ repo is public. Point ` DOTFILES_REPO ` at your own and the script clones it;
759+ leave it unset and the box still gets everything else, with each account keeping
760+ whatever dotfiles it already had. Running the script from inside a dotfiles
761+ checkout also works — it recognises one by its content, not its name.
762+
763+ ** No credentials, ever, in the file itself.** ` ACME_EMAIL ` has no default,
764+ because a public script must not ship somebody's address and a made-up one sends
765+ a stranger's certificate warnings into a black hole. There is no default ad slot
766+ for the same shape of reason: a slot id is an account, so shipping one would bill
767+ every box that ever ran this to whoever owned it.
768+
686769## As a moshcode plugin
687770
688771This repo is also a plugin marketplace:
0 commit comments