FRE-4534: Remove standalone scripter files moved to ~/code/scripter

- brand/ (identity.md)
- scripts/ (deploy, export, load-test, outreach)
- server/types/ (project.ts — re-exported Drizzle types)
- server/websocket/ (Yjs CRDT sync server)
- .eslintrc.json (TypeScript ESLint config)
- FRE-4510-IMPLEMENTATION.md
- Merged FrenoCorp .gitignore entries into scripter's .gitignore

Cross-dependent items (src/, src-tauri/, server/trpc/, marketing/,
public/, dist/, docs/, .gitignore) delegated to FRE-4535 for
favor-newer comparison.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-02 10:20:01 -04:00
parent 078e19790b
commit 4e07718e69
12 changed files with 0 additions and 1325 deletions

View File

@@ -1,88 +0,0 @@
import puppeteer from 'puppeteer-core';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
const OUTPUT_DIR = path.join(__dirname, '..', 'marketing', 'product-hunt-assets', 'screenshots');
const PAGES = [
{ url: 'https://scripter.app/pricing', filename: 'ph-screenshot-01-pricing-1920x1080.png' },
{ url: 'https://scripter.app/features', filename: 'ph-screenshot-02-features-1920x1080.png' },
{ url: 'https://scripter.app/', filename: 'ph-screenshot-03-home-1920x1080.png' },
{ url: 'https://scripter.app/waitlist', filename: 'ph-screenshot-04-waitlist-1920x1080.png' }
];
// Chromium executable path (adjust if needed)
const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser';
async function captureScreenshots() {
console.log('🎬 Starting Product Hunt screenshot capture...\n');
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`✅ Created output directory: ${OUTPUT_DIR}\n`);
}
let browser;
try {
// Launch browser
console.log('🚀 Launching browser...');
browser = await puppeteer.launch({
executablePath: CHROMIUM_PATH,
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
]
});
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
// Capture each page
for (const { url, filename } of PAGES) {
try {
console.log(`📸 Capturing: ${url}`);
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// Wait for any lazy-loaded content
await new Promise(resolve => setTimeout(resolve, 2000));
const outputPath = path.join(OUTPUT_DIR, filename);
await page.screenshot({
path: outputPath,
fullPage: true,
type: 'png'
});
console.log(`✅ Saved: ${filename}\n`);
} catch (error) {
console.error(`❌ Failed to capture ${url}: ${error.message}\n`);
}
}
console.log('🎉 Screenshot capture complete!');
console.log(`📁 Files saved to: ${OUTPUT_DIR}`);
} catch (error) {
console.error('💥 Error:', error.message);
process.exit(1);
} finally {
if (browser) {
await browser.close();
}
}
}
// Run the script
captureScreenshots();

View File

@@ -1,46 +0,0 @@
#!/bin/bash
# Deploy/update scripter.app frontend
# Run from the FrenoCorp repo root after building
# Usage: bash scripts/deploy-scripter.sh
set -e
echo "=== Deploying scripter.app ==="
# 1. Build (if needed)
if [ "$1" != "--skip-build" ]; then
echo "[1/4] Building frontend..."
npm run build
else
echo "[1/4] Skipping build (--skip-build)"
fi
# 2. Copy to web directory
echo "[2/4] Copying to web directory..."
docker run --rm \
-v /home/mike/code/FrenoCorp/dist:/dist:ro \
-v /var/www/scripter:/target \
alpine sh -c "cp -r /dist/* /target/ && chmod -R 755 /target/"
echo " Copied $(find /var/www/scripter -type f | wc -l) files"
# 3. Reload nginx
echo "[3/4] Reloading nginx..."
if docker run --rm --pid=host --privileged alpine sh -c "kill -HUP 1280" 2>&1; then
echo " Nginx reloaded"
else
echo " WARNING: Could not reload nginx (try manually: sudo systemctl reload nginx)"
fi
# 4. Verify
echo "[4/4] Verifying..."
sleep 1
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" https://scripter.app/ --resolve scripter.app:443:66.108.41.120 2>/dev/null || echo "failed")
if [ "$HTTP_CODE" = "200" ]; then
echo " ✅ Site is serving HTTP 200"
else
echo " ❌ Site returned HTTP $HTTP_CODE"
fi
echo ""
echo "=== Deploy complete ==="
echo "Verify at: curl -skI https://scripter.app/ --resolve scripter.app:443:66.108.41.120"

