Compare commits
5 Commits
86a924d92a
...
5a9940c082
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9940c082 | ||
|
|
99e25d175e | ||
|
|
d6df39e90f | ||
|
|
dc8c92d189 | ||
|
|
b2d3bdef19 |
@ -0,0 +1,79 @@
|
|||||||
|
%{
|
||||||
|
title: "Generate an init script to get you(r team) up and running",
|
||||||
|
author: "Willem van den Ende",
|
||||||
|
tags: ~w(pi.dev AI deterministic continuous-delivery),
|
||||||
|
description: "Checking out a repository on a fresh machine often involves some fiddling around. An init scripts costs only one prompt and gets you going now, and months later.",
|
||||||
|
published: true
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
I like to hit the ground running when starting work in a repository. And I want my colleagues to get up and running quickly as well.
|
||||||
|
One thing I picked up from Anthropics [Harnesses for long running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) last November was creating an ~init~ script that sets everything up in a runnable and testable state.
|
||||||
|
|
||||||
|
Run the script, get to work. It only costs one prompt, once, to make, and every time after that is deterministic and fast.
|
||||||
|
|
||||||
|
It is one of those things where I go: "Why didn't I think of that before?". And an un-metered, local, model is powerful enough to do it while I do something else. It only took two minutes, so not too bad.
|
||||||
|
|
||||||
|
## Why make an init script for Firehose (this blog) now?
|
||||||
|
|
||||||
|
The best time to make an init script is when you start the project. The second best time is now.
|
||||||
|
|
||||||
|
I had Firehose running on another machine already. Running one command is usually enough to set up a Phoenix Liveview project, but I had just added another javascript dependency that was not covered by that. So instead of adding that by hand, and figuring out how to do that, I improved the process by adding the init script now.
|
||||||
|
|
||||||
|
I know now that it pays off very quickly. I had just used another project that had an init script on a new machine. No need to remember, it just works. I like software that "just works".
|
||||||
|
|
||||||
|
It is very satisfying when it runs, as you get an overview of next steps you can take. I like 'micro prompts' like these. And it paid of immediately, I will explain after the screenshot.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
When I started the server, the port was already taken. As the script outputs the PORT variable, I knew immediately what to do, set the port and got to work.
|
||||||
|
|
||||||
|
Aside: starting the server is now muscle memory for me, and standard in Phoenix Liveview, no need to script, although the generated init script does have a `--server` option for those who don't know. Executable documentation for the win.
|
||||||
|
|
||||||
|
|
||||||
|
This was one fairly lazy prompt:
|
||||||
|
|
||||||
|
> I think it would be handy to have an init script for this repository that also downloads the js dependencies. the init.sh script should get the dependencies, run the mix setup so there is a working db, run the tests and download the js assets. the goal
|
||||||
|
> is for an agent or a person to be able to start working. At the end of the script, print handy instructions e.g. to run make, and what PORT the server will run on
|
||||||
|
|
||||||
|
It only took this prompt. It did take quite a few turns. My Pi turn-limit extension alerted me that it had gone over 25 turns. I chose not to let Pi run further. What happened was that the agent was done, but it did not have permissions to actually run the init script because the sandbox did not allow everything in it. So I ran that by hand outside the sandbox.
|
||||||
|
|
||||||
|
Do you have init scripts or something like it? Would it be useful to you? What do you think?
|
||||||
|
|
||||||
|
|
||||||
|
## Further reading
|
||||||
|
|
||||||
|
A Full ;-), [very brief transcript](`/images/blog/2026/transcripts/make-init-script.html`) of the session, so you can see all the turns taken in detail.
|
||||||
|
|
||||||
|
I wrote earlier on the QWAN blog about working deterministically where possible, and the pay-offs in [Stochastic to Deterministic](https://www.qwan.eu/2025/12/11/stochastic-to-deterministic.html). Even though my local model is un-metered, running a script is still a lot faster and much more predictable.
|
||||||
|
|
||||||
|
I used the 35B model from [my local agentic dev setup](/blog/engineering/my-local-agentic-dev-setup-today) It was more than up to the job. Sandboxing is a bit of work though.
|
||||||
|
|
||||||
|
|
||||||
|
## Bonus item - what the init script does, and local agent performance
|
||||||
|
|
||||||
|
According to the agent:
|
||||||
|
|
||||||
|
1. Pre-flight checks — verifies elixir, mix, node, npm, and PostgreSQL are available
|
||||||
|
2. make deps — fetches all Elixir dependencies
|
||||||
|
3. mix ecto.setup — creates the DB, runs migrations, seeds demo user
|
||||||
|
4. npm install — installs JS dependencies (highlight.js, etc.) in assets/
|
||||||
|
5. mix assets.setup && mix assets.build — installs esbuild/tailwind and bundles the JS/CSS
|
||||||
|
6. make test — runs the full test suite
|
||||||
|
|
||||||
|
It ends with a summary showing how to start the server (port 8056), demo credentials, and handy make commands.
|
||||||
|
|
||||||
|
The snippet below is from my performance widget. Token generation is not super fast, but all 32 turns ran while attending Chris Parsons’ webinar. 2.5 minutes for a script that ’just works’ is good enough for me.
|
||||||
|
|
||||||
|
📊 Performance: llama.cpp/Qwen3.6-35B-A3B-MXFP4<sub>MOE.gguf</sub>
|
||||||
|
Prefill: 28,376 tokens @ 5253.8 tok/s
|
||||||
|
Generation: 5,699 tokens @ 38.8 tok/s
|
||||||
|
Combined: 34,075 tokens @ 223.5 tok/s (2.5m total)
|
||||||
|
TTFT: 5401ms
|
||||||
|
Turns: 32
|
||||||
|
|
||||||
|
|
||||||
|
## Things don't always go to plan
|
||||||
|
|
||||||
|
I am integrating the Microprints library that I open sourced earlier - to keep an eye on changes. My local model seems to have trouble getting the correct files to render. I fixed most of the CSS issues (Claude Opus 4.6 struggled with that as well). I am still learning about how to make Phoenix Liveview applications modular, some of that can be done by reading, other learning happens the hard way.
|
||||||
BIN
app/priv/static/images/blog/2026/init-script-result.png
Normal file
BIN
app/priv/static/images/blog/2026/init-script-result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
103
init.sh
Executable file
103
init.sh
Executable file
@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# init.sh — One-command setup for the Firehose monorepo.
|
||||||
|
# Installs deps, sets up the DB, builds JS assets, and runs tests.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[ WARN ]${NC} $*"; }
|
||||||
|
fail() { echo -e "${RED}[ FAIL ]${NC} $*"; }
|
||||||
|
|
||||||
|
# ── Pre-flight checks ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
|
||||||
|
for cmd in elixir mix node npm; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
fail "Missing required tools: ${missing[*]}"
|
||||||
|
echo "Install them via mise (mise install) or your package manager."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for PostgreSQL
|
||||||
|
if ! pg_isready &>/dev/null; then
|
||||||
|
fail "PostgreSQL is not running or not reachable on localhost."
|
||||||
|
echo "Start it with: brew services start postgresql (or equivalent)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 1. Get Elixir dependencies ─────────────────────────────────────
|
||||||
|
|
||||||
|
info "Fetching Elixir dependencies ..."
|
||||||
|
make deps
|
||||||
|
ok "Dependencies fetched."
|
||||||
|
|
||||||
|
# ── 2. Set up the database ─────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Setting up the database (create + migrate + seed) ..."
|
||||||
|
cd app && mise exec -- mix ecto.setup
|
||||||
|
ok "Database ready."
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# ── 3. Install npm dependencies for assets ─────────────────────────
|
||||||
|
|
||||||
|
info "Installing npm dependencies in assets/ ..."
|
||||||
|
cd app/assets
|
||||||
|
# Use a temp cache dir to avoid root-owned-file issues in ~/.npm
|
||||||
|
npm install --cache /tmp/pi-npm-cache 2>/dev/null || npm install
|
||||||
|
cd ../..
|
||||||
|
ok "npm dependencies installed."
|
||||||
|
|
||||||
|
# ── 4. Build JS assets ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Building JS assets ..."
|
||||||
|
cd app && mise exec -- mix assets.setup && mise exec -- mix assets.build
|
||||||
|
ok "JS assets built."
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# ── 5. Run tests ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Running tests ..."
|
||||||
|
make test
|
||||||
|
ok "All tests passed."
|
||||||
|
|
||||||
|
# ── Done ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=============================================="
|
||||||
|
echo " 🎉 Firehose is ready to go!"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
echo " Useful commands:"
|
||||||
|
echo ""
|
||||||
|
echo " make test Run the full test suite"
|
||||||
|
echo " make check Static analysis (credo + format)"
|
||||||
|
echo " make compile Compile the project"
|
||||||
|
echo " make format Format all code"
|
||||||
|
echo ""
|
||||||
|
echo " Start the dev server:"
|
||||||
|
echo ""
|
||||||
|
echo " cd app && mix phx.server"
|
||||||
|
echo ""
|
||||||
|
echo " The server will be on: http://localhost:8056"
|
||||||
|
echo ""
|
||||||
|
echo " Demo user (created by seed):"
|
||||||
|
echo " email: demo@example.com"
|
||||||
|
echo " pass: password123!"
|
||||||
|
echo ""
|
||||||
|
echo " Database: firehose_dev (PostgreSQL on localhost)"
|
||||||
|
echo ""
|
||||||
|
echo " Port: 8056 (override with PORT=xxxx)"
|
||||||
|
echo ""
|
||||||
|
echo "=============================================="
|
||||||
42
scripts/scheduled
Executable file
42
scripts/scheduled
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Show all blog post files that are scheduled (published: true, future-dated).
|
||||||
|
# Usage: scripts/scheduled (from repo root)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Resolve repo root: scripts/ is one level below the repo root
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
BLOG_DIR="$REPO_ROOT/app/priv/blog"
|
||||||
|
|
||||||
|
if [ ! -d "$BLOG_DIR" ]; then
|
||||||
|
echo "Error: blog directory not found at $BLOG_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get today's date as YYYY-MM-DD for comparison
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
found=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Extract frontmatter (between first line and first ---)
|
||||||
|
frontmatter=$(sed -n '1,/^---$/p' "$file" | sed '$d')
|
||||||
|
|
||||||
|
# Skip drafts (published: false)
|
||||||
|
if echo "$frontmatter" | grep -q 'published: *false'; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract year from the path: .../blog/<blog>/YYYY/...
|
||||||
|
year=$(echo "$file" | sed -n 's|.*/blog/[^/]*/\([0-9]\{4\}\)/.*|\1|p')
|
||||||
|
|
||||||
|
if [ -n "$year" ] && [ "$year" -gt "$(date +%Y)" ] 2>/dev/null; then
|
||||||
|
echo "$file"
|
||||||
|
found=$((found + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$BLOG_DIR" -name '*.md' -type f -print0 | sort -z)
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
echo "(no scheduled posts found)"
|
||||||
|
fi
|
||||||
33
scripts/show_drafts
Executable file
33
scripts/show_drafts
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Show all blog post files that are drafts (published: false in frontmatter).
|
||||||
|
# Usage: make show-drafts (from repo root)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Resolve repo root: script/ is one level below the repo root
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
BLOG_DIR="$REPO_ROOT/app/priv/blog"
|
||||||
|
|
||||||
|
if [ ! -d "$BLOG_DIR" ]; then
|
||||||
|
echo "Error: blog directory not found at $BLOG_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find .md files with frontmatter containing published: false
|
||||||
|
# Uses a multi-line grep approach: find files where the frontmatter block
|
||||||
|
# (between first --- markers) contains published: false
|
||||||
|
found=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Extract frontmatter (between first line and first ---)
|
||||||
|
frontmatter=$(sed -n '1,/^---$/p' "$file" | sed '$d')
|
||||||
|
if echo "$frontmatter" | grep -q 'published: *false'; then
|
||||||
|
echo "$file"
|
||||||
|
found=$((found + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$BLOG_DIR" -name '*.md' -type f -print0 | sort -z)
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
echo "(no drafts found)"
|
||||||
|
fi
|
||||||
Loading…
x
Reference in New Issue
Block a user