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/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index e51102625..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'; @@ -78,6 +80,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 +187,70 @@ function isExternalTemplate(template: string): boolean { ); } +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, + templatePath: string, + apiKey: string +): Promise { + try { + const referralCode = await extractReferralCodeFromTemplate(templatePath); + + if (!referralCode) { + log.info('No referral code found in template echo.config.json'); + return; + } + + 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'}` + ); + 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'}` + ); + } +} + function resolveTemplateRepo(template: string): string { let repo = template; @@ -366,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)) {