View File

@@ -1,58 +0,0 @@
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { waitlistSignups } from "../src/db/schema/waitlist.js";
import * as schema from "../src/db/schema/index.js";
import { writeFile } from "fs/promises";
const DB_URL = process.env.TURSO_DATABASE_URL;
const AUTH_TOKEN = process.env.TURSO_AUTH_TOKEN;
if (!DB_URL) {
console.error("TURSO_DATABASE_URL is required");
process.exit(1);
}
async function exportWaitlist() {
const client = createClient({ url: DB_URL, authToken: AUTH_TOKEN });
const db = drizzle(client, { schema });
const signups = await db.select().from(waitlistSignups).orderBy(waitlistSignups.createdAt);
if (signups.length === 0) {
console.log("No waitlist signups found.");
await client.close();
return;
}
const jsonData = signups.map(s => ({
id: s.id,
email: s.email,
name: s.name,
source: s.source,
status: s.status,
metadata: s.metadata ? JSON.parse(s.metadata) : null,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
}));
const csvHeader = "id,email,name,source,status,metadata,createdAt,updatedAt";
const csvRows = signups.map(s => {
const meta = s.metadata ? `"${s.metadata.replace(/"/g, '""')}"` : "";
return `${s.id},"${s.email}","${s.name || ""}","${s.source}","${s.status}",${meta},"${s.createdAt}","${s.updatedAt}"`;
});
const csv = [csvHeader, ...csvRows].join("\n");
await writeFile("waitlist-export.json", JSON.stringify(jsonData, null, 2));
await writeFile("waitlist-export.csv", csv);
console.log(`Exported ${signups.length} signups`);
console.log(" waitlist-export.json");
console.log(" waitlist-export.csv");
await client.close();
}
exportWaitlist().catch(err => {
console.error("Export failed:", err.message);
process.exit(1);
});

View File

@@ -1,66 +0,0 @@
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { waitlistSignups } from "../src/db/schema/waitlist";
import * as schema from "../src/db/schema";
import { writeFile } from "fs/promises";
const DB_URL = process.env.TURSO_DATABASE_URL;
const AUTH_TOKEN = process.env.TURSO_AUTH_TOKEN;
if (!DB_URL) {
console.error("TURSO_DATABASE_URL is required");
console.error("Set it via environment variable or .env file");
process.exit(1);
}
async function exportWaitlist() {
const client = createClient({ url: DB_URL!, authToken: AUTH_TOKEN ?? undefined });
const db = drizzle(client, { schema });
const signups = await db.select().from(waitlistSignups).orderBy(waitlistSignups.createdAt);
if (signups.length === 0) {
console.log("No waitlist signups found.");
await client.close();
return;
}
console.log(`Found ${signups.length} signups\n`);
// JSON export
const jsonData = signups.map(s => ({
id: s.id,
email: s.email,
name: s.name,
source: s.source,
status: s.status,
metadata: s.metadata ? JSON.parse(s.metadata) : null,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
}));
// CSV export
const csvHeader = "id,email,name,source,status,metadata,createdAt,updatedAt";
const csvRows = signups.map(s => {
const meta = s.metadata ? `"${s.metadata.replace(/"/g, '""')}"` : "";
return `${s.id},"${s.email}","${s.name || ""}","${s.source}","${s.status}",${meta},"${s.createdAt}","${s.updatedAt}"`;
});
const csv = [csvHeader, ...csvRows].join("\n");
const jsonFile = "waitlist-export.json";
const csvFile = "waitlist-export.csv";
await writeFile(jsonFile, JSON.stringify(jsonData, null, 2));
await writeFile(csvFile, csv);
console.log(`Exported ${signups.length} signups`);
console.log(` JSON: ${jsonFile}`);
console.log(` CSV: ${csvFile}`);
await client.close();
}
exportWaitlist().catch(err => {
console.error("Export failed:", err.message);
process.exit(1);
});

View File

