Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ All notable changes to LynxPrompt will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - February 2026

### BREAKING CHANGES
- **Self-hostable platform**: LynxPrompt is now a self-hostable platform. Companies can deploy on their own infrastructure.
- **Subscription tiers removed**: All features are available to all users. No more Free/Pro/Max/Teams tiers.
- **GlitchTip removed**: Error tracking is now optional via generic `SENTRY_DSN` env var.
- **ClickHouse removed**: Analytics engine fully removed from the stack.
- **Database consolidation**: Default deployment uses a single PostgreSQL database.

### Added
- **Feature flags system**: All features configurable via environment variables (`ENABLE_AI`, `ENABLE_BLOG`, `ENABLE_STRIPE`, etc.).
- **Self-hosting docker-compose**: New `docker-compose.selfhost.yml` for minimal 1-Postgres deployment.
- **Custom branding**: `APP_NAME`, `APP_URL`, `APP_LOGO_URL` env vars for white-labeling.
- **Dynamic CSP headers**: Content-Security-Policy built from enabled services at startup.
- **Auto-migration on startup**: `entrypoint.sh` runs Prisma migrations automatically.
- **FeatureFlagsProvider**: Client-side React context for feature flag access.
- **Self-hosting documentation**: Comprehensive `/docs/self-hosting` guide with env var reference.
- **Health check enhancement**: `/api/health` now checks database connectivity.
- **Configurable auth**: Toggle GitHub/Google OAuth, Email, Passkeys, SSO, Turnstile, and user registration independently.
- **Configurable AI**: `ENABLE_AI`, `AI_MODEL` env vars control AI feature availability.
- **Configurable content modules**: Blog and support forum toggled via env vars (default off).
- **Registration control**: `ENABLE_USER_REGISTRATION=false` for invite-only instances.

### Changed
- **Stripe is optional**: Marketplace payments controlled by `ENABLE_STRIPE`. When enabled, platform commission routes through LynxPrompt's Stripe by default.
- **SSO promoted to first-class**: No longer gated behind Teams subscription.
- **Team management simplified**: Billing removed, kept as organizational grouping.
- **Sentry is optional**: Only initializes when DSN is configured.
- **Umami script URL configurable**: `UMAMI_SCRIPT_URL` env var replaces hardcoded URL.
- **CLI simplified**: Removed plan display from `whoami` and `wizard`.
- **README rewritten**: Self-hostable platform positioning with deployment guide.

### Removed
- **Pricing page**: `/pricing` route deleted.
- **Subscription billing**: Stripe subscription checkout, webhooks for subscriptions, plan change API.
- **TeamBillingRecord model**: Schema and all references removed.
- **Upgrade CTAs**: All "Upgrade to Teams" prompts removed from UI, CLI, and docs.
- **GlitchTip infrastructure**: Containers, Caddy entry, DNS record deleted.
- **ClickHouse**: All analytics code, env vars, docker-compose service removed.
- **Percona pg_tde**: Replaced with standard PostgreSQL in dev docker-compose.

---

## [1.6.0] - February 2026

