# scaffold.sh — lay down David's standard dmwd-io Astro 5 (SSR) starter and
# install dependencies. One command, no reinvention.
# scaffold.sh <target-dir> [--name <pkg-name>] [--no-install] [--force]
# <target-dir> where to create the project (created if missing)
# --name <name> package.json name (default: the target dir basename)
# --no-install lay files down but skip `pnpm install`
# --force overwrite existing template files in the target
# 1. Copies the template tree (.npmrc, astro.config.mjs, tsconfig.json,
# biome.json, src/content/config.ts, Taskfile.yml) into <target-dir>.
# 2. Writes a package.json pinned to the dmwd-io Astro 5 stack.
# 3. Adds .gitignore, .env.example, and a minimal src/ entry so the app runs.
# 4. Runs `pnpm install` (unless --no-install), warning if NPM_TOKEN is unset.
# Exit codes: 0 ok · 1 usage/precondition error · 2 copy/write failure.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATES="$SCRIPT_DIR/../templates/astro-starter"
# Pinned stack versions — bump deliberately, in one place. (Astro 5 line.)
NODE_ADAPTER_VERSION="^9.5.5"
DESIGN_SYSTEM_VERSION="latest"
TYPESCRIPT_VERSION="^6.0.3"
TS_PLUGIN_VERSION="^1.10.0"
# --- args -------------------------------------------------------------------
die() { echo "scaffold.sh: $*" >&2; exit 1; }
--name) PKG_NAME="${2:-}"; shift 2 ;;
--no-install) DO_INSTALL=0; shift ;;
--force) FORCE=1; shift ;;
-h | --help) sed -n '2,30p' "$0"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$TARGET" ] && TARGET="$1" && shift || die "unexpected arg: $1" ;;
[ -n "$TARGET" ] || die "missing <target-dir> (try --help)"
[ -d "$TEMPLATES" ] || die "templates dir not found at $TEMPLATES"
command -v pnpm >/dev/null 2>&1 || die "pnpm not found — install it: npm i -g pnpm"
TARGET="$(cd "$TARGET" && pwd)"
[ -z "$PKG_NAME" ] && PKG_NAME="$(basename "$TARGET")"
echo "==> Scaffolding dmwd-io Astro 5 project: $PKG_NAME"
# --- copy guard -------------------------------------------------------------
if [ -e "$dest" ] && [ "$FORCE" -ne 1 ]; then
echo " skip (exists, use --force): ${dest#"$TARGET"/}"
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest" || die "failed to copy $src"
echo " wrote: ${dest#"$TARGET"/}"
# --- 1. template tree -------------------------------------------------------
copy "$TEMPLATES/.npmrc" "$TARGET/.npmrc"
copy "$TEMPLATES/astro.config.mjs" "$TARGET/astro.config.mjs"
copy "$TEMPLATES/tsconfig.json" "$TARGET/tsconfig.json"
copy "$TEMPLATES/biome.json" "$TARGET/biome.json"
copy "$TEMPLATES/Taskfile.yml" "$TARGET/Taskfile.yml"
copy "$TEMPLATES/src/content/config.ts" "$TARGET/src/content/config.ts"
# --- 2. package.json --------------------------------------------------------
PKG_JSON="$TARGET/package.json"
if [ -e "$PKG_JSON" ] && [ "$FORCE" -ne 1 ]; then
echo " skip (exists, use --force): package.json"
"packageManager": "pnpm@10.0.0",
"engines": { "node": ">=20.0.0" },
"preview": "node ./dist/server/entry.mjs",
"format": "biome format --write ."
"astro": "$ASTRO_VERSION",
"@astrojs/node": "$NODE_ADAPTER_VERSION",
"@dmwd-io/design-system": "$DESIGN_SYSTEM_VERSION"
"@astrojs/ts-plugin": "$TS_PLUGIN_VERSION",
"@biomejs/biome": "$BIOME_VERSION",
"typescript": "$TYPESCRIPT_VERSION"
echo " wrote: package.json"
# --- 3. supporting files ----------------------------------------------------
GITIGNORE="$TARGET/.gitignore"
if [ -e "$GITIGNORE" ] && [ "$FORCE" -ne 1 ]; then
echo " skip (exists, use --force): .gitignore"
cat > "$GITIGNORE" <<'GIT'
# env / secrets — never commit
echo " wrote: .gitignore"
ENV_EXAMPLE="$TARGET/.env.example"
if [ -e "$ENV_EXAMPLE" ] && [ "$FORCE" -ne 1 ]; then
echo " skip (exists, use --force): .env.example"
cat > "$ENV_EXAMPLE" <<'ENV'
# Copy to .env.local and fill in. .env.local is gitignored.
# Required to install the private @dmwd-io design-system from GitHub Packages.
# A GitHub PAT (or CI GITHUB_TOKEN) with read:packages scope.
# Astro standalone server bind address (defaults are fine locally).
echo " wrote: .env.example"
# Minimal runnable entry so `task dev` shows a page immediately.
INDEX="$TARGET/src/pages/index.astro"
if [ -e "$INDEX" ] && [ "$FORCE" -ne 1 ]; then
echo " skip (exists, use --force): src/pages/index.astro"
mkdir -p "$TARGET/src/pages"
// SSR by default (astro.config.mjs output: "server").
// Opt a page out with: export const prerender = true;
const now = new Date().toLocaleString("en-US");
<meta name="viewport" content="width=device-width" />
<title>dmwd-io Astro starter</title>
<h1>dmwd-io Astro 5 starter</h1>
<p>Server-rendered at {now}. Edit <code>src/pages/index.astro</code>.</p>
echo " wrote: src/pages/index.astro"
# Keep the blog content dir tracked so the content collection resolves.
mkdir -p "$TARGET/src/content/blog"
[ -e "$TARGET/src/content/blog/.gitkeep" ] || : > "$TARGET/src/content/blog/.gitkeep"
# --- 4. install -------------------------------------------------------------
if [ "$DO_INSTALL" -eq 1 ]; then
if [ -z "${NPM_TOKEN:-}" ]; then
echo " WARNING: NPM_TOKEN is not set. @dmwd-io/design-system will 401."
echo " Export it, then re-run: cd $TARGET && pnpm install"
echo " (Skipping install so the scaffold still completes.)"
(cd "$TARGET" && pnpm install)
echo " --no-install: skipped dependency install."
[ "$DO_INSTALL" -eq 1 ] && [ -z "${NPM_TOKEN:-}" ] && echo " export NPM_TOKEN=... # then: pnpm install"
echo " task dev # or: pnpm dev"