Skip to content
Open
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
108 changes: 108 additions & 0 deletions packages/app/control/scripts/seed-test-referral-data.ts
Original file line number Diff line number Diff line change
@@ -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 = '[email protected]';
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();
});
101 changes: 100 additions & 1 deletion packages/sdk/echo-start/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
outro,
select,
text,
confirm,
spinner,
log,
isCancel,
Expand All @@ -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';

Expand Down Expand Up @@ -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})`)}`);
Expand Down Expand Up @@ -181,6 +187,70 @@ function isExternalTemplate(template: string): boolean {
);
}

async function extractReferralCodeFromTemplate(
templatePath: string
): Promise<string | null> {
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<void> {
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;

Expand Down Expand Up @@ -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)) {
Expand Down
Loading