@@ -1,68 +0,0 @@
// Simple load test for FRE-634: Launch Week Technical Readiness
// Tests concurrent user handling
import http from 'http';
const CONFIG = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
concurrentUsers: 1000,
testDurationMs: 60000,
endpoints: [
'/',
'/api/projects',
'/api/characters',
'/api/revisions'
]
};
async function makeRequest(endpoint) {
return new Promise((resolve, reject) => {
const start = Date.now();
http.get(`${CONFIG.baseUrl}${endpoint}`, (res) => {
resolve({
endpoint,
statusCode: res.statusCode,
duration: Date.now() - start
});
}).on('error', (err) => {
reject({ endpoint, error: err.message });
});
});
}
async function loadTest() {
console.log(`Starting load test: ${CONFIG.concurrentUsers} users, ${CONFIG.testDurationMs}ms`);
console.log(`Target: ${CONFIG.baseUrl}`);
const results = [];
const startTime = Date.now();
while (Date.now() - startTime < CONFIG.testDurationMs) {
const promises = [];
for (let i = 0; i < CONFIG.concurrentUsers; i++) {
const endpoint = CONFIG.endpoints[i % CONFIG.endpoints.length];
promises.push(makeRequest(endpoint));
}
const batchResults = await Promise.all(promises);
results.push(...batchResults);
}
const successRate = results.filter(r => r.statusCode === 200).length / results.length * 100;
const avgDuration = results.reduce((a, b) => a + b.duration, 0) / results.length;
console.log(`\nResults:`);
console.log(`- Total requests: ${results.length}`);
console.log(`- Success rate: ${successRate.toFixed(2)}%`);
console.log(`- Average latency: ${avgDuration.toFixed(2)}ms`);
console.log(`- Max concurrent: ${CONFIG.concurrentUsers}`);
return {
totalRequests: results.length,
successRate,
avgLatency: avgDuration,
maxConcurrent: CONFIG.concurrentUsers
};
}
loadTest().catch(console.error);

View File

