From 78850f80b6d195ff6dd600bec600f1b60510234b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?=
<33158051+yacosta738@users.noreply.github.com>
Date: Tue, 17 Feb 2026 11:03:52 +0100
Subject: [PATCH 1/3] fix(config): change gateway host to 127.0.0.1 in config
and Dockerfile
---
clients/agent-runtime/Dockerfile | 7 +++----
dev/config.template.toml | 3 +--
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/clients/agent-runtime/Dockerfile b/clients/agent-runtime/Dockerfile
index 01a339bd1..8995aa105 100755
--- a/clients/agent-runtime/Dockerfile
+++ b/clients/agent-runtime/Dockerfile
@@ -41,8 +41,7 @@ default_temperature = 0.7
[gateway]
port = 3000
-host = "[::]"
-allow_public_bind = true
+host = "127.0.0.1"
EOF
RUN chown -R 65534:65534 /corvus-data
@@ -82,7 +81,7 @@ WORKDIR /corvus-data
USER 65534:65534
EXPOSE 3000
ENTRYPOINT ["corvus"]
-CMD ["gateway", "--port", "3000", "--host", "[::]"]
+CMD ["gateway", "--port", "3000", "--host", "127.0.0.1"]
# ── Stage 4: Production Runtime (Distroless) ─────────────────
FROM gcr.io/distroless/cc-debian13:nonroot AS release
@@ -104,4 +103,4 @@ WORKDIR /corvus-data
USER 65534:65534
EXPOSE 3000
ENTRYPOINT ["corvus"]
-CMD ["gateway", "--port", "3000", "--host", "[::]"]
+CMD ["gateway", "--port", "3000", "--host", "127.0.0.1"]
diff --git a/dev/config.template.toml b/dev/config.template.toml
index f08c1cc8b..acb9e58da 100755
--- a/dev/config.template.toml
+++ b/dev/config.template.toml
@@ -8,5 +8,4 @@ default_temperature = 0.7
[gateway]
port = 3000
-host = "[::]"
-allow_public_bind = true
+host = "127.0.0.1"
From 5e81a1fb7576999f3acd1be5e36cda6e792afa1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?=
<33158051+yacosta738@users.noreply.github.com>
Date: Tue, 17 Feb 2026 11:10:23 +0100
Subject: [PATCH 2/3] feat(marketing): add marketing app with landing pages and
funnels
---
clients/web/README.md | 82 ++--
clients/web/apps/landing/README.md | 21 -
clients/web/apps/landing/package.json | 17 -
clients/web/apps/marketing/README.md | 35 ++
clients/web/apps/marketing/astro.config.mjs | 7 +
clients/web/apps/marketing/package.json | 25 ++
clients/web/apps/marketing/public/favicon.svg | 4 +
clients/web/apps/marketing/public/install | 283 ++++++++++++
clients/web/apps/marketing/src/env.d.ts | 1 +
.../src/layouts/MarketingLayout.astro | 39 ++
.../marketing/src/pages/funnels/index.astro | 45 ++
.../src/pages/funnels/product-demo.astro | 36 ++
.../src/pages/funnels/waitlist.astro | 36 ++
.../web/apps/marketing/src/pages/index.astro | 198 +++++++++
.../web/apps/marketing/src/styles/global.css | 419 ++++++++++++++++++
clients/web/apps/marketing/tsconfig.json | 6 +
clients/web/build.gradle.kts | 2 +-
clients/web/package.json | 16 +-
clients/web/pnpm-lock.yaml | 16 +-
19 files changed, 1196 insertions(+), 92 deletions(-)
delete mode 100644 clients/web/apps/landing/README.md
delete mode 100644 clients/web/apps/landing/package.json
create mode 100644 clients/web/apps/marketing/README.md
create mode 100644 clients/web/apps/marketing/astro.config.mjs
create mode 100644 clients/web/apps/marketing/package.json
create mode 100644 clients/web/apps/marketing/public/favicon.svg
create mode 100755 clients/web/apps/marketing/public/install
create mode 100644 clients/web/apps/marketing/src/env.d.ts
create mode 100644 clients/web/apps/marketing/src/layouts/MarketingLayout.astro
create mode 100644 clients/web/apps/marketing/src/pages/funnels/index.astro
create mode 100644 clients/web/apps/marketing/src/pages/funnels/product-demo.astro
create mode 100644 clients/web/apps/marketing/src/pages/funnels/waitlist.astro
create mode 100644 clients/web/apps/marketing/src/pages/index.astro
create mode 100644 clients/web/apps/marketing/src/styles/global.css
create mode 100644 clients/web/apps/marketing/tsconfig.json
diff --git a/clients/web/README.md b/clients/web/README.md
index 99324844e..1afaac9a7 100644
--- a/clients/web/README.md
+++ b/clients/web/README.md
@@ -1,42 +1,43 @@
# Corvus Web Monorepo
-Estructura multi-app para proyectos web de Corvus.
+Monorepo para apps web de Corvus, incluyendo docs, marketing y futuros frontends.
## 📁 Estructura
-```
-apps/web/
+```text
+clients/web/
├── apps/
-│ ├── docs/ # Documentación Starlight (actual)
-│ ├── landing/ # Landing page (futuro)
-│ └── dashboard/ # Frontend web (futuro)
+│ ├── docs/ # Documentación (Astro + Starlight)
+│ ├── marketing/ # Landing y funnels de marketing (Astro)
+│ └── dashboard/ # Dashboard web (pendiente)
├── packages/
-│ └── shared/ # Componentes/utilidades compartidas
-├── package.json # Workspace root
-└── pnpm-workspace.yaml # Configuración pnpm workspace
+│ └── shared/ # Utilidades compartidas
+├── package.json
+└── pnpm-workspace.yaml
```
## 🚀 Apps
### docs
-- **Framework**: Astro + Starlight
-- **Puerto**: 4321
-- **Uso**: Documentación del proyecto
-### landing (futuro)
-- **Framework**: Astro/Vue/React (por definir)
-- **Puerto**: 4322
-- **Uso**: Landing page marketing
+- Framework: Astro + Starlight
+- Puerto por defecto: 4321
+
+### marketing
+
+- Framework: Astro
+- Puerto por defecto: 4322
+- Incluye script público de instalación en `/install`
+
+### dashboard
-### dashboard (futuro)
-- **Framework**: Vue/React (por definir)
-- **Puerto**: 4323
-- **Uso**: Panel de administración
+- Estado: placeholder
+- Puerto por defecto: 4323
## 🛠️ Comandos
```bash
-# Instalar dependencias en todas las apps
+# Instalar dependencias workspace
pnpm install
# Build de todas las apps
@@ -44,37 +45,28 @@ pnpm build
# Build individual
pnpm build:docs
-pnpm build:landing
+pnpm build:marketing
pnpm build:dashboard
+# Compatibilidad (alias antiguo)
+pnpm build:landing
+
# Development
-pnpm dev # docs por defecto
-pnpm dev:landing
+pnpm dev
+pnpm dev:marketing
pnpm dev:dashboard
-# Lint/Format
+# Compatibilidad (alias antiguo)
+pnpm dev:landing
+
+# Quality
pnpm format
pnpm check
```
-## 📦 Packages Compartidos
-
-Los paquetes en `packages/` pueden ser importados por cualquier app:
-
-```typescript
-import { Button } from '@corvus/shared';
-```
-
-## 🏗️ Agregar una nueva app
-
-1. Crear directorio en `apps//`
-2. Agregar `package.json` con nombre `@corvus/`
-3. Ejecutar `pnpm install` desde root
-4. Agregar scripts en `package.json` root si es necesario
-5. Actualizar `build.gradle.kts` para incluir la nueva app
-
-## 📝 Notas
+## 📦 Añadir más proyectos web
-- Cada app tiene su propio `node_modules` (a través de pnpm)
-- Las dependencias compartidas se instalan en root y se symlinkan
-- El build de Gradle construye todas las apps automáticamente
+1. Crear `apps//` con su `package.json`
+2. Ejecutar `pnpm install` en `clients/web`
+3. Ajustar scripts en `clients/web/package.json` si aplica
+4. Confirmar que `clients/web/build.gradle.kts` tenga el puerto/config correspondiente
diff --git a/clients/web/apps/landing/README.md b/clients/web/apps/landing/README.md
deleted file mode 100644
index 213bdd2a8..000000000
--- a/clients/web/apps/landing/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Landing Page
-
-Corvus landing page - coming soon.
-
-## Development Plan
-
-- **Framework**: Astro/Vue/React (TBD)
-- **Features**:
- - Hero section with product showcase
- - Features grid
- - Pricing section
- - Testimonials
- - CTA sections
- - Footer with links
-
-## Getting Started
-
-```bash
-# From web root
-pnpm dev:landing
-```
diff --git a/clients/web/apps/landing/package.json b/clients/web/apps/landing/package.json
deleted file mode 100644
index 8908e4b6e..000000000
--- a/clients/web/apps/landing/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "@corvus/landing",
- "version": "0.1.0",
- "private": true,
- "description": "Corvus Landing Page",
- "type": "module",
- "scripts": {
- "dev": "echo 'Landing app not yet implemented' && exit 1",
- "build": "echo 'Landing app not yet implemented' && exit 1",
- "preview": "echo 'Landing app not yet implemented' && exit 1",
- "format": "echo 'No files to format'",
- "check": "echo 'No files to check'",
- "clean": "rm -rf dist"
- },
- "dependencies": {},
- "devDependencies": {}
-}
diff --git a/clients/web/apps/marketing/README.md b/clients/web/apps/marketing/README.md
new file mode 100644
index 000000000..853ae4ee5
--- /dev/null
+++ b/clients/web/apps/marketing/README.md
@@ -0,0 +1,35 @@
+# Marketing (Astro)
+
+Sitio de marketing y funnels de venta para Corvus.
+
+## Objetivo
+
+Este proyecto en `clients/web/apps/marketing` centraliza:
+
+- Landing principal
+- Funnels de adquisición y activación
+- Experimentos de campañas
+- Script público de instalación (`/install`)
+
+## Desarrollo
+
+```bash
+# Desde clients/web
+pnpm dev:marketing
+
+# Build estático
+pnpm build:marketing
+
+# Validaciones
+pnpm --filter @corvus/marketing run check
+```
+
+## Instalador
+
+Este proyecto publica el script wizard en:
+
+```bash
+curl -fsSL https://profiletailors.com/install | bash
+```
+
+Archivo fuente: `public/install`
diff --git a/clients/web/apps/marketing/astro.config.mjs b/clients/web/apps/marketing/astro.config.mjs
new file mode 100644
index 000000000..4568b65c2
--- /dev/null
+++ b/clients/web/apps/marketing/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from "astro/config";
+
+export default defineConfig({
+ site: "https://profiletailors.com",
+ output: "static",
+ compressHTML: true,
+});
diff --git a/clients/web/apps/marketing/package.json b/clients/web/apps/marketing/package.json
new file mode 100644
index 000000000..c6837788f
--- /dev/null
+++ b/clients/web/apps/marketing/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@corvus/marketing",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Corvus Marketing Site built with Astro",
+ "type": "module",
+ "scripts": {
+ "dev": "astro dev --host",
+ "start": "astro dev --host",
+ "build": "astro build",
+ "preview": "astro preview --host",
+ "astro": "astro",
+ "format": "biome format --write src public package.json astro.config.mjs tsconfig.json README.md",
+ "check": "biome check src public package.json astro.config.mjs tsconfig.json README.md",
+ "clean": "rm -rf dist .astro"
+ },
+ "dependencies": {
+ "astro": "^5.17.2",
+ "sharp": "^0.34.5"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "2.3.15",
+ "typescript": "^5.9.2"
+ }
+}
diff --git a/clients/web/apps/marketing/public/favicon.svg b/clients/web/apps/marketing/public/favicon.svg
new file mode 100644
index 000000000..32425b389
--- /dev/null
+++ b/clients/web/apps/marketing/public/favicon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/clients/web/apps/marketing/public/install b/clients/web/apps/marketing/public/install
new file mode 100755
index 000000000..dae447614
--- /dev/null
+++ b/clients/web/apps/marketing/public/install
@@ -0,0 +1,283 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+
+readonly CLI_PACKAGE="@corvus/cli"
+readonly REPO_SLUG="dallay/corvus"
+readonly DEFAULT_INSTALL_DIR="$HOME/.local/bin"
+
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+INSTALL_METHOD=""
+
+has_cmd() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+print_info() {
+ printf "%b\n" "${BLUE}$*${NC}"
+}
+
+print_success() {
+ printf "%b\n" "${GREEN}$*${NC}"
+}
+
+print_warn() {
+ printf "%b\n" "${YELLOW}$*${NC}"
+}
+
+print_error() {
+ printf "%b\n" "${RED}$*${NC}"
+}
+
+is_interactive() {
+ [ -t 0 ] && [ -t 1 ]
+}
+
+confirm() {
+ local prompt="$1"
+ local default_answer="${2:-y}"
+ local answer=""
+
+ if ! is_interactive; then
+ [ "$default_answer" = "y" ]
+ return
+ fi
+
+ if [ "$default_answer" = "y" ]; then
+ read -r -p "$prompt [Y/n] " answer || true
+ answer="${answer:-y}"
+ else
+ read -r -p "$prompt [y/N] " answer || true
+ answer="${answer:-n}"
+ fi
+
+ case "$answer" in
+ y|Y|yes|YES)
+ return 0
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+require_supported_shell() {
+ if [ -z "${BASH_VERSION:-}" ]; then
+ print_error "This installer requires bash."
+ exit 1
+ fi
+}
+
+detect_binary_asset() {
+ local os=""
+ local arch=""
+
+ os="$(uname -s | tr '[:upper:]' '[:lower:]')"
+ arch="$(uname -m)"
+
+ case "$arch" in
+ x86_64|amd64)
+ arch="x64"
+ ;;
+ arm64|aarch64)
+ arch="arm64"
+ ;;
+ *)
+ print_error "Unsupported architecture: $arch"
+ return 1
+ ;;
+ esac
+
+ case "$os" in
+ darwin)
+ printf "corvus-darwin-%s" "$arch"
+ ;;
+ linux)
+ printf "corvus-linux-%s" "$arch"
+ ;;
+ *)
+ print_error "Unsupported OS: $os"
+ return 1
+ ;;
+ esac
+}
+
+resolve_latest_tag() {
+ local api_url="https://api.github.com/repos/${REPO_SLUG}/releases/latest"
+ local tag=""
+
+ if ! has_cmd curl; then
+ print_error "curl is required to fetch release metadata."
+ return 1
+ fi
+
+ tag="$(
+ curl --proto '=https' --tlsv1.2 -fsSL "$api_url" \
+ | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
+ | head -n 1
+ )"
+
+ if ! printf "%s" "$tag" | grep -Eq '^v[0-9A-Za-z._-]+$'; then
+ print_error "Could not resolve a valid release tag."
+ return 1
+ fi
+
+ printf "%s" "$tag"
+}
+
+install_via_binary() {
+ local asset=""
+ local release_tag=""
+ local install_dir="${CORVUS_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
+ local target_path=""
+ local download_url=""
+
+ asset="$(detect_binary_asset)"
+ release_tag="${CORVUS_VERSION:-latest}"
+
+ if [ "$release_tag" = "latest" ]; then
+ release_tag="$(resolve_latest_tag)"
+ fi
+
+ if ! printf "%s" "$release_tag" | grep -Eq '^v[0-9A-Za-z._-]+$'; then
+ print_error "Invalid CORVUS_VERSION value: $release_tag"
+ return 1
+ fi
+
+ mkdir -p "$install_dir"
+
+ target_path="${install_dir}/corvus"
+ download_url="https://github.com/${REPO_SLUG}/releases/download/${release_tag}/${asset}"
+
+ print_info "Downloading ${download_url}"
+
+ curl --proto '=https' --tlsv1.2 -fsSL "$download_url" -o "$target_path"
+ chmod 755 "$target_path"
+
+ print_success "Corvus binary installed at ${target_path}"
+
+ if ! printf "%s" ":$PATH:" | grep -q ":${install_dir}:"; then
+ print_warn "${install_dir} is not in PATH for this shell."
+ print_warn "Add it with: export PATH=\"${install_dir}:\$PATH\""
+ fi
+}
+
+install_via_package_manager() {
+ case "$INSTALL_METHOD" in
+ pnpm)
+ pnpm add --global "$CLI_PACKAGE"
+ ;;
+ npm)
+ npm install --global "$CLI_PACKAGE"
+ ;;
+ yarn)
+ yarn global add "$CLI_PACKAGE"
+ ;;
+ bun)
+ bun add --global "$CLI_PACKAGE"
+ ;;
+ *)
+ print_error "Unknown install method: $INSTALL_METHOD"
+ return 1
+ ;;
+ esac
+}
+
+select_install_method() {
+ local candidates=()
+ local choice=""
+ local index=1
+
+ if [ -n "${CORVUS_INSTALL_METHOD:-}" ]; then
+ INSTALL_METHOD="$CORVUS_INSTALL_METHOD"
+ return
+ fi
+
+ has_cmd pnpm && candidates+=("pnpm")
+ has_cmd npm && candidates+=("npm")
+ has_cmd yarn && candidates+=("yarn")
+ has_cmd bun && candidates+=("bun")
+
+ if [ "${#candidates[@]}" -eq 0 ]; then
+ INSTALL_METHOD="binary"
+ return
+ fi
+
+ if ! is_interactive; then
+ INSTALL_METHOD="binary"
+ return
+ fi
+
+ print_info "Choose installation method:"
+ for method in "${candidates[@]}"; do
+ printf " %s) %s\n" "$index" "$method"
+ index=$((index + 1))
+ done
+ printf " %s) binary (direct download)\n" "$index"
+
+ read -r -p "Method [1]: " choice || true
+ choice="${choice:-1}"
+
+ if [ "$choice" -eq "$index" ] 2>/dev/null; then
+ INSTALL_METHOD="binary"
+ return
+ fi
+
+ if [ "$choice" -ge 1 ] && [ "$choice" -lt "$index" ] 2>/dev/null; then
+ INSTALL_METHOD="${candidates[$((choice - 1))]}"
+ return
+ fi
+
+ print_warn "Invalid selection, using ${candidates[0]}"
+ INSTALL_METHOD="${candidates[0]}"
+}
+
+post_install_steps() {
+ if has_cmd corvus; then
+ print_success "Corvus CLI available: $(corvus --version 2>/dev/null || echo 'installed')"
+
+ if is_interactive && confirm "Run interactive onboarding now?" "y"; then
+ corvus onboard --interactive || print_warn "Onboarding exited with a non-zero status."
+ else
+ print_info "You can run onboarding later with: corvus onboard --interactive"
+ fi
+ else
+ print_warn "corvus command not found in current PATH yet."
+ print_warn "Open a new terminal and run: corvus --help"
+ fi
+}
+
+main() {
+ require_supported_shell
+
+ print_info "Corvus installer"
+ print_info "Security note: this script uses HTTPS downloads and avoids eval/dynamic shell execution."
+
+ select_install_method
+
+ print_info "Using install method: ${INSTALL_METHOD}"
+
+ case "$INSTALL_METHOD" in
+ binary)
+ install_via_binary
+ ;;
+ pnpm|npm|yarn|bun)
+ install_via_package_manager
+ ;;
+ *)
+ print_error "Unsupported method: $INSTALL_METHOD"
+ exit 1
+ ;;
+ esac
+
+ post_install_steps
+
+ print_success "Done. Try: corvus status"
+}
+
+main "$@"
diff --git a/clients/web/apps/marketing/src/env.d.ts b/clients/web/apps/marketing/src/env.d.ts
new file mode 100644
index 000000000..f964fe0cf
--- /dev/null
+++ b/clients/web/apps/marketing/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/clients/web/apps/marketing/src/layouts/MarketingLayout.astro b/clients/web/apps/marketing/src/layouts/MarketingLayout.astro
new file mode 100644
index 000000000..f3aea6561
--- /dev/null
+++ b/clients/web/apps/marketing/src/layouts/MarketingLayout.astro
@@ -0,0 +1,39 @@
+---
+import "../styles/global.css";
+
+interface Props {
+ title?: string;
+ description?: string;
+}
+
+const {
+ title = "Corvus | Autonomous AI Infrastructure",
+ description =
+ "Corvus is a lightweight autonomous AI runtime designed for secure operations, tiny footprint, and fast deployment.",
+} = Astro.props;
+
+const canonical = new URL(Astro.url.pathname, Astro.site ?? "https://profiletailors.com").toString();
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
diff --git a/clients/web/apps/marketing/src/pages/funnels/index.astro b/clients/web/apps/marketing/src/pages/funnels/index.astro
new file mode 100644
index 000000000..3c9b18915
--- /dev/null
+++ b/clients/web/apps/marketing/src/pages/funnels/index.astro
@@ -0,0 +1,45 @@
+---
+import MarketingLayout from "../../layouts/MarketingLayout.astro";
+
+const funnels = [
+ {
+ href: "/funnels/product-demo",
+ title: "Product Demo Funnel",
+ description: "Capture qualified leads and route high-intent prospects to live demos.",
+ },
+ {
+ href: "/funnels/waitlist",
+ title: "Early Access Funnel",
+ description: "Build anticipation with a waitlist sequence for launches and beta programs.",
+ },
+];
+---
+
+
+
+ ← Back to landing
+ MARKETING WORKSPACE
+ Funnels repository
+
+ This `marketing` app is ready to host landing pages, acquisition funnels, and campaign
+ experiments in one Astro workspace.
+
+
+
+ {
+ funnels.map((funnel) => (
+
+ {funnel.title}
+ {funnel.description}
+
+ Open funnel →
+
+
+ ))
+ }
+
+
+
diff --git a/clients/web/apps/marketing/src/pages/funnels/product-demo.astro b/clients/web/apps/marketing/src/pages/funnels/product-demo.astro
new file mode 100644
index 000000000..105db200c
--- /dev/null
+++ b/clients/web/apps/marketing/src/pages/funnels/product-demo.astro
@@ -0,0 +1,36 @@
+---
+import MarketingLayout from "../../layouts/MarketingLayout.astro";
+---
+
+
+
+ ← Back to funnels
+ FUNNEL TEMPLATE
+ Product Demo Funnel
+
+ Goal: convert traffic from technical audiences into booked demos with qualification signals.
+
+
+
+
+ Flow
+
+ Landing page with benchmark + proof section
+ CTA to 5-question qualification form
+ Routing: self-serve docs or sales calendar
+
+
+
+ Assets
+
+ Short benchmark graphic
+ Case snippet from existing customer
+ Install command for technical champions
+
+
+
+
+
diff --git a/clients/web/apps/marketing/src/pages/funnels/waitlist.astro b/clients/web/apps/marketing/src/pages/funnels/waitlist.astro
new file mode 100644
index 000000000..b0cbd9133
--- /dev/null
+++ b/clients/web/apps/marketing/src/pages/funnels/waitlist.astro
@@ -0,0 +1,36 @@
+---
+import MarketingLayout from "../../layouts/MarketingLayout.astro";
+---
+
+
+
+ ← Back to funnels
+ FUNNEL TEMPLATE
+ Early Access Funnel
+
+ Goal: build launch demand and collect segmented users for beta invites and onboarding waves.
+
+
+
+
+ Flow
+
+ Problem-led prelaunch page
+ Email capture with role and use-case
+ Automated sequence with onboarding teaser
+
+
+
+ Assets
+
+ Feature preview cards
+ Technical FAQ block
+ Install wizard preview snippet
+
+
+
+
+
diff --git a/clients/web/apps/marketing/src/pages/index.astro b/clients/web/apps/marketing/src/pages/index.astro
new file mode 100644
index 000000000..49e380bcd
--- /dev/null
+++ b/clients/web/apps/marketing/src/pages/index.astro
@@ -0,0 +1,198 @@
+---
+import MarketingLayout from "../layouts/MarketingLayout.astro";
+
+const featureCards = [
+ {
+ title: "Secure by Default",
+ body: "Pairing, sandboxed tools, explicit allowlists and workspace scoping are enabled from day one.",
+ },
+ {
+ title: "Tiny Runtime",
+ body: "Small Rust core with fast cold-start and low memory usage to run on edge hardware.",
+ },
+ {
+ title: "Swappable Stack",
+ body: "Providers, channels, memory and tools are composable modules. Replace one layer without rewiring everything.",
+ },
+ {
+ title: "Production Signals",
+ body: "Built-in doctor/status flows and deployment-friendly service commands for predictable operations.",
+ },
+];
+
+const testimonials = [
+ {
+ quote:
+ "We moved from heavy orchestration to Corvus in two sprints and cut infra cost without giving up reliability.",
+ author: "Platform Team, FinOps Startup",
+ },
+ {
+ quote:
+ "The onboarding flow made rollout easy. New developers go from zero to first task in minutes.",
+ author: "Engineering Lead, Growth Studio",
+ },
+ {
+ quote:
+ "Finally an AI runtime that behaves like software infrastructure, not a fragile demo stack.",
+ author: "CTO, B2B SaaS",
+ },
+];
+---
+
+
+
+
+
+
+ AUTONOMOUS INFRASTRUCTURE • RUST CORE • MULTI-CHANNEL
+ Ship AI operators that stay fast, secure, and online.
+
+ Corvus gives your team an operational runtime for agents: strict defaults, tiny footprint,
+ and a distribution model that works from laptops to edge boxes.
+
+
+
+
+ One-line installer
+ curl -fsSL https://profiletailors.com/install | bash
+ Copy
+
+
+
+
+ < 10ms
+ cold start target
+
+
+ < 5MB
+ runtime memory profile
+
+
+ 22+
+ provider integrations
+
+
+
+
+
+
+
PLATFORM
+
Designed for serious runtime work, not demos.
+
+
+ {
+ featureCards.map((item) => (
+
+ {item.title}
+ {item.body}
+
+ ))
+ }
+
+
+
+
+
+
POSITIONING
+
Lean architecture with explicit tradeoffs.
+
+
+
+ Traditional agent stacks
+
+ Large runtime overhead before first task
+ Security model bolted on later
+ Difficult to run on low-cost hardware
+
+
+
+ Corvus approach
+
+ Small binary footprint with quick startup
+ Pairing, allowlists and scoped tools by default
+ Portable runtime for local, cloud, and edge
+
+
+
+
+
+
+
+
CUSTOMER SIGNALS
+
Teams adopt Corvus for operational confidence.
+
+
+ {
+ testimonials.map((item) => (
+
+ “{item.quote}”
+ {item.author}
+
+ ))
+ }
+
+
+
+
+ Launch your first production-ready agent today.
+ Use the installer wizard, pick your provider, and onboard in minutes.
+
+
+
+
+
+
+
+
diff --git a/clients/web/apps/marketing/src/styles/global.css b/clients/web/apps/marketing/src/styles/global.css
new file mode 100644
index 000000000..44b575bc0
--- /dev/null
+++ b/clients/web/apps/marketing/src/styles/global.css
@@ -0,0 +1,419 @@
+@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Syne:wght@500;700;800&display=swap");
+
+:root {
+ --bg: #04101b;
+ --panel: rgba(9, 24, 39, 0.85);
+ --panel-strong: #0d253b;
+ --text: #ecf5ff;
+ --muted: #a8bfd8;
+ --primary: #44e0b3;
+ --primary-strong: #20b88e;
+ --line: rgba(164, 201, 234, 0.22);
+ --ring: rgba(68, 224, 179, 0.5);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ min-height: 100%;
+ background: radial-gradient(circle at 15% -10%, #15314d 0%, transparent 40%),
+ radial-gradient(circle at 80% 0%, #1f4255 0%, transparent 34%), var(--bg);
+ color: var(--text);
+ font-family: "Manrope", "Avenir Next", "Segoe UI", sans-serif;
+}
+
+a {
+ color: inherit;
+}
+
+main {
+ display: block;
+}
+
+h1,
+h2,
+h3 {
+ margin: 0;
+ font-family: "Syne", "Trebuchet MS", sans-serif;
+ line-height: 1.1;
+}
+
+p {
+ margin: 0;
+}
+
+ul {
+ margin: 0;
+ padding-left: 1rem;
+ color: var(--muted);
+}
+
+li + li {
+ margin-top: 0.55rem;
+}
+
+.site-backdrop {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: -1;
+ background: linear-gradient(130deg, rgba(68, 224, 179, 0.08), transparent 45%),
+ linear-gradient(310deg, rgba(44, 127, 255, 0.1), transparent 40%);
+}
+
+.shell {
+ width: min(1120px, calc(100% - 2.5rem));
+ margin-inline: auto;
+}
+
+.topbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ padding-top: 1.5rem;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.65rem;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+.brand-mark {
+ width: 14px;
+ height: 14px;
+ border-radius: 999px;
+ background: radial-gradient(circle at 30% 30%, #7fffff, var(--primary));
+ box-shadow: 0 0 18px rgba(68, 224, 179, 0.65);
+}
+
+nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+nav a {
+ text-decoration: none;
+ font-size: 0.95rem;
+ color: var(--muted);
+}
+
+nav a:hover,
+.inline-link:hover,
+.back-link:hover {
+ color: var(--primary);
+}
+
+.hero {
+ padding-top: 4.5rem;
+ padding-bottom: 4.5rem;
+}
+
+.eyebrow {
+ font-size: 0.75rem;
+ letter-spacing: 0.16em;
+ color: var(--primary);
+ font-weight: 700;
+}
+
+.hero h1,
+.section h1 {
+ margin-top: 1rem;
+ font-size: clamp(2.2rem, 6vw, 4.45rem);
+ max-width: 17ch;
+}
+
+.subtitle {
+ margin-top: 1.25rem;
+ font-size: clamp(1rem, 2vw, 1.2rem);
+ line-height: 1.65;
+ color: var(--muted);
+ max-width: 62ch;
+}
+
+.hero-actions {
+ margin-top: 2rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.85rem;
+}
+
+.btn {
+ text-decoration: none;
+ border-radius: 999px;
+ padding: 0.78rem 1.2rem;
+ border: 1px solid transparent;
+ font-weight: 700;
+ font-size: 0.95rem;
+}
+
+.btn-primary {
+ color: #062316;
+ background: linear-gradient(100deg, #9afadf, var(--primary));
+}
+
+.btn-primary:hover {
+ background: linear-gradient(100deg, #b7ffe9, #5af2ca);
+}
+
+.btn-ghost {
+ border-color: var(--line);
+ color: var(--text);
+ background: rgba(8, 21, 34, 0.5);
+}
+
+.btn-ghost:hover {
+ border-color: var(--ring);
+}
+
+.command-card {
+ margin-top: 2rem;
+ border-radius: 1rem;
+ border: 1px solid var(--line);
+ background: linear-gradient(140deg, rgba(11, 35, 55, 0.88), rgba(6, 20, 32, 0.88));
+ padding: 1rem;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.command-card .label {
+ grid-column: span 2;
+ color: var(--muted);
+ font-size: 0.78rem;
+}
+
+.command-card code {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ font-size: 0.93rem;
+ color: #cefce9;
+}
+
+.copy-btn {
+ border: 1px solid var(--line);
+ border-radius: 0.8rem;
+ background: rgba(9, 28, 43, 0.95);
+ color: var(--text);
+ padding: 0.5rem 0.78rem;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.copy-btn:hover {
+ border-color: var(--ring);
+}
+
+.metric-grid {
+ margin-top: 2rem;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.8rem;
+}
+
+.metric-grid article {
+ border: 1px solid var(--line);
+ border-radius: 1rem;
+ padding: 1.1rem;
+ background: var(--panel);
+}
+
+.metric-grid strong {
+ display: block;
+ font-family: "Syne", sans-serif;
+ font-size: 1.4rem;
+}
+
+.metric-grid span {
+ color: var(--muted);
+ font-size: 0.92rem;
+}
+
+.section {
+ padding: 2rem 0 3.4rem;
+}
+
+.section-head {
+ margin-bottom: 1.2rem;
+}
+
+.section-head h2 {
+ margin-top: 0.5rem;
+ font-size: clamp(1.5rem, 3vw, 2.6rem);
+ max-width: 18ch;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.9rem;
+}
+
+.feature-card {
+ padding: 1.2rem;
+ border-radius: 1rem;
+ border: 1px solid var(--line);
+ background: var(--panel);
+ backdrop-filter: blur(6px);
+}
+
+.feature-card h2,
+.feature-card h3 {
+ font-size: 1.18rem;
+}
+
+.feature-card p {
+ margin-top: 0.6rem;
+ line-height: 1.6;
+ color: var(--muted);
+}
+
+.inline-link {
+ margin-top: 0.8rem;
+ display: inline-block;
+ color: #8deed4;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+.comparison-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.comparison-grid article {
+ padding: 1.2rem;
+ border: 1px solid var(--line);
+ border-radius: 1rem;
+ background: var(--panel);
+}
+
+.comparison-grid h2,
+.comparison-grid h3 {
+ margin-bottom: 0.75rem;
+ font-size: 1.15rem;
+}
+
+.testimonial-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.9rem;
+}
+
+.testimonial-card {
+ border: 1px solid var(--line);
+ background: var(--panel);
+ border-radius: 1rem;
+ padding: 1.1rem;
+}
+
+.testimonial-card p {
+ color: #d7ebfe;
+ line-height: 1.6;
+}
+
+.testimonial-card span {
+ margin-top: 0.8rem;
+ display: inline-block;
+ color: var(--muted);
+ font-size: 0.85rem;
+}
+
+.cta-band {
+ border: 1px solid var(--line);
+ border-radius: 1rem;
+ background: linear-gradient(145deg, rgba(14, 42, 61, 0.82), rgba(7, 24, 37, 0.92));
+ padding: 1.6rem;
+}
+
+.cta-band h2 {
+ font-size: clamp(1.5rem, 4vw, 2.45rem);
+}
+
+.cta-band p {
+ margin-top: 0.8rem;
+ color: var(--muted);
+ line-height: 1.55;
+}
+
+.footer {
+ padding: 1.8rem 0 2.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.9rem;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.footer div {
+ display: flex;
+ gap: 0.8rem;
+}
+
+.footer a,
+.back-link {
+ color: var(--muted);
+ text-decoration: none;
+}
+
+.back-link {
+ margin-bottom: 0.9rem;
+ display: inline-block;
+ font-weight: 700;
+}
+
+@keyframes rise {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero,
+.section,
+.footer {
+ animation: rise 480ms ease both;
+}
+
+@media (max-width: 960px) {
+ .feature-grid,
+ .comparison-grid,
+ .testimonial-row,
+ .metric-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero {
+ padding-top: 3rem;
+ }
+
+ .command-card {
+ grid-template-columns: 1fr;
+ }
+
+ .command-card .label {
+ grid-column: 1;
+ }
+
+ .footer {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
diff --git a/clients/web/apps/marketing/tsconfig.json b/clients/web/apps/marketing/tsconfig.json
new file mode 100644
index 000000000..801d1ab12
--- /dev/null
+++ b/clients/web/apps/marketing/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "baseUrl": "."
+ }
+}
diff --git a/clients/web/build.gradle.kts b/clients/web/build.gradle.kts
index b89fdf522..6a3fe4238 100644
--- a/clients/web/build.gradle.kts
+++ b/clients/web/build.gradle.kts
@@ -62,7 +62,7 @@ data class WebAppConfig(
// App configurations (extend as needed)
val appConfigs = mapOf(
"docs" to WebAppConfig("docs", "dist", 4321),
- "landing" to WebAppConfig("landing", "dist", 4322),
+ "marketing" to WebAppConfig("marketing", "dist", 4322),
"dashboard" to WebAppConfig("dashboard", "dist", 4323),
)
diff --git a/clients/web/package.json b/clients/web/package.json
index 0afebc25b..daaf7d261 100644
--- a/clients/web/package.json
+++ b/clients/web/package.json
@@ -2,16 +2,18 @@
"name": "@corvus/web-monorepo",
"version": "0.1.0",
"private": true,
- "description": "Corvus Web Apps Monorepo - Landing, Docs, Dashboard",
+ "description": "Corvus Web Apps Monorepo - Marketing, Docs, Dashboard",
"type": "module",
"scripts": {
"build": "pnpm -r run build",
- "build:docs": "pnpm --filter docs run build",
- "build:landing": "pnpm --filter landing run build",
- "build:dashboard": "pnpm --filter dashboard run build",
- "dev": "pnpm --filter docs run dev",
- "dev:landing": "pnpm --filter landing run dev",
- "dev:dashboard": "pnpm --filter dashboard run dev",
+ "build:docs": "pnpm --filter @corvus/docs run build",
+ "build:marketing": "pnpm --filter @corvus/marketing run build",
+ "build:landing": "pnpm run build:marketing",
+ "build:dashboard": "pnpm --filter @corvus/dashboard run build",
+ "dev": "pnpm --filter @corvus/docs run dev",
+ "dev:marketing": "pnpm --filter @corvus/marketing run dev",
+ "dev:landing": "pnpm run dev:marketing",
+ "dev:dashboard": "pnpm --filter @corvus/dashboard run dev",
"format": "pnpm -r run format",
"check": "pnpm -r run check",
"clean": "pnpm -r run clean"
diff --git a/clients/web/pnpm-lock.yaml b/clients/web/pnpm-lock.yaml
index 007c06600..1fdb10bb2 100644
--- a/clients/web/pnpm-lock.yaml
+++ b/clients/web/pnpm-lock.yaml
@@ -36,7 +36,21 @@ importers:
specifier: 2.1.11
version: 2.1.11
- apps/landing: {}
+ apps/marketing:
+ dependencies:
+ astro:
+ specifier: ^5.17.2
+ version: 5.17.2(rollup@4.57.1)(typescript@5.9.3)
+ sharp:
+ specifier: ^0.34.5
+ version: 0.34.5
+ devDependencies:
+ '@biomejs/biome':
+ specifier: 2.3.15
+ version: 2.3.15
+ typescript:
+ specifier: ^5.9.2
+ version: 5.9.3
packages/shared: {}
From b4f63f636446cea2403f605aa901981d6b17b6a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?=
<33158051+yacosta738@users.noreply.github.com>
Date: Tue, 17 Feb 2026 11:39:26 +0100
Subject: [PATCH 3/3] feat(marketing): enhance marketing site with
cross-compilation support and environment configuration
---
.github/workflows/_publish.yml | 68 ++++++++
clients/web/README.md | 4 +-
clients/web/apps/marketing/.env.example | 4 +
clients/web/apps/marketing/README.md | 28 +++-
clients/web/apps/marketing/astro.config.mjs | 40 ++++-
clients/web/apps/marketing/package.json | 10 +-
clients/web/apps/marketing/public/install | 27 ++++
.../src/layouts/MarketingLayout.astro | 2 +-
.../marketing/src/pages/funnels/index.astro | 45 ------
.../src/pages/funnels/product-demo.astro | 36 -----
.../src/pages/funnels/waitlist.astro | 36 -----
.../web/apps/marketing/src/pages/index.astro | 18 ++-
.../web/apps/marketing/src/styles/global.css | 10 +-
clients/web/build.gradle.kts | 2 +-
clients/web/packages/shared/README.md | 38 +++--
clients/web/packages/shared/env.mjs | 153 ++++++++++++++++++
clients/web/packages/shared/package.json | 3 +
clients/web/pnpm-lock.yaml | 8 +-
18 files changed, 375 insertions(+), 157 deletions(-)
create mode 100644 clients/web/apps/marketing/.env.example
delete mode 100644 clients/web/apps/marketing/src/pages/funnels/index.astro
delete mode 100644 clients/web/apps/marketing/src/pages/funnels/product-demo.astro
delete mode 100644 clients/web/apps/marketing/src/pages/funnels/waitlist.astro
create mode 100644 clients/web/packages/shared/env.mjs
diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml
index c9a490f4c..bfca58783 100755
--- a/.github/workflows/_publish.yml
+++ b/.github/workflows/_publish.yml
@@ -52,6 +52,18 @@ jobs:
rustup default stable
cargo --version
+ - name: 🦀 Install cross-compilation tools
+ if: ${{ inputs.release }}
+ run: |
+ # Install cross for easy cross-compilation
+ cargo install cross --locked
+
+ # Add required targets
+ rustup target add x86_64-unknown-linux-gnu
+ rustup target add aarch64-unknown-linux-gnu
+ rustup target add x86_64-apple-darwin
+ rustup target add aarch64-apple-darwin
+
- name: ☕ Setup Java
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
@@ -226,11 +238,67 @@ jobs:
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
+ - name: 🔨 Build native binaries for GitHub Release
+ if: ${{ inputs.release }}
+ run: |
+ set -euo pipefail
+ cd clients/agent-runtime
+ mkdir -p dist
+
+ # Build for Linux x64 using cross
+ echo "Building for Linux x64..."
+ cross build --release --target x86_64-unknown-linux-gnu
+ cp target/x86_64-unknown-linux-gnu/release/corvus dist/corvus-linux-x64
+
+ # Build for Linux arm64 using cross
+ echo "Building for Linux arm64..."
+ cross build --release --target aarch64-unknown-linux-gnu
+ cp target/aarch64-unknown-linux-gnu/release/corvus dist/corvus-linux-arm64
+
+ # Build for macOS x64 using cross
+ echo "Building for macOS x64..."
+ if cross build --release --target x86_64-apple-darwin 2>/dev/null; then
+ cp target/x86_64-apple-darwin/release/corvus dist/corvus-darwin-x64
+ else
+ echo "⚠️ Cross-compilation for macOS x64 not available, skipping..."
+ echo "Note: macOS binaries must be built on macOS runners or with special tooling"
+ fi
+
+ # Build for macOS arm64 using cross
+ echo "Building for macOS arm64..."
+ if cross build --release --target aarch64-apple-darwin 2>/dev/null; then
+ cp target/aarch64-apple-darwin/release/corvus dist/corvus-darwin-arm64
+ else
+ echo "⚠️ Cross-compilation for macOS arm64 not available, skipping..."
+ echo "Note: macOS binaries must be built on macOS runners or with special tooling"
+ fi
+
+ ls -la dist/
+
+ - name: 📋 Generate SHA256 checksums
+ if: ${{ inputs.release }}
+ run: |
+ set -euo pipefail
+ cd clients/agent-runtime/dist
+
+ for binary in corvus-*; do
+ if [ -f "$binary" ] && [[ ! "$binary" == *.sha256 ]] && [[ ! "$binary" == *.skip ]]; then
+ echo "Generating checksum for $binary..."
+ sha256sum "$binary" > "$binary.sha256"
+ cat "$binary.sha256"
+ fi
+ done
+
+ echo "Checksum files generated:"
+ ls -la *.sha256 || echo "No checksum files found"
+
- name: 🚀 Create GitHub Release
if: ${{ inputs.release }}
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
body: ${{ steps.changelog.outputs.changelog }}
generate_release_notes: true
+ files: |
+ clients/agent-runtime/dist/corvus-*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/clients/web/README.md b/clients/web/README.md
index 1afaac9a7..c920de157 100644
--- a/clients/web/README.md
+++ b/clients/web/README.md
@@ -8,7 +8,7 @@ Monorepo para apps web de Corvus, incluyendo docs, marketing y futuros frontends
clients/web/
├── apps/
│ ├── docs/ # Documentación (Astro + Starlight)
-│ ├── marketing/ # Landing y funnels de marketing (Astro)
+│ ├── marketing/ # Landing y páginas de marketing (Astro)
│ └── dashboard/ # Dashboard web (pendiente)
├── packages/
│ └── shared/ # Utilidades compartidas
@@ -26,7 +26,7 @@ clients/web/
### marketing
- Framework: Astro
-- Puerto por defecto: 4322
+- URL configurable con `MARKETING_URL` (dev default: `http://localhost:9988`)
- Incluye script público de instalación en `/install`
### dashboard
diff --git a/clients/web/apps/marketing/.env.example b/clients/web/apps/marketing/.env.example
new file mode 100644
index 000000000..6a0b1f0ff
--- /dev/null
+++ b/clients/web/apps/marketing/.env.example
@@ -0,0 +1,4 @@
+# Marketing canonical URL used by Astro config
+# Dev example: http://localhost:9988
+# Prod example: https://profiletailors.com
+MARKETING_URL=http://localhost:9988
diff --git a/clients/web/apps/marketing/README.md b/clients/web/apps/marketing/README.md
index 853ae4ee5..422d0377a 100644
--- a/clients/web/apps/marketing/README.md
+++ b/clients/web/apps/marketing/README.md
@@ -1,14 +1,13 @@
# Marketing (Astro)
-Sitio de marketing y funnels de venta para Corvus.
+Sitio de marketing para Corvus.
## Objetivo
Este proyecto en `clients/web/apps/marketing` centraliza:
- Landing principal
-- Funnels de adquisición y activación
-- Experimentos de campañas
+- Páginas de campañas
- Script público de instalación (`/install`)
## Desarrollo
@@ -24,6 +23,29 @@ pnpm build:marketing
pnpm --filter @corvus/marketing run check
```
+## Configuración de dominio y puerto
+
+- Dev default: `http://localhost:9988`
+- Prod default: `https://profiletailors.com`
+- El puerto de `dev/preview` se toma de `MARKETING_URL` si incluye puerto; si no, usa `9988`
+
+Variable soportada:
+
+```bash
+# Forzar URL para cualquier entorno
+MARKETING_URL=https://staging.profiletailors.com pnpm build:marketing
+```
+
+Puedes cargarla por entorno con archivos `.env`:
+
+```bash
+# .env.development
+MARKETING_URL=http://localhost:9988
+
+# .env.production
+MARKETING_URL=https://profiletailors.com
+```
+
## Instalador
Este proyecto publica el script wizard en:
diff --git a/clients/web/apps/marketing/astro.config.mjs b/clients/web/apps/marketing/astro.config.mjs
index 4568b65c2..fe7ed2578 100644
--- a/clients/web/apps/marketing/astro.config.mjs
+++ b/clients/web/apps/marketing/astro.config.mjs
@@ -1,7 +1,39 @@
import { defineConfig } from "astro/config";
+import { loadEnv } from "vite";
+import { PORTS, getPortFromUrl, resolveSiteUrl } from "@corvus/shared/env";
-export default defineConfig({
- site: "https://profiletailors.com",
- output: "static",
- compressHTML: true,
+const DEFAULT_DEV_URL = `http://localhost:${PORTS.MARKETING}`;
+const DEFAULT_PROD_URL = "https://profiletailors.com";
+
+export default defineConfig(({ command, mode }) => {
+ const env = loadEnv(mode, process.cwd(), "");
+ const isProdLike = mode === "production" || command === "build";
+ const marketingUrl = resolveSiteUrl({
+ env,
+ primaryKey: "MARKETING_URL",
+ localDefault: DEFAULT_DEV_URL,
+ productionDefault: DEFAULT_PROD_URL,
+ genericKeys: ["SITE_URL"],
+ providerKeys: {
+ cloudflare: "CF_PAGES_URL",
+ vercel: "VERCEL_URL",
+ netlify: "URL",
+ },
+ isProdLike,
+ });
+ const resolvedPort = getPortFromUrl(marketingUrl, PORTS.MARKETING);
+
+ return {
+ site: marketingUrl,
+ output: "static",
+ compressHTML: true,
+ server: {
+ host: true,
+ port: resolvedPort,
+ },
+ preview: {
+ host: true,
+ port: resolvedPort,
+ },
+ };
});
diff --git a/clients/web/apps/marketing/package.json b/clients/web/apps/marketing/package.json
index c6837788f..c92575d45 100644
--- a/clients/web/apps/marketing/package.json
+++ b/clients/web/apps/marketing/package.json
@@ -5,21 +5,23 @@
"description": "Corvus Marketing Site built with Astro",
"type": "module",
"scripts": {
- "dev": "astro dev --host",
- "start": "astro dev --host",
+ "dev": "astro dev",
+ "start": "astro dev",
"build": "astro build",
- "preview": "astro preview --host",
+ "preview": "astro preview",
"astro": "astro",
"format": "biome format --write src public package.json astro.config.mjs tsconfig.json README.md",
"check": "biome check src public package.json astro.config.mjs tsconfig.json README.md",
"clean": "rm -rf dist .astro"
},
"dependencies": {
+ "@corvus/shared": "workspace:*",
"astro": "^5.17.2",
"sharp": "^0.34.5"
},
"devDependencies": {
"@biomejs/biome": "2.3.15",
- "typescript": "^5.9.2"
+ "typescript": "^5.9.3",
+ "vite": "^6.4.1"
}
}
diff --git a/clients/web/apps/marketing/public/install b/clients/web/apps/marketing/public/install
index dae447614..a8122bfb3 100755
--- a/clients/web/apps/marketing/public/install
+++ b/clients/web/apps/marketing/public/install
@@ -136,6 +136,9 @@ install_via_binary() {
local install_dir="${CORVUS_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
local target_path=""
local download_url=""
+ local checksum_url=""
+ local expected_checksum=""
+ local computed_checksum=""
asset="$(detect_binary_asset)"
release_tag="${CORVUS_VERSION:-latest}"
@@ -153,10 +156,34 @@ install_via_binary() {
target_path="${install_dir}/corvus"
download_url="https://github.com/${REPO_SLUG}/releases/download/${release_tag}/${asset}"
+ checksum_url="${download_url}.sha256"
print_info "Downloading ${download_url}"
+ # Download binary
curl --proto '=https' --tlsv1.2 -fsSL "$download_url" -o "$target_path"
+
+ # Verify checksum for security
+ print_info "Verifying checksum..."
+ if ! expected_checksum="$(curl --proto '=https' --tlsv1.2 -fsSL "$checksum_url" 2>/dev/null | awk '{print $1}')"; then
+ print_warn "Could not download checksum file. Skipping verification."
+ print_warn "For production use, consider verifying the binary manually."
+ elif [ -n "$expected_checksum" ]; then
+ if ! has_cmd sha256sum; then
+ print_warn "sha256sum not available. Skipping checksum verification."
+ else
+ computed_checksum="$(sha256sum "$target_path" | awk '{print $1}')"
+ if [ "$computed_checksum" != "$expected_checksum" ]; then
+ print_error "Checksum verification failed!"
+ print_error "Expected: $expected_checksum"
+ print_error "Computed: $computed_checksum"
+ rm -f "$target_path"
+ return 1
+ fi
+ print_success "Checksum verified successfully"
+ fi
+ fi
+
chmod 755 "$target_path"
print_success "Corvus binary installed at ${target_path}"
diff --git a/clients/web/apps/marketing/src/layouts/MarketingLayout.astro b/clients/web/apps/marketing/src/layouts/MarketingLayout.astro
index f3aea6561..ab14a45d6 100644
--- a/clients/web/apps/marketing/src/layouts/MarketingLayout.astro
+++ b/clients/web/apps/marketing/src/layouts/MarketingLayout.astro
@@ -12,7 +12,7 @@ const {
"Corvus is a lightweight autonomous AI runtime designed for secure operations, tiny footprint, and fast deployment.",
} = Astro.props;
-const canonical = new URL(Astro.url.pathname, Astro.site ?? "https://profiletailors.com").toString();
+const canonical = new URL(Astro.url.pathname, Astro.site ?? "http://localhost:9988").toString();
---
diff --git a/clients/web/apps/marketing/src/pages/funnels/index.astro b/clients/web/apps/marketing/src/pages/funnels/index.astro
deleted file mode 100644
index 3c9b18915..000000000
--- a/clients/web/apps/marketing/src/pages/funnels/index.astro
+++ /dev/null
@@ -1,45 +0,0 @@
----
-import MarketingLayout from "../../layouts/MarketingLayout.astro";
-
-const funnels = [
- {
- href: "/funnels/product-demo",
- title: "Product Demo Funnel",
- description: "Capture qualified leads and route high-intent prospects to live demos.",
- },
- {
- href: "/funnels/waitlist",
- title: "Early Access Funnel",
- description: "Build anticipation with a waitlist sequence for launches and beta programs.",
- },
-];
----
-
-
-
- ← Back to landing
- MARKETING WORKSPACE
- Funnels repository
-
- This `marketing` app is ready to host landing pages, acquisition funnels, and campaign
- experiments in one Astro workspace.
-
-
-
- {
- funnels.map((funnel) => (
-
- {funnel.title}
- {funnel.description}
-
- Open funnel →
-
-
- ))
- }
-
-
-
diff --git a/clients/web/apps/marketing/src/pages/funnels/product-demo.astro b/clients/web/apps/marketing/src/pages/funnels/product-demo.astro
deleted file mode 100644
index 105db200c..000000000
--- a/clients/web/apps/marketing/src/pages/funnels/product-demo.astro
+++ /dev/null
@@ -1,36 +0,0 @@
----
-import MarketingLayout from "../../layouts/MarketingLayout.astro";
----
-
-
-
- ← Back to funnels
- FUNNEL TEMPLATE
- Product Demo Funnel
-
- Goal: convert traffic from technical audiences into booked demos with qualification signals.
-
-
-
-
- Flow
-
- Landing page with benchmark + proof section
- CTA to 5-question qualification form
- Routing: self-serve docs or sales calendar
-
-
-
- Assets
-
- Short benchmark graphic
- Case snippet from existing customer
- Install command for technical champions
-
-
-
-
-
diff --git a/clients/web/apps/marketing/src/pages/funnels/waitlist.astro b/clients/web/apps/marketing/src/pages/funnels/waitlist.astro
deleted file mode 100644
index b0cbd9133..000000000
--- a/clients/web/apps/marketing/src/pages/funnels/waitlist.astro
+++ /dev/null
@@ -1,36 +0,0 @@
----
-import MarketingLayout from "../../layouts/MarketingLayout.astro";
----
-
-
-
- ← Back to funnels
- FUNNEL TEMPLATE
- Early Access Funnel
-
- Goal: build launch demand and collect segmented users for beta invites and onboarding waves.
-
-
-
-
- Flow
-
- Problem-led prelaunch page
- Email capture with role and use-case
- Automated sequence with onboarding teaser
-
-
-
- Assets
-
- Feature preview cards
- Technical FAQ block
- Install wizard preview snippet
-
-
-
-
-
diff --git a/clients/web/apps/marketing/src/pages/index.astro b/clients/web/apps/marketing/src/pages/index.astro
index 49e380bcd..978be6f4b 100644
--- a/clients/web/apps/marketing/src/pages/index.astro
+++ b/clients/web/apps/marketing/src/pages/index.astro
@@ -51,7 +51,6 @@ const testimonials = [
Platform
Proof
- Funnels
Install
@@ -66,7 +65,7 @@ const testimonials = [
@@ -155,7 +154,7 @@ const testimonials = [
Use the installer wizard, pick your provider, and onboard in minutes.
@@ -163,8 +162,7 @@ const testimonials = [
@@ -180,8 +178,16 @@ const testimonials = [
if (!target) {
return;
}
+ const text = target.textContent?.trim();
+ if (!text) {
+ button.textContent = "Nothing to copy";
+ setTimeout(() => {
+ button.textContent = "Copy";
+ }, 1500);
+ return;
+ }
try {
- await navigator.clipboard.writeText(target.textContent?.trim() ?? "");
+ await navigator.clipboard.writeText(text);
button.textContent = "Copied";
setTimeout(() => {
button.textContent = "Copy";
diff --git a/clients/web/apps/marketing/src/styles/global.css b/clients/web/apps/marketing/src/styles/global.css
index 44b575bc0..555a67d18 100644
--- a/clients/web/apps/marketing/src/styles/global.css
+++ b/clients/web/apps/marketing/src/styles/global.css
@@ -386,10 +386,12 @@ nav a:hover,
}
}
-.hero,
-.section,
-.footer {
- animation: rise 480ms ease both;
+@media (prefers-reduced-motion: no-preference) {
+ .hero,
+ .section,
+ .footer {
+ animation: rise 480ms ease both;
+ }
}
@media (max-width: 960px) {
diff --git a/clients/web/build.gradle.kts b/clients/web/build.gradle.kts
index 6a3fe4238..807993aab 100644
--- a/clients/web/build.gradle.kts
+++ b/clients/web/build.gradle.kts
@@ -62,7 +62,7 @@ data class WebAppConfig(
// App configurations (extend as needed)
val appConfigs = mapOf(
"docs" to WebAppConfig("docs", "dist", 4321),
- "marketing" to WebAppConfig("marketing", "dist", 4322),
+ "marketing" to WebAppConfig("marketing", "dist", 9988),
"dashboard" to WebAppConfig("dashboard", "dist", 4323),
)
diff --git a/clients/web/packages/shared/README.md b/clients/web/packages/shared/README.md
index 1710149fc..ea3cba58c 100644
--- a/clients/web/packages/shared/README.md
+++ b/clients/web/packages/shared/README.md
@@ -2,23 +2,33 @@
Shared components, utilities, and styles for Corvus web applications.
-## 📦 Contents
+## Env Utilities
-- `components/` - Reusable UI components
-- `styles/` - Shared CSS/styling utilities
-- `utils/` - Helper functions
-- `types/` - Shared TypeScript types
+This package exposes environment helpers under `@corvus/shared/env`.
-## 🚀 Usage
+Goals:
-```typescript
-import { Button, Card } from '@corvus/shared/components';
-import { formatDate } from '@corvus/shared/utils';
-import type { User } from '@corvus/shared/types';
+- deterministic env precedence
+- provider-aware defaults (Cloudflare, Vercel, Netlify)
+- URL normalization and protocol validation (`http`/`https` only)
+- single source of truth for common ports
+
+Usage example:
+
+```js
+import { PORTS, resolveSiteUrl } from "@corvus/shared/env";
+
+const site = resolveSiteUrl({
+ env,
+ primaryKey: "MARKETING_URL",
+ localDefault: `http://localhost:${PORTS.MARKETING}`,
+ productionDefault: "https://profiletailors.com",
+ isProdLike,
+});
```
-## 🏗️ Adding shared code
+## Adding shared code
-1. Add files to appropriate subdirectory
-2. Export from `index.ts`
-3. Run `pnpm build` to compile
+1. Add files to package root or dedicated subfolders
+2. Expose them through `package.json` exports
+3. Keep APIs runtime-safe for Node + browser contexts
diff --git a/clients/web/packages/shared/env.mjs b/clients/web/packages/shared/env.mjs
new file mode 100644
index 000000000..68f993b25
--- /dev/null
+++ b/clients/web/packages/shared/env.mjs
@@ -0,0 +1,153 @@
+/**
+ * Shared environment helpers for web apps.
+ *
+ * Security defaults:
+ * - Reject non-http(s) URLs
+ * - Normalize host-only provider URLs to https
+ * - Trim trailing slash for stable canonical values
+ */
+
+/** @typedef {'cloudflare' | 'vercel' | 'netlify' | 'local'} DeploymentProvider */
+
+/**
+ * @typedef {Object.
} EnvMap
+ */
+
+/**
+ * @param {EnvMap} env
+ * @returns {DeploymentProvider}
+ */
+export function detectDeploymentProvider(env = {}) {
+ if (env.CF_PAGES || env.CF_PAGES_URL) {
+ return "cloudflare";
+ }
+ if (env.VERCEL || env.VERCEL_URL) {
+ return "vercel";
+ }
+ if (env.NETLIFY || env.URL || env.DEPLOY_URL) {
+ return "netlify";
+ }
+ return "local";
+}
+
+/**
+ * Read an env key with optional prefixes.
+ * Order is deterministic and explicit to avoid hidden precedence bugs.
+ *
+ * @param {EnvMap} env
+ * @param {string} key
+ * @param {string[]} [prefixes]
+ * @returns {string | undefined}
+ */
+export function getEnv(env, key, prefixes = ["", "PUBLIC_", "VITE_"]) {
+ for (const prefix of prefixes) {
+ const candidate = env[`${prefix}${key}`];
+ if (candidate && candidate.trim().length > 0) {
+ return candidate.trim();
+ }
+ }
+ return undefined;
+}
+
+/**
+ * @param {string} raw
+ * @returns {string}
+ */
+function ensureHttpProtocol(raw) {
+ const trimmed = raw.trim();
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
+ return trimmed;
+ }
+ return `https://${trimmed}`;
+}
+
+/**
+ * @param {string} raw
+ * @returns {string}
+ */
+export function normalizeHttpUrl(raw) {
+ const withProtocol = ensureHttpProtocol(raw);
+ const parsed = new URL(withProtocol);
+
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
+ throw new Error(`Unsupported URL protocol for site config: ${parsed.protocol}`);
+ }
+
+ const normalizedPath = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
+ const normalized = `${parsed.protocol}//${parsed.host}${normalizedPath}`;
+
+ return normalized;
+}
+
+/**
+ * @param {Object} config
+ * @param {EnvMap} config.env
+ * @param {string} config.primaryKey
+ * @param {string} config.localDefault
+ * @param {string} config.productionDefault
+ * @param {string[]} [config.genericKeys]
+ * @param {Object} [config.providerKeys]
+ * @param {string} [config.providerKeys.cloudflare]
+ * @param {string} [config.providerKeys.vercel]
+ * @param {string} [config.providerKeys.netlify]
+ * @param {boolean} [config.isProdLike]
+ * @returns {string}
+ */
+export function resolveSiteUrl(config) {
+ const {
+ env,
+ primaryKey,
+ localDefault,
+ productionDefault,
+ genericKeys = ["SITE_URL"],
+ providerKeys,
+ isProdLike = false,
+ } = config;
+
+ const explicit = getEnv(env, primaryKey);
+ if (explicit) {
+ return normalizeHttpUrl(explicit);
+ }
+
+ const provider = detectDeploymentProvider(env);
+ if (providerKeys) {
+ const providerKey = providerKeys[provider];
+ if (providerKey) {
+ const providerValue = getEnv(env, providerKey, [""]);
+ if (providerValue) {
+ return normalizeHttpUrl(providerValue);
+ }
+ }
+ }
+
+ for (const key of genericKeys) {
+ const value = getEnv(env, key);
+ if (value) {
+ return normalizeHttpUrl(value);
+ }
+ }
+
+ return normalizeHttpUrl(isProdLike ? productionDefault : localDefault);
+}
+
+/**
+ * @param {string} siteUrl
+ * @param {number} fallbackPort
+ * @returns {number}
+ */
+export function getPortFromUrl(siteUrl, fallbackPort) {
+ const parsed = new URL(siteUrl);
+ if (parsed.port) {
+ const parsedPort = Number.parseInt(parsed.port, 10);
+ if (Number.isFinite(parsedPort)) {
+ return parsedPort;
+ }
+ }
+ return fallbackPort;
+}
+
+export const PORTS = {
+ MARKETING: 9988,
+ DOCS: 4321,
+ DASHBOARD: 4323,
+};
diff --git a/clients/web/packages/shared/package.json b/clients/web/packages/shared/package.json
index bbd6a8a8e..f953dbe29 100644
--- a/clients/web/packages/shared/package.json
+++ b/clients/web/packages/shared/package.json
@@ -10,6 +10,9 @@
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
+ },
+ "./env": {
+ "import": "./env.mjs"
}
},
"scripts": {
diff --git a/clients/web/pnpm-lock.yaml b/clients/web/pnpm-lock.yaml
index 1fdb10bb2..8c08dab24 100644
--- a/clients/web/pnpm-lock.yaml
+++ b/clients/web/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
apps/marketing:
dependencies:
+ '@corvus/shared':
+ specifier: workspace:*
+ version: link:../../packages/shared
astro:
specifier: ^5.17.2
version: 5.17.2(rollup@4.57.1)(typescript@5.9.3)
@@ -49,8 +52,11 @@ importers:
specifier: 2.3.15
version: 2.3.15
typescript:
- specifier: ^5.9.2
+ specifier: ^5.9.3
version: 5.9.3
+ vite:
+ specifier: ^6.4.1
+ version: 6.4.1
packages/shared: {}