From f2e1e7c11cd6b09f4681dfef1ae60676a517fc15 Mon Sep 17 00:00:00 2001 From: Scott Yeager Date: Wed, 15 Oct 2025 16:40:45 -0700 Subject: [PATCH 01/11] Update install script --- install_v.sh | 542 ++++++++++++++++++++------------------------------- 1 file changed, 209 insertions(+), 333 deletions(-) diff --git a/install_v.sh b/install_v.sh index 3b98561b..09fe5127 100755 --- a/install_v.sh +++ b/install_v.sh @@ -1,8 +1,25 @@ #!/bin/bash set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" +#============================================================================== +# GLOBAL VARIABLES +#============================================================================== +RESET=false +REMOVE=false +INSTALL_ANALYZER=false +HEROLIB=false +START_REDIS=false + +export DIR_BASE="$HOME" +export DIR_BUILD="/tmp" +export DIR_CODE="$DIR_BASE/code" +export DIR_CODE_V="$DIR_BASE/_code" +export OSNAME="" + + +#============================================================================== +# FUNCTION DEFINITIONS +#============================================================================== # Help function print_help() { @@ -16,6 +33,7 @@ print_help() { echo " --remove Remove V installation and exit" echo " --analyzer Install/update v-analyzer" echo " --herolib Install our herolib" + echo " --start-redis Start the Redis service if installed" echo echo "Examples:" echo " $0" @@ -27,38 +45,6 @@ print_help() { echo } -# Parse arguments -RESET=false -REMOVE=false -INSTALL_ANALYZER=false -HEROLIB=false - -for arg in "$@"; do - case $arg in - -h|--help) - print_help - exit 0 - ;; - --reset) - RESET=true - ;; - --remove) - REMOVE=true - ;; - --herolib) - HEROLIB=true - ;; - --analyzer) - INSTALL_ANALYZER=true - ;; - *) - echo "Unknown option: $arg" - echo "Use -h or --help to see available options" - exit 1 - ;; - esac -done - # Function to check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 @@ -80,25 +66,20 @@ function run_sudo() { fi } -export DIR_BASE="$HOME" -export DIR_BUILD="/tmp" -export DIR_CODE="$DIR_BASE/code" -export DIR_CODE_V="$DIR_BASE/_code" - check_release() { if ! command -v lsb_release >/dev/null 2>&1; then echo "❌ lsb_release command not found. Install 'lsb-release' package first." exit 1 fi - + CODENAME=$(lsb_release -sc) RELEASE=$(lsb_release -rs) - + if dpkg --compare-versions "$RELEASE" lt "24.04"; then echo "ℹ️ Detected Ubuntu $RELEASE ($CODENAME). Skipping mirror fix (requires 24.04+)." return 1 fi - + return 0 } @@ -108,25 +89,25 @@ ubuntu_sources_fix() { echo "ℹ️ Not running on Ubuntu. Skipping mirror fix." return 1 fi - + if check_release; then local CODENAME CODENAME=$(lsb_release -sc) local TIMESTAMP TIMESTAMP=$(date +%Y%m%d_%H%M%S) - + echo "🔎 Fixing apt mirror setup for Ubuntu $(lsb_release -rs) ($CODENAME)..." - + if [ -f /etc/apt/sources.list ]; then echo "📦 Backing up /etc/apt/sources.list -> /etc/apt/sources.list.backup.$TIMESTAMP" sudo mv /etc/apt/sources.list /etc/apt/sources.list.backup.$TIMESTAMP fi - + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then echo "📦 Backing up /etc/apt/sources.list.d/ubuntu.sources -> /etc/apt/sources.list.d/ubuntu.sources.backup.$TIMESTAMP" sudo mv /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup.$TIMESTAMP fi - + echo "📝 Writing new /etc/apt/sources.list.d/ubuntu.sources" sudo tee /etc/apt/sources.list.d/ubuntu.sources >/dev/null <> ~/.ssh/known_hosts fi git config --global pull.rebase false - + } -function package_check_install { - local command_name="$1" - if command -v "$command_name" >/dev/null 2>&1; then - echo "command '$command_name' is already installed." - else - package_install '$command_name' - fi -} - -function package_install { - local command_name="$1" - if [[ "${OSNAME}" == "ubuntu" ]]; then - if is_github_actions; then - run_sudo apt -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" install $1 -q -y --allow-downgrades --allow-remove-essential - else - apt -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" install $1 -q -y --allow-downgrades --allow-remove-essential - fi - - elif [[ "${OSNAME}" == "darwin"* ]]; then - brew install $command_name - elif [[ "${OSNAME}" == "alpine"* ]]; then - apk add $command_name - elif [[ "${OSNAME}" == "arch"* ]]; then - pacman --noconfirm -Su $command_name - else - echo "platform : ${OSNAME} not supported" - exit 1 - fi +# Performs a non-interactive, forceful apt installation. +# WARNING: This is designed for CI/automated environments. It can be dangerous +# on a personal machine as it may remove essential packages to resolve conflicts. +function apt_force_install { + run_sudo apt -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" install "$1" -q -y --allow-downgrades --allow-remove-essential } is_github_actions() { @@ -201,11 +159,10 @@ is_github_actions() { fi } - function myplatform { if [[ "${OSTYPE}" == "darwin"* ]]; then export OSNAME='darwin' - elif [ -e /etc/os-release ]; then + elif [ -e /etc/os-release ]; then # Read the ID field from the /etc/os-release file export OSNAME=$(grep '^ID=' /etc/os-release | cut -d= -f2) if [ "${OSNAME,,}" == "ubuntu" ]; then @@ -221,27 +178,14 @@ function myplatform { echo "Unable to determine the operating system." exit 1 fi - - - # if [ "$(uname -m)" == "x86_64" ]; then - # echo "This system is running a 64-bit processor." - # else - # echo "This system is not running a 64-bit processor." - # exit 1 - # fi - } -myplatform - -function os_update { +function update_system { + echo ' - System Update' if [[ "${OSNAME}" == "ubuntu" ]]; then ubuntu_sources_fix - fi - echo ' - os update' - if [[ "${OSNAME}" == "ubuntu" ]]; then if is_github_actions; then - echo "github actions" + echo "github actions: preparing system" else rm -f /var/lib/apt/lists/lock rm -f /var/cache/apt/archives/lock @@ -252,54 +196,56 @@ function os_update { run_sudo dpkg --configure -a run_sudo apt update -y if is_github_actions; then - echo "** IN GITHUB ACTIONS, DON'T DO UPDATE" + echo "** IN GITHUB ACTIONS, DON'T DO SYSTEM UPGRADE" else set +e - echo "** UPDATE" + echo "** System Upgrade" apt-mark hold grub-efi-amd64-signed set -e apt upgrade -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes apt autoremove -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes fi - #apt install apt-transport-https ca-certificates curl software-properties-common -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes - package_install "apt-transport-https ca-certificates curl wget software-properties-common tmux make gcc" - package_install "rclone rsync mc redis-server screen net-tools git dnsutils htop ca-certificates screen lsb-release binutils pkg-config libssl-dev iproute2" - - elif [[ "${OSNAME}" == "darwin"* ]]; then - if command -v brew >/dev/null 2>&1; then - echo ' - homebrew installed' - else + elif [[ "${OSNAME}" == "darwin"* ]]; then + if ! command -v brew >/dev/null 2>&1; then + echo ' - Installing Homebrew' export NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" unset NONINTERACTIVE fi + elif [[ "${OSNAME}" == "alpine"* ]]; then + apk update + elif [[ "${OSNAME}" == "arch"* ]]; then + pacman -Syyu --noconfirm + fi + echo ' - System Update Done' +} + +function install_packages { + echo ' - Installing Packages' + if [[ "${OSNAME}" == "ubuntu" ]]; then + apt_force_install apt-transport-https ca-certificates curl wget software-properties-common tmux make gcc rclone rsync mc redis-server screen net-tools git dnsutils htop lsb-release binutils pkg-config libssl-dev iproute2 + elif [[ "${OSNAME}" == "darwin"* ]]; then + # The set +e is to prevent script failure if some packages are already installed. set +e brew install mc redis curl tmux screen htop wget rclone tcc set -e - elif [[ "${OSNAME}" == "alpine"* ]]; then - apk update screen git htop tmux - apk add mc curl rsync htop redis bash bash-completion screen git rclone + elif [[ "${OSNAME}" == "alpine"* ]]; then + apk add --no-cache screen git htop tmux mc curl rsync redis bash bash-completion rclone + # Switch default shell to bash for better interactive use sed -i 's#/bin/ash#/bin/bash#g' /etc/passwd - elif [[ "${OSNAME}" == "arch"* ]]; then - pacman -Syy --noconfirm - pacman -Syu --noconfirm - pacman -Su --noconfirm arch-install-scripts gcc mc git tmux curl htop redis wget screen net-tools git sudo htop ca-certificates lsb-release screen rclone - + elif [[ "${OSNAME}" == "arch"* ]]; then + pacman -Su --noconfirm arch-install-scripts gcc mc git tmux curl htop redis wget screen net-tools sudo lsb-release rclone + # Check if builduser exists, create if not if ! id -u builduser > /dev/null 2>&1; then useradd -m builduser echo "builduser:$(openssl rand -base64 32 | sha256sum | base64 | head -c 32)" | chpasswd echo 'builduser ALL=(ALL) NOPASSWD: ALL' | tee /etc/sudoers.d/builduser fi - - # if [[ -n "${DEBUG}" ]]; then - # execute_with_marker "paru_install" paru_install - # fi fi - echo ' - os update done' + echo ' - Package Installation Done' } - function hero_lib_pull { pushd $DIR_CODE/github/incubaid/herolib 2>&1 >> /dev/null if [[ $(git status -s) ]]; then @@ -311,7 +257,7 @@ function hero_lib_pull { } function hero_lib_get { - + mkdir -p $DIR_CODE/github/incubaid if [[ -d "$DIR_CODE/github/incubaid/herolib" ]] then @@ -323,44 +269,6 @@ function hero_lib_get { fi } -# function install_secp256k1 { - -# echo "Installing secp256k1..." -# if [[ "${OSNAME}" == "darwin"* ]]; then -# # Attempt installation only if not already found -# echo "Attempting secp256k1 installation via Homebrew..." -# brew install secp256k1 -# elif [[ "${OSNAME}" == "ubuntu" ]]; then -# # Install build dependencies -# package_install "build-essential wget autoconf libtool" - -# # Download and extract secp256k1 -# cd "${DIR_BUILD}" -# wget https://github.com/bitcoin-core/secp256k1/archive/refs/tags/v0.3.2.tar.gz -# tar -xvf v0.3.2.tar.gz - -# # Build and install -# cd secp256k1-0.3.2/ -# ./autogen.sh -# ./configure -# make -j 5 -# if is_github_actions; then -# run_sudo make install -# else -# make install -# fi - -# # Cleanup -# cd .. -# rm -rf secp256k1-0.3.2 v0.3.2.tar.gz -# else -# echo "secp256k1 installation not implemented for ${OSNAME}" -# exit 1 -# fi -# echo "secp256k1 installation complete!" -# } - - remove_all() { echo "Removing V installation..." # Set reset to true to use existing reset functionality @@ -377,7 +285,7 @@ remove_all() { echo "Removing v-analyzer from system..." run_sudo rm -f $(which v-analyzer) fi - + # Remove v-analyzer path from rc files for RC_FILE in ~/.zshrc ~/.bashrc; do if [ -f "$RC_FILE" ]; then @@ -393,127 +301,54 @@ remove_all() { echo "Cleaned up $RC_FILE" fi done - + echo "V removal complete" } +# Starts the Redis service if it is not already running. +function start_redis_service() { + echo "Attempting to start Redis service..." + # Check if redis-server is even installed + if ! command_exists redis-server; then + echo "Warning: redis-server command not found. Skipping." + return 0 + fi + # Check if redis is already running by pinging it + if redis-cli ping > /dev/null 2>&1; then + echo "Redis is already running." + return 0 + fi -# Function to check if a service is running and start it if needed -check_and_start_redis() { - - # Normal service management for non-container environments - if [[ "${OSNAME}" == "ubuntu" ]] || [[ "${OSNAME}" == "debian" ]]; then - - # Handle Redis installation for GitHub Actions environment - if is_github_actions; then - - # Import Redis GPG key - curl -fsSL https://packages.redis.io/gpg | run_sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg - # Add Redis repository - echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | run_sudo tee /etc/apt/sources.list.d/redis.list - # Install Redis - run_sudo apt-get update - run_sudo apt-get install -y redis - - # Start Redis - redis-server --daemonize yes - - # Print versions - redis-cli --version - redis-server --version - - return - fi - - # Check if running inside a container - if grep -q "/docker/" /proc/1/cgroup || [ ! -d "/run/systemd/system" ]; then - echo "Running inside a container. Starting redis directly." - - if pgrep redis-server > /dev/null; then - echo "redis is already running." - else - echo "redis is not running. Starting it in the background..." - redis-server --daemonize yes - if pgrep redis-server > /dev/null; then - echo "redis started successfully." - else - echo "Failed to start redis. Please check logs for details." - exit 1 - fi - fi - return - fi - - if command_exists zinit; then - # Check if redis service is managed by zinit and is running - if zinit status redis | grep -q "state: Running"; then - echo "redis is already running and managed by zinit." - return - else - echo "zinit is installed, but redis is not running or not managed by zinit. Proceeding with other checks." - fi - fi - - if systemctl is-active --quiet "redis"; then - echo "redis is already running." - else - echo "redis is not running. Starting it..." - run_sudo systemctl start "redis" - if systemctl is-active --quiet "redis"; then - echo "redis started successfully." - else - echo "Failed to start redis. Please check logs for details." - exit 1 - fi - fi - elif [[ "${OSNAME}" == "darwin"* ]]; then - # Check if we're in GitHub Actions - if is_github_actions; then - echo "Running in GitHub Actions on macOS. Starting redis directly..." - if pgrep redis-server > /dev/null; then - echo "redis is already running." - else - echo "redis is not running. Starting it in the background..." - redis-server --daemonize yes - if pgrep redis-server > /dev/null; then - echo "redis started successfully." - else - echo "Failed to start redis. Please check logs for details." - exit 1 - fi - fi - else - # For regular macOS environments, use brew services - if brew services list | grep -q "^redis.*started"; then - echo "redis is already running." - else - echo "redis is not running. Starting it..." - brew services start redis - fi - fi - elif [[ "${OSNAME}" == "alpine"* ]]; then - if rc-service "redis" status | grep -q "running"; then - echo "redis is already running." - else - echo "redis is not running. Starting it..." - rc-service "redis" start - fi - elif [[ "${OSNAME}" == "arch"* ]]; then - if systemctl is-active --quiet "redis"; then - echo "redis is already running." - else - echo "redis is not running. Starting it..." - run_sudo systemctl start "redis" + echo "Redis is not running. Attempting to start it..." + if command_exists systemctl; then + run_sudo systemctl start redis + # For Alpine, use rc-service + elif command_exists rc-service; then + run_sudo rc-service redis start + elif [[ "${OSNAME}" == "darwin"* ]]; then + # For macOS, use brew services + if ! brew services list | grep -q "^redis.*started"; then + brew services start redis fi else - echo "Service management for redis is not implemented for platform: $OSNAME" + echo "No service manager found, starting Redis manually..." + redis-server --daemonize yes + return 1 + fi + + # Final check to see if it started + sleep 1 # Give it a second to start up + if redis-cli ping > /dev/null 2>&1; then + echo "Redis started successfully." + else + echo "Error: Failed to start Redis." exit 1 fi } v-install() { - + # Check if v is already installed and in PATH if command_exists v; then echo "V is already installed and in PATH." @@ -521,8 +356,8 @@ v-install() { # For now, just exit the function assuming it's okay return 0 fi - - + + # Only clone and install if directory doesn't exist # Note: The original check was for ~/code/v, but the installation happens in ~/_code/v. if [ ! -d ~/_code/v ]; then @@ -535,8 +370,8 @@ v-install() { exit 1 fi fi - - + + # Only clone and install if directory doesn't exist # Note: The original check was for ~/code/v, but the installation happens in ~/_code/v. # Adjusting the check to the actual installation directory. @@ -555,43 +390,43 @@ v-install() { fi echo "V built successfully. Creating symlink..." run_sudo ./v symlink - + # Verify v is in path if ! command_exists v; then echo "Error: V installation failed or not in PATH" echo "Please ensure ~/code/v is in your PATH" exit 1 fi - + echo "V installation successful!" - + } v-analyzer() { - + set -ex - + # Install v-analyzer if requested if [ "$INSTALL_ANALYZER" = true ]; then echo "Installing v-analyzer..." cd /tmp v download -RD https://raw.githubusercontent.com/vlang/v-analyzer/main/install.vsh - + # Check if v-analyzer bin directory exists if [ ! -d "$HOME/.config/v-analyzer/bin" ]; then echo "Error: v-analyzer bin directory not found at $HOME/.config/v-analyzer/bin" echo "Please ensure v-analyzer was installed correctly" exit 1 fi - + echo "v-analyzer installation successful!" fi - + # Add v-analyzer to PATH if installed if [ -d "$HOME/.config/v-analyzer/bin" ]; then V_ANALYZER_PATH='export PATH="$PATH:$HOME/.config/v-analyzer/bin"' - + # Function to add path to rc file if not present add_to_rc() { local RC_FILE="$1" @@ -605,7 +440,7 @@ v-analyzer() { fi fi } - + # Add to both .zshrc and .bashrc if they exist add_to_rc ~/.zshrc if [ "$(uname)" = "Darwin" ] && [ -f ~/.bashrc ]; then @@ -615,70 +450,111 @@ v-analyzer() { } +#============================================================================== +# MAIN EXECUTION +#============================================================================== +main() { + # Make sure we're running in the directory where the script is + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + cd "$SCRIPT_DIR" -# Handle remove if requested -if [ "$REMOVE" = true ]; then - remove_all - exit 0 -fi + # Parse arguments + for arg in "$@"; do + case $arg in + -h|--help) + print_help + exit 0 + ;; + --reset) + RESET=true + ;; + --remove) + REMOVE=true + ;; + --herolib) + HEROLIB=true + ;; + --analyzer) + INSTALL_ANALYZER=true + ;; + --start-redis) + START_REDIS=true + ;; + *) + echo "Unknown option: $arg" + echo "Use -h or --help to see available options" + exit 1 + ;; + esac +done -# Create code directory if it doesn't exist -mkdir -p ~/code + myplatform + + # Handle remove if requested + if [ "$REMOVE" = true ]; then + remove_all + exit 0 + fi + + # Create code directory if it doesn't exist + mkdir -p ~/code -# Check if v needs to be installed -if [ "$RESET" = true ] || ! command_exists v; then - - os_update - - sshknownkeysadd - - # Install secp256k1 - - v-install - - - -fi + # Check if v needs to be installed + if [ "$RESET" = true ] || ! command_exists v; then -# set -x -check_and_start_redis + update_system + install_packages -if [ "$HEROLIB" = true ]; then - echo "=== Herolib Installation ===" - echo "Current directory: $(pwd)" - echo "Checking for install_herolib.vsh: $([ -f "./install_herolib.vsh" ] && echo "found" || echo "not found")" - echo "Checking for lib directory: $([ -d "./lib" ] && echo "found" || echo "not found")" + sshknownkeysadd - # Check if we're in GitHub Actions and already in the herolib directory - if is_github_actions; then - # In GitHub Actions, check if we're already in a herolib checkout - if [ -f "./install_herolib.vsh" ] && [ -d "./lib" ]; then - echo "✓ Running in GitHub Actions, using current directory for herolib installation" - HEROLIB_DIR="$(pwd)" + # Install secp256k1 + + v-install + fi + + if [ "$START_REDIS" = true ]; then + start_redis_service + fi + + if [ "$HEROLIB" = true ]; then + echo "=== Herolib Installation ===" + echo "Current directory: $(pwd)" + echo "Checking for install_herolib.vsh: $([ -f "./install_herolib.vsh" ] && echo "found" || echo "not found")" + echo "Checking for lib directory: $([ -d "./lib" ] && echo "found" || echo "not found")" + + # Check if we're in GitHub Actions and already in the herolib directory + if is_github_actions; then + # In GitHub Actions, check if we're already in a herolib checkout + if [ -f "./install_herolib.vsh" ] && [ -d "./lib" ]; then + echo "✓ Running in GitHub Actions, using current directory for herolib installation" + HEROLIB_DIR="$(pwd)" + else + echo "⚠ Running in GitHub Actions, but not in herolib directory. Cloning..." + hero_lib_get + HEROLIB_DIR="$HOME/code/github/incubaid/herolib" + fi else - echo "⚠ Running in GitHub Actions, but not in herolib directory. Cloning..." + echo "Not in GitHub Actions, using standard installation path" hero_lib_get HEROLIB_DIR="$HOME/code/github/incubaid/herolib" fi - else - echo "Not in GitHub Actions, using standard installation path" - hero_lib_get - HEROLIB_DIR="$HOME/code/github/incubaid/herolib" + + echo "Installing herolib from: $HEROLIB_DIR" + "$HEROLIB_DIR/install_herolib.vsh" fi - echo "Installing herolib from: $HEROLIB_DIR" - "$HEROLIB_DIR/install_herolib.vsh" -fi - -if [ "$INSTALL_ANALYZER" = true ]; then - # Only install v-analyzer if not in GitHub Actions environment - if ! is_github_actions; then - v-analyzer + if [ "$INSTALL_ANALYZER" = true ]; then + # Only install v-analyzer if not in GitHub Actions environment + if ! is_github_actions; then + v-analyzer + fi + echo "Run 'source ~/.bashrc' or 'source ~/.zshrc' to update PATH for v-analyzer" fi - echo "Run 'source ~/.bashrc' or 'source ~/.zshrc' to update PATH for v-analyzer" -fi -echo "Installation complete!" + echo "Installation complete!" +} + +main "$@" From 9f3f1914cef1e8962d88914bdcdb3a4a3815cd96 Mon Sep 17 00:00:00 2001 From: Scott Yeager Date: Mon, 20 Oct 2025 13:58:15 -0700 Subject: [PATCH 02/11] Remove build workarounds --- .github/workflows/hero_build.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/hero_build.yml b/.github/workflows/hero_build.yml index 272d4e04..be790bed 100644 --- a/.github/workflows/hero_build.yml +++ b/.github/workflows/hero_build.yml @@ -46,9 +46,6 @@ jobs: cd v make ./v symlink - if [ "${{ runner.os }}" = "macOS" ]; then - sudo sed -i '' '618,631d' /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/math.h - fi cd - mkdir -p ~/.vmodules/incubaid @@ -92,7 +89,7 @@ jobs: ' else - v -w -d use_openssl -enable-globals -gc none -cc tcc cli/hero.v -o cli/hero-${{ matrix.target }} + v -w -d use_openssl -enable-globals -cc clang cli/hero.v -o cli/hero-${{ matrix.target }} fi - name: Upload glibc binary From a2eaf6096e3b6f92ac9da488ef35d7cd310fbb46 Mon Sep 17 00:00:00 2001 From: despiegk Date: Thu, 6 Nov 2025 16:14:34 +0400 Subject: [PATCH 03/11] ... --- lib/clients/ipapi/readme.md | 4 ++-- lib/core/playcmds/play_all.v | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/clients/ipapi/readme.md b/lib/clients/ipapi/readme.md index 96346431..0e2422e5 100644 --- a/lib/clients/ipapi/readme.md +++ b/lib/clients/ipapi/readme.md @@ -1,11 +1,11 @@ -# ipapi +# ip api (IP INFO SERVICE CLIENT) To get started ```v -import incubaid.herolib.clients. ipapi +import incubaid.herolib.clients.ipapi mut client:= ipapi.get()! diff --git a/lib/core/playcmds/play_all.v b/lib/core/playcmds/play_all.v index 8632e5a3..3791b94c 100644 --- a/lib/core/playcmds/play_all.v +++ b/lib/core/playcmds/play_all.v @@ -25,6 +25,8 @@ import incubaid.herolib.installers.infra.coredns import incubaid.herolib.installers.infra.gitea import incubaid.herolib.installers.infra.livekit import incubaid.herolib.installers.infra.zinit_installer +import incubaid.herolib.installers.k8s.cryptpad +import incubaid.herolib.installers.k8s.element_chat import incubaid.herolib.installers.lang.golang import incubaid.herolib.installers.lang.nodejs import incubaid.herolib.installers.lang.python @@ -37,6 +39,7 @@ import incubaid.herolib.installers.threefold.griddriver import incubaid.herolib.installers.virt.cloudhypervisor import incubaid.herolib.installers.virt.docker import incubaid.herolib.installers.virt.herorunner +import incubaid.herolib.installers.virt.kubernetes_installer import incubaid.herolib.installers.virt.lima import incubaid.herolib.installers.virt.pacman import incubaid.herolib.installers.virt.podman @@ -80,6 +83,8 @@ pub fn run_all(args_ PlayArgs) ! { gitea.play(mut plbook)! livekit.play(mut plbook)! zinit_installer.play(mut plbook)! + cryptpad.play(mut plbook)! + element_chat.play(mut plbook)! golang.play(mut plbook)! nodejs.play(mut plbook)! python.play(mut plbook)! @@ -92,6 +97,7 @@ pub fn run_all(args_ PlayArgs) ! { cloudhypervisor.play(mut plbook)! docker.play(mut plbook)! herorunner.play(mut plbook)! + kubernetes_installer.play(mut plbook)! lima.play(mut plbook)! pacman.play(mut plbook)! podman.play(mut plbook)! From c033cacd5bf7098d9aae0e85237ba17588d5f9eb Mon Sep 17 00:00:00 2001 From: Scott Yeager Date: Wed, 12 Nov 2025 05:11:01 -0800 Subject: [PATCH 04/11] Fix redis package name for alpine --- lib/installers/base/installer.v | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/installers/base/installer.v b/lib/installers/base/installer.v index c280bbf1..ba31a6b4 100644 --- a/lib/installers/base/installer.v +++ b/lib/installers/base/installer.v @@ -59,7 +59,7 @@ pub fn install(args_ InstallArgs) ! { } else if pl == .alpine { console.print_header(' - Alpine prepare') osal.package_refresh()! - osal.package_install('git,curl,mc,tmux,screen,git-lfs,redis-server')! + osal.package_install('git,curl,mc,tmux,screen,git-lfs,redis')! } else if pl == .arch { console.print_header(' - Arch prepare') osal.package_refresh()! From 1958f24528a33d6095eb366e26ff814cc7b20eb5 Mon Sep 17 00:00:00 2001 From: Scott Yeager Date: Wed, 12 Nov 2025 05:23:34 -0800 Subject: [PATCH 05/11] Add herolib version arg --- install_v.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/install_v.sh b/install_v.sh index 09fe5127..66ce3d63 100755 --- a/install_v.sh +++ b/install_v.sh @@ -33,6 +33,7 @@ print_help() { echo " --remove Remove V installation and exit" echo " --analyzer Install/update v-analyzer" echo " --herolib Install our herolib" + echo " --herolib-version VERSION Install specific herolib tag/branch (default: development)" echo " --start-redis Start the Redis service if installed" echo echo "Examples:" @@ -267,6 +268,17 @@ function hero_lib_get { git clone --depth 1 --no-single-branch https://github.com/incubaid/herolib.git popd 2>&1 >> /dev/null fi + + # Checkout specific version if requested + if [ -n "${HEROLIB_VERSION:-}" ]; then + pushd $DIR_CODE/github/incubaid/herolib 2>&1 >> /dev/null + if ! git checkout "$HEROLIB_VERSION"; then + echo "Failed to checkout herolib version: $HEROLIB_VERSION" + popd 2>&1 >> /dev/null + return 1 + fi + popd 2>&1 >> /dev/null + fi } remove_all() { @@ -474,6 +486,15 @@ main() { --herolib) HEROLIB=true ;; + --herolib-version) + if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then + HEROLIB_VERSION="$2" + shift + else + echo "Error: --herolib-version requires a version argument" + exit 1 + fi + ;; --analyzer) INSTALL_ANALYZER=true ;; From 7246223e3b3e20850083509e7a32e39713998dbc Mon Sep 17 00:00:00 2001 From: Scott Yeager Date: Wed, 12 Nov 2025 05:37:56 -0800 Subject: [PATCH 06/11] Use run_sudo everywhere --- install_v.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/install_v.sh b/install_v.sh index 66ce3d63..8ab6af5d 100755 --- a/install_v.sh +++ b/install_v.sh @@ -101,16 +101,16 @@ ubuntu_sources_fix() { if [ -f /etc/apt/sources.list ]; then echo "📦 Backing up /etc/apt/sources.list -> /etc/apt/sources.list.backup.$TIMESTAMP" - sudo mv /etc/apt/sources.list /etc/apt/sources.list.backup.$TIMESTAMP + run_sudo mv /etc/apt/sources.list /etc/apt/sources.list.backup.$TIMESTAMP fi if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then echo "📦 Backing up /etc/apt/sources.list.d/ubuntu.sources -> /etc/apt/sources.list.d/ubuntu.sources.backup.$TIMESTAMP" - sudo mv /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup.$TIMESTAMP + run_sudo mv /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup.$TIMESTAMP fi - echo "📝 Writing new /etc/apt/sources.list.d/ubuntu.sources" - sudo tee /etc/apt/sources.list.d/ubuntu.sources >/dev/null </dev/null < Date: Thu, 13 Nov 2025 08:04:17 -0800 Subject: [PATCH 07/11] Update installer --- install_v.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/install_v.sh b/install_v.sh index 8ab6af5d..03ecf1b1 100755 --- a/install_v.sh +++ b/install_v.sh @@ -33,7 +33,7 @@ print_help() { echo " --remove Remove V installation and exit" echo " --analyzer Install/update v-analyzer" echo " --herolib Install our herolib" - echo " --herolib-version VERSION Install specific herolib tag/branch (default: development)" + echo " --herolib-version=VERSION Install specific herolib tag/branch (default: development)" echo " --start-redis Start the Redis service if installed" echo echo "Examples:" @@ -145,7 +145,7 @@ function sshknownkeysadd { # WARNING: This is designed for CI/automated environments. It can be dangerous # on a personal machine as it may remove essential packages to resolve conflicts. function apt_force_install { - run_sudo apt -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" install "$1" -q -y --allow-downgrades --allow-remove-essential + run_sudo apt -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" install "$@" -q -y --allow-downgrades --allow-remove-essential } is_github_actions() { @@ -486,12 +486,11 @@ main() { --herolib) HEROLIB=true ;; - --herolib-version) - if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then - HEROLIB_VERSION="$2" - shift - else + --herolib-version=*) + HEROLIB_VERSION="${arg#*=}" + if [ -z "$HEROLIB_VERSION" ]; then echo "Error: --herolib-version requires a version argument" + echo "Example: $0 --herolib-version=v1.0.0" exit 1 fi ;; From e7611d4dc25af981ef899095cd2d3d166843d7fa Mon Sep 17 00:00:00 2001 From: despiegk Date: Fri, 14 Nov 2025 08:51:32 +0200 Subject: [PATCH 08/11] ... --- lib/core/generator/generic/generate.v | 3 +-- lib/core/playcmds/factory.v | 1 - lib/core/playcmds/play_all.v | 8 ++++++++ lib/data/atlas/client/client.v | 2 +- lib/data/atlas/error.v | 26 +++++++++++++------------- lib/web/docusaurus/config.v | 8 ++++---- lib/web/site/model_page.v | 20 ++++++++++---------- 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/lib/core/generator/generic/generate.v b/lib/core/generator/generic/generate.v index defd790b..534e25b8 100644 --- a/lib/core/generator/generic/generate.v +++ b/lib/core/generator/generic/generate.v @@ -205,8 +205,7 @@ pub fn create_heroscript(args ModuleMeta) ! { '1' } else { '0' - } - }" + }}" } if !os.exists(args.path) { os.mkdir(args.path)! diff --git a/lib/core/playcmds/factory.v b/lib/core/playcmds/factory.v index 05232aaf..e5876ef0 100644 --- a/lib/core/playcmds/factory.v +++ b/lib/core/playcmds/factory.v @@ -57,7 +57,6 @@ pub fn run(args_ PlayArgs) ! { // Website / docs site.play(mut plbook)! - incatokens.play(mut plbook)! atlas.play(mut plbook)! docusaurus.play(mut plbook)! diff --git a/lib/core/playcmds/play_all.v b/lib/core/playcmds/play_all.v index 8632e5a3..3c04824b 100644 --- a/lib/core/playcmds/play_all.v +++ b/lib/core/playcmds/play_all.v @@ -25,6 +25,8 @@ import incubaid.herolib.installers.infra.coredns import incubaid.herolib.installers.infra.gitea import incubaid.herolib.installers.infra.livekit import incubaid.herolib.installers.infra.zinit_installer +import incubaid.herolib.installers.k8s.cryptpad +import incubaid.herolib.installers.k8s.element_chat import incubaid.herolib.installers.lang.golang import incubaid.herolib.installers.lang.nodejs import incubaid.herolib.installers.lang.python @@ -37,7 +39,9 @@ import incubaid.herolib.installers.threefold.griddriver import incubaid.herolib.installers.virt.cloudhypervisor import incubaid.herolib.installers.virt.docker import incubaid.herolib.installers.virt.herorunner +import incubaid.herolib.installers.virt.kubernetes_installer import incubaid.herolib.installers.virt.lima +import incubaid.herolib.installers.virt.myhypervisor import incubaid.herolib.installers.virt.pacman import incubaid.herolib.installers.virt.podman import incubaid.herolib.installers.virt.youki @@ -80,6 +84,8 @@ pub fn run_all(args_ PlayArgs) ! { gitea.play(mut plbook)! livekit.play(mut plbook)! zinit_installer.play(mut plbook)! + cryptpad.play(mut plbook)! + element_chat.play(mut plbook)! golang.play(mut plbook)! nodejs.play(mut plbook)! python.play(mut plbook)! @@ -92,7 +98,9 @@ pub fn run_all(args_ PlayArgs) ! { cloudhypervisor.play(mut plbook)! docker.play(mut plbook)! herorunner.play(mut plbook)! + kubernetes_installer.play(mut plbook)! lima.play(mut plbook)! + myhypervisor.play(mut plbook)! pacman.play(mut plbook)! podman.play(mut plbook)! youki.play(mut plbook)! diff --git a/lib/data/atlas/client/client.v b/lib/data/atlas/client/client.v index 2365d3cb..01140d90 100644 --- a/lib/data/atlas/client/client.v +++ b/lib/data/atlas/client/client.v @@ -313,7 +313,7 @@ pub fn (mut c AtlasClient) copy_files(collection_name string, page_name string, } if link.status == .external { continue - } + } // println(link) // Get file path and copy file_path := c.get_file_path(link.target_collection_name, link.target_item_name)! diff --git a/lib/data/atlas/error.v b/lib/data/atlas/error.v index 0245f27c..4ed8d1db 100644 --- a/lib/data/atlas/error.v +++ b/lib/data/atlas/error.v @@ -1,34 +1,34 @@ module atlas pub struct CollectionNotFound { - Error + Error pub: - name string - msg string + name string + msg string } pub fn (err CollectionNotFound) msg() string { - return 'Collection ${err.name} not found: ${err.msg}' + return 'Collection ${err.name} not found: ${err.msg}' } pub struct PageNotFound { - Error + Error pub: - collection string - page string + collection string + page string } pub fn (err PageNotFound) msg() string { - return 'Page ${err.page} not found in collection ${err.collection}' + return 'Page ${err.page} not found in collection ${err.collection}' } pub struct FileNotFound { - Error + Error pub: - collection string - file string + collection string + file string } pub fn (err FileNotFound) msg() string { - return 'File ${err.file} not found in collection ${err.collection}' -} \ No newline at end of file + return 'File ${err.file} not found in collection ${err.collection}' +} diff --git a/lib/web/docusaurus/config.v b/lib/web/docusaurus/config.v index 13df4e52..6418be43 100644 --- a/lib/web/docusaurus/config.v +++ b/lib/web/docusaurus/config.v @@ -18,8 +18,8 @@ pub mut: template_update bool coderoot string // Client configuration - use_atlas bool // true = atlas_client, false = doctreeclient - atlas_dir string // Required when use_atlas = true + use_atlas bool // true = atlas_client, false = doctreeclient + atlas_dir string // Required when use_atlas = true } @[params] @@ -32,8 +32,8 @@ pub mut: template_update bool coderoot string // Client configuration - use_atlas bool // true = atlas_client, false = doctreeclient - atlas_dir string // Required when use_atlas = true + use_atlas bool // true = atlas_client, false = doctreeclient + atlas_dir string // Required when use_atlas = true } // return the last know config diff --git a/lib/web/site/model_page.v b/lib/web/site/model_page.v index 8b188900..30bfeaed 100644 --- a/lib/web/site/model_page.v +++ b/lib/web/site/model_page.v @@ -2,15 +2,15 @@ module site pub struct Page { pub mut: - name string - title string - description string - draft bool - position int - hide_title bool - src string @[required] // always in format collection:page_name, can use the default collection if no : specified - path string @[required] //is without the page name, so just the path to the folder where the page is in + name string + title string + description string + draft bool + position int + hide_title bool + src string @[required] // always in format collection:page_name, can use the default collection if no : specified + path string @[required] // is without the page name, so just the path to the folder where the page is in section_name string - title_nr int - slug string + title_nr int + slug string } From 78d848783a4f91e01310764d82beffe0829ea772 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:26:45 +0100 Subject: [PATCH 09/11] fix breaking code --- lib/core/playcmds/play_all.v | 4 ++-- lib/schemas/jsonrpc/model_request.v | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/playcmds/play_all.v b/lib/core/playcmds/play_all.v index 3c04824b..10ce3a55 100644 --- a/lib/core/playcmds/play_all.v +++ b/lib/core/playcmds/play_all.v @@ -41,7 +41,7 @@ import incubaid.herolib.installers.virt.docker import incubaid.herolib.installers.virt.herorunner import incubaid.herolib.installers.virt.kubernetes_installer import incubaid.herolib.installers.virt.lima -import incubaid.herolib.installers.virt.myhypervisor +// import incubaid.herolib.installers.virt.myhypervisor import incubaid.herolib.installers.virt.pacman import incubaid.herolib.installers.virt.podman import incubaid.herolib.installers.virt.youki @@ -100,7 +100,7 @@ pub fn run_all(args_ PlayArgs) ! { herorunner.play(mut plbook)! kubernetes_installer.play(mut plbook)! lima.play(mut plbook)! - myhypervisor.play(mut plbook)! + // myhypervisor.play(mut plbook)! pacman.play(mut plbook)! podman.play(mut plbook)! youki.play(mut plbook)! diff --git a/lib/schemas/jsonrpc/model_request.v b/lib/schemas/jsonrpc/model_request.v index 00802ad4..40dab837 100644 --- a/lib/schemas/jsonrpc/model_request.v +++ b/lib/schemas/jsonrpc/model_request.v @@ -70,7 +70,7 @@ pub fn decode_request(data string) !Request { // Returns: // - A JSON string representation of the Request pub fn (req Request) encode() string { - return json2.encode(req, prettify: true) + return json2.encode(req) } // validate checks if the Request object contains all required fields From d267c1131ff803050648b311e9f85b7537beb7a5 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 15 Nov 2025 06:15:02 +0200 Subject: [PATCH 10/11] ... --- .../ai_instructions_hero_models.md | 5 +- lib/core/playcmds/play_all.v | 2 - .../db/ai_instructions_hero_models_old.md | 94 ------ lib/hero/heromodels/message.v | 1 + lib/hero/heromodels/planning.md | 6 - lib/hero/heromodels/prd.md | 275 ++++++++++++++++++ lib/hero/heromodels/prd.v | 97 ++++++ 7 files changed, 376 insertions(+), 104 deletions(-) rename {lib/hero/db => aiprompts}/ai_instructions_hero_models.md (97%) delete mode 100644 lib/hero/db/ai_instructions_hero_models_old.md delete mode 100644 lib/hero/heromodels/planning.md create mode 100644 lib/hero/heromodels/prd.md create mode 100644 lib/hero/heromodels/prd.v diff --git a/lib/hero/db/ai_instructions_hero_models.md b/aiprompts/ai_instructions_hero_models.md similarity index 97% rename from lib/hero/db/ai_instructions_hero_models.md rename to aiprompts/ai_instructions_hero_models.md index 7373ae2b..5f4630b5 100644 --- a/lib/hero/db/ai_instructions_hero_models.md +++ b/aiprompts/ai_instructions_hero_models.md @@ -2,11 +2,12 @@ ## Overview -This document provides clear instructions for AI agents to create new HeroDB models similar to `message.v`. These models are used to store structured data in Redis using the HeroDB system. +This document provides clear instructions for AI agents to create new HeroDB models similar to `message.v`. +These models are used to store structured data in Redis using the HeroDB system. +The message.v can be found in `lib/hero/heromodels/message.v`.s ## Key Concepts -- Each model represents a data type stored in Redis hash sets - Models must implement serialization/deserialization using the `encoder` module - Models inherit from the `Base` struct which provides common fields - The database uses a factory pattern for model access diff --git a/lib/core/playcmds/play_all.v b/lib/core/playcmds/play_all.v index 3c04824b..3791b94c 100644 --- a/lib/core/playcmds/play_all.v +++ b/lib/core/playcmds/play_all.v @@ -41,7 +41,6 @@ import incubaid.herolib.installers.virt.docker import incubaid.herolib.installers.virt.herorunner import incubaid.herolib.installers.virt.kubernetes_installer import incubaid.herolib.installers.virt.lima -import incubaid.herolib.installers.virt.myhypervisor import incubaid.herolib.installers.virt.pacman import incubaid.herolib.installers.virt.podman import incubaid.herolib.installers.virt.youki @@ -100,7 +99,6 @@ pub fn run_all(args_ PlayArgs) ! { herorunner.play(mut plbook)! kubernetes_installer.play(mut plbook)! lima.play(mut plbook)! - myhypervisor.play(mut plbook)! pacman.play(mut plbook)! podman.play(mut plbook)! youki.play(mut plbook)! diff --git a/lib/hero/db/ai_instructions_hero_models_old.md b/lib/hero/db/ai_instructions_hero_models_old.md deleted file mode 100644 index cbc513be..00000000 --- a/lib/hero/db/ai_instructions_hero_models_old.md +++ /dev/null @@ -1,94 +0,0 @@ - - -the main data is in key value stor: - -- each object has u32 id -- each object has u16 version (version of same data) -- each object has u16 schemaid (if schema changes) -- each object has tags u32 (to tag table) -- each object has a created_at timestamp -- each object has a updated_at timestamp -- each object has binary content (the data) -- each object has link to who can read/write/delete (lists of u32 per read/write/delete to group or user), link to security policy u32 -- each object has a signature of the data by the user who created/updated it - - -- there are users & groups -- groups can have other groups and users inside -- users & groups are unique u32 as well in the DB, so no collision - -this database does not know what the data is about, its agnostic to schema - - -now make the 4 structs which represent above - -- data -- user -- group ([]u32) each links to user or group, name, description -- tags ([]string which gets a unique id, so its shorter to link to data object) -- securitypolicy (see below) - -and encoding scheme using lib/data/encoder, we need encode/decode on the structs, so we have densest possible encoding - -now we need the implementation details for each struct, including the fields and their types, as well as the encoding/decoding logic. - -the outside is a server over openrpc which has - -- set (userid:u32, id:u32, data: Data, signature: string, tags:[]string) -> u32. (id can be 0 then its new, if existing we need to check if user can do it), tags will be recalculated based on []string (lower case, sorted list then md5 -> u32) -- get (userid:u32, id: u32, signedid: string) -> Data,Tags as []string -- exist (userid:u32, id: u32) -> bool //this we allow without signature -- delete (userid:u32, id: u32, signedid: string) -> bool -- list (userid:u32, signature: string, based on tags, schemaid, from creation/update and to creation/update), returns max 200 items -> u32 - - -the interface is stateless, no previous connection known, based on signature the server can verify the user is allowed to perform the action - -the backend database is redis (hsets and sets) - - -## signing implementation - -the signing is in the same redis implemented, so no need to use vlang for that - -```bash -# Generate an ephemeral signing keypair -redis-cli -p $PORT AGE GENSIGN -# Example output: -# 1) "" -# 2) "" - -# Sign a message with the secret -redis-cli -p $PORT AGE SIGN "" "msg" -# → returns "" - -# Verify with the public key -redis-cli -p $PORT AGE VERIFY "" "msg" "" -# → 1 (valid) or 0 (invalid) -``` - - -versioning: when stored we don't have to worry about version the database will check if it exists, newest version and then update - - -## some of the base objects - -```v -@[heap] -pub struct SecurityPolicy { -pub mut: - id u32 - read []u32 //links to users & groups - write []u32 //links to users & groups - delete []u32 //links to users & groups - public bool -} - - -@[heap] -pub struct Tags { -pub mut: - id u32 - names []string //unique per id - md5 string //of sorted names, to make easy to find unique id -} -``` \ No newline at end of file diff --git a/lib/hero/heromodels/message.v b/lib/hero/heromodels/message.v index 525e9089..98c0efdb 100644 --- a/lib/hero/heromodels/message.v +++ b/lib/hero/heromodels/message.v @@ -239,6 +239,7 @@ pub fn (mut self DBMessages) list(args MessageListArg) ![]Message { return filtered_messages } +// is how we implement the openrpc calls pub fn message_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response { match method { 'get' { diff --git a/lib/hero/heromodels/planning.md b/lib/hero/heromodels/planning.md deleted file mode 100644 index 7042e546..00000000 --- a/lib/hero/heromodels/planning.md +++ /dev/null @@ -1,6 +0,0 @@ -## notes around how to do a calendly feature - -- make an agenda for the planning and call it as such, this has the timeboxes available for planning -- create template for calendar_event -- create planning item and link to this template -- select the rules which work for recurrence diff --git a/lib/hero/heromodels/prd.md b/lib/hero/heromodels/prd.md new file mode 100644 index 00000000..c39b02ab --- /dev/null +++ b/lib/hero/heromodels/prd.md @@ -0,0 +1,275 @@ +# 📘 **PRD Manual + +# 1. **Product Overview** + +### **What to Write** + +A 2–4 sentence summary describing **what the product is**, **what problem it solves**, and **who it is for**. + +### **Fields** + +* `product_name` +* `version` +* `overview` +* `vision` + +### **Example** + +``` +product_name: "Lumina PRD Builder" +version: "v1.0" +overview: "Lumina PRD Builder allows teams to generate structured, validated Product Requirements Documents using templates and AI guidance." +vision: "Enable any team to create clear requirements in minutes, improving alignment and execution speed." +``` + +--- + +# 2. **Goals** + +### **What to Write** + +A list of measurable outcomes that define success. + +### **Fields** + +* `id` +* `title` +* `description` +* `gtype` → product / business / operational + +### **Example** + +``` +{ + id: "G1" + title: "Faster Requirements Creation" + description: "Reduce PRD creation time from 2 weeks to under 1 day for all teams." + gtype: .product +}, +{ + id: "G2" + title: "Increase Adoption" + description: "Achieve 500 monthly active users within 90 days of launch." + gtype: .business +} +``` + +--- + +# 3. **Use Cases** + +### **What to Write** + +Realistic user interactions showing how the product will be used. + +### **UseCase Fields** + +* id +* title +* actor +* goal +* steps +* success +* failure + +### **Example** + +``` +{ + id: "UC1" + title: "Generate a PRD from Template" + actor: "Product Manager" + goal: "Create a validated PRD quickly" + steps: [ + "User selects 'New PRD'", + "User chooses template type", + "User fills fields or uses AI suggestions", + "User exports PRD to Markdown" + ] + success: "A complete PRD is generated without missing required fields." + failure: "Validation fails due to missing required data." +} +``` + +--- + +# 4. **Requirements** + +### **What to Write** + +Describe *what the system must do*, in clear, testable language. + +### **Requirement Fields** + +* id +* category +* title +* rtype +* description +* priority +* criteria +* dependencies + +--- + +### **Example Requirement** + +``` +{ + id: "R1" + category: "PRD Editor" + title: "Template Selection" + rtype: .functional + description: "The system must allow users to select from a list of predefined PRD templates." + priority: .high + criteria: [ + { + id: "AC1" + description: "UI displays at least 5 templates" + condition: "List contains >= 5 template entries" + } + ] + dependencies: [] +} +``` + +--- + +### **Example Requirement with Dependency** + +``` +{ + id: "R3" + category: "Export" + title: "Export PRD to Markdown" + rtype: .functional + description: "Users must be able to export the completed PRD to a Markdown file." + priority: .medium + criteria: [ + { + id: "AC4" + description: "File saved in .md format" + condition: "Output file ends with '.md'" + } + ] + dependencies: ["R1", "R2"] +} +``` + +--- + +# 5. **Constraints** + +### **What to Write** + +Non-negotiable boundaries the solution must respect. + +### **Constraint Fields** + +* id +* title +* description +* ctype + +### **Example** + +``` +{ + id: "C1" + title: "ARM64 Only" + description: "The system must run on ARM64 servers to match company infrastructure." + ctype: .technica +}, +{ + id: "C2" + title: "Q1 Deadline" + description: "The first release must be launched before March 31." + ctype: .business +}, +{ + id: "C3" + title: "GDPR Requirement" + description: "All user data must be deletable within 24 hours of a user request." + ctype: .compliance +} +``` + +--- + +# 6. **Risks** + +### **What to Write** + +Potential problems + mitigation strategies. + +### **Example** + +``` +risks: { + "RISK1": "Template library may be too small → Mitigate by allowing community contributions" + "RISK2": "AI suggestions may be inaccurate → Add review/approve workflow" + "RISK3": "Export format inconsistencies → Create automated format tests" +} +``` + +--- + +# 🔧 7. **Minimum PRD Example (Compact)** + +Here is a minimal but valid PRD instance: + +``` +ProductRequirementsDoc{ + product_name: "Lumina PRD Builder" + version: "v1.0" + overview: "Tool to create structured PRDs." + vision: "Fast, accurate requirements for all teams." + + goals: [ + Goal{ + id: "G1" + title: "Speed" + description: "Generate PRDs in under 10 minutes." + gtype: .product + } + ] + + use_cases: [ + UseCase{ + id: "UC1" + title: "Create PRD" + actor: "PM" + goal: "Produce PRD quickly" + steps: ["Click new", "Fill data", "Export"] + success: "Valid PRD generated" + failure: "Missing fields" + } + ] + + requirements: [ + Requirement{ + id: "R1" + category: "Editor" + title: "Input Fields" + rtype: .functional + description: "User can fill out PRD fields" + priority: .high + criteria: [] + dependencies: [] + } + ] + + constraints: [ + constraint{ + id: "C1" + title: "Must Support Markdown" + description: "Export only in .md format" + ctype: .technica + } + ] + + risks: { + "R1": "User confusion → Add tooltips" + } +} +``` diff --git a/lib/hero/heromodels/prd.v b/lib/hero/heromodels/prd.v new file mode 100644 index 00000000..5b39f173 --- /dev/null +++ b/lib/hero/heromodels/prd.v @@ -0,0 +1,97 @@ +module prd + +// Basic enums for clarity + +// Core PRD type, this is the root object +pub struct ProductRequirementsDoc { +pub: + product_name string + version string + overview string + vision string + goals []Goal + use_cases []UseCase + requirements []Requirement + constraints []Constraint + risks map[string]string // risk_id -> mitigation +} + + +pub enum PRDPriority { + low + medium + high + critical +} + +pub enum RequirementType { + functional + non_functional + performance + reliability +} + +// A reusable acceptance criterion type +pub struct AcceptanceCriterion { +pub: + id string + description string + condition string // testable condition +} + +// A generic requirement type (functional or NFR) +pub struct Requirement { +pub: + id string + category string // to group requirements + title string + rtype RequirementType + description string + priority PRDPriority + criteria []AcceptanceCriterion + dependencies []string // list of requirement IDs this one depends on +} + +// A use case type +pub struct UseCase { +pub: + id string + title string + actor string + goal string + steps []string + success string + failure string +} + +pub enum GoalType { + product + business + operational +} + +pub struct Goal { +pub: + id string + title string + description string + gtype GoalType +} + +pub enum ConstraintType { + technica + business + operational + scale + compliance + design +} + +pub struct constraint { +pub: + id string + title string + description string + ctype ConstraintType +} + From 8a7987b9c3829d1e1051469d1314d353c44ca513 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 15 Nov 2025 07:09:56 +0200 Subject: [PATCH 11/11] ... --- examples/hero/heromodels/prd.vsh | 93 +++++++++ lib/hero/heromodels/factory.v | 4 + lib/hero/heromodels/prd.v | 326 ++++++++++++++++++++++++++++++- lib/hero/heromodels/prd_test.v | 226 +++++++++++++++++++++ 4 files changed, 645 insertions(+), 4 deletions(-) create mode 100644 examples/hero/heromodels/prd.vsh create mode 100644 lib/hero/heromodels/prd_test.v diff --git a/examples/hero/heromodels/prd.vsh b/examples/hero/heromodels/prd.vsh new file mode 100644 index 00000000..b797011a --- /dev/null +++ b/examples/hero/heromodels/prd.vsh @@ -0,0 +1,93 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.hero.heromodels + +// Initialize database +mut mydb := heromodels.new()! + +// Create goals +mut goals := [ + heromodels.Goal{ + id: 'G1' + title: 'Faster Requirements' + description: 'Reduce PRD creation time to under 1 day' + gtype: .product + } +] + +// Create use cases +mut use_cases := [ + heromodels.UseCase{ + id: 'UC1' + title: 'Generate PRD' + actor: 'Product Manager' + goal: 'Create validated PRD' + steps: ['Select template', 'Fill fields', 'Export to Markdown'] + success: 'Complete PRD generated' + failure: 'Validation failed' + } +] + +// Create requirements +mut criterion := heromodels.AcceptanceCriterion{ + id: 'AC1' + description: 'Display template list' + condition: 'List contains >= 5 templates' +} + +mut requirements := [ + heromodels.Requirement{ + id: 'R1' + category: 'Editor' + title: 'Template Selection' + rtype: .functional + description: 'User can select from templates' + priority: .high + criteria: [criterion] + dependencies: [] + } +] + +// Create constraints +mut constraints := [ + heromodels.Constraint{ + id: 'C1' + title: 'ARM64 Support' + description: 'Must run on ARM64 infrastructure' + ctype: .technica + } +] + +// Create risks +mut risks := map[string]string{} +risks['RISK1'] = 'Templates too limited → Add community contributions' +risks['RISK2'] = 'AI suggestions inaccurate → Add review workflow' + +// Create a new PRD object +mut prd := mydb.prd.new( + product_name: 'Lumina PRD Builder' + version: 'v1.0' + overview: 'Tool to create structured PRDs quickly' + vision: 'Enable teams to generate clear requirements in minutes' + goals: goals + use_cases: use_cases + requirements: requirements + constraints: constraints + risks: risks +)! + +// Save to database +prd = mydb.prd.set(prd)! +println('✓ Created PRD with ID: ${prd.id}') + +// Retrieve from database +mut retrieved := mydb.prd.get(prd.id)! +println('✓ Retrieved PRD: ${retrieved.product_name}') + +// List all PRDs +mut all_prds := mydb.prd.list()! +println('✓ Total PRDs in database: ${all_prds.len}') + +// Check if exists +exists := mydb.prd.exist(prd.id)! +println('✓ PRD exists: ${exists}') \ No newline at end of file diff --git a/lib/hero/heromodels/factory.v b/lib/hero/heromodels/factory.v index 1272ba7e..1172e103 100644 --- a/lib/hero/heromodels/factory.v +++ b/lib/hero/heromodels/factory.v @@ -31,6 +31,7 @@ pub mut: registration_desk DBRegistrationDesk messages DBMessages tags DBTags + prd DBPrd rpc_handler &Handler } @@ -91,6 +92,9 @@ pub fn new(args NewArgs) !&ModelsFactory { tags: DBTags{ db: &mydb } + prd: DBPrd{ + db: &mydb + } rpc_handler: &h } diff --git a/lib/hero/heromodels/prd.v b/lib/hero/heromodels/prd.v index 5b39f173..05514de1 100644 --- a/lib/hero/heromodels/prd.v +++ b/lib/hero/heromodels/prd.v @@ -1,10 +1,19 @@ -module prd +module heromodels + +import incubaid.herolib.data.encoder +import incubaid.herolib.data.ourtime +import incubaid.herolib.hero.db +import incubaid.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_true } +import incubaid.herolib.hero.user { UserRef } +import json // Basic enums for clarity // Core PRD type, this is the root object +@[heap] pub struct ProductRequirementsDoc { -pub: + db.Base +pub mut: product_name string version string overview string @@ -13,9 +22,12 @@ pub: use_cases []UseCase requirements []Requirement constraints []Constraint - risks map[string]string // risk_id -> mitigation } +pub struct DBPrd { +pub mut: + db &db.DB @[skip; str: skip] +} pub enum PRDPriority { low @@ -87,7 +99,7 @@ pub enum ConstraintType { design } -pub struct constraint { +pub struct Constraint { pub: id string title string @@ -95,3 +107,309 @@ pub: ctype ConstraintType } +pub fn (self ProductRequirementsDoc) type_name() string { + return 'prd' +} + +pub fn (self ProductRequirementsDoc) description(methodname string) string { + match methodname { + 'set' { + return 'Create or update a product requirements document. Returns the ID of the PRD.' + } + 'get' { + return 'Retrieve a PRD by ID. Returns the complete PRD object.' + } + 'delete' { + return 'Delete a PRD by ID. Returns true if successful.' + } + 'exist' { + return 'Check if a PRD exists by ID. Returns true or false.' + } + 'list' { + return 'List all PRDs. Returns an array of PRD objects.' + } + else { + return 'Generic method for PRD operations.' + } + } +} + +pub fn (self ProductRequirementsDoc) example(methodname string) (string, string) { + match methodname { + 'set' { + return '{"product_name": "Test Product", "version": "v1.0", "overview": "A test product", "vision": "To test the system", "goals": [], "use_cases": [], "requirements": [], "constraints": []}', '1' + } + 'get' { + return '{"id": 1}', '{"product_name": "Test Product", "version": "v1.0", "overview": "A test product", "vision": "To test the system", "goals": [], "use_cases": [], "requirements": [], "constraints": []}' + } + 'delete' { + return '{"id": 1}', 'true' + } + 'exist' { + return '{"id": 1}', 'true' + } + 'list' { + return '{}', '[{"product_name": "Test Product", "version": "v1.0"}]' + } + else { + return '{}', '{}' + } + } +} + +pub fn (self ProductRequirementsDoc) dump(mut e encoder.Encoder) ! { + e.add_string(self.product_name) + e.add_string(self.version) + e.add_string(self.overview) + e.add_string(self.vision) + + // Encode goals array + e.add_u16(u16(self.goals.len)) + for goal in self.goals { + e.add_string(goal.id) + e.add_string(goal.title) + e.add_string(goal.description) + e.add_u8(u8(goal.gtype)) + } + + // Encode use_cases array + e.add_u16(u16(self.use_cases.len)) + for uc in self.use_cases { + e.add_string(uc.id) + e.add_string(uc.title) + e.add_string(uc.actor) + e.add_string(uc.goal) + e.add_list_string(uc.steps) + e.add_string(uc.success) + e.add_string(uc.failure) + } + + // Encode requirements array + e.add_u16(u16(self.requirements.len)) + for req in self.requirements { + e.add_string(req.id) + e.add_string(req.category) + e.add_string(req.title) + e.add_u8(u8(req.rtype)) + e.add_string(req.description) + e.add_u8(u8(req.priority)) + + // Encode acceptance criteria + e.add_u16(u16(req.criteria.len)) + for criterion in req.criteria { + e.add_string(criterion.id) + e.add_string(criterion.description) + e.add_string(criterion.condition) + } + + // Encode dependencies + e.add_list_string(req.dependencies) + } + + // Encode constraints array + e.add_u16(u16(self.constraints.len)) + for constraint in self.constraints { + e.add_string(constraint.id) + e.add_string(constraint.title) + e.add_string(constraint.description) + e.add_u8(u8(constraint.ctype)) + } +} + +pub fn (mut self DBPrd) load(mut o ProductRequirementsDoc, mut e encoder.Decoder) ! { + o.product_name = e.get_string()! + o.version = e.get_string()! + o.overview = e.get_string()! + o.vision = e.get_string()! + + // Decode goals + goals_len := e.get_u16()! + mut goals := []Goal{} + for _ in 0 .. goals_len { + goals << Goal{ + id: e.get_string()! + title: e.get_string()! + description: e.get_string()! + gtype: unsafe { GoalType(e.get_u8()!) } + } + } + o.goals = goals + + // Decode use_cases + use_cases_len := e.get_u16()! + mut use_cases := []UseCase{} + for _ in 0 .. use_cases_len { + use_cases << UseCase{ + id: e.get_string()! + title: e.get_string()! + actor: e.get_string()! + goal: e.get_string()! + steps: e.get_list_string()! + success: e.get_string()! + failure: e.get_string()! + } + } + o.use_cases = use_cases + + // Decode requirements + requirements_len := e.get_u16()! + mut requirements := []Requirement{} + for _ in 0 .. requirements_len { + req_id := e.get_string()! + req_category := e.get_string()! + req_title := e.get_string()! + req_rtype := unsafe { RequirementType(e.get_u8()!) } + req_description := e.get_string()! + req_priority := unsafe { PRDPriority(e.get_u8()!) } + + // Decode criteria + criteria_len := e.get_u16()! + mut criteria := []AcceptanceCriterion{} + for _ in 0 .. criteria_len { + criteria << AcceptanceCriterion{ + id: e.get_string()! + description: e.get_string()! + condition: e.get_string()! + } + } + + // Decode dependencies + dependencies := e.get_list_string()! + + requirements << Requirement{ + id: req_id + category: req_category + title: req_title + rtype: req_rtype + description: req_description + priority: req_priority + criteria: criteria + dependencies: dependencies + } + } + o.requirements = requirements + + // Decode constraints + constraints_len := e.get_u16()! + mut constraints := []Constraint{} + for _ in 0 .. constraints_len { + constraints << Constraint{ + id: e.get_string()! + title: e.get_string()! + description: e.get_string()! + ctype: unsafe { ConstraintType(e.get_u8()!) } + } + } + o.constraints = constraints +} + +@[params] +pub struct PrdArg { +pub mut: + id u32 + product_name string @[required] + version string + overview string + vision string + goals []Goal + use_cases []UseCase + requirements []Requirement + constraints []Constraint + securitypolicy u32 + tags []string +} + +pub fn (mut self DBPrd) new(args PrdArg) !ProductRequirementsDoc { + mut o := ProductRequirementsDoc{ + product_name: args.product_name + version: args.version + overview: args.overview + vision: args.vision + goals: args.goals + use_cases: args.use_cases + requirements: args.requirements + constraints: args.constraints + updated_at: ourtime.now().unix() + } + + o.securitypolicy = args.securitypolicy + o.tags = self.db.tags_get(args.tags)! + + return o +} + +pub fn (mut self DBPrd) set(o ProductRequirementsDoc) !ProductRequirementsDoc { + return self.db.set[ProductRequirementsDoc](o)! +} + +pub fn (mut self DBPrd) delete(id u32) !bool { + if !self.db.exists[ProductRequirementsDoc](id)! { + return false + } + self.db.delete[ProductRequirementsDoc](id)! + return true +} + +pub fn (mut self DBPrd) exist(id u32) !bool { + return self.db.exists[ProductRequirementsDoc](id)! +} + +pub fn (mut self DBPrd) get(id u32) !ProductRequirementsDoc { + mut o, data := self.db.get_data[ProductRequirementsDoc](id)! + mut e_decoder := encoder.decoder_new(data) + self.load(mut o, mut e_decoder)! + return o +} + +pub fn (mut self DBPrd) list() ![]ProductRequirementsDoc { + return self.db.list[ProductRequirementsDoc]()!.map(self.get(it)!) +} + +pub fn prd_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response { + match method { + 'get' { + id := db.decode_u32(params)! + res := f.prd.get(id)! + return new_response(rpcid, json.encode(res)) + } + 'set' { + mut args := db.decode_generic[PrdArg](params)! + mut o := f.prd.new(args)! + if args.id != 0 { + o.id = args.id + } + o = f.prd.set(o)! + return new_response_int(rpcid, int(o.id)) + } + 'delete' { + id := db.decode_u32(params)! + deleted := f.prd.delete(id)! + if deleted { + return new_response_true(rpcid) + } else { + return new_error(rpcid, + code: 404 + message: 'PRD with ID ${id} not found' + ) + } + } + 'exist' { + id := db.decode_u32(params)! + if f.prd.exist(id)! { + return new_response_true(rpcid) + } else { + return new_response_false(rpcid) + } + } + 'list' { + res := f.prd.list()! + return new_response(rpcid, json.encode(res)) + } + else { + return new_error(rpcid, + code: 32601 + message: 'Method ${method} not found on prd' + ) + } + } +} diff --git a/lib/hero/heromodels/prd_test.v b/lib/hero/heromodels/prd_test.v new file mode 100644 index 00000000..ebd47ebc --- /dev/null +++ b/lib/hero/heromodels/prd_test.v @@ -0,0 +1,226 @@ +module heromodels + +import incubaid.herolib.hero.db + +fn test_prd_new() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut args := PrdArg{ + product_name: 'Test Product' + version: 'v1.0' + overview: 'This is a test product.' + vision: 'To revolutionize testing.' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + prd := db_prd.new(args)! + + assert prd.product_name == 'Test Product' + assert prd.version == 'v1.0' + assert prd.overview == 'This is a test product.' + assert prd.vision == 'To revolutionize testing.' + assert prd.goals.len == 0 + assert prd.use_cases.len == 0 + assert prd.requirements.len == 0 + assert prd.constraints.len == 0 + assert prd.risks.len == 0 + assert prd.updated_at > 0 + + println('✓ PRD new test passed!') +} + +fn test_prd_crud_operations() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + // Create a new PRD + mut args := PrdArg{ + product_name: 'CRUD Test Product' + version: 'v1.0' + overview: 'This is a test product for CRUD.' + vision: 'To test CRUD operations.' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + original_id := prd.id + + // Test get + retrieved_prd := db_prd.get(original_id)! + assert retrieved_prd.product_name == 'CRUD Test Product' + assert retrieved_prd.version == 'v1.0' + assert retrieved_prd.id == original_id + + // Test exist + exists := db_prd.exist(original_id)! + assert exists == true + + // Test delete + db_prd.delete(original_id)! + exists_after_delete := db_prd.exist(original_id)! + assert exists_after_delete == false + + println('✓ PRD CRUD operations test passed!') +} + +fn test_prd_encoding_decoding_complex() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut goal := Goal{ + id: 'G1' + title: 'Speed' + description: 'Generate PRDs in minutes' + gtype: .product + } + + mut use_case := UseCase{ + id: 'UC1' + title: 'Create PRD' + actor: 'Product Manager' + goal: 'Produce PRD quickly' + steps: ['Click new', 'Fill data', 'Export'] + success: 'Valid PRD generated' + failure: 'Missing fields' + } + + mut criterion := AcceptanceCriterion{ + id: 'AC1' + description: 'System displays template list' + condition: 'List contains >= 5 templates' + } + + mut requirement := Requirement{ + id: 'R1' + category: 'Editor' + title: 'Template Selection' + rtype: .functional + description: 'User can select from predefined templates' + priority: .high + criteria: [criterion] + dependencies: [] + } + + mut constraint := Constraint{ + id: 'C1' + title: 'ARM64 Only' + description: 'Must run on ARM64 servers' + ctype: .technica + } + + mut risks := map[string]string{} + risks['RISK1'] = 'Mitigation strategy here' + + mut args := PrdArg{ + product_name: 'Complex Test Product' + version: 'v2.0' + overview: 'Complete test with all fields' + vision: 'Full feature test' + goals: [goal] + use_cases: [use_case] + requirements: [requirement] + constraints: [constraint] + risks: risks + } + + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + prd_id := prd.id + + // Retrieve and verify + retrieved_prd := db_prd.get(prd_id)! + + assert retrieved_prd.product_name == 'Complex Test Product' + assert retrieved_prd.goals.len == 1 + assert retrieved_prd.goals[0].id == 'G1' + assert retrieved_prd.goals[0].gtype == .product + + assert retrieved_prd.use_cases.len == 1 + assert retrieved_prd.use_cases[0].id == 'UC1' + assert retrieved_prd.use_cases[0].steps.len == 3 + + assert retrieved_prd.requirements.len == 1 + assert retrieved_prd.requirements[0].id == 'R1' + assert retrieved_prd.requirements[0].criteria.len == 1 + assert retrieved_prd.requirements[0].priority == .high + + assert retrieved_prd.constraints.len == 1 + assert retrieved_prd.constraints[0].id == 'C1' + assert retrieved_prd.constraints[0].ctype == .technica + + assert retrieved_prd.risks.len == 1 + assert retrieved_prd.risks['RISK1'] == 'Mitigation strategy here' + + println('✓ PRD encoding/decoding complex test passed!') +} + +fn test_prd_type_name() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut args := PrdArg{ + product_name: 'Type Name Test' + version: 'v1.0' + overview: 'Test' + vision: 'Test' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + prd := db_prd.new(args)! + type_name := prd.type_name() + assert type_name == 'prd' + + println('✓ PRD type_name test passed!') +} + +fn test_prd_list() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + // Create multiple PRDs + for i in 0 .. 3 { + mut args := PrdArg{ + product_name: 'Product ${i}' + version: 'v1.0' + overview: 'Overview ${i}' + vision: 'Vision ${i}' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + } + + // List all PRDs + all_prds := db_prd.list()! + assert all_prds.len == 3 + + println('✓ PRD list test passed!') +}