@@ -1,316 +0,0 @@
#!/usr/bin/env node
/**
* Priority 1 Influencer Outreach Email Sender
* Issue: FRE-667
* Send Date: April 26, 2026
*
* Contacts (5 total):
* 1. John Finn - johnfinn@business.youtube.com
* 2. No Film School - tips@nofilmschool.com
* 3. Script Lab - info@scriptlab.com
* 4. ScreenCraft - info@screencraft.org
* 5. Go Into The Story - scott@thestorydepartment.com
*/
import { Resend } from 'resend';
// Initialize Resend (free tier: 100 emails/day, 3000/month)
const resend = new Resend(process.env.RESEND_API_KEY || 're_test_key');
const emails = [
{
to: 'johnfinn@business.youtube.com',
subject: 'Free lifetime Pro account - modern screenwriting tool for your channel',
name: 'John Finn',
template: 'john-finn'
},
{
to: 'tips@nofilmschool.com',
subject: 'Beta access: Modern screenwriting platform for NFTS community',
name: 'No Film School',
template: 'no-film-school'
},
{
to: 'info@scriptlab.com',
subject: 'Collaboration: Beta access + potential partnership',
name: 'Script Lab',
template: 'script-lab'
},
{
to: 'info@screencraft.org',
subject: 'Beta partnership: Modern screenwriting tool for ScreenCraft community',
name: 'ScreenCraft',
template: 'screencraft'
},
{
to: 'scott@thestorydepartment.com',
subject: 'WGA blog + modern screenwriting tools - partnership opportunity?',
name: 'Scott Myers',
template: 'go-into-the-story'
}
];
// Email templates from /marketing/beta-outreach-priority-1.md
const templates = {
'john-finn': `
<p>Hi John,</p>
<p>I've been following your channel for years - your Final Draft tutorials are legendary in the screenwriting community. The way you break down screenplay format is exactly what new writers need.</p>
<p>I'm reaching out from <strong>Scripter</strong>, a new screenwriting platform launching soon. We're building a modern alternative to Final Draft with:</p>
<ul>
<li><strong>Real-time collaboration</strong> (like Google Docs for screenplays)</li>
<li><strong>AI writing assistant</strong> (optional, writer-controlled)</li>
<li><strong>Cloud-native</strong> (works on any device, no install needed)</li>
<li><strong>Affordable pricing</strong> (Pro at $9.99/month vs Final Draft's $200 one-time)</li>
</ul>
<p><strong>The Ask:</strong><br>
I'd love to give you <strong>free lifetime Pro access</strong> in exchange for:</p>
<ol>
<li>Honest feedback on bugs, UX, features</li>
<li>Optional: A video review if you genuinely like it (no pressure!)</li>
</ol>
<p>We're limiting our beta to 500 writers, and I think your audience would love to see a modern alternative covered on your channel.</p>
<p><strong>Next Steps:</strong><br>
Interested in a quick 15-min demo? Here's my Calendly: <a href="https://calendly.com/scripter-cmo">Calendly Link</a></p>
<p>Or just reply to this email and I'll get you set up with beta access immediately.</p>
<p>Thanks for all the amazing content you create for the screenwriting community!</p>
<p>Best,<br>
CMO, Scripter</p>
<p><strong>P.S.</strong> Happy to provide an exclusive discount code for your viewers if/when we launch!</p>
`,
'no-film-school': `
<p>Hi NFTS Team,</p>
<p>Love what you're doing with No Film School - it's the go-to resource for indie filmmakers and screenwriters.</p>
<p>I'm reaching out from <strong>Scripter</strong>, a new screenwriting platform built for how writers actually work in 2026:</p>
<p><strong>Key Features:</strong></p>
<ul>
<li>Real-time collaboration (multiple writers in the same script)</li>
<li>AI-assisted outlining and dialogue suggestions</li>
<li>Cloud-native, works on any device</li>
<li>Free tier + Pro at $9.99/month (vs Final Draft's $200)</li>
</ul>
<p><strong>The Opportunity:</strong><br>
We're launching our beta program (500 users max) and would love to have the NFTS community represented. We can offer:</p>
<ol>
<li><strong>Free lifetime Pro accounts</strong> for your team</li>
<li><strong>Exclusive discount code</strong> for your readers/viewers</li>
<li><strong>Guest post opportunity</strong>: "How AI and collaboration tools are changing screenwriting" (no pitch, pure value)</li>
</ol>
<p>We're not asking for coverage - just honest feedback from people who actually know filmmaking.</p>
<p>Interested in early access?</p>
<p>Best,<br>
CMO, Scripter</p>
<p><strong>P.S.</strong> We're launching on Product Hunt May 7 - happy to coordinate if you're interested in featuring us!</p>
`,
'script-lab': `
<p>Hi Script Lab Team,</p>
<p>I've been following Script Lab for years - your screenplay analysis videos and software reviews are incredibly valuable to the screenwriting community.</p>
<p>I'm reaching out from <strong>Scripter</strong>, a new screenwriting platform launching soon. Given that you've reviewed Final Draft, WriterDuet, and other tools, I thought you might be interested in what we're building.</p>
<p><strong>What Makes Scripter Different:</strong></p>
<ul>
<li><strong>Real-time collaboration</strong> (Final Draft wishes it had this)</li>
<li><strong>AI writing assistant</strong> (writer-controlled, optional)</li>
<li><strong>Cloud-native</strong> (no install, works anywhere)</li>
<li><strong>Modern pricing</strong> (Free tier + $9.99/month Pro)</li>
</ul>
<p><strong>Partnership Opportunity:</strong><br>
We're launching our beta program and would love to partner with Script Lab:</p>
<ol>
<li><strong>Free lifetime Pro access</strong> for your team</li>
<li><strong>Exclusive early review opportunity</strong> (embargoed access if you want)</li>
<li><strong>Affiliate program</strong> (we can discuss revenue share)</li>
<li><strong>Guest content exchange</strong> (we'll write for your blog, you guest post on ours)</li>
</ol>
<p>We're limiting beta to 500 users, and I'd love to have Script Lab as one of our founding partners.</p>
<p>Interested in chatting?</p>
<p>Best,<br>
CMO, Scripter</p>
`,
'screencraft': `
<p>Hi ScreenCraft Team,</p>
<p>Huge fan of what you're doing with ScreenCraft - the competitions, resources, and blog are incredibly valuable for working screenwriters.</p>
<p>I'm reaching out from <strong>Scripter</strong>, a new screenwriting platform launching in May 2026. We're building a modern alternative to Final Draft with real-time collaboration and AI assistance.</p>
<p><strong>Why I'm Reaching Out:</strong><br>
Your community is exactly who we're building for - serious writers who want professional tools without the $200 price tag.</p>
<p><strong>Partnership Ideas:</strong></p>
<ol>
<li><strong>Beta access for ScreenCraft community</strong> - Free Pro accounts for competition winners/finalists</li>
<li><strong>Educational discount</strong> - Special pricing for your readers</li>
<li><strong>Co-hosted webinar</strong> - "The Future of Screenwriting Tools" (no pitch, pure education)</li>
<li><strong>Sponsored content</strong> - We'll write educational posts for your blog</li>
</ol>
<p><strong>What We're Asking:</strong></p>
<ul>
<li>Honest feedback from your team on our beta</li>
<li>Willingness to explore partnership opportunities</li>
<li>Optional: Mention in your newsletter if you think it's valuable for your readers</li>
</ul>
<p>We're not asking for free coverage - we want to provide genuine value to your community.</p>
<p>Interested in exploring this?</p>
<p>Best,<br>
CMO, Scripter</p>
`,
'go-into-the-story': `
<p>Hi Scott,</p>
<p>I've been reading Go Into The Story since the beginning - it's the gold standard for screenwriting education. Your posts on story structure have taught me more than any book.</p>
<p>I'm reaching out from <strong>Scripter</strong>, a new screenwriting platform launching soon. Given that you write about the craft (not just tools), I wanted to get your perspective on what we're building.</p>
<p><strong>The Vision:</strong><br>
We believe screenwriting tools should:</p>
<ol>
<li><strong>Get out of the way</strong> and let you write</li>
<li><strong>Enable collaboration</strong> (writing is often a team sport)</li>
<li><strong>Use AI thoughtfully</strong> (assist, don't replace)</li>
<li><strong>Be accessible</strong> (free tier, affordable Pro)</li>
</ol>
<p><strong>The Ask:</strong><br>
I'd love to offer you <strong>free lifetime Pro access</strong> for your own writing, no strings attached. If you find it valuable and want to mention it to your readers, that's great - but no pressure at all.</p>
<p>We're also happy to:</p>
<ul>
<li>Write a guest post on "How Technology is Changing Screenwriting"</li>
<li>Sponsor a Screenwriting Soirée or event</li>
<li>Provide beta access for WGA members</li>
</ul>
<p>Would you be open to a quick call to discuss?</p>
<p>Best,<br>
CMO, Scripter</p>
<p><strong>P.S.</strong> I know you get pitched constantly - this isn't a pitch for coverage. Just offering a tool that might help your writing.</p>
`
};
async function sendEmails() {
console.log('🚀 Starting Priority 1 influencer outreach...\n');
const results = [];
for (const email of emails) {
try {
console.log(`📧 Sending to ${email.name} (${email.to})...`);
const data = await resend.emails.send({
from: 'Scripter CMO <cmo@scripter.app>',
to: [email.to],
subject: email.subject,
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1a336b; max-width: 600px; margin: 0 auto; padding: 20px; }
a { color: #518ac8; }
ul, ol { padding-left: 20px; }
</style>
</head>
<body>
${templates[email.template]}
</body>
</html>
`,
headers: {
'X-Priority': '1',
'X-Campaign': 'FRE-667-Priority1-Outreach'
}
});
results.push({
contact: email.name,
email: email.to,
status: 'sent',
id: data.id,
timestamp: new Date().toISOString()
});
console.log(`✅ Sent successfully (ID: ${data.id})\n`);
// Rate limiting: wait 2 seconds between emails
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
console.error(`❌ Failed to send to ${email.name}:`, error.message);
results.push({
contact: email.name,
email: email.to,
status: 'failed',
error: error.message,
timestamp: new Date().toISOString()
});
}
}
// Print summary
console.log('\n' + '='.repeat(60));
console.log('📊 SEND SUMMARY');
console.log('='.repeat(60));
const sent = results.filter(r => r.status === 'sent').length;
const failed = results.filter(r => r.status === 'failed').length;
console.log(`\n✅ Sent: ${sent}/${emails.length}`);
console.log(`❌ Failed: ${failed}/${emails.length}`);
console.log('\nDetailed Results:');
results.forEach(r => {
console.log(` ${r.status === 'sent' ? '✅' : '❌'} ${r.contact} (${r.email})`);
if (r.id) console.log(` ID: ${r.id}`);
if (r.error) console.log(` Error: ${r.error}`);
});
console.log('\n' + '='.repeat(60));
console.log('📅 Follow-up Schedule:');
console.log(' • Follow-up #1: April 29 (Day 3)');
console.log(' • Follow-up #2: May 3 (Day 7)');
console.log(' • Follow-up #3: May 10 (Day 14 - break up)');
console.log('='.repeat(60));
return results;
}
// Run if executed directly
if (process.argv[1]?.includes('send-priority-1-outreach.js')) {
sendEmails().catch(console.error);
}
export { sendEmails };