From 5570d2e3dc59f5441eb3f683f76e42ceccd2c49c Mon Sep 17 00:00:00 2001 From: Trynax Date: Fri, 7 Nov 2025 23:57:50 +0100 Subject: [PATCH 1/4] add template referral registration endpoint --- .../v1/referrals/register-template/route.ts | 56 ++++++++ .../src/services/db/apps/template-referral.ts | 136 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 packages/app/control/src/app/api/v1/referrals/register-template/route.ts create mode 100644 packages/app/control/src/services/db/apps/template-referral.ts diff --git a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts new file mode 100644 index 000000000..39d44dceb --- /dev/null +++ b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import { authRoute } from '../../../../../lib/api/auth-route'; +import { + registerTemplateReferral, + registerTemplateReferralSchema, +} from '@/services/db/apps/template-referral'; + +export const POST = authRoute + .body(registerTemplateReferralSchema) + .handler(async (_, context) => { + const { appId, githubUsername, templateUrl } = context.body; + + try { + const result = await registerTemplateReferral(context.ctx.userId, { + appId, + githubUsername, + templateUrl, + }); + + if (result.status === 'registered') { + return NextResponse.json({ + success: true, + status: 'registered', + message: `Template creator ${result.referrerUsername} registered as referrer`, + referrerUsername: result.referrerUsername, + }); + } + + if (result.status === 'skipped') { + return NextResponse.json({ + success: true, + status: 'skipped', + message: 'App already has a referrer', + reason: result.reason, + }); + } + + return NextResponse.json({ + success: true, + status: 'not_found', + message: 'Template creator not found on Echo', + reason: result.reason, + }); + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown error occurred'; + + return NextResponse.json( + { + success: false, + message, + }, + { status: 400 } + ); + } + }); diff --git a/packages/app/control/src/services/db/apps/template-referral.ts b/packages/app/control/src/services/db/apps/template-referral.ts new file mode 100644 index 000000000..f679b3975 --- /dev/null +++ b/packages/app/control/src/services/db/apps/template-referral.ts @@ -0,0 +1,136 @@ +import { z } from 'zod'; +import { db } from '@/services/db/client'; +import { appIdSchema } from './lib/schemas'; + +export const registerTemplateReferralSchema = z.object({ + appId: appIdSchema, + githubUsername: z.string().min(1), + templateUrl: z.string().url(), +}); + +export type RegisterTemplateReferralInput = z.infer< + typeof registerTemplateReferralSchema +>; + +export interface RegisterTemplateReferralResult { + status: 'registered' | 'skipped' | 'not_found'; + reason?: string; + referrerUsername?: string; + referralCodeId?: string; +} + +export async function registerTemplateReferral( + userId: string, + input: RegisterTemplateReferralInput +): Promise { + const { appId, githubUsername, templateUrl } = input; + + const app = await db.echoApp.findUnique({ + where: { id: appId }, + include: { + appMemberships: { + where: { + userId, + role: 'owner', + }, + }, + }, + }); + + if (!app || app.appMemberships.length === 0) { + throw new Error('App not found or user is not the owner'); + } + + const membership = await db.appMembership.findUnique({ + where: { + userId_echoAppId: { + userId, + echoAppId: appId, + }, + }, + select: { + referrerId: true, + }, + }); + + if (!membership) { + throw new Error('App membership not found'); + } + + if (membership.referrerId) { + return { + status: 'skipped', + reason: 'existing_referrer', + }; + } + + const githubLink = await db.githubLink.findFirst({ + where: { + githubUrl: { + contains: githubUsername, + }, + githubType: 'user', + isArchived: false, + }, + include: { + user: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!githubLink || !githubLink.user) { + return { + status: 'not_found', + reason: 'template_creator_not_on_echo', + }; + } + + let referralCode = await db.referralCode.findFirst({ + where: { + userId: githubLink.user.id, + isArchived: false, + }, + select: { + id: true, + }, + }); + + if (!referralCode) { + const code = crypto.randomUUID(); + const expiresAt = new Date(); + expiresAt.setFullYear(expiresAt.getFullYear() + 1); + + referralCode = await db.referralCode.create({ + data: { + code, + userId: githubLink.user.id, + expiresAt, + }, + select: { + id: true, + }, + }); + } + + await db.appMembership.update({ + where: { + userId_echoAppId: { + userId, + echoAppId: appId, + }, + }, + data: { + referrerId: referralCode.id, + }, + }); + + return { + status: 'registered', + referrerUsername: githubUsername, + referralCodeId: referralCode.id, + }; +} From 1feed4cf14e69dcb21985212abb8c661062a1b3a Mon Sep 17 00:00:00 2001 From: Trynax Date: Sat, 8 Nov 2025 15:00:48 +0100 Subject: [PATCH 2/4] auto register template creators as referrers in echo-start --- .../scripts/seed-test-referral-data.ts | 108 ++++++++++++++++++ .../v1/referrals/register-template/route.ts | 106 ++++++++++------- .../src/services/db/apps/template-referral.ts | 3 +- packages/sdk/echo-start/src/index.ts | 70 ++++++++++++ 4 files changed, 243 insertions(+), 44 deletions(-) create mode 100644 packages/app/control/scripts/seed-test-referral-data.ts diff --git a/packages/app/control/scripts/seed-test-referral-data.ts b/packages/app/control/scripts/seed-test-referral-data.ts new file mode 100644 index 000000000..37de23046 --- /dev/null +++ b/packages/app/control/scripts/seed-test-referral-data.ts @@ -0,0 +1,108 @@ +/** + * Script to seed test data for local template referral testing + * + * Creates: + * - Test user with GitHub link + * - Test app with membership + * - Returns app ID for use with echo-start + */ + +import { PrismaClient } from '../src/generated/prisma/index.js'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Seeding test data for template referral system...\n'); + + // Create or get test user + const testEmail = 'test-template-user@example.com'; + let user = await prisma.user.findUnique({ + where: { email: testEmail }, + include: { githubLink: true }, + }); + + if (!user) { + console.log('Creating test user...'); + user = await prisma.user.create({ + data: { + email: testEmail, + name: 'Test Template User', + githubLink: { + create: { + githubId: 123456, + githubType: 'user', + githubUrl: 'https://github.com/Trynax', + }, + }, + }, + include: { githubLink: true }, + }); + console.log(`Created user: ${user.email} (ID: ${user.id})`); + } else { + console.log(`Found existing user: ${user.email} (ID: ${user.id})`); + + // Ensure GitHub link exists + if (!user.githubLink) { + await prisma.githubLink.create({ + data: { + userId: user.id, + githubId: 123456, + githubType: 'user', + githubUrl: 'https://github.com/Trynax', + }, + }); + console.log('✅ Added GitHub link for Trynax'); + } + } + + // Create or get test app + const testAppName = 'Test Template Referral App'; + let app = await prisma.echoApp.findFirst({ + where: { name: testAppName }, + include: { appMemberships: true }, + }); + + if (!app) { + console.log('\nCreating test app...'); + app = await prisma.echoApp.create({ + data: { + name: testAppName, + appMemberships: { + create: { + userId: user.id, + role: 'OWNER', + totalSpent: 0, + }, + }, + }, + include: { appMemberships: true }, + }); + console.log(`Created app: ${app.name} (ID: ${app.id})`); + } else { + console.log(`\nFound existing app: ${app.name} (ID: ${app.id})`); + } + + console.log('\nTest Data Summary:'); + console.log('─────────────────────────────────────────────────────'); + console.log(`User ID: ${user.id}`); + console.log(`User Email: ${user.email}`); + console.log(`GitHub URL: ${user.githubLink?.githubUrl || 'https://github.com/Trynax'}`); + console.log(`App ID: ${app.id}`); + console.log(`App Name: ${app.name}`); + console.log('─────────────────────────────────────────────────────'); + + console.log('\nTest Command:'); + console.log(`cd /tmp && /root/developments/opensource/echo/packages/sdk/echo-start/dist/index.js \\`); + console.log(` --template https://github.com/Trynax/commitcraft \\`); + console.log(` --app-id ${app.id} \\`); + console.log(` test-echo-local\n`); +} + +main() + .catch((e) => { + console.error('Error seeding data:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts index 39d44dceb..5a8551114 100644 --- a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts +++ b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts @@ -1,56 +1,76 @@ -import { NextResponse } from 'next/server'; -import { authRoute } from '../../../../../lib/api/auth-route'; +import { NextResponse, NextRequest } from 'next/server'; import { registerTemplateReferral, registerTemplateReferralSchema, } from '@/services/db/apps/template-referral'; +import { db } from '@/services/db/client'; +import { AppRole } from '@/services/db/apps/permissions/types'; -export const POST = authRoute - .body(registerTemplateReferralSchema) - .handler(async (_, context) => { - const { appId, githubUsername, templateUrl } = context.body; +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const validatedData = registerTemplateReferralSchema.parse(body); + const { appId, githubUsername, templateUrl } = validatedData; - try { - const result = await registerTemplateReferral(context.ctx.userId, { - appId, - githubUsername, - templateUrl, + const app = await db.echoApp.findUnique({ + where: { id: appId }, + include: { + appMemberships: { + where: { role: AppRole.OWNER }, + take: 1, + }, + }, + }); + + if (!app || app.appMemberships.length === 0) { + return NextResponse.json( + { success: false, message: 'App not found' }, + { status: 404 } + ); + } + + const userId = app.appMemberships[0].userId; + + const result = await registerTemplateReferral(userId, { + appId, + githubUsername, + templateUrl, + }); + + if (result.status === 'registered') { + return NextResponse.json({ + success: true, + status: 'registered', + message: `Template creator ${result.referrerUsername} registered as referrer`, + referrerUsername: result.referrerUsername, }); + } - if (result.status === 'registered') { - return NextResponse.json({ - success: true, - status: 'registered', - message: `Template creator ${result.referrerUsername} registered as referrer`, - referrerUsername: result.referrerUsername, - }); - } - - if (result.status === 'skipped') { - return NextResponse.json({ - success: true, - status: 'skipped', - message: 'App already has a referrer', - reason: result.reason, - }); - } - + if (result.status === 'skipped') { return NextResponse.json({ success: true, - status: 'not_found', - message: 'Template creator not found on Echo', + status: 'skipped', + message: result.reason || 'Referral skipped', reason: result.reason, }); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Unknown error occurred'; - - return NextResponse.json( - { - success: false, - message, - }, - { status: 400 } - ); } - }); + + return NextResponse.json({ + success: true, + status: 'not_found', + message: result.reason || 'Template creator not found on Echo', + reason: result.reason, + }); + } catch (error) { + const message = + error instanceof Error ? error.message : 'Unknown error occurred'; + + return NextResponse.json( + { + success: false, + message, + }, + { status: 400 } + ); + } +} \ No newline at end of file diff --git a/packages/app/control/src/services/db/apps/template-referral.ts b/packages/app/control/src/services/db/apps/template-referral.ts index f679b3975..50df23d4a 100644 --- a/packages/app/control/src/services/db/apps/template-referral.ts +++ b/packages/app/control/src/services/db/apps/template-referral.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { db } from '@/services/db/client'; import { appIdSchema } from './lib/schemas'; +import { AppRole } from './permissions/types'; export const registerTemplateReferralSchema = z.object({ appId: appIdSchema, @@ -31,7 +32,7 @@ export async function registerTemplateReferral( appMemberships: { where: { userId, - role: 'owner', + role: AppRole.OWNER, }, }, }, diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index e51102625..62c68a097 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -78,6 +78,10 @@ const DEFAULT_TEMPLATES = { type TemplateName = keyof typeof DEFAULT_TEMPLATES; type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun'; +const ECHO_BASE_URL = + (typeof process !== 'undefined' && process.env?.ECHO_BASE_URL) || + 'https://echo.merit.systems'; + function printHeader(): void { console.log(); console.log(`${chalk.cyan('Echo Start')} ${chalk.gray(`(${VERSION})`)}`); @@ -181,6 +185,68 @@ function isExternalTemplate(template: string): boolean { ); } +function extractGithubUsername(templateUrl: string): string | null { + const match = templateUrl.match( + /github\.com\/([^\/]+)/ + ); + return match?.[1] ?? null; +} + +async function registerTemplateReferral( + appId: string, + templateUrl: string +): Promise { + const githubUsername = extractGithubUsername(templateUrl); + + if (!githubUsername) { + log.warn('Could not extract GitHub username from template URL'); + return; + } + + try { + log.step(`Registering template referral for ${githubUsername}...`); + + const response = await fetch( + `${ECHO_BASE_URL}/api/v1/referrals/register-template`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + appId, + githubUsername, + templateUrl, + }), + } + ); + + if (!response.ok) { + const errorText = await response.text(); + log.warn(`Referral registration failed: ${response.status} - ${errorText}`); + return; + } + + const result = (await response.json()) as { + status?: string; + referrerUsername?: string; + message?: string; + }; + + if (result.status === 'registered' && result.referrerUsername) { + log.success( + `Template creator ${result.referrerUsername} registered as referrer` + ); + } else if (result.status === 'skipped') { + log.info(`Referral skipped: ${result.message || 'Unknown reason'}`); + } else if (result.status === 'not_found') { + log.info(`Template creator ${githubUsername} not found on Echo`); + } + } catch (error) { + log.warn(`Referral registration error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + function resolveTemplateRepo(template: string): string { let repo = template; @@ -306,6 +372,10 @@ async function createApp(projectDir: string, options: CreateAppOptions) { log.step(`Using App ID: ${appId}`); + if (isExternal) { + await registerTemplateReferral(appId, template); + } + const absoluteProjectPath = path.resolve(projectDir); // Check if directory already exists From ce56d03e85c17de59bd188331a542e6903030dd3 Mon Sep 17 00:00:00 2001 From: Trynax Date: Mon, 10 Nov 2025 16:47:12 +0100 Subject: [PATCH 3/4] use case-insensitive match for GitHub usernames --- packages/app/control/src/services/db/apps/template-referral.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/control/src/services/db/apps/template-referral.ts b/packages/app/control/src/services/db/apps/template-referral.ts index 50df23d4a..b663a3f1d 100644 --- a/packages/app/control/src/services/db/apps/template-referral.ts +++ b/packages/app/control/src/services/db/apps/template-referral.ts @@ -68,7 +68,8 @@ export async function registerTemplateReferral( const githubLink = await db.githubLink.findFirst({ where: { githubUrl: { - contains: githubUsername, + equals: `https://github.com/${githubUsername}`, + mode: 'insensitive', }, githubType: 'user', isArchived: false, From f2cb8f84d3754ee8883a37b270ba023bcdff0310 Mon Sep 17 00:00:00 2001 From: Trynax Date: Tue, 11 Nov 2025 01:50:51 +0100 Subject: [PATCH 4/4] added auth requirement, with echo.config.json for referral code --- .../v1/referrals/register-template/route.ts | 76 ---------- .../src/services/db/apps/template-referral.ts | 138 ------------------ packages/sdk/echo-start/src/index.ts | 131 ++++++++++------- 3 files changed, 80 insertions(+), 265 deletions(-) delete mode 100644 packages/app/control/src/app/api/v1/referrals/register-template/route.ts delete mode 100644 packages/app/control/src/services/db/apps/template-referral.ts diff --git a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts deleted file mode 100644 index 5a8551114..000000000 --- a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { NextResponse, NextRequest } from 'next/server'; -import { - registerTemplateReferral, - registerTemplateReferralSchema, -} from '@/services/db/apps/template-referral'; -import { db } from '@/services/db/client'; -import { AppRole } from '@/services/db/apps/permissions/types'; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const validatedData = registerTemplateReferralSchema.parse(body); - const { appId, githubUsername, templateUrl } = validatedData; - - const app = await db.echoApp.findUnique({ - where: { id: appId }, - include: { - appMemberships: { - where: { role: AppRole.OWNER }, - take: 1, - }, - }, - }); - - if (!app || app.appMemberships.length === 0) { - return NextResponse.json( - { success: false, message: 'App not found' }, - { status: 404 } - ); - } - - const userId = app.appMemberships[0].userId; - - const result = await registerTemplateReferral(userId, { - appId, - githubUsername, - templateUrl, - }); - - if (result.status === 'registered') { - return NextResponse.json({ - success: true, - status: 'registered', - message: `Template creator ${result.referrerUsername} registered as referrer`, - referrerUsername: result.referrerUsername, - }); - } - - if (result.status === 'skipped') { - return NextResponse.json({ - success: true, - status: 'skipped', - message: result.reason || 'Referral skipped', - reason: result.reason, - }); - } - - return NextResponse.json({ - success: true, - status: 'not_found', - message: result.reason || 'Template creator not found on Echo', - reason: result.reason, - }); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Unknown error occurred'; - - return NextResponse.json( - { - success: false, - message, - }, - { status: 400 } - ); - } -} \ No newline at end of file diff --git a/packages/app/control/src/services/db/apps/template-referral.ts b/packages/app/control/src/services/db/apps/template-referral.ts deleted file mode 100644 index b663a3f1d..000000000 --- a/packages/app/control/src/services/db/apps/template-referral.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { z } from 'zod'; -import { db } from '@/services/db/client'; -import { appIdSchema } from './lib/schemas'; -import { AppRole } from './permissions/types'; - -export const registerTemplateReferralSchema = z.object({ - appId: appIdSchema, - githubUsername: z.string().min(1), - templateUrl: z.string().url(), -}); - -export type RegisterTemplateReferralInput = z.infer< - typeof registerTemplateReferralSchema ->; - -export interface RegisterTemplateReferralResult { - status: 'registered' | 'skipped' | 'not_found'; - reason?: string; - referrerUsername?: string; - referralCodeId?: string; -} - -export async function registerTemplateReferral( - userId: string, - input: RegisterTemplateReferralInput -): Promise { - const { appId, githubUsername, templateUrl } = input; - - const app = await db.echoApp.findUnique({ - where: { id: appId }, - include: { - appMemberships: { - where: { - userId, - role: AppRole.OWNER, - }, - }, - }, - }); - - if (!app || app.appMemberships.length === 0) { - throw new Error('App not found or user is not the owner'); - } - - const membership = await db.appMembership.findUnique({ - where: { - userId_echoAppId: { - userId, - echoAppId: appId, - }, - }, - select: { - referrerId: true, - }, - }); - - if (!membership) { - throw new Error('App membership not found'); - } - - if (membership.referrerId) { - return { - status: 'skipped', - reason: 'existing_referrer', - }; - } - - const githubLink = await db.githubLink.findFirst({ - where: { - githubUrl: { - equals: `https://github.com/${githubUsername}`, - mode: 'insensitive', - }, - githubType: 'user', - isArchived: false, - }, - include: { - user: { - select: { - id: true, - name: true, - }, - }, - }, - }); - - if (!githubLink || !githubLink.user) { - return { - status: 'not_found', - reason: 'template_creator_not_on_echo', - }; - } - - let referralCode = await db.referralCode.findFirst({ - where: { - userId: githubLink.user.id, - isArchived: false, - }, - select: { - id: true, - }, - }); - - if (!referralCode) { - const code = crypto.randomUUID(); - const expiresAt = new Date(); - expiresAt.setFullYear(expiresAt.getFullYear() + 1); - - referralCode = await db.referralCode.create({ - data: { - code, - userId: githubLink.user.id, - expiresAt, - }, - select: { - id: true, - }, - }); - } - - await db.appMembership.update({ - where: { - userId_echoAppId: { - userId, - echoAppId: appId, - }, - }, - data: { - referrerId: referralCode.id, - }, - }); - - return { - status: 'registered', - referrerUsername: githubUsername, - referralCodeId: referralCode.id, - }; -} diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index 62c68a097..0ec4f5581 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -5,6 +5,7 @@ import { outro, select, text, + confirm, spinner, log, isCancel, @@ -14,6 +15,7 @@ import chalk from 'chalk'; import { Command } from 'commander'; import degit from 'degit'; import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'; +import { readFile } from 'fs/promises'; import path from 'path'; import { spawn } from 'child_process'; @@ -185,65 +187,67 @@ function isExternalTemplate(template: string): boolean { ); } -function extractGithubUsername(templateUrl: string): string | null { - const match = templateUrl.match( - /github\.com\/([^\/]+)/ - ); - return match?.[1] ?? null; +async function extractReferralCodeFromTemplate( + templatePath: string +): Promise { + const configFile = path.join(templatePath, 'echo.config.json'); + + if (!existsSync(configFile)) { + return null; + } + + try { + const content = await readFile(configFile, 'utf-8'); + const config = JSON.parse(content); + return config.referralCode || config.echo?.referralCode || null; + } catch { + return null; + } } async function registerTemplateReferral( appId: string, - templateUrl: string + templatePath: string, + apiKey: string ): Promise { - const githubUsername = extractGithubUsername(templateUrl); - - if (!githubUsername) { - log.warn('Could not extract GitHub username from template URL'); - return; - } - try { - log.step(`Registering template referral for ${githubUsername}...`); - - const response = await fetch( - `${ECHO_BASE_URL}/api/v1/referrals/register-template`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - appId, - githubUsername, - templateUrl, - }), - } - ); + const referralCode = await extractReferralCodeFromTemplate(templatePath); - if (!response.ok) { - const errorText = await response.text(); - log.warn(`Referral registration failed: ${response.status} - ${errorText}`); + if (!referralCode) { + log.info('No referral code found in template echo.config.json'); return; } - const result = (await response.json()) as { - status?: string; - referrerUsername?: string; - message?: string; - }; - - if (result.status === 'registered' && result.referrerUsername) { - log.success( - `Template creator ${result.referrerUsername} registered as referrer` + log.step(`Found template referral code, applying...`); + + const response = await fetch(`${ECHO_BASE_URL}/api/v1/user/referral`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + echoAppId: appId, + code: referralCode, + }), + }); + + if (!response.ok) { + const errorData = (await response.json()) as { message?: string }; + log.warn( + `Referral code could not be applied: ${errorData.message || 'Unknown error'}` ); - } else if (result.status === 'skipped') { - log.info(`Referral skipped: ${result.message || 'Unknown reason'}`); - } else if (result.status === 'not_found') { - log.info(`Template creator ${githubUsername} not found on Echo`); + return; + } + + const result = (await response.json()) as { success?: boolean }; + if (result.success) { + log.success('Template referral code applied successfully'); } } catch (error) { - log.warn(`Referral registration error: ${error instanceof Error ? error.message : 'Unknown error'}`); + log.warn( + `Referral registration error: ${error instanceof Error ? error.message : 'Unknown error'}` + ); } } @@ -372,10 +376,6 @@ async function createApp(projectDir: string, options: CreateAppOptions) { log.step(`Using App ID: ${appId}`); - if (isExternal) { - await registerTemplateReferral(appId, template); - } - const absoluteProjectPath = path.resolve(projectDir); // Check if directory already exists @@ -436,7 +436,36 @@ async function createApp(projectDir: string, options: CreateAppOptions) { log.step('Configuring project files'); - // Update package.json with the name of the project + if (isExternal) { + const referralCode = await extractReferralCodeFromTemplate( + absoluteProjectPath + ); + + if (referralCode) { + const shouldApplyReferral = await confirm({ + message: 'This template includes a referral code. Apply it?', + initialValue: true, + }); + + if (!isCancel(shouldApplyReferral) && shouldApplyReferral) { + const apiKey = await text({ + message: 'Enter your Echo API key:', + placeholder: 'Your API key from https://echo.merit.systems/keys', + validate: (value: string) => { + if (!value.trim()) { + return 'API key is required to apply referral code'; + } + return; + }, + }); + + if (!isCancel(apiKey)) { + await registerTemplateReferral(appId, absoluteProjectPath, apiKey); + } + } + } + } + const packageJsonPath = path.join(absoluteProjectPath, 'package.json'); // Technically this is checked above, but good practice to check again if (existsSync(packageJsonPath)) {