### Added
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ RUN npx prisma generate --config=prisma/prisma.config-app.ts & \
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ARG NEXT_PUBLIC_SENTRY_DSN
ARG UMAMI_SCRIPT_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY}
ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ENV UMAMI_SCRIPT_URL=${UMAMI_SCRIPT_URL}
ENV NEXT_TELEMETRY_DISABLED=1
ENV TSC_COMPILE_ON_ERROR=true
# Disable Turbopack for production builds (fixes font resolution issues in Next.js 16)
Expand Down
305 changes: 118 additions & 187 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lynxprompt",
"version": "1.6.0",
"version": "2.0.0",
"description": "CLI for LynxPrompt - Generate AI IDE configuration files",
"author": "LynxPrompt Contributors",
"license": "GPL-3.0",
Expand Down
7 changes: 1 addition & 6 deletions cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,7 @@ export interface UserResponse {
display_name: string | null;
persona: string | null;
skill_level: string | null;
subscription: {
plan: string;
status: string | null;
interval: string | null;
current_period_end: string | null;
};
plan: string;
stats: {
blueprints_count: number;
};
Expand Down
72 changes: 72 additions & 0 deletions cli/src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import chalk from "chalk";
import { getApiUrl, setApiUrl, getConfigPath } from "../config.js";

export async function configCommand(
action?: string,
valueArg?: string,
): Promise<void> {
if (!action || action === "show") {
console.log();
console.log(chalk.cyan("⚙️ LynxPrompt CLI Configuration"));
console.log();
console.log(
chalk.white(" API URL: ") + chalk.green(getApiUrl()),
);
console.log(
chalk.white(" Config: ") + chalk.gray(getConfigPath()),
);
if (process.env.LYNXPROMPT_API_URL) {
console.log(
chalk.gray(
" (API URL overridden by LYNXPROMPT_API_URL env var)",
),
);
}
console.log();
return;
}

if (action === "set-url") {
if (!valueArg) {
console.error(
chalk.red("Usage: lynxp config set-url <url>"),
);
process.exit(1);
}
try {
new URL(valueArg);
} catch {
console.error(chalk.red(`Invalid URL: ${valueArg}`));
process.exit(1);
}
const clean = valueArg.replace(/\/+$/, "");
setApiUrl(clean);
console.log(
chalk.green("✓") +
` API URL set to ${chalk.cyan(clean)}`,
);
console.log(
chalk.gray(" Run 'lynxp login' to authenticate with this instance."),
);
return;
}

if (action === "reset-url") {
setApiUrl("https://lynxprompt.com");
console.log(
chalk.green("✓") +
" API URL reset to " +
chalk.cyan("https://lynxprompt.com"),
);
return;
}

if (action === "path") {
console.log(getConfigPath());
return;
}

console.error(chalk.red(`Unknown config action: ${action}`));
console.error(chalk.gray("Available: show, set-url <url>, reset-url, path"));
process.exit(1);
}
25 changes: 2 additions & 23 deletions cli/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,38 +130,25 @@ interface UserInfo {
}

function displayWelcome(user: UserInfo): void {
const plan = user.plan?.toUpperCase() || "FREE";
const name = user.name || user.email.split("@")[0];

// Plan colors and emojis - simplified to Users and Teams only
const planConfig: Record<string, { color: (s: string) => string; emoji: string; badge: string }> = {
FREE: { color: chalk.gray, emoji: "🆓", badge: "Users" },
TEAMS: { color: chalk.cyan, emoji: "👥", badge: "Teams" },
};

// Map legacy PRO/MAX to FREE (Users)
const effectivePlan = plan === "PRO" || plan === "MAX" ? "FREE" : plan;
const config = planConfig[effectivePlan] || planConfig.FREE;
const W = 46; // inner width (46 to make square with 48 total)
const W = 46;
const b = chalk.bold;
const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - s.length));

console.log();
console.log(b("┌" + "─".repeat(W) + "┐"));
console.log(b("│") + " ".repeat(W) + b("│"));
console.log(b("│") + pad(` ${config.emoji} Welcome to LynxPrompt CLI!`, W) + b("│"));
console.log(b("│") + pad(" 🐱 Welcome to LynxPrompt CLI!", W) + b("│"));
console.log(b("│") + " ".repeat(W) + b("│"));
console.log(b("│") + pad(` User: ${name}`, W) + b("│"));
console.log(b("│") + pad(` Plan: ${config.badge}`, W) + b("│"));
console.log(b("│") + " ".repeat(W) + b("│"));
console.log(b("└" + "─".repeat(W) + "┘"));
console.log();

// Show capabilities based on plan
console.log(chalk.bold("📋 Your CLI Capabilities:"));
console.log();

