#!/usr/bin/env bash # User onboarding: renders the user profile (user/USER.md) from the canon # template. User name is REQUIRED; everything else is optional at this time # (skillset is intended to be filled ongoing by agents in later layers). # # Usage: # scripts/onboard.sh # interactive prompts (TTY) # scripts/onboard.sh --name "Jason" [--timezone "Europe/Berlin"] [...] # # Fields: name (required), profession, marital-status, age, gender, # education, location, timezone, skillset, interests, hobbies, pets # # The rendered profile is dispatched to every agent launch. Re-running # onboarding replaces the profile (the previous file is kept as .bak). set -euo pipefail cd "$(dirname "$0")/.." # shellcheck source=common.sh source scripts/common.sh load_config load_release bootstrap_runtime_dir TEMPLATE="templates/USER.md" OUT_DIR="$MOSAIC_DEV_DIR/user" OUT="$OUT_DIR/USER.md" [ -f "$TEMPLATE" ] || { echo "onboard: template missing: $TEMPLATE" >&2; exit 4; } NAME="" PROFESSION="" MARITAL_STATUS="" AGE="" GENDER="" EDUCATION="" LOCATION="" TIMEZONE="" SKILLSET="" INTERESTS="" HOBBIES="" PETS="" while [ $# -gt 0 ]; do case "$1" in --name) NAME="${2:-}"; shift 2 ;; --profession) PROFESSION="${2:-}"; shift 2 ;; --marital-status) MARITAL_STATUS="${2:-}"; shift 2 ;; --age) AGE="${2:-}"; shift 2 ;; --gender) GENDER="${2:-}"; shift 2 ;; --education) EDUCATION="${2:-}"; shift 2 ;; --location) LOCATION="${2:-}"; shift 2 ;; --timezone) TIMEZONE="${2:-}"; shift 2 ;; --skillset) SKILLSET="${2:-}"; shift 2 ;; --interests) INTERESTS="${2:-}"; shift 2 ;; --hobbies) HOBBIES="${2:-}"; shift 2 ;; --pets) PETS="${2:-}"; shift 2 ;; *) echo "onboard: unknown option: $1" >&2; exit 4 ;; esac done INTERACTIVE="no" if [ -t 0 ] && [ -t 1 ]; then INTERACTIVE="yes"; fi trim() { local v="$1"; v="${v#"${v%%[![:space:]]*}"}"; v="${v%"${v##*[![:space:]]}"}"; printf '%s' "$v"; } valid_value() { local v="$1"; [ -n "$v" ] && [ "${#v}" -le 500 ] && [ "${v//$'\n'/}" = "$v" ] && [ "${v//$'\r'/}" = "$v" ]; } # Required: name if [ -z "$(trim "$NAME")" ]; then if [ "$INTERACTIVE" = "yes" ]; then while [ -z "$(trim "$NAME")" ]; do read -r -p "Your name (required): " NAME || NAME="" NAME="$(trim "$NAME")" done else echo "onboard: user name is required (use --name \"...\", or run from a terminal)" >&2 exit 4 fi fi NAME="$(trim "$NAME")" valid_value "$NAME" || { echo "onboard: invalid name" >&2; exit 2; } # Optional fields: prompt interactively when not supplied by flag ask_optional() { # varname label current local __label="$2" __val="" if [ -z "$(trim "$3")" ] && [ "$INTERACTIVE" = "yes" ]; then read -r -p "$__label (optional, Enter to skip): " __val || __val="" __val="$(trim "$__val")" if valid_value "$__val"; then printf '%s' "$__val"; return; fi printf '%s' "$3" else printf '%s' "$3" fi } PROFESSION="$(ask_optional PROFESSION "Profession" "$PROFESSION")" MARITAL_STATUS="$(ask_optional MARITAL_STATUS "Marital status" "$MARITAL_STATUS")" AGE="$(ask_optional AGE "Age" "$AGE")" GENDER="$(ask_optional GENDER "Gender" "$GENDER")" EDUCATION="$(ask_optional EDUCATION "Education level" "$EDUCATION")" LOCATION="$(ask_optional LOCATION "Location" "$LOCATION")" TIMEZONE="$(ask_optional TIMEZONE "Timezone" "$TIMEZONE")" SKILLSET="$(ask_optional SKILLSET "Skillset" "$SKILLSET")" INTERESTS="$(ask_optional INTERESTS "Interests" "$INTERESTS")" HOBBIES="$(ask_optional HOBBIES "Hobbies" "$HOBBIES")" PETS="$(ask_optional PETS "Pets" "$PETS")" for pair in "profession:$PROFESSION" "marital:$MARITAL_STATUS" "age:$AGE" "gender:$GENDER" "education:$EDUCATION" "location:$LOCATION" "timezone:$TIMEZONE" "skillset:$SKILLSET" "interests:$INTERESTS" "hobbies:$HOBBIES" "pets:$PETS"; do val="${pair#*:}" [ -n "$(trim "$val")" ] || continue # empty optional fields are valid valid_value "$(trim "$val")" || { echo "onboard: invalid value for ${pair%%:*}" >&2; exit 2; } done mkdir -p "$OUT_DIR" # Preserve the previous profile before replacing it (onboard re-runs are # deliberate, but the old copy costs nothing to keep). [ -f "$OUT" ] && cp -f "$OUT" "$OUT_DIR/USER.md.bak" # Render: template placeholders -> values; unfilled -> "(not provided)". # Node keeps the rendering exact regardless of slashes/& in values. NAME="$NAME" PROFESSION="$PROFESSION" MARITAL_STATUS="$MARITAL_STATUS" AGE="$AGE" \ GENDER="$GENDER" EDUCATION="$EDUCATION" LOCATION="$LOCATION" TIMEZONE="$TIMEZONE" \ SKILLSET="$SKILLSET" INTERESTS="$INTERESTS" HOBBIES="$HOBBIES" PETS="$PETS" \ node -e ' const fs = require("fs"); const t = fs.readFileSync(process.argv[1], "utf8"); const v = process.env; const out = t.replace(/\{\{(\w+)\}\}/g, (m, k) => { const val = v[k]; return val === undefined || val === "" ? "(not provided)" : val; }); fs.writeFileSync(process.argv[2], out); ' "$TEMPLATE" "$OUT.new" mv -f "$OUT.new" "$OUT" echo "onboard: user profile written: $OUT" echo "onboard: dispatched to every agent launch; edit freely or re-run onboard"