From be35352f4c0b6887730edcf8c2fc501ff148dc9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:09:03 +0000 Subject: [PATCH 1/8] Initial plan From 1249fcbc0cb30c7bc537108c6acc3c7f17ac351a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:16:59 +0000 Subject: [PATCH 2/8] feat: Add @dynamia-tools/cli package with scaffold wizard, backend/frontend generators, and install.sh Co-authored-by: marioserrano09 <5221275+marioserrano09@users.noreply.github.com> Agent-Logs-Url: https://github.com/dynamiatools/framework/sessions/07bbccf6-9bbf-4a03-832d-6df9a79eba92 --- platform/packages/cli/README.md | 119 ++ platform/packages/cli/cli.properties | 92 ++ platform/packages/cli/install.sh | 217 ++++ platform/packages/cli/package-lock.json | 1056 +++++++++++++++++ platform/packages/cli/package.json | 59 + platform/packages/cli/src/commands/new.ts | 267 +++++ .../packages/cli/src/generators/backend.ts | 92 ++ .../packages/cli/src/generators/frontend.ts | 129 ++ platform/packages/cli/src/index.ts | 15 + platform/packages/cli/src/utils/config.ts | 154 +++ platform/packages/cli/src/utils/env.ts | 76 ++ platform/packages/cli/src/utils/logger.ts | 42 + platform/packages/cli/src/utils/replace.ts | 218 ++++ platform/packages/cli/tsconfig.json | 18 + 14 files changed, 2554 insertions(+) create mode 100644 platform/packages/cli/README.md create mode 100644 platform/packages/cli/cli.properties create mode 100755 platform/packages/cli/install.sh create mode 100644 platform/packages/cli/package-lock.json create mode 100644 platform/packages/cli/package.json create mode 100644 platform/packages/cli/src/commands/new.ts create mode 100644 platform/packages/cli/src/generators/backend.ts create mode 100644 platform/packages/cli/src/generators/frontend.ts create mode 100644 platform/packages/cli/src/index.ts create mode 100644 platform/packages/cli/src/utils/config.ts create mode 100644 platform/packages/cli/src/utils/env.ts create mode 100644 platform/packages/cli/src/utils/logger.ts create mode 100644 platform/packages/cli/src/utils/replace.ts create mode 100644 platform/packages/cli/tsconfig.json diff --git a/platform/packages/cli/README.md b/platform/packages/cli/README.md new file mode 100644 index 00000000..a26d60ee --- /dev/null +++ b/platform/packages/cli/README.md @@ -0,0 +1,119 @@ +# @dynamia-tools/cli + +> Scaffold new [Dynamia Platform](https://dynamia.tools) projects from the command line. + +--- + +## Installation + +### One-line bootstrap (Linux & macOS) + +Installs JDK 25, Node.js LTS, and the CLI: + +```bash +curl -fsSL https://get.dynamia.tools | bash +``` + +### npm (global) + +```bash +npm install -g @dynamia-tools/cli +``` + +### Requirements + +| Tool | Minimum version | +|---|---| +| Node.js | 22 | +| git | any recent version (required) | +| Java | 25 (recommended) | + +--- + +## Usage + +```bash +dynamia new +``` + +The wizard guides you through: + +1. **Project name** — lowercase, letters/numbers/hyphens only +2. **Scaffold choice** — Backend + Frontend / Backend only / Frontend only +3. **Backend language** — Java, Kotlin, or Groovy +4. **Maven coordinates** — Group ID, Artifact ID, version, description +5. **Frontend framework** — Vue 3 or React +6. **Package manager** — pnpm, npm, or yarn +7. **Confirm** — shows a summary table before generating + +--- + +## What gets generated + +``` +my-erp-app/ +├── backend/ Spring Boot + Dynamia Tools (Java/Kotlin/Groovy) +└── frontend/ Vue 3 or React + Vite + @dynamia-tools/vue|sdk +``` + +### Backend + +- Cloned from a GitHub template repo (e.g. `dynamiatools/template-backend-java`) +- Placeholder package `com.example.demo` renamed to your `groupId.artifactId` +- All tokens replaced in `.java`, `.kt`, `.groovy`, `.xml`, `.yml`, `.properties`, `.md` +- `DemoApplication.java` renamed to `Application.java` + +### Frontend + +- Cloned from a GitHub template repo (e.g. `dynamiatools/template-frontend-vue`) +- Falls back to `npm create vite@latest` if clone fails +- `@dynamia-tools/sdk` and `@dynamia-tools/ui-core` installed automatically + +--- + +## Configuration + +All versions, URLs, and template repositories live in `cli.properties` — the single source of truth. TypeScript code never hardcodes versions or URLs. + +Key sections: + +| Section | Description | +|---|---| +| `dynamia.*` | Framework version and docs URL | +| `java.*` | JDK version and SDKMAN candidate | +| `template.backend..*` | Backend template repos (java, kotlin, groovy) | +| `template.frontend..*` | Frontend template repos (vue, react) | +| `token.*` | Placeholder tokens used inside template repos | +| `vite.*` | Vite fallback config | + +--- + +## Template author conventions + +When creating template repos (e.g. `dynamiatools/template-backend-java`): + +- Default Java source package: `com.example.demo` +- `pom.xml` groupId: `com.example`, artifactId: `demo` +- Main class: `src/main/java/com/example/demo/DemoApplication.java` +- Use these placeholders in files: + - `{{GROUP_ID}}` — user's group ID + - `{{ARTIFACT_ID}}` — user's artifact ID + - `{{BASE_PACKAGE}}` — computed base package + - `{{PROJECT_NAME}}` — project name + - `{{PROJECT_VERSION}}` — project version + - `{{DYNAMIA_VERSION}}` — Dynamia Tools version + - `{{SPRING_BOOT_VERSION}}` — Spring Boot version + +--- + +## Links + +- [Dynamia Tools docs](https://dynamia.tools/docs) +- [GitHub org](https://github.com/dynamiatools) +- [Framework repo](https://github.com/dynamiatools/framework) + +--- + +## License + +Apache-2.0 — © Dynamia Soluciones IT SAS diff --git a/platform/packages/cli/cli.properties b/platform/packages/cli/cli.properties new file mode 100644 index 00000000..0f5a88d7 --- /dev/null +++ b/platform/packages/cli/cli.properties @@ -0,0 +1,92 @@ +# Dynamia Tools CLI Configuration +# All versions, URLs, template repos, and token names live here. +# Updating any version or adding a new template never requires touching TypeScript. + +# --- +# dynamia.* +# --- +dynamia.version=26.3.0 +dynamia.docs.url=https://dynamia.tools/docs + +# --- +# java.* +# --- +java.version=25 +java.sdkman.candidate=25-tem + +# --- +# node.* +# --- +node.minimum.version=22 + +# --- +# spring.* (kept as reference metadata, not used for generation) +# --- +spring.initializr.url=https://start.spring.io +spring.initializr.metadata.url=https://start.spring.io/metadata/client +spring.boot.version=3.4.0 + +# --- +# template.backend..* +# --- +template.backend.java.label=Java +template.backend.java.repo=https://github.com/dynamiatools/template-backend-java +template.backend.java.branch=main +template.backend.java.description=Spring Boot + Dynamia Tools (Java) + +template.backend.kotlin.label=Kotlin +template.backend.kotlin.repo=https://github.com/dynamiatools/template-backend-kotlin +template.backend.kotlin.branch=main +template.backend.kotlin.description=Spring Boot + Dynamia Tools (Kotlin) + +template.backend.groovy.label=Groovy +template.backend.groovy.repo=https://github.com/dynamiatools/template-backend-groovy +template.backend.groovy.branch=main +template.backend.groovy.description=Spring Boot + Dynamia Tools (Groovy) + +# --- +# template.frontend..* +# --- +template.frontend.vue.label=Vue 3 +template.frontend.vue.repo=https://github.com/dynamiatools/template-frontend-vue +template.frontend.vue.branch=main +template.frontend.vue.description=Vue 3 + Vite + @dynamia-tools/vue + +template.frontend.react.label=React +template.frontend.react.repo=https://github.com/dynamiatools/template-frontend-react +template.frontend.react.branch=main +template.frontend.react.description=React + Vite + @dynamia-tools/sdk + +# --- +# vite.* (fallback when git clone fails) +# --- +vite.fallback.enabled=true +vite.templates.vue=vue-ts +vite.templates.react=react-ts + +# --- +# npm.* (SDK package names and versions) +# --- +npm.sdk.package=@dynamia-tools/sdk +npm.sdk.version=26.3.0 +npm.ui-core.package=@dynamia-tools/ui-core +npm.ui-core.version=26.3.0 +npm.vue.package=@dynamia-tools/vue +npm.vue.version=26.3.0 + +# --- +# installer.* +# --- +installer.sdkman.url=https://get.sdkman.io +installer.fnm.url=https://fnm.vercel.app/install + +# --- +# token.* (placeholders used in template repos) +# --- +token.group.id={{GROUP_ID}} +token.artifact.id={{ARTIFACT_ID}} +token.base.package={{BASE_PACKAGE}} +token.project.name={{PROJECT_NAME}} +token.project.version={{PROJECT_VERSION}} +token.dynamia.version={{DYNAMIA_VERSION}} +token.spring.boot.version={{SPRING_BOOT_VERSION}} diff --git a/platform/packages/cli/install.sh b/platform/packages/cli/install.sh new file mode 100755 index 00000000..74ceaf64 --- /dev/null +++ b/platform/packages/cli/install.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# Dynamia Tools — Full Toolchain Bootstrap +# Usage: curl -fsSL https://get.dynamia.tools | bash +# +# Installs: git, curl, zip/unzip, JDK 25 (via SDKMAN), Node.js LTS (via fnm), +# and the @dynamia-tools/cli npm package. +# +# Supported: Ubuntu/Debian, Fedora/RHEL/CentOS, Arch Linux, macOS (Homebrew) +# Not supported: Windows + +set -euo pipefail + +# --------------------------------------------------------------------------- +# ANSI colors (no external deps) +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +info() { echo -e "${CYAN}ℹ $*${RESET}"; } +success() { echo -e "${GREEN}✓ $*${RESET}"; } +warn() { echo -e "${YELLOW}⚠ $*${RESET}"; } +error() { echo -e "${RED}✗ $*${RESET}" >&2; exit 1; } +banner() { + echo -e "${CYAN}${BOLD}" + echo ' ____ _ _____ _ ' + echo ' | _ \ _ _ _ __ __ _ _ __ ___ (_) __ |_ _|__ ___ | |___ ' + echo ' | | | | | | | '"'"'_ \ / _` | '"'"'_ ` _ \| |/ _` || |/ _ \ / _ \| / __|' + echo ' | |_| | |_| | | | | (_| | | | | | | | (_| || | (_) | (_) | \__ \' + echo ' |____/ \__, |_| |_|\__,_|_| |_| |_|_|\__,_||_|\___/ \___/|_|___/' + echo ' |___/ ' + echo -e "${RESET}" + echo -e "${BOLD} Dynamia Tools — Toolchain Installer${RESET}" + echo "" +} + +# --------------------------------------------------------------------------- +# OS / package manager detection +# --------------------------------------------------------------------------- +OS="" +PM="" + +detect_os() { + if [[ "$OSTYPE" == "darwin"* ]]; then + OS="macos" + PM="brew" + elif command -v apt-get &>/dev/null; then + OS="debian" + PM="apt-get" + elif command -v dnf &>/dev/null; then + OS="fedora" + PM="dnf" + elif command -v pacman &>/dev/null; then + OS="arch" + PM="pacman" + else + error "Unsupported OS. Please install dependencies manually and then run: npm install -g @dynamia-tools/cli" + fi + info "Detected OS: ${OS} (package manager: ${PM})" +} + +# --------------------------------------------------------------------------- +# Install a single prerequisite package if missing +# --------------------------------------------------------------------------- +install_pkg() { + local cmd="$1" + local pkg="${2:-$1}" + + if command -v "$cmd" &>/dev/null; then + success "$cmd already installed" + return + fi + + info "Installing $pkg..." + case "$PM" in + brew) brew install "$pkg" ;; + apt-get) sudo apt-get install -y "$pkg" ;; + dnf) sudo dnf install -y "$pkg" ;; + pacman) sudo pacman -S --noconfirm "$pkg" ;; + esac +} + +# --------------------------------------------------------------------------- +# Install prerequisites +# --------------------------------------------------------------------------- +install_prerequisites() { + info "Checking prerequisites..." + + install_pkg curl + install_pkg git + + # git is REQUIRED — hard stop if unavailable + if ! command -v git &>/dev/null; then + error "git could not be installed. git is required for template cloning. Please install git manually and re-run this script." + fi + + install_pkg zip + install_pkg unzip + + success "Prerequisites ready" +} + +# --------------------------------------------------------------------------- +# JDK 25 via SDKMAN +# --------------------------------------------------------------------------- +install_java() { + info "Checking JDK 25..." + + if ! command -v sdk &>/dev/null; then + info "Installing SDKMAN..." + curl -s "https://get.sdkman.io" | bash + # shellcheck source=/dev/null + source "$HOME/.sdkman/bin/sdkman-init.sh" + fi + + if java -version 2>&1 | grep -qE '^(openjdk|java) version "25'; then + success "JDK 25 already installed" + else + info "Installing JDK 25 via SDKMAN..." + sdk install java 25-tem + sdk default java 25-tem + success "JDK 25 installed" + fi +} + +# --------------------------------------------------------------------------- +# Node.js LTS via fnm +# --------------------------------------------------------------------------- +install_node() { + info "Checking Node.js..." + + if ! command -v fnm &>/dev/null; then + info "Installing fnm (Fast Node Manager)..." + curl -fsSL https://fnm.vercel.app/install | bash + export PATH="$HOME/.local/share/fnm:$PATH" + eval "$(fnm env --use-on-cd)" + fi + + if command -v node &>/dev/null; then + success "Node.js already installed ($(node --version))" + else + info "Installing Node.js LTS via fnm..." + fnm install --lts + fnm use lts-latest + fnm default lts-latest + success "Node.js LTS installed" + fi +} + +# --------------------------------------------------------------------------- +# Patch shell profile (SDKMAN + fnm init lines) +# --------------------------------------------------------------------------- +patch_shell_profile() { + local profile="" + + case "$SHELL" in + */zsh) profile="$HOME/.zshrc" ;; + */bash) profile="$HOME/.bashrc" ;; + *) profile="$HOME/.profile" ;; + esac + + info "Patching shell profile: $profile" + + # SDKMAN init + local sdkman_line='source "$HOME/.sdkman/bin/sdkman-init.sh"' + if ! grep -q "sdkman-init.sh" "$profile" 2>/dev/null; then + echo "" >> "$profile" + echo "# SDKMAN" >> "$profile" + echo 'export SDKMAN_DIR="$HOME/.sdkman"' >> "$profile" + echo "[[ -s \"\$HOME/.sdkman/bin/sdkman-init.sh\" ]] && $sdkman_line" >> "$profile" + success "Added SDKMAN init to $profile" + else + success "SDKMAN already configured in $profile" + fi + + # fnm init + local fnm_line='eval "$(fnm env --use-on-cd)"' + if ! grep -q "fnm env" "$profile" 2>/dev/null; then + echo "" >> "$profile" + echo "# fnm (Fast Node Manager)" >> "$profile" + echo 'export PATH="$HOME/.local/share/fnm:$PATH"' >> "$profile" + echo "$fnm_line" >> "$profile" + success "Added fnm init to $profile" + else + success "fnm already configured in $profile" + fi +} + +# --------------------------------------------------------------------------- +# Install CLI and launch +# --------------------------------------------------------------------------- +install_cli() { + info "Installing @dynamia-tools/cli..." + npm install -g @dynamia-tools/cli + success "@dynamia-tools/cli installed" + echo "" + info "Launching project wizard..." + dynamia new +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + banner + detect_os + install_prerequisites + install_java + install_node + patch_shell_profile + install_cli +} + +main "$@" diff --git a/platform/packages/cli/package-lock.json b/platform/packages/cli/package-lock.json new file mode 100644 index 00000000..4819e6d4 --- /dev/null +++ b/platform/packages/cli/package-lock.json @@ -0,0 +1,1056 @@ +{ + "name": "@dynamia-tools/cli", + "version": "26.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@dynamia-tools/cli", + "version": "26.3.0", + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^7.0.0", + "chalk": "^5.0.0", + "execa": "^9.0.0", + "ora": "^8.0.0" + }, + "bin": { + "dynamia": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/platform/packages/cli/package.json b/platform/packages/cli/package.json new file mode 100644 index 00000000..e1443794 --- /dev/null +++ b/platform/packages/cli/package.json @@ -0,0 +1,59 @@ +{ + "name": "@dynamia-tools/cli", + "version": "26.3.0", + "description": "Dynamia Tools CLI — Scaffold new Dynamia Platform projects", + "keywords": [ + "dynamia", + "cli", + "scaffold", + "spring-boot", + "vue", + "typescript" + ], + "homepage": "https://dynamia.tools", + "bugs": { + "url": "https://github.com/dynamiatools/framework/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/dynamiatools/framework.git", + "directory": "platform/packages/cli" + }, + "license": "Apache-2.0", + "author": "Dynamia Soluciones IT SAS", + "type": "module", + "bin": { + "dynamia": "./dist/index.js" + }, + "files": [ + "dist", + "cli.properties", + "install.sh", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@inquirer/prompts": "^7.0.0", + "chalk": "^5.0.0", + "execa": "^9.0.0", + "ora": "^8.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=22" + } +} diff --git a/platform/packages/cli/src/commands/new.ts b/platform/packages/cli/src/commands/new.ts new file mode 100644 index 00000000..eaec26be --- /dev/null +++ b/platform/packages/cli/src/commands/new.ts @@ -0,0 +1,267 @@ +import { input, select, confirm } from '@inquirer/prompts' +import { join } from 'node:path' +import { loadConfig, type CliConfig } from '../utils/config.js' +import { runChecks } from '../utils/env.js' +import { banner, info, success, error, warn } from '../utils/logger.js' +import { generateBackend } from '../generators/backend.js' +import { generateFrontend } from '../generators/frontend.js' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Fetch the latest Spring Boot version from Spring Initializr metadata. */ +async function fetchSpringBootVersion(_config: CliConfig): Promise { + try { + const metaRes = await fetch('https://start.spring.io/metadata/client') + if (metaRes.ok) { + const meta = await metaRes.json() as Record + const bootVersion = (meta as { bootVersion?: { default?: string } }).bootVersion?.default + if (bootVersion) return bootVersion + } + } catch { + // ignore — use fallback + } + // Fallback from config (spring.boot.version kept as reference) + return '3.4.0' +} + +/** Print the final success message. */ +function printSuccessMessage(opts: { + projectName: string + targetDir: string + generateBackend: boolean + generateFrontend: boolean + language: string + framework: string + packageManager: string + config: CliConfig + springBootVersion: string +}): void { + const { + projectName, + targetDir, + generateBackend: doBackend, + generateFrontend: doFrontend, + language, + framework, + packageManager: pm, + config, + springBootVersion, + } = opts + + console.log('') + console.log(`✓ Project "${projectName}" created successfully!`) + console.log('') + console.log(' 📁 Structure:') + console.log(` ${projectName}/`) + if (doBackend) { + console.log(` ├── backend/ (${language.charAt(0).toUpperCase() + language.slice(1)} · Spring Boot ${springBootVersion} · Dynamia Tools ${config.dynamia.version})`) + } + if (doFrontend) { + const frontendLabel = config.templates.frontend[framework]?.label ?? framework + const sdkVersion = config.npm['vue']?.version ?? config.npm['sdk']?.version ?? config.dynamia.version + const prefix = doBackend ? '└' : '├' + console.log(` ${prefix}── frontend/ (${frontendLabel} · Vite · @dynamia-tools/${framework === 'vue' ? 'vue' : 'sdk'} ${sdkVersion})`) + } + console.log('') + console.log(' 🚀 Next steps:') + console.log('') + if (doBackend) { + console.log(' Backend:') + console.log(` cd ${projectName}/backend`) + console.log(' ./mvnw spring-boot:run') + console.log('') + } + if (doFrontend) { + const installCmd = pm === 'npm' ? 'npm install' : pm === 'yarn' ? 'yarn' : 'pnpm install' + const devCmd = pm === 'npm' ? 'npm run dev' : pm === 'yarn' ? 'yarn dev' : 'pnpm dev' + console.log(' Frontend:') + console.log(` cd ${projectName}/frontend`) + console.log(` ${installCmd} && ${devCmd}`) + console.log('') + } + console.log(` 📖 Docs: ${config.dynamia.docsUrl}`) + console.log('') +} + +// --------------------------------------------------------------------------- +// Command entry point +// --------------------------------------------------------------------------- + +export async function runNew(): Promise { + banner() + + // Load configuration from cli.properties + let config: CliConfig + try { + config = await loadConfig() + } catch (err) { + error(`Failed to load CLI configuration: ${(err as Error).message}`) + } + + // Environment checks (git missing = hard stop) + await runChecks() + + // --- Step 1: Project name --- + const projectName = await input({ + message: 'Project name:', + validate: (value: string) => { + if (!value.trim()) return 'Project name cannot be empty' + if (!/^[a-z0-9-]+$/.test(value.trim())) { + return 'Only lowercase letters, numbers, and hyphens are allowed' + } + return true + }, + }) + + // --- Step 2: What to generate --- + type ScaffoldChoice = 'both' | 'backend' | 'frontend' + const scaffoldChoice = await select({ + message: 'What do you want to scaffold?', + choices: [ + { name: 'Backend + Frontend', value: 'both' }, + { name: 'Backend only', value: 'backend' }, + { name: 'Frontend only', value: 'frontend' }, + ], + }) + + const doBackend = scaffoldChoice === 'both' || scaffoldChoice === 'backend' + const doFrontend = scaffoldChoice === 'both' || scaffoldChoice === 'frontend' + + // --- Step 3: Backend language --- + let language = 'java' + let groupId = 'com.example' + let artifactId = projectName + let version = '1.0.0-SNAPSHOT' + let description = '' + + if (doBackend) { + const backendTemplates = config!.templates.backend + const languageChoices = Object.entries(backendTemplates).map(([id, entry]) => ({ + name: entry.label, + value: id, + description: entry.description, + })) + + language = await select({ + message: 'Backend language:', + choices: languageChoices, + }) + + // --- Step 4: Maven coordinates --- + groupId = await input({ + message: 'Group ID:', + default: 'com.example', + }) + + artifactId = await input({ + message: 'Artifact ID:', + default: projectName, + }) + + version = await input({ + message: 'Version:', + default: '1.0.0-SNAPSHOT', + }) + + description = await input({ + message: 'Description (optional):', + default: '', + }) + } + + // --- Step 5: Frontend framework --- + let framework = 'vue' + let packageManager = 'pnpm' + + if (doFrontend) { + const frontendTemplates = config!.templates.frontend + const frameworkChoices = Object.entries(frontendTemplates).map(([id, entry]) => ({ + name: entry.label, + value: id, + description: entry.description, + })) + + framework = await select({ + message: 'Frontend framework:', + choices: frameworkChoices, + }) + + // --- Step 6: Package manager --- + packageManager = await select({ + message: 'Package manager:', + choices: [ + { name: 'pnpm', value: 'pnpm' }, + { name: 'npm', value: 'npm' }, + { name: 'yarn', value: 'yarn' }, + ], + }) + } + + // --- Step 7: Confirm --- + console.log('') + console.log(' Summary:') + console.log(` Project: ${projectName}`) + if (doBackend) { + console.log(` Backend: ${language} | ${groupId}:${artifactId}:${version}`) + } + if (doFrontend) { + console.log(` Frontend: ${framework} | ${packageManager}`) + } + console.log('') + + const confirmed = await confirm({ + message: 'Generate project?', + default: true, + }) + + if (!confirmed) { + info('Cancelled.') + process.exit(0) + } + + // Fetch Spring Boot version (best-effort) + const springBootVersion = await fetchSpringBootVersion(config!) + + // Target directory is CWD / projectName + const targetDir = join(process.cwd(), projectName) + + // Generate backend + if (doBackend) { + await generateBackend({ + projectName, + language, + groupId, + artifactId, + version, + description, + targetDir: join(targetDir, 'backend'), + config: config!, + springBootVersion, + }) + } + + // Generate frontend + if (doFrontend) { + await generateFrontend({ + projectName, + framework, + packageManager, + targetDir: join(targetDir, 'frontend'), + config: config!, + }) + } + + printSuccessMessage({ + projectName, + targetDir, + generateBackend: doBackend, + generateFrontend: doFrontend, + language, + framework, + packageManager, + config: config!, + springBootVersion, + }) +} diff --git a/platform/packages/cli/src/generators/backend.ts b/platform/packages/cli/src/generators/backend.ts new file mode 100644 index 00000000..28eb7f03 --- /dev/null +++ b/platform/packages/cli/src/generators/backend.ts @@ -0,0 +1,92 @@ +import { join } from 'node:path' +import { execa } from 'execa' +import { rmSync, mkdirSync } from 'node:fs' +import { type CliConfig } from '../utils/config.js' +import { renameJavaPackages } from '../utils/replace.js' +import { spin, success } from '../utils/logger.js' + +export interface BackendOptions { + projectName: string + language: string + groupId: string + artifactId: string + version: string + description: string + targetDir: string + config: CliConfig + springBootVersion: string +} + +/** + * Clone a backend template from GitHub and rename packages/tokens. + */ +async function cloneBackendTemplate( + language: string, + targetDir: string, + config: CliConfig, +): Promise { + const template = config.templates.backend[language] + if (!template) { + throw new Error(`Unknown backend language: ${language}`) + } + + await execa('git', [ + 'clone', + '--depth=1', + '--branch', + template.branch, + template.repo, + targetDir, + ]) + + // Remove .git directory so the project starts fresh + rmSync(join(targetDir, '.git'), { recursive: true, force: true }) +} + +/** + * Generate the backend project by cloning a template and performing token replacement. + */ +export async function generateBackend(options: BackendOptions): Promise { + const { + language, + groupId, + artifactId, + version, + description, + targetDir, + config, + springBootVersion, + } = options + + const spinner = spin(`Cloning backend template (${language})…`) + + try { + mkdirSync(targetDir, { recursive: true }) + await cloneBackendTemplate(language, targetDir, config) + spinner.succeed('Backend template cloned') + } catch (err) { + spinner.fail('Failed to clone backend template') + throw err + } + + const renameSpinner = spin('Renaming packages and replacing tokens…') + try { + await renameJavaPackages({ + projectDir: targetDir, + groupId, + artifactId, + version, + description, + dynamiaVersion: config.dynamia.version, + springBootVersion, + language: language as 'java' | 'kotlin' | 'groovy', + tokens: config.tokens, + }) + renameSpinner.succeed('Packages renamed') + } catch (err) { + renameSpinner.fail('Failed to rename packages') + throw err + } + + success(`Backend (${language}) generated at ${targetDir}`) +} diff --git a/platform/packages/cli/src/generators/frontend.ts b/platform/packages/cli/src/generators/frontend.ts new file mode 100644 index 00000000..b63f5b32 --- /dev/null +++ b/platform/packages/cli/src/generators/frontend.ts @@ -0,0 +1,129 @@ +import { join } from 'node:path' +import { rmSync, mkdirSync } from 'node:fs' +import { execa } from 'execa' +import { type CliConfig } from '../utils/config.js' +import { replaceTokensInDir } from '../utils/replace.js' +import { spin, warn, success } from '../utils/logger.js' + +// Fallback package names used when cli.properties entries are unavailable +const DEFAULT_SDK_PACKAGE = '@dynamia-tools/sdk' +const DEFAULT_UI_CORE_PACKAGE = '@dynamia-tools/ui-core' + +export interface FrontendOptions { + projectName: string + framework: string + packageManager: string + targetDir: string + config: CliConfig +} + +/** + * Clone a frontend template from GitHub. + */ +async function cloneFrontendTemplate( + framework: string, + targetDir: string, + config: CliConfig, +): Promise { + const template = config.templates.frontend[framework] + if (!template) { + throw new Error(`Unknown frontend framework: ${framework}`) + } + + await execa('git', [ + 'clone', + '--depth=1', + '--branch', + template.branch, + template.repo, + targetDir, + ]) + + rmSync(join(targetDir, '.git'), { recursive: true, force: true }) +} + +/** + * Fallback: scaffold frontend using Vite when the git clone fails. + */ +async function generateWithVite( + framework: string, + targetDir: string, + pm: string, + config: CliConfig, +): Promise { + const viteTemplate = config.vite.templates[framework] + if (!viteTemplate) { + throw new Error(`No Vite template configured for framework: ${framework}`) + } + + await execa(pm, ['create', 'vite@latest', targetDir, '--template', viteTemplate], { + stdio: 'inherit', + }) + + // Install Dynamia SDK packages + const sdkPkg = config.npm['sdk']?.package ?? DEFAULT_SDK_PACKAGE + const uiPkg = config.npm['ui-core']?.package ?? DEFAULT_UI_CORE_PACKAGE + await execa(pm, ['install', sdkPkg, uiPkg], { + cwd: targetDir, + stdio: 'inherit', + }) +} + +/** + * Generate the frontend project. + * Tries to clone the template first; falls back to Vite if clone fails (and fallback is enabled). + */ +export async function generateFrontend(options: FrontendOptions): Promise { + const { projectName, framework, packageManager, targetDir, config } = options + + const tokenMap: Record = { + '{{PROJECT_NAME}}': projectName, + } + + // Map standard tokens too + for (const [key, placeholder] of Object.entries(config.tokens)) { + if (key === 'projectName') { + tokenMap[placeholder] = projectName + } + } + + mkdirSync(targetDir, { recursive: true }) + + let cloneSuccess = false + const spinner = spin(`Cloning frontend template (${framework})…`) + + try { + await cloneFrontendTemplate(framework, targetDir, config) + spinner.succeed('Frontend template cloned') + cloneSuccess = true + } catch (err) { + spinner.warn(`Could not clone frontend template: ${(err as Error).message}`) + } + + if (!cloneSuccess) { + if (!config.vite.fallbackEnabled) { + throw new Error('Frontend template clone failed and Vite fallback is disabled.') + } + warn('Falling back to Vite scaffold…') + const viteSpinner = spin('Scaffolding with Vite…') + try { + await generateWithVite(framework, targetDir, packageManager, config) + viteSpinner.succeed('Vite scaffold complete') + } catch (err) { + viteSpinner.fail('Vite scaffold failed') + throw err + } + } else { + // Replace tokens in cloned template + const replaceSpinner = spin('Replacing project tokens…') + try { + replaceTokensInDir(targetDir, tokenMap) + replaceSpinner.succeed('Tokens replaced') + } catch (err) { + replaceSpinner.fail('Token replacement failed') + throw err + } + } + + success(`Frontend (${framework}) generated at ${targetDir}`) +} diff --git a/platform/packages/cli/src/index.ts b/platform/packages/cli/src/index.ts new file mode 100644 index 00000000..7a018782 --- /dev/null +++ b/platform/packages/cli/src/index.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import { runNew } from './commands/new.js' + +const [, , ...args] = process.argv +const command = args[0] ?? 'new' + +switch (command) { + case 'new': + await runNew() + break + default: + console.error(`Unknown command: ${command}`) + console.error('Usage: dynamia new') + process.exit(1) +} diff --git a/platform/packages/cli/src/utils/config.ts b/platform/packages/cli/src/utils/config.ts new file mode 100644 index 00000000..065a36fb --- /dev/null +++ b/platform/packages/cli/src/utils/config.ts @@ -0,0 +1,154 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join, resolve } from 'node:path' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface TemplateEntry { + label: string + repo: string + branch: string + description: string +} + +export interface CliConfig { + dynamia: { version: string; docsUrl: string } + java: { version: string; sdkmanCandidate: string } + node: { minimumVersion: string } + installer: { sdkmanUrl: string; fnmUrl: string } + templates: { + backend: Record + frontend: Record + } + npm: Record + vite: { fallbackEnabled: boolean; templates: Record } + tokens: Record +} + +// --------------------------------------------------------------------------- +// Parser +// --------------------------------------------------------------------------- + +function parseProperties(content: string): Record { + const result: Record = {} + for (const raw of content.split('\n')) { + const line = raw.trim() + if (!line || line.startsWith('#')) continue + const eqIdx = line.indexOf('=') + if (eqIdx === -1) continue + const key = line.slice(0, eqIdx).trim() + const value = line.slice(eqIdx + 1).trim() + result[key] = value + } + return result +} + +// --------------------------------------------------------------------------- +// Loader +// --------------------------------------------------------------------------- + +export async function loadConfig(): Promise { + // Locate cli.properties relative to this compiled file (dist/utils/config.js) + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + const propsPath = resolve(__dirname, '../../cli.properties') + + const content = readFileSync(propsPath, 'utf-8') + const props = parseProperties(content) + + // --- helpers --- + const get = (key: string, fallback = ''): string => props[key] ?? fallback + + // --- templates --- + const backendTemplates: Record = {} + const frontendTemplates: Record = {} + + for (const [key, value] of Object.entries(props)) { + const backMatch = key.match(/^template\.backend\.(\w+)\.(label|repo|branch|description)$/) + if (backMatch) { + const id = backMatch[1]! + const field = backMatch[2] as keyof TemplateEntry + if (!backendTemplates[id]) { + backendTemplates[id] = { label: '', repo: '', branch: 'main', description: '' } + } + backendTemplates[id]![field] = value + continue + } + + const frontMatch = key.match(/^template\.frontend\.(\w+)\.(label|repo|branch|description)$/) + if (frontMatch) { + const id = frontMatch[1]! + const field = frontMatch[2] as keyof TemplateEntry + if (!frontendTemplates[id]) { + frontendTemplates[id] = { label: '', repo: '', branch: 'main', description: '' } + } + frontendTemplates[id]![field] = value + } + } + + // --- npm packages --- + const npmPackages: Record = {} + for (const [key, value] of Object.entries(props)) { + const m = key.match(/^npm\.(\w[\w-]*)\.(\w+)$/) + if (m) { + const id = m[1]! + const field = m[2]! + if (!npmPackages[id]) npmPackages[id] = { package: '', version: '' } + if (field === 'package' || field === 'version') { + npmPackages[id]![field] = value + } + } + } + + // --- vite fallback templates --- + const viteTemplates: Record = {} + for (const [key, value] of Object.entries(props)) { + const m = key.match(/^vite\.templates\.(\w+)$/) + if (m) { + viteTemplates[m[1]!] = value + } + } + + // --- tokens --- + const tokens: Record = {} + for (const [key, value] of Object.entries(props)) { + if (key.startsWith('token.')) { + // Convert "token.group.id" → "groupId" camelCase key for easy lookup + const parts = key.slice('token.'.length).split('.').filter((p) => p.length > 0) + const camel = parts + .map((p, i) => (i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1))) + .join('') + tokens[camel] = value + } + } + + return { + dynamia: { + version: get('dynamia.version'), + docsUrl: get('dynamia.docs.url'), + }, + java: { + version: get('java.version'), + sdkmanCandidate: get('java.sdkman.candidate'), + }, + node: { + minimumVersion: get('node.minimum.version'), + }, + installer: { + sdkmanUrl: get('installer.sdkman.url'), + fnmUrl: get('installer.fnm.url'), + }, + templates: { + backend: backendTemplates, + frontend: frontendTemplates, + }, + npm: npmPackages, + vite: { + fallbackEnabled: get('vite.fallback.enabled') === 'true', + templates: viteTemplates, + }, + tokens, + } +} diff --git a/platform/packages/cli/src/utils/env.ts b/platform/packages/cli/src/utils/env.ts new file mode 100644 index 00000000..75b744ca --- /dev/null +++ b/platform/packages/cli/src/utils/env.ts @@ -0,0 +1,76 @@ +import { execa } from 'execa' +import { warn, error, info } from './logger.js' + +/** + * Check that JDK 25 is available. + * Returns true if found. Logs a warning (not an error) if a different version is present. + */ +export async function checkJava(): Promise { + try { + const result = await execa('java', ['-version'], { stderr: 'pipe', stdout: 'pipe' }) + // java -version writes to stderr + const output = result.stderr + result.stdout + if (output.includes('25')) { + return true + } + warn('JDK 25 not detected. A different Java version is installed. Some features may not work as expected.') + return true // not a hard stop — just warn + } catch { + warn('Java is not installed. Install JDK 25 via SDKMAN: sdk install java 25-tem') + return false + } +} + +/** + * Check that Node.js >= 22 is available. + * Returns true if the requirement is satisfied. + */ +export async function checkNode(): Promise { + try { + const result = await execa('node', ['--version'], { stdout: 'pipe' }) + const version = result.stdout.trim().replace(/^v/, '') + const majorStr = version.split('.')[0] + const major = majorStr && majorStr.length > 0 ? parseInt(majorStr, 10) : 0 + if (!isNaN(major) && major >= 22) { + return true + } + warn(`Node.js ${version} found but version >=22 is required. Please upgrade Node.js.`) + return false + } catch { + warn('Node.js is not installed. Install it via fnm: curl -fsSL https://fnm.vercel.app/install | bash') + return false + } +} + +/** + * Check that git is available. + * Returns true if found. Exits the process if git is missing — it is REQUIRED. + */ +export async function checkGit(): Promise { + try { + await execa('git', ['--version'], { stdout: 'pipe' }) + return true + } catch { + error( + 'git is not installed. git is required for template cloning.\n' + + ' Install it with your package manager:\n' + + ' Ubuntu/Debian: sudo apt-get install git\n' + + ' macOS: brew install git\n' + + ' Fedora: sudo dnf install git\n' + + ' Arch: sudo pacman -S git', + ) + // error() calls process.exit(1) — this line is never reached + return false + } +} + +/** + * Run all environment checks at startup. + * git missing = hard stop; other warnings are non-fatal. + */ +export async function runChecks(): Promise { + info('Checking environment...') + await checkGit() // exits if missing + await checkJava() // warning only + await checkNode() // warning only +} diff --git a/platform/packages/cli/src/utils/logger.ts b/platform/packages/cli/src/utils/logger.ts new file mode 100644 index 00000000..43d507c0 --- /dev/null +++ b/platform/packages/cli/src/utils/logger.ts @@ -0,0 +1,42 @@ +import chalk from 'chalk' +import ora, { type Ora } from 'ora' + +/** Start a spinner and return the Ora instance so callers can stop it. */ +export function spin(text: string): Ora { + return ora(text).start() +} + +/** Print a green success message. */ +export function success(msg: string): void { + console.log(chalk.green(`✓ ${msg}`)) +} + +/** Print a yellow warning message. */ +export function warn(msg: string): void { + console.warn(chalk.yellow(`⚠ ${msg}`)) +} + +/** Print a red error message and exit with code 1. */ +export function error(msg: string): never { + console.error(chalk.red(`✗ ${msg}`)) + process.exit(1) +} + +/** Print a cyan informational message. */ +export function info(msg: string): void { + console.log(chalk.cyan(`ℹ ${msg}`)) +} + +/** Print the Dynamia Tools ASCII banner. */ +export function banner(): void { + const teal = chalk.hex('#00BCD4') + console.log(teal(' ____ _ _____ _ ')) + console.log(teal(' | _ \\ _ _ _ __ __ _ _ __ ___ (_) __ |_ _|__ ___ | |___ ')) + console.log(teal(' | | | | | | | \'_ \\ / _` | \'_ ` _ \\| |/ _` || |/ _ \\ / _ \\| / __|')) + console.log(teal(' | |_| | |_| | | | | (_| | | | | | | | (_| || | (_) | (_) | \\__ \\')) + console.log(teal(' |____/ \\__, |_| |_|\\__,_|_| |_| |_|_|\\__,_||_|\\___/ \\___/|_|___/')) + console.log(teal(' |___/ ')) + console.log('') + console.log(chalk.bold(' Dynamia Tools CLI')) + console.log('') +} diff --git a/platform/packages/cli/src/utils/replace.ts b/platform/packages/cli/src/utils/replace.ts new file mode 100644 index 00000000..5c7881b2 --- /dev/null +++ b/platform/packages/cli/src/utils/replace.ts @@ -0,0 +1,218 @@ +import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, renameSync, rmdirSync } from 'node:fs' +import { join, extname } from 'node:path' + +// File extensions that will have token replacement applied +const TEXT_EXTENSIONS = new Set([ + '.java', '.kt', '.groovy', + '.xml', '.yml', '.yaml', '.properties', + '.md', '.txt', '.json', '.gradle', '.kts', +]) + +/** + * Replace all token occurrences in a single text file. + * Non-text files (by extension) are silently skipped. + */ +function replaceInFile(filePath: string, tokenMap: Record): void { + const ext = extname(filePath).toLowerCase() + if (!TEXT_EXTENSIONS.has(ext)) return + + let content: string + try { + content = readFileSync(filePath, 'utf-8') + } catch { + return // skip unreadable files + } + + let updated = content + for (const [token, replacement] of Object.entries(tokenMap)) { + updated = updated.split(token).join(replacement) + } + + if (updated !== content) { + writeFileSync(filePath, updated, 'utf-8') + } +} + +/** + * Recursively walk a directory and apply token replacement to all text files. + */ +export function replaceTokensInDir( + dir: string, + tokenMap: Record, +): void { + const entries = readdirSync(dir) + for (const entry of entries) { + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) { + replaceTokensInDir(fullPath, tokenMap) + } else { + replaceInFile(fullPath, tokenMap) + } + } +} + +export interface RenameOptions { + projectDir: string + groupId: string + artifactId: string + version: string + description: string + dynamiaVersion: string + springBootVersion: string + language: 'java' | 'kotlin' | 'groovy' + /** Token map from cli.properties (e.g. { groupId: '{{GROUP_ID}}', ... }) */ + tokens: Record +} + +/** + * Perform full package renaming and token replacement after cloning a backend template. + * + * Steps: + * 1. Compute the base package from groupId + artifactId + * 2. Rename the source/test directories to match the new package + * 3. Replace all tokens in all text files + * 4. Rename the main Application class file + */ +export async function renameJavaPackages(options: RenameOptions): Promise { + const { + projectDir, + groupId, + artifactId, + version, + dynamiaVersion, + springBootVersion, + language, + tokens, + } = options + + // 1. Compute base package: "com.mycompany" + "my-erp" → "com.mycompany.my.erp" + const basePackage = `${groupId}.${artifactId.replace(/-/g, '.')}` + const basePackagePath = basePackage.replace(/\./g, '/') + + // 2. Rename source directories + const srcExtension = language === 'kotlin' ? 'kotlin' : language === 'groovy' ? 'groovy' : 'java' + const oldPackagePath = 'com/example/demo' + const srcDirs = [ + join(projectDir, 'src', 'main', srcExtension), + join(projectDir, 'src', 'test', srcExtension), + ] + + for (const srcBase of srcDirs) { + const oldDir = join(srcBase, oldPackagePath) + const newDir = join(srcBase, basePackagePath) + + try { + statSync(oldDir) + } catch { + continue // directory doesn't exist — skip + } + + mkdirSync(newDir, { recursive: true }) + + // Move all files from oldDir to newDir recursively + moveDirectoryContents(oldDir, newDir) + + // Clean up old (now empty) package directories + removeEmptyDirs(join(srcBase, 'com')) + } + + // 3. Build token map: map cli.properties token values to user values + const replacements: Record = {} + + for (const [key, placeholder] of Object.entries(tokens)) { + switch (key) { + case 'groupId': replacements[placeholder] = groupId; break + case 'artifactId': replacements[placeholder] = artifactId; break + case 'basePackage': replacements[placeholder] = basePackage; break + case 'projectName': replacements[placeholder] = artifactId; break + case 'projectVersion': replacements[placeholder] = version; break + case 'dynamiaVersion': replacements[placeholder] = dynamiaVersion; break + case 'springBootVersion': replacements[placeholder] = springBootVersion; break + } + } + + // Always replace the placeholder package string itself in source files + replacements['com.example.demo'] = basePackage + replacements['com/example/demo'] = basePackagePath + + // 4. Replace tokens in all text files + replaceTokensInDir(projectDir, replacements) + + // 5. Rename the main Application class file + renameApplicationClass(projectDir, artifactId, basePackagePath, srcExtension) +} + +/** + * Move all files and subdirectories from src into dest. + */ +function moveDirectoryContents(src: string, dest: string): void { + const entries = readdirSync(src) + for (const entry of entries) { + const srcPath = join(src, entry) + const destPath = join(dest, entry) + const stat = statSync(srcPath) + if (stat.isDirectory()) { + mkdirSync(destPath, { recursive: true }) + moveDirectoryContents(srcPath, destPath) + try { rmdirSync(srcPath) } catch { /* not empty — skip */ } + } else { + renameSync(srcPath, destPath) + } + } +} + +/** + * Remove empty directories recursively (best-effort). + */ +function removeEmptyDirs(dir: string): void { + try { + const entries = readdirSync(dir) + for (const entry of entries) { + const full = join(dir, entry) + if (statSync(full).isDirectory()) { + removeEmptyDirs(full) + } + } + try { rmdirSync(dir) } catch { /* not empty or doesn't exist */ } + } catch { /* ignore */ } +} + +/** + * Rename DemoApplication.java → MyErpApplication.java (or .kt / .groovy). + */ +function renameApplicationClass( + projectDir: string, + artifactId: string, + basePackagePath: string, + srcExtension: string, +): void { + // Build new class name: "my-erp" → "MyErp" → "MyErpApplication" + const newClassName = + artifactId + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + 'Application' + + const fileExtMap: Record = { + kotlin: '.kt', + groovy: '.groovy', + java: '.java', + } + const fileExt = fileExtMap[srcExtension] ?? '.java' + const oldFileName = `DemoApplication${fileExt}` + const newFileName = `${newClassName}${fileExt}` + + const searchDir = join(projectDir, 'src', 'main', srcExtension, basePackagePath) + try { + const files = readdirSync(searchDir) + for (const file of files) { + if (file === oldFileName) { + renameSync(join(searchDir, file), join(searchDir, newFileName)) + break + } + } + } catch { + // Directory doesn't exist or file not found — skip silently + } +} diff --git a/platform/packages/cli/tsconfig.json b/platform/packages/cli/tsconfig.json new file mode 100644 index 00000000..4174b878 --- /dev/null +++ b/platform/packages/cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 2cc95e473a23bbe3613e2879b7dbbfdfff0d1cb2 Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 16:32:10 -0500 Subject: [PATCH 3/8] chore: update dependencies and minimum version requirements to 26.4.1 --- platform/packages/cli/README.md | 10 +++++----- platform/packages/cli/cli.properties | 12 ++++++------ platform/packages/cli/install.sh | 24 +++++++++++++++--------- platform/packages/cli/package-lock.json | 23 ++++++++++++----------- platform/packages/cli/package.json | 6 +++--- 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/platform/packages/cli/README.md b/platform/packages/cli/README.md index a26d60ee..09102182 100644 --- a/platform/packages/cli/README.md +++ b/platform/packages/cli/README.md @@ -22,11 +22,11 @@ npm install -g @dynamia-tools/cli ### Requirements -| Tool | Minimum version | -|---|---| -| Node.js | 22 | -| git | any recent version (required) | -| Java | 25 (recommended) | +| Tool | Minimum version | +|---|--------------------------------| +| Node.js | 24 | +| git | any recent version (required) | +| Java | 25 (recommended) | --- diff --git a/platform/packages/cli/cli.properties b/platform/packages/cli/cli.properties index 0f5a88d7..ce05a0ae 100644 --- a/platform/packages/cli/cli.properties +++ b/platform/packages/cli/cli.properties @@ -5,7 +5,7 @@ # --- # dynamia.* # --- -dynamia.version=26.3.0 +dynamia.version=26.4.1 dynamia.docs.url=https://dynamia.tools/docs # --- @@ -17,14 +17,14 @@ java.sdkman.candidate=25-tem # --- # node.* # --- -node.minimum.version=22 +node.minimum.version=24 # --- # spring.* (kept as reference metadata, not used for generation) # --- spring.initializr.url=https://start.spring.io spring.initializr.metadata.url=https://start.spring.io/metadata/client -spring.boot.version=3.4.0 +spring.boot.version=4.0.5 # --- # template.backend..* @@ -68,11 +68,11 @@ vite.templates.react=react-ts # npm.* (SDK package names and versions) # --- npm.sdk.package=@dynamia-tools/sdk -npm.sdk.version=26.3.0 +npm.sdk.version=26.4.1 npm.ui-core.package=@dynamia-tools/ui-core -npm.ui-core.version=26.3.0 +npm.ui-core.version=26.4.1 npm.vue.package=@dynamia-tools/vue -npm.vue.version=26.3.0 +npm.vue.version=26.4.1 # --- # installer.* diff --git a/platform/packages/cli/install.sh b/platform/packages/cli/install.sh index 74ceaf64..e0502229 100755 --- a/platform/packages/cli/install.sh +++ b/platform/packages/cli/install.sh @@ -109,6 +109,17 @@ install_prerequisites() { install_java() { info "Checking JDK 25..." + if java -version 2>&1 | grep -qE '^(openjdk|java) version "25'; then + success "JDK 25 already installed" + return + fi + + # Only bootstrap SDKMAN when JDK 25 is missing + if [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]]; then + # shellcheck source=/dev/null + source "$HOME/.sdkman/bin/sdkman-init.sh" + fi + if ! command -v sdk &>/dev/null; then info "Installing SDKMAN..." curl -s "https://get.sdkman.io" | bash @@ -116,14 +127,10 @@ install_java() { source "$HOME/.sdkman/bin/sdkman-init.sh" fi - if java -version 2>&1 | grep -qE '^(openjdk|java) version "25'; then - success "JDK 25 already installed" - else - info "Installing JDK 25 via SDKMAN..." - sdk install java 25-tem - sdk default java 25-tem - success "JDK 25 installed" - fi + info "Installing JDK 25 via SDKMAN..." + sdk install java 25-tem + sdk default java 25-tem + success "JDK 25 installed" } # --------------------------------------------------------------------------- @@ -131,7 +138,6 @@ install_java() { # --------------------------------------------------------------------------- install_node() { info "Checking Node.js..." - if ! command -v fnm &>/dev/null; then info "Installing fnm (Fast Node Manager)..." curl -fsSL https://fnm.vercel.app/install | bash diff --git a/platform/packages/cli/package-lock.json b/platform/packages/cli/package-lock.json index 4819e6d4..8ba6d016 100644 --- a/platform/packages/cli/package-lock.json +++ b/platform/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamia-tools/cli", - "version": "26.3.0", + "version": "26.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dynamia-tools/cli", - "version": "26.3.0", + "version": "26.4.1", "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^7.0.0", @@ -18,11 +18,11 @@ "dynamia": "dist/index.js" }, "devDependencies": { - "@types/node": "^22.0.0", + "@types/node": "^24.0.0", "typescript": "^5.7.0" }, "engines": { - "node": ">=22" + "node": ">=24" } }, "node_modules/@inquirer/ansi": { @@ -378,13 +378,14 @@ } }, "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/ansi-regex": { @@ -940,9 +941,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "devOptional": true, "license": "MIT" }, diff --git a/platform/packages/cli/package.json b/platform/packages/cli/package.json index e1443794..b927182c 100644 --- a/platform/packages/cli/package.json +++ b/platform/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/cli", - "version": "26.3.0", + "version": "26.4.1", "description": "Dynamia Tools CLI — Scaffold new Dynamia Platform projects", "keywords": [ "dynamia", @@ -46,7 +46,7 @@ "ora": "^8.0.0" }, "devDependencies": { - "@types/node": "^22.0.0", + "@types/node": "^24.0.0", "typescript": "^5.7.0" }, "publishConfig": { @@ -54,6 +54,6 @@ "registry": "https://registry.npmjs.org/" }, "engines": { - "node": ">=22" + "node": ">=24" } } From e4af264fa161ce912589de8489ccea5a8b80bda4 Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 17:41:20 -0500 Subject: [PATCH 4/8] feat: enhance CLI tool with template validation and improved error handling --- platform/packages/cli/README.md | 11 ++- platform/packages/cli/cli.properties | 17 ++++ platform/packages/cli/src/commands/new.ts | 85 ++++++++++-------- .../packages/cli/src/generators/backend.ts | 31 +++++-- .../packages/cli/src/generators/frontend.ts | 34 ++++++-- platform/packages/cli/src/index.ts | 6 +- platform/packages/cli/src/utils/config.ts | 51 +++++++++-- platform/packages/cli/src/utils/logger.ts | 86 +++++++++++++++++++ platform/packages/cli/src/utils/replace.ts | 55 +++++++++++- .../packages/cli/src/utils/template-repo.ts | 42 +++++++++ 10 files changed, 356 insertions(+), 62 deletions(-) create mode 100644 platform/packages/cli/src/utils/template-repo.ts diff --git a/platform/packages/cli/README.md b/platform/packages/cli/README.md index 09102182..2f475d28 100644 --- a/platform/packages/cli/README.md +++ b/platform/packages/cli/README.md @@ -46,6 +46,10 @@ The wizard guides you through: 6. **Package manager** — pnpm, npm, or yarn 7. **Confirm** — shows a summary table before generating +> Beta note: this CLI is in active beta. If a command or template is not ready yet, you will get a friendly "not available yet" message. + +Errors include short support codes such as `DT-TEMPLATE-002` to make issue reporting easier. + --- ## What gets generated @@ -59,6 +63,7 @@ my-erp-app/ ### Backend - Cloned from a GitHub template repo (e.g. `dynamiatools/template-backend-java`) +- Template repository and branch are validated before cloning - Placeholder package `com.example.demo` renamed to your `groupId.artifactId` - All tokens replaced in `.java`, `.kt`, `.groovy`, `.xml`, `.yml`, `.properties`, `.md` - `DemoApplication.java` renamed to `Application.java` @@ -66,7 +71,8 @@ my-erp-app/ ### Frontend - Cloned from a GitHub template repo (e.g. `dynamiatools/template-frontend-vue`) -- Falls back to `npm create vite@latest` if clone fails +- Template repository and branch are validated before cloning +- Falls back to `npm create vite@latest` if template clone/validation fails - `@dynamia-tools/sdk` and `@dynamia-tools/ui-core` installed automatically --- @@ -80,9 +86,12 @@ Key sections: | Section | Description | |---|---| | `dynamia.*` | Framework version and docs URL | +| `beta.*` | Beta-mode UX messages | | `java.*` | JDK version and SDKMAN candidate | | `template.backend..*` | Backend template repos (java, kotlin, groovy) | | `template.frontend..*` | Frontend template repos (vue, react) | +| `template.*..enabled` | Enable/disable template options in the wizard | +| `template.*..availabilityMessage` | Message shown when a template is disabled | | `token.*` | Placeholder tokens used inside template repos | | `vite.*` | Vite fallback config | diff --git a/platform/packages/cli/cli.properties b/platform/packages/cli/cli.properties index ce05a0ae..e96ec625 100644 --- a/platform/packages/cli/cli.properties +++ b/platform/packages/cli/cli.properties @@ -8,6 +8,13 @@ dynamia.version=26.4.1 dynamia.docs.url=https://dynamia.tools/docs +# --- +# beta.* +# --- +beta.enabled=true +beta.intro.message.enabled=true +beta.not.available.message.enabled=true + # --- # java.* # --- @@ -33,16 +40,22 @@ template.backend.java.label=Java template.backend.java.repo=https://github.com/dynamiatools/template-backend-java template.backend.java.branch=main template.backend.java.description=Spring Boot + Dynamia Tools (Java) +template.backend.java.enabled=true +template.backend.java.availabilityMessage=Backend Java template is not available yet. template.backend.kotlin.label=Kotlin template.backend.kotlin.repo=https://github.com/dynamiatools/template-backend-kotlin template.backend.kotlin.branch=main template.backend.kotlin.description=Spring Boot + Dynamia Tools (Kotlin) +template.backend.kotlin.enabled=true +template.backend.kotlin.availabilityMessage=Backend Kotlin template is not available yet. template.backend.groovy.label=Groovy template.backend.groovy.repo=https://github.com/dynamiatools/template-backend-groovy template.backend.groovy.branch=main template.backend.groovy.description=Spring Boot + Dynamia Tools (Groovy) +template.backend.groovy.enabled=true +template.backend.groovy.availabilityMessage=Backend Groovy template is not available yet. # --- # template.frontend..* @@ -51,11 +64,15 @@ template.frontend.vue.label=Vue 3 template.frontend.vue.repo=https://github.com/dynamiatools/template-frontend-vue template.frontend.vue.branch=main template.frontend.vue.description=Vue 3 + Vite + @dynamia-tools/vue +template.frontend.vue.enabled=true +template.frontend.vue.availabilityMessage=Vue frontend template is not available yet. template.frontend.react.label=React template.frontend.react.repo=https://github.com/dynamiatools/template-frontend-react template.frontend.react.branch=main template.frontend.react.description=React + Vite + @dynamia-tools/sdk +template.frontend.react.enabled=true +template.frontend.react.availabilityMessage=React frontend template is not available yet. # --- # vite.* (fallback when git clone fails) diff --git a/platform/packages/cli/src/commands/new.ts b/platform/packages/cli/src/commands/new.ts index eaec26be..8be3ade0 100644 --- a/platform/packages/cli/src/commands/new.ts +++ b/platform/packages/cli/src/commands/new.ts @@ -2,7 +2,7 @@ import { input, select, confirm } from '@inquirer/prompts' import { join } from 'node:path' import { loadConfig, type CliConfig } from '../utils/config.js' import { runChecks } from '../utils/env.js' -import { banner, info, success, error, warn } from '../utils/logger.js' +import { banner, info, error, beta, errorMessage, errorWithCode } from '../utils/logger.js' import { generateBackend } from '../generators/backend.js' import { generateFrontend } from '../generators/frontend.js' @@ -23,13 +23,12 @@ async function fetchSpringBootVersion(_config: CliConfig): Promise { // ignore — use fallback } // Fallback from config (spring.boot.version kept as reference) - return '3.4.0' + return '4.0.5' } /** Print the final success message. */ function printSuccessMessage(opts: { projectName: string - targetDir: string generateBackend: boolean generateFrontend: boolean language: string @@ -40,7 +39,6 @@ function printSuccessMessage(opts: { }): void { const { projectName, - targetDir, generateBackend: doBackend, generateFrontend: doFrontend, language, @@ -97,7 +95,12 @@ export async function runNew(): Promise { try { config = await loadConfig() } catch (err) { - error(`Failed to load CLI configuration: ${(err as Error).message}`) + const reason = err instanceof Error ? err.message : 'Unknown configuration error.' + errorWithCode('DT-CONFIG-001', `Failed to load CLI configuration: ${reason}`) + } + + if (config.beta.enabled && config.beta.introMessageEnabled) { + beta('Dynamia Tools CLI is in beta. Some features may still be under active development.') } // Environment checks (git missing = hard stop) @@ -137,13 +140,18 @@ export async function runNew(): Promise { let description = '' if (doBackend) { - const backendTemplates = config!.templates.backend + const backendTemplates = config.templates.backend const languageChoices = Object.entries(backendTemplates).map(([id, entry]) => ({ name: entry.label, value: id, - description: entry.description, + description: entry.enabled ? entry.description : `${entry.description} — ${entry.availabilityMessage}`, + disabled: entry.enabled ? false : entry.availabilityMessage, })) + if (!languageChoices.some((choice) => !choice.disabled)) { + errorWithCode('DT-BACKEND-005', 'Backend templates are not available yet. Please try frontend only for now.') + } + language = await select({ message: 'Backend language:', choices: languageChoices, @@ -176,13 +184,18 @@ export async function runNew(): Promise { let packageManager = 'pnpm' if (doFrontend) { - const frontendTemplates = config!.templates.frontend + const frontendTemplates = config.templates.frontend const frameworkChoices = Object.entries(frontendTemplates).map(([id, entry]) => ({ name: entry.label, value: id, - description: entry.description, + description: entry.enabled ? entry.description : `${entry.description} — ${entry.availabilityMessage}`, + disabled: entry.enabled ? false : entry.availabilityMessage, })) + if (!frameworkChoices.some((choice) => !choice.disabled)) { + errorWithCode('DT-FRONTEND-005', 'Frontend templates are not available yet. Please try again soon.') + } + framework = await select({ message: 'Frontend framework:', choices: frameworkChoices, @@ -222,46 +235,50 @@ export async function runNew(): Promise { } // Fetch Spring Boot version (best-effort) - const springBootVersion = await fetchSpringBootVersion(config!) + const springBootVersion = await fetchSpringBootVersion(config) // Target directory is CWD / projectName const targetDir = join(process.cwd(), projectName) // Generate backend - if (doBackend) { - await generateBackend({ - projectName, - language, - groupId, - artifactId, - version, - description, - targetDir: join(targetDir, 'backend'), - config: config!, - springBootVersion, - }) - } + try { + if (doBackend) { + await generateBackend({ + projectName, + language, + groupId, + artifactId, + version, + description, + targetDir: join(targetDir, 'backend'), + config, + springBootVersion, + }) + } - // Generate frontend - if (doFrontend) { - await generateFrontend({ - projectName, - framework, - packageManager, - targetDir: join(targetDir, 'frontend'), - config: config!, - }) + // Generate frontend + if (doFrontend) { + await generateFrontend({ + projectName, + framework, + packageManager, + targetDir: join(targetDir, 'frontend'), + config, + }) + } + } catch (err) { + const reason = errorMessage(err, 'DT-RUN-001') + error(`${reason}\nIf this keeps happening, please open an issue at https://github.com/dynamiatools/framework/issues`) } printSuccessMessage({ projectName, - targetDir, generateBackend: doBackend, generateFrontend: doFrontend, language, framework, packageManager, - config: config!, + config, springBootVersion, }) } diff --git a/platform/packages/cli/src/generators/backend.ts b/platform/packages/cli/src/generators/backend.ts index 28eb7f03..afd899dc 100644 --- a/platform/packages/cli/src/generators/backend.ts +++ b/platform/packages/cli/src/generators/backend.ts @@ -3,7 +3,8 @@ import { execa } from 'execa' import { rmSync, mkdirSync } from 'node:fs' import { type CliConfig } from '../utils/config.js' import { renameJavaPackages } from '../utils/replace.js' -import { spin, success } from '../utils/logger.js' +import { spin, success, friendlyGitError, notAvailableYet, cliError, errorMessage } from '../utils/logger.js' +import { validateTemplateRepo } from '../utils/template-repo.js' export interface BackendOptions { projectName: string @@ -27,7 +28,21 @@ async function cloneBackendTemplate( ): Promise { const template = config.templates.backend[language] if (!template) { - throw new Error(`Unknown backend language: ${language}`) + throw cliError('DT-BACKEND-001', `Unknown backend language: ${language}.`) + } + if (!template.enabled) { + throw cliError('DT-BACKEND-002', template.availabilityMessage) + } + + const validation = await validateTemplateRepo({ + repo: template.repo, + branch: template.branch, + }) + if (!validation.ok) { + throw cliError( + 'DT-TEMPLATE-001', + `Backend template validation failed for ${template.repo}#${template.branch}. ${validation.reason ?? ''}`.trim(), + ) } await execa('git', [ @@ -48,6 +63,7 @@ async function cloneBackendTemplate( */ export async function generateBackend(options: BackendOptions): Promise { const { + projectName, language, groupId, artifactId, @@ -65,14 +81,19 @@ export async function generateBackend(options: BackendOptions): Promise { await cloneBackendTemplate(language, targetDir, config) spinner.succeed('Backend template cloned') } catch (err) { - spinner.fail('Failed to clone backend template') - throw err + spinner.fail('Could not prepare backend template') + const reason = friendlyGitError(err) + if (config.beta.notAvailableMessageEnabled) { + notAvailableYet('Automatic backend generation for this template setup', 'DT-BACKEND-003') + } + throw cliError('DT-BACKEND-003', `${reason}\nTip: Please verify template.backend.${language}.* entries in cli.properties.`) } const renameSpinner = spin('Renaming packages and replacing tokens…') try { await renameJavaPackages({ projectDir: targetDir, + projectName, groupId, artifactId, version, @@ -85,7 +106,7 @@ export async function generateBackend(options: BackendOptions): Promise { renameSpinner.succeed('Packages renamed') } catch (err) { renameSpinner.fail('Failed to rename packages') - throw err + throw cliError('DT-BACKEND-004', `Backend token/package replacement failed: ${errorMessage(err)}`) } success(`Backend (${language}) generated at ${targetDir}`) diff --git a/platform/packages/cli/src/generators/frontend.ts b/platform/packages/cli/src/generators/frontend.ts index b63f5b32..0eee68a4 100644 --- a/platform/packages/cli/src/generators/frontend.ts +++ b/platform/packages/cli/src/generators/frontend.ts @@ -3,7 +3,8 @@ import { rmSync, mkdirSync } from 'node:fs' import { execa } from 'execa' import { type CliConfig } from '../utils/config.js' import { replaceTokensInDir } from '../utils/replace.js' -import { spin, warn, success } from '../utils/logger.js' +import { spin, warn, success, friendlyGitError, cliError, errorMessage } from '../utils/logger.js' +import { validateTemplateRepo } from '../utils/template-repo.js' // Fallback package names used when cli.properties entries are unavailable const DEFAULT_SDK_PACKAGE = '@dynamia-tools/sdk' @@ -27,7 +28,21 @@ async function cloneFrontendTemplate( ): Promise { const template = config.templates.frontend[framework] if (!template) { - throw new Error(`Unknown frontend framework: ${framework}`) + throw cliError('DT-FRONTEND-001', `Unknown frontend framework: ${framework}.`) + } + if (!template.enabled) { + throw cliError('DT-FRONTEND-002', template.availabilityMessage) + } + + const validation = await validateTemplateRepo({ + repo: template.repo, + branch: template.branch, + }) + if (!validation.ok) { + throw cliError( + 'DT-TEMPLATE-002', + `Frontend template validation failed for ${template.repo}#${template.branch}. ${validation.reason ?? ''}`.trim(), + ) } await execa('git', [ @@ -53,7 +68,7 @@ async function generateWithVite( ): Promise { const viteTemplate = config.vite.templates[framework] if (!viteTemplate) { - throw new Error(`No Vite template configured for framework: ${framework}`) + throw cliError('DT-FRONTEND-003', `No Vite template configured for framework: ${framework}`) } await execa(pm, ['create', 'vite@latest', targetDir, '--template', viteTemplate], { @@ -97,21 +112,24 @@ export async function generateFrontend(options: FrontendOptions): Promise spinner.succeed('Frontend template cloned') cloneSuccess = true } catch (err) { - spinner.warn(`Could not clone frontend template: ${(err as Error).message}`) + spinner.warn(`Could not clone frontend template: ${friendlyGitError(err)}`) } if (!cloneSuccess) { if (!config.vite.fallbackEnabled) { - throw new Error('Frontend template clone failed and Vite fallback is disabled.') + throw cliError( + 'DT-FRONTEND-003', + 'Frontend template is currently not available and Vite fallback is disabled. Please try again later or enable vite.fallback.enabled in cli.properties.', + ) } - warn('Falling back to Vite scaffold…') + warn('Template is unavailable right now, so we will use Vite fallback.') const viteSpinner = spin('Scaffolding with Vite…') try { await generateWithVite(framework, targetDir, packageManager, config) viteSpinner.succeed('Vite scaffold complete') } catch (err) { viteSpinner.fail('Vite scaffold failed') - throw err + throw cliError('DT-FRONTEND-004', `Vite fallback failed: ${errorMessage(err)}`) } } else { // Replace tokens in cloned template @@ -121,7 +139,7 @@ export async function generateFrontend(options: FrontendOptions): Promise replaceSpinner.succeed('Tokens replaced') } catch (err) { replaceSpinner.fail('Token replacement failed') - throw err + throw cliError('DT-FRONTEND-004', `Frontend token replacement failed: ${errorMessage(err)}`) } } diff --git a/platform/packages/cli/src/index.ts b/platform/packages/cli/src/index.ts index 7a018782..ad169ae4 100644 --- a/platform/packages/cli/src/index.ts +++ b/platform/packages/cli/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { runNew } from './commands/new.js' +import { errorWithCode, notAvailableYet } from './utils/logger.js' const [, , ...args] = process.argv const command = args[0] ?? 'new' @@ -9,7 +10,6 @@ switch (command) { await runNew() break default: - console.error(`Unknown command: ${command}`) - console.error('Usage: dynamia new') - process.exit(1) + notAvailableYet(`Command "${command}"`, 'DT-COMMAND-001') + errorWithCode('DT-COMMAND-002', 'Usage: dynamia new') } diff --git a/platform/packages/cli/src/utils/config.ts b/platform/packages/cli/src/utils/config.ts index 065a36fb..da9637fb 100644 --- a/platform/packages/cli/src/utils/config.ts +++ b/platform/packages/cli/src/utils/config.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' -import { dirname, join, resolve } from 'node:path' +import { dirname, resolve } from 'node:path' // --------------------------------------------------------------------------- // Types @@ -11,10 +11,17 @@ export interface TemplateEntry { repo: string branch: string description: string + enabled: boolean + availabilityMessage: string } export interface CliConfig { dynamia: { version: string; docsUrl: string } + beta: { + enabled: boolean + introMessageEnabled: boolean + notAvailableMessageEnabled: boolean + } java: { version: string; sdkmanCandidate: string } node: { minimumVersion: string } installer: { sdkmanUrl: string; fnmUrl: string } @@ -65,26 +72,47 @@ export async function loadConfig(): Promise { const backendTemplates: Record = {} const frontendTemplates: Record = {} + const createTemplate = (): TemplateEntry => ({ + label: '', + repo: '', + branch: 'main', + description: '', + enabled: true, + availabilityMessage: 'Not available yet in this beta release.', + }) + for (const [key, value] of Object.entries(props)) { - const backMatch = key.match(/^template\.backend\.(\w+)\.(label|repo|branch|description)$/) + const backMatch = key.match(/^template\.backend\.(\w+)\.(label|repo|branch|description|enabled|availabilityMessage)$/) if (backMatch) { const id = backMatch[1]! - const field = backMatch[2] as keyof TemplateEntry + const field = backMatch[2]! if (!backendTemplates[id]) { - backendTemplates[id] = { label: '', repo: '', branch: 'main', description: '' } + backendTemplates[id] = createTemplate() + } + if (field === 'enabled') { + backendTemplates[id]!.enabled = value !== 'false' + } else if (field === 'availabilityMessage') { + backendTemplates[id]!.availabilityMessage = value + } else { + backendTemplates[id]![field as 'label' | 'repo' | 'branch' | 'description'] = value } - backendTemplates[id]![field] = value continue } - const frontMatch = key.match(/^template\.frontend\.(\w+)\.(label|repo|branch|description)$/) + const frontMatch = key.match(/^template\.frontend\.(\w+)\.(label|repo|branch|description|enabled|availabilityMessage)$/) if (frontMatch) { const id = frontMatch[1]! - const field = frontMatch[2] as keyof TemplateEntry + const field = frontMatch[2]! if (!frontendTemplates[id]) { - frontendTemplates[id] = { label: '', repo: '', branch: 'main', description: '' } + frontendTemplates[id] = createTemplate() + } + if (field === 'enabled') { + frontendTemplates[id]!.enabled = value !== 'false' + } else if (field === 'availabilityMessage') { + frontendTemplates[id]!.availabilityMessage = value + } else { + frontendTemplates[id]![field as 'label' | 'repo' | 'branch' | 'description'] = value } - frontendTemplates[id]![field] = value } } @@ -129,6 +157,11 @@ export async function loadConfig(): Promise { version: get('dynamia.version'), docsUrl: get('dynamia.docs.url'), }, + beta: { + enabled: get('beta.enabled', 'true') === 'true', + introMessageEnabled: get('beta.intro.message.enabled', 'true') === 'true', + notAvailableMessageEnabled: get('beta.not.available.message.enabled', 'true') === 'true', + }, java: { version: get('java.version'), sdkmanCandidate: get('java.sdkman.candidate'), diff --git a/platform/packages/cli/src/utils/logger.ts b/platform/packages/cli/src/utils/logger.ts index 43d507c0..7ab449fc 100644 --- a/platform/packages/cli/src/utils/logger.ts +++ b/platform/packages/cli/src/utils/logger.ts @@ -1,6 +1,43 @@ import chalk from 'chalk' import ora, { type Ora } from 'ora' +export type CliErrorCode = + | 'DT-CONFIG-001' + | 'DT-COMMAND-001' + | 'DT-COMMAND-002' + | 'DT-RUN-001' + | 'DT-BACKEND-001' + | 'DT-BACKEND-002' + | 'DT-BACKEND-003' + | 'DT-BACKEND-004' + | 'DT-BACKEND-005' + | 'DT-FRONTEND-001' + | 'DT-FRONTEND-002' + | 'DT-FRONTEND-003' + | 'DT-FRONTEND-004' + | 'DT-FRONTEND-005' + | 'DT-TEMPLATE-001' + | 'DT-TEMPLATE-002' + | 'DT-UNKNOWN-001' + +export class CliError extends Error { + constructor( + public readonly code: CliErrorCode, + message: string, + ) { + super(message) + this.name = 'CliError' + } +} + +export function cliError(code: CliErrorCode, message: string): CliError { + return new CliError(code, message) +} + +export function withErrorCode(code: CliErrorCode, message: string): string { + return `[${code}] ${message}` +} + /** Start a spinner and return the Ora instance so callers can stop it. */ export function spin(text: string): Ora { return ora(text).start() @@ -22,11 +59,60 @@ export function error(msg: string): never { process.exit(1) } +export function errorWithCode(code: CliErrorCode, message: string): never { + return error(withErrorCode(code, message)) +} + /** Print a cyan informational message. */ export function info(msg: string): void { console.log(chalk.cyan(`ℹ ${msg}`)) } +/** Print a beta notice in a friendly tone. */ +export function beta(msg: string): void { + console.log(chalk.magenta(`β ${msg}`)) +} + +/** Print a standard message for features not available in beta yet. */ +export function notAvailableYet(feature: string, code?: CliErrorCode): void { + const message = `${feature} is not available yet in this beta release. We're actively working on it.` + warn(code ? withErrorCode(code, message) : message) +} + +/** Convert unknown errors into user-friendly text. */ +export function errorMessage(err: unknown, fallbackCode: CliErrorCode = 'DT-UNKNOWN-001'): string { + if (err instanceof CliError) { + return withErrorCode(err.code, err.message) + } + if (err instanceof Error) { + const maybeShort = err.message.trim() + if (maybeShort.length > 0) return withErrorCode(fallbackCode, maybeShort) + return withErrorCode(fallbackCode, 'Unexpected error.') + } + return withErrorCode(fallbackCode, 'Unexpected error.') +} + +/** Improve common git-related raw errors for end users. */ +export function friendlyGitError(err: unknown): string { + const raw = errorMessage(err) + const normalized = raw.toLowerCase() + + if (normalized.includes('repository not found')) { + return 'Template repository not found. Please verify the template repository URL in cli.properties.' + } + if (normalized.includes('could not resolve host')) { + return 'Could not reach the template repository host. Please check your internet connection and DNS settings.' + } + if (normalized.includes('remote branch') && normalized.includes('not found')) { + return 'Template branch was not found in the repository. Please verify the configured branch in cli.properties.' + } + if (normalized.includes('authentication failed') || normalized.includes('permission denied')) { + return 'Could not access the template repository. Check repository visibility and access permissions.' + } + + return raw +} + /** Print the Dynamia Tools ASCII banner. */ export function banner(): void { const teal = chalk.hex('#00BCD4') diff --git a/platform/packages/cli/src/utils/replace.ts b/platform/packages/cli/src/utils/replace.ts index 5c7881b2..87add47d 100644 --- a/platform/packages/cli/src/utils/replace.ts +++ b/platform/packages/cli/src/utils/replace.ts @@ -54,6 +54,7 @@ export function replaceTokensInDir( export interface RenameOptions { projectDir: string + projectName: string groupId: string artifactId: string version: string @@ -77,9 +78,11 @@ export interface RenameOptions { export async function renameJavaPackages(options: RenameOptions): Promise { const { projectDir, + projectName, groupId, artifactId, version, + description, dynamiaVersion, springBootVersion, language, @@ -125,13 +128,22 @@ export async function renameJavaPackages(options: RenameOptions): Promise case 'groupId': replacements[placeholder] = groupId; break case 'artifactId': replacements[placeholder] = artifactId; break case 'basePackage': replacements[placeholder] = basePackage; break - case 'projectName': replacements[placeholder] = artifactId; break + case 'projectName': replacements[placeholder] = projectName; break case 'projectVersion': replacements[placeholder] = version; break + case 'projectDescription': replacements[placeholder] = description; break case 'dynamiaVersion': replacements[placeholder] = dynamiaVersion; break case 'springBootVersion': replacements[placeholder] = springBootVersion; break } } + // Fallbacks for templates that still use Spring Initializr defaults instead of placeholders. + replacements['com.example'] = `${groupId}` + replacements['demo'] = `${artifactId}` + replacements['0.0.1-SNAPSHOT'] = `${version}` + replacements['DynamiaTools App Backend'] = `${projectName}` + replacements['26.4.1'] = `${dynamiaVersion}` + replacements['4.0.5'] = `${springBootVersion}` + // Always replace the placeholder package string itself in source files replacements['com.example.demo'] = basePackage replacements['com/example/demo'] = basePackagePath @@ -139,6 +151,9 @@ export async function renameJavaPackages(options: RenameOptions): Promise // 4. Replace tokens in all text files replaceTokensInDir(projectDir, replacements) + // Ensure a user-provided description updates pom.xml even when template has multiline defaults. + replacePomDescription(projectDir, description) + // 5. Rename the main Application class file renameApplicationClass(projectDir, artifactId, basePackagePath, srcExtension) } @@ -178,6 +193,28 @@ function removeEmptyDirs(dir: string): void { } catch { /* ignore */ } } +/** + * Update pom.xml description content when template uses a hardcoded multiline block. + */ +function replacePomDescription(projectDir: string, description: string): void { + if (!description) return + + const pomPath = join(projectDir, 'pom.xml') + let pomContent: string + try { + pomContent = readFileSync(pomPath, 'utf-8') + } catch { + return + } + + const normalized = description.trim().replace(/[\r\n]+/g, ' ') + const updated = pomContent.replace(/[\s\S]*?<\/description>/, `${normalized}`) + + if (updated !== pomContent) { + writeFileSync(pomPath, updated, 'utf-8') + } +} + /** * Rename DemoApplication.java → MyErpApplication.java (or .kt / .groovy). */ @@ -202,13 +239,27 @@ function renameApplicationClass( const fileExt = fileExtMap[srcExtension] ?? '.java' const oldFileName = `DemoApplication${fileExt}` const newFileName = `${newClassName}${fileExt}` + const oldClassName = 'DemoApplication' const searchDir = join(projectDir, 'src', 'main', srcExtension, basePackagePath) try { const files = readdirSync(searchDir) for (const file of files) { if (file === oldFileName) { - renameSync(join(searchDir, file), join(searchDir, newFileName)) + const oldFilePath = join(searchDir, file) + const newFilePath = join(searchDir, newFileName) + + try { + const content = readFileSync(oldFilePath, 'utf-8') + const updated = content.split(oldClassName).join(newClassName) + if (updated !== content) { + writeFileSync(oldFilePath, updated, 'utf-8') + } + } catch { + // If class-content replacement fails, continue with file rename attempt. + } + + renameSync(oldFilePath, newFilePath) break } } diff --git a/platform/packages/cli/src/utils/template-repo.ts b/platform/packages/cli/src/utils/template-repo.ts new file mode 100644 index 00000000..3afec14a --- /dev/null +++ b/platform/packages/cli/src/utils/template-repo.ts @@ -0,0 +1,42 @@ +import { execa } from 'execa' + +export interface TemplateRef { + repo: string + branch: string +} + +export interface ValidationResult { + ok: boolean + reason?: string +} + +/** + * Validate that a git repository is reachable and that a branch exists before cloning. + */ +export async function validateTemplateRepo(template: TemplateRef): Promise { + try { + // Fast check for repo access. + await execa('git', ['ls-remote', '--heads', template.repo], { stdout: 'pipe', stderr: 'pipe' }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Repository is not reachable.' + return { ok: false, reason: message } + } + + try { + const result = await execa( + 'git', + ['ls-remote', '--heads', template.repo, template.branch], + { stdout: 'pipe', stderr: 'pipe' }, + ) + + if (!result.stdout.trim()) { + return { ok: false, reason: `Branch "${template.branch}" was not found.` } + } + + return { ok: true } + } catch (err) { + const message = err instanceof Error ? err.message : 'Branch validation failed.' + return { ok: false, reason: message } + } +} + From d8ed4f41586a2d279f75f1c680603693ecc674a4 Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 17:55:21 -0500 Subject: [PATCH 5/8] feat: add empty ModuleProvider bean for improved module handling --- .../dynamia/app/DynamiaBaseConfiguration.java | 15 +++++++++++++++ .../dynamia/app/DynamiaToolsWebApplication.java | 10 ---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/platform/app/src/main/java/tools/dynamia/app/DynamiaBaseConfiguration.java b/platform/app/src/main/java/tools/dynamia/app/DynamiaBaseConfiguration.java index 5c671355..3dd5447f 100644 --- a/platform/app/src/main/java/tools/dynamia/app/DynamiaBaseConfiguration.java +++ b/platform/app/src/main/java/tools/dynamia/app/DynamiaBaseConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import tools.dynamia.app.reports.JasperReportCompiler; +import tools.dynamia.commons.StringUtils; import tools.dynamia.domain.services.CrudService; import tools.dynamia.domain.services.impl.NoOpCrudService; import tools.dynamia.integration.ms.MessageService; @@ -31,6 +32,8 @@ import tools.dynamia.integration.search.NoOpSearchProvider; import tools.dynamia.integration.search.SearchResultProvider; import tools.dynamia.integration.search.SearchService; +import tools.dynamia.navigation.Module; +import tools.dynamia.navigation.ModuleProvider; import tools.dynamia.reports.ReportCompiler; import tools.dynamia.templates.TemplateEngine; import tools.dynamia.web.navigation.RestApiNavigationConfiguration; @@ -178,4 +181,16 @@ public TemplateEngine templateEngine() { + /** + * Provides an empty {@link ModuleProvider} bean if none is registered. + * + * @return a ModuleProvider that returns a dummy module with a random name and message + */ + @Bean + @ConditionalOnMissingBean(ModuleProvider.class) + public ModuleProvider emptyModuleProvider() { + return () -> new Module(StringUtils.randomString(), "No modules registered"); + } + + } diff --git a/platform/app/src/main/java/tools/dynamia/app/DynamiaToolsWebApplication.java b/platform/app/src/main/java/tools/dynamia/app/DynamiaToolsWebApplication.java index 0f200b87..1bbb0f8f 100644 --- a/platform/app/src/main/java/tools/dynamia/app/DynamiaToolsWebApplication.java +++ b/platform/app/src/main/java/tools/dynamia/app/DynamiaToolsWebApplication.java @@ -77,15 +77,5 @@ public PWAManifestController pwaManifestController(PWAManifest manifest) { return new PWAManifestController(manifest); } - /** - * Provides an empty {@link ModuleProvider} bean if none is registered. - * - * @return a ModuleProvider that returns a dummy module with a random name and message - */ - @Bean - @ConditionalOnMissingBean(ModuleProvider.class) - public ModuleProvider emptyModuleProvider() { - return () -> new Module(StringUtils.randomString(), "No modules registered"); - } } From b57254129d392fb5c8e58b540c1f44044bc4b48b Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 17:55:25 -0500 Subject: [PATCH 6/8] feat: normalize Spring Boot version for improved compatibility in project summary --- platform/packages/cli/src/commands/new.ts | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/platform/packages/cli/src/commands/new.ts b/platform/packages/cli/src/commands/new.ts index 8be3ade0..8af84422 100644 --- a/platform/packages/cli/src/commands/new.ts +++ b/platform/packages/cli/src/commands/new.ts @@ -11,13 +11,18 @@ import { generateFrontend } from '../generators/frontend.js' // --------------------------------------------------------------------------- /** Fetch the latest Spring Boot version from Spring Initializr metadata. */ +function normalizeSpringBootVersion(version: string): string { + // Initializr can return legacy values like "4.0.5.RELEASE"; Maven/Gradle expect "4.0.5". + return version.trim().replace(/\.RELEASE$/i, '') +} + async function fetchSpringBootVersion(_config: CliConfig): Promise { try { const metaRes = await fetch('https://start.spring.io/metadata/client') if (metaRes.ok) { const meta = await metaRes.json() as Record const bootVersion = (meta as { bootVersion?: { default?: string } }).bootVersion?.default - if (bootVersion) return bootVersion + if (bootVersion) return normalizeSpringBootVersion(bootVersion) } } catch { // ignore — use fallback @@ -48,13 +53,15 @@ function printSuccessMessage(opts: { springBootVersion, } = opts + const displaySpringBootVersion = normalizeSpringBootVersion(springBootVersion) + console.log('') console.log(`✓ Project "${projectName}" created successfully!`) console.log('') console.log(' 📁 Structure:') console.log(` ${projectName}/`) if (doBackend) { - console.log(` ├── backend/ (${language.charAt(0).toUpperCase() + language.slice(1)} · Spring Boot ${springBootVersion} · Dynamia Tools ${config.dynamia.version})`) + console.log(` ├── backend/ (${language.charAt(0).toUpperCase() + language.slice(1)} · Spring Boot ${displaySpringBootVersion} · Dynamia Tools ${config.dynamia.version})`) } if (doFrontend) { const frontendLabel = config.templates.frontend[framework]?.label ?? framework @@ -212,12 +219,18 @@ export async function runNew(): Promise { }) } - // --- Step 7: Confirm --- + // --- Step 7: Resolve Spring Boot version for backend summary and generation --- + let springBootVersion = '' + if (doBackend) { + springBootVersion = normalizeSpringBootVersion(await fetchSpringBootVersion(config)) + } + + // --- Step 8: Confirm --- console.log('') console.log(' Summary:') console.log(` Project: ${projectName}`) if (doBackend) { - console.log(` Backend: ${language} | ${groupId}:${artifactId}:${version}`) + console.log(` Backend: ${language} | ${groupId}:${artifactId}:${version} | Spring Boot ${springBootVersion}`) } if (doFrontend) { console.log(` Frontend: ${framework} | ${packageManager}`) @@ -234,9 +247,6 @@ export async function runNew(): Promise { process.exit(0) } - // Fetch Spring Boot version (best-effort) - const springBootVersion = await fetchSpringBootVersion(config) - // Target directory is CWD / projectName const targetDir = join(process.cwd(), projectName) From 428d55c7880c4ef9d4f50cd718ead0669eea5f70 Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 17:55:30 -0500 Subject: [PATCH 7/8] feat: add support for Spring Boot version placeholder replacement in project configuration --- platform/packages/cli/src/utils/replace.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/packages/cli/src/utils/replace.ts b/platform/packages/cli/src/utils/replace.ts index 87add47d..ba539eff 100644 --- a/platform/packages/cli/src/utils/replace.ts +++ b/platform/packages/cli/src/utils/replace.ts @@ -143,6 +143,7 @@ export async function renameJavaPackages(options: RenameOptions): Promise replacements['DynamiaTools App Backend'] = `${projectName}` replacements['26.4.1'] = `${dynamiaVersion}` replacements['4.0.5'] = `${springBootVersion}` + replacements['4.0.5.RELEASE'] = `${springBootVersion}` // Always replace the placeholder package string itself in source files replacements['com.example.demo'] = basePackage From cd0efac20330be7830109e58d190d3a43a8dc6b7 Mon Sep 17 00:00:00 2001 From: Mario Serrano Date: Sun, 19 Apr 2026 18:03:32 -0500 Subject: [PATCH 8/8] feat: add Git initialization prompt for new project setup --- platform/packages/cli/src/commands/new.ts | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/platform/packages/cli/src/commands/new.ts b/platform/packages/cli/src/commands/new.ts index 8af84422..bfdf9d5e 100644 --- a/platform/packages/cli/src/commands/new.ts +++ b/platform/packages/cli/src/commands/new.ts @@ -1,8 +1,10 @@ import { input, select, confirm } from '@inquirer/prompts' import { join } from 'node:path' +import { existsSync } from 'node:fs' +import { execa } from 'execa' import { loadConfig, type CliConfig } from '../utils/config.js' import { runChecks } from '../utils/env.js' -import { banner, info, error, beta, errorMessage, errorWithCode } from '../utils/logger.js' +import { banner, info, error, beta, errorMessage, errorWithCode, success, warn } from '../utils/logger.js' import { generateBackend } from '../generators/backend.js' import { generateFrontend } from '../generators/frontend.js' @@ -291,4 +293,27 @@ export async function runNew(): Promise { config, springBootVersion, }) + + const initGit = await confirm({ + message: 'Initialize a Git repository in the project root?', + default: true, + }) + + if (!initGit) { + info('Git initialization skipped.') + return + } + + const gitDir = join(targetDir, '.git') + if (existsSync(gitDir)) { + info('Git repository is already initialized.') + return + } + + try { + await execa('git', ['init'], { cwd: targetDir, stdout: 'pipe', stderr: 'pipe' }) + success(`Git repository initialized at ${targetDir}`) + } catch (err) { + warn(`Project was created, but Git initialization failed: ${errorMessage(err, 'DT-RUN-001')}`) + } }