// All users get these
console.log(chalk.green(" ✓") + " " + chalk.white("lynxp wizard") + chalk.gray(" - Interactive config wizard"));
console.log(chalk.green(" ✓") + " " + chalk.white("lynxp list") + chalk.gray(" - List your blueprints"));
console.log(chalk.green(" ✓") + " " + chalk.white("lynxp pull <id>") + chalk.gray(" - Download blueprints"));
Expand All @@ -171,14 +158,6 @@ function displayWelcome(user: UserInfo): void {
console.log(chalk.green(" ✓") + " " + chalk.white("lynxp whoami") + chalk.gray(" - Show account info"));
console.log(chalk.green(" ✓") + " " + chalk.white("lynxp logout") + chalk.gray(" - Sign out of CLI"));

// Plan-specific features - Teams users get extra features
if (effectivePlan === "TEAMS") {
console.log();
console.log(chalk.cyan(" ⚡") + " " + chalk.white("AI-powered editing") + chalk.gray(" - AI assistant for configs"));
console.log(chalk.cyan(" 👥") + " " + chalk.white("Team blueprints") + chalk.gray(" - Share with your team"));
console.log(chalk.cyan(" 👥") + " " + chalk.white("SSO integration") + chalk.gray(" - Enterprise authentication"));
}

console.log();
console.log(chalk.gray("Token stored securely. Run ") + chalk.cyan("lynxp --help") + chalk.gray(" to see all commands."));
console.log();
Expand Down
26 changes: 2 additions & 24 deletions cli/src/commands/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function whoamiCommand(): Promise<void> {
id: user.id,
email: user.email,
name: user.name,
plan: user.subscription.plan,
plan: user.plan,
});

console.log();
Expand All @@ -33,14 +33,6 @@ export async function whoamiCommand(): Promise<void> {
if (user.display_name) {
console.log(` ${chalk.gray("Display:")} ${user.display_name}`);
}
console.log(` ${chalk.gray("Plan:")} ${formatPlan(user.subscription.plan)}`);
if (user.subscription.status) {
console.log(` ${chalk.gray("Status:")} ${user.subscription.status}`);
}
if (user.subscription.current_period_end) {
const endDate = new Date(user.subscription.current_period_end);
console.log(` ${chalk.gray("Renews:")} ${endDate.toLocaleDateString()}`);
}
console.log();
console.log(` ${chalk.gray("Blueprints:")} ${user.stats.blueprints_count}`);
console.log(` ${chalk.gray("Member since:")} ${new Date(user.created_at).toLocaleDateString()}`);
Expand All @@ -52,8 +44,7 @@ export async function whoamiCommand(): Promise<void> {
if (error.statusCode === 401) {
console.error(chalk.red("Your session has expired. Please run 'lynxprompt login' again."));
} else if (error.statusCode === 403) {
console.error(chalk.red("API access error. Please check your subscription."));
console.error(chalk.gray("Visit https://lynxprompt.com/pricing for plan details."));
console.error(chalk.red("API access error."));
} else {
console.error(chalk.red(`Error: ${error.message}`));
}
Expand All @@ -64,19 +55,6 @@ export async function whoamiCommand(): Promise<void> {
}
}

function formatPlan(plan: string): string {
const planColors: Record<string, (s: string) => string> = {
FREE: chalk.gray,
TEAMS: chalk.cyan,
};

// Map legacy PRO/MAX to FREE display
const displayPlan = plan === "PRO" || plan === "MAX" ? "FREE" : plan;
const displayName = displayPlan === "FREE" ? "Users" : displayPlan;
const colorFn = planColors[displayPlan] || chalk.white;
return colorFn(displayName);
}




7 changes: 1 addition & 6 deletions cli/src/commands/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1142,18 +1142,13 @@ async function runWizardWithDraftProtection(options: WizardOptions): Promise<voi
const authenticated = isAuthenticated();
const user = getUser();
const userPlanRaw = user?.plan?.toLowerCase() || "free";
// Map legacy pro/max to users (free), only teams is a paid tier now
const userTier: UserTier = userPlanRaw === "teams" ? "teams" : "users";
const userPlanDisplay = userTier === "teams" ? "TEAMS" : "USERS";

if (!authenticated) {
// Brief notice that cloud features require login
console.log(chalk.gray(` 👤 Running as guest. ${chalk.cyan("lynxp login")} for cloud sync & sharing.`));
console.log();
} else {
// Show logged-in status with plan
const planEmoji = userTier === "teams" ? "👥" : "🆓";
console.log(chalk.green(` ✓ Logged in as ${chalk.bold(user?.name || user?.email)} ${planEmoji} ${chalk.gray(userPlanDisplay)}`));
console.log(chalk.green(` ✓ Logged in as ${chalk.bold(user?.name || user?.email)}`));
console.log();
}

Expand Down
12 changes: 12 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { convertCommand } from "./commands/convert.js";
import { mergeCommand } from "./commands/merge.js";
import { importCommand } from "./commands/import.js";
import { hierarchiesCommand } from "./commands/hierarchies.js";
import { configCommand } from "./commands/config.js";

// CLI version injected at build time via tsup.config.ts define option
const CLI_VERSION = process.env.CLI_VERSION || "0.0.0";
Expand Down Expand Up @@ -191,6 +192,11 @@ program
.description("Show current authenticated user")
.action(whoamiCommand);

program
.command("config [action] [value]")
.description("View or change CLI settings (set-url, reset-url, path)")
.action(configCommand);

// ============================================
// Help formatting
// ============================================
Expand Down Expand Up @@ -236,6 +242,12 @@ ${chalk.cyan("Blueprint Tracking:")}
${chalk.cyan("CI/CD:")}
${chalk.white("$ lynxp check --ci")} ${chalk.gray("Validate config (exit code)")}

${chalk.cyan("Configuration:")}
${chalk.white("$ lynxp config")} ${chalk.gray("Show current settings")}
${chalk.white("$ lynxp config set-url <url>")} ${chalk.gray("Point CLI to a self-hosted instance")}
${chalk.white("$ lynxp config reset-url")} ${chalk.gray("Reset to https://lynxprompt.com")}
${chalk.white("$ lynxp config path")} ${chalk.gray("Show config file location")}

${chalk.gray("Docs: https://lynxprompt.com/docs/cli")}
`
);
Expand Down
48 changes: 48 additions & 0 deletions docker-compose.selfhost.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# docker-compose.selfhost.yml — Minimal self-hosted LynxPrompt deployment
# Usage: docker compose -f docker-compose.selfhost.yml up -d
#
# Required env vars (set in .env or environment):
# NEXTAUTH_SECRET — generate with: openssl rand -base64 32
# ADMIN_EMAIL — your email (auto-promoted to superadmin)
#
# Optional:
# DB_PASSWORD — database password (default: changeme)
# APP_URL — your domain (default: http://localhost:3000)

services:
postgres:
image: postgres:18-alpine
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: lynxprompt
POSTGRES_USER: lynxprompt
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lynxprompt -d lynxprompt"]
interval: 5s
timeout: 5s
retries: 5

lynxprompt:
image: drumsergio/lynxprompt:2.0.0
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL_APP: postgresql://lynxprompt:${DB_PASSWORD:-changeme}@postgres:5432/lynxprompt?schema=public
DATABASE_URL_USERS: postgresql://lynxprompt:${DB_PASSWORD:-changeme}@postgres:5432/lynxprompt?schema=public
DATABASE_URL_BLOG: postgresql://lynxprompt:${DB_PASSWORD:-changeme}@postgres:5432/lynxprompt?schema=public
DATABASE_URL_SUPPORT: postgresql://lynxprompt:${DB_PASSWORD:-changeme}@postgres:5432/lynxprompt?schema=public
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${APP_URL:-http://localhost:3000}
APP_URL: ${APP_URL:-http://localhost:3000}
SUPERADMIN_EMAIL: ${ADMIN_EMAIL:-}
NODE_ENV: production

volumes:
pgdata:
Loading
Loading