bake canonical build harness (deterministic /ship for forks) [music]
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// AnyDB Client Stub — auto-included in every AUTO_DB React project
|
||||
// ---------------------------------------------------------------------------
|
||||
// When AnyDB is connected, this file is replaced by the real client
|
||||
// generated by anydbService.getClientCode(). Until then, this stub
|
||||
// returns empty results so components don't crash.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STUB_COLLECTION = {
|
||||
find: async (_filter?: any, _opts?: any) => ({ data: [], total: 0, page: 1, limit: 50, pages: 0 }),
|
||||
insertOne: async (doc: any) => ({ ...doc, _id: `stub_${Date.now()}` }),
|
||||
updateOne: async (_filter: any, _update: any) => ({ modifiedCount: 0 }),
|
||||
deleteOne: async (_filter: any) => ({ deletedCount: 0 }),
|
||||
aggregate: async (_pipeline: any[]) => [],
|
||||
};
|
||||
|
||||
const anydb = {
|
||||
collection: async (_name: string) => STUB_COLLECTION,
|
||||
};
|
||||
|
||||
export default anydb;
|
||||
@@ -0,0 +1,121 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// OddSockets Client — realtime pub/sub + presence for generated sites
|
||||
// ---------------------------------------------------------------------------
|
||||
// The realtime leg of the tyga backend bundle (alongside lib/anydb.ts data +
|
||||
// lib/tygarun.ts SaaS). Channels, presence and history via OddSockets.
|
||||
//
|
||||
// This stub ships UNCONFIGURED. When the project connects OddSockets, the
|
||||
// builder replaces ODDSOCKETS_CONFIG with real values (like lib/tygarun.ts)
|
||||
// and this file is swapped for the real client (oddsocketsService.getClientCode,
|
||||
// which imports `oddsockets-js`). Until then isConfigured() is false and the
|
||||
// hooks fall back to a LOCAL in-memory demo bus so the UI is alive out of the
|
||||
// box. The demo bus is clearly ephemeral — a broken real connect must surface
|
||||
// as an error, never be masked by the demo state.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ODDSOCKETS_CONFIG = {
|
||||
// Always connect the manager LB — it assigns the worker and the SDK follows
|
||||
// the handoff. NEVER hardcode a worker URL.
|
||||
connectUrl: "__ODDSOCKETS_CONNECT_URL__", // e.g. https://connect.oddsockets.tyga.network
|
||||
// Per-site key (Authorization: Bearer ak_live_...). Isolation boundary = the
|
||||
// key owner (account). Provisioned per site by our reseller rail on connect.
|
||||
apiKey: "__ODDSOCKETS_KEY__",
|
||||
};
|
||||
|
||||
export function isConfigured(): boolean {
|
||||
return !ODDSOCKETS_CONFIG.apiKey.startsWith("__");
|
||||
}
|
||||
|
||||
export const CONNECT_URL = ODDSOCKETS_CONFIG.connectUrl;
|
||||
|
||||
export interface PresenceMember {
|
||||
id: string;
|
||||
state?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ChannelMessage {
|
||||
id: string;
|
||||
data: any;
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface OddSocketsChannel {
|
||||
subscribe(cb: (msg: ChannelMessage) => void, opts?: Record<string, any>): () => void;
|
||||
publish(data: any, opts?: Record<string, any>): Promise<void>;
|
||||
getHistory(opts?: { count?: number; start?: string; end?: string }): Promise<ChannelMessage[]>;
|
||||
getPresence(): Promise<PresenceMember[]>;
|
||||
updateState(state: Record<string, any>): Promise<void>;
|
||||
unsubscribe(): void;
|
||||
}
|
||||
|
||||
export interface OddSocketsClient {
|
||||
channel(name: string): OddSocketsChannel;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
// ── Local in-memory demo bus (used until OddSockets is connected) ────────────
|
||||
// Simulates pub/sub + presence within the tab so realtime UIs render live in
|
||||
// demo mode. Not shared across clients — purely local, seed/demo only.
|
||||
type Listener = (msg: ChannelMessage) => void;
|
||||
|
||||
function createDemoBus(): OddSocketsClient {
|
||||
const listeners: Record<string, Set<Listener>> = {};
|
||||
const history: Record<string, ChannelMessage[]> = {};
|
||||
const presence: Record<string, Record<string, PresenceMember>> = {};
|
||||
const selfId = `demo_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
function channel(name: string): OddSocketsChannel {
|
||||
listeners[name] = listeners[name] || new Set();
|
||||
history[name] = history[name] || [];
|
||||
presence[name] = presence[name] || {};
|
||||
|
||||
return {
|
||||
subscribe(cb, _opts) {
|
||||
listeners[name].add(cb);
|
||||
// announce demo self-presence
|
||||
presence[name][selfId] = { id: selfId, state: {} };
|
||||
return () => listeners[name].delete(cb);
|
||||
},
|
||||
async publish(data, _opts) {
|
||||
const msg: ChannelMessage = {
|
||||
id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
data,
|
||||
at: new Date().toISOString(),
|
||||
};
|
||||
history[name].push(msg);
|
||||
if (history[name].length > 200) history[name].shift();
|
||||
listeners[name].forEach((cb) => cb(msg));
|
||||
},
|
||||
async getHistory(opts = {}) {
|
||||
const all = history[name] || [];
|
||||
return opts.count ? all.slice(-opts.count) : all.slice();
|
||||
},
|
||||
async getPresence() {
|
||||
return Object.values(presence[name] || {});
|
||||
},
|
||||
async updateState(state) {
|
||||
presence[name][selfId] = { id: selfId, state };
|
||||
},
|
||||
unsubscribe() {
|
||||
listeners[name].clear();
|
||||
delete presence[name][selfId];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { channel, connected: false };
|
||||
}
|
||||
|
||||
// Singleton client — demo bus in the stub; the real client (oddsockets-js)
|
||||
// replaces this whole file on connect via oddsocketsService.getClientCode.
|
||||
const client: OddSocketsClient = createDemoBus();
|
||||
|
||||
export default client;
|
||||
|
||||
// Usage:
|
||||
// import oddsockets, { isConfigured } from '@/lib/oddsockets';
|
||||
// const ch = oddsockets.channel('room:lobby');
|
||||
// const off = ch.subscribe((m) => console.log(m.data));
|
||||
// await ch.publish({ text: 'hello' });
|
||||
// await ch.updateState({ typing: true });
|
||||
// const members = await ch.getPresence();
|
||||
@@ -0,0 +1,89 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// safe-icons.ts — Verified lucide-react icon re-exports
|
||||
// ---------------------------------------------------------------------------
|
||||
// PRE-INSTALLED in every React project. CodeWriterAgent imports from here
|
||||
// instead of directly from lucide-react, preventing non-existent icon errors.
|
||||
// import { ArrowRight, Star, Menu, X } from "@/lib/safe-icons"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
// Navigation & UI
|
||||
Menu, X, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
|
||||
ChevronsUpDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown,
|
||||
ArrowUpRight, ExternalLink, MoreHorizontal, MoreVertical,
|
||||
Search, Filter, SlidersHorizontal, Loader2, RefreshCw,
|
||||
|
||||
// Actions
|
||||
Plus, Minus, Check, CheckCircle, CheckCircle2, XCircle,
|
||||
Copy, Download, Upload, Share2, Send, Save, Trash2,
|
||||
Edit, PenLine, Pencil, Eye, EyeOff, LogIn, LogOut,
|
||||
Settings, Settings2, Sliders,
|
||||
|
||||
// Communication
|
||||
Mail, Phone, MessageCircle, MessageSquare, Bell, BellRing,
|
||||
Inbox, Send as SendIcon, AtSign,
|
||||
|
||||
// Media
|
||||
Image, Camera, Video, Music, Play, Pause, Volume2, Mic,
|
||||
PlayCircle, Film,
|
||||
|
||||
// Social
|
||||
Github, Twitter, Linkedin, Instagram, Facebook, Youtube,
|
||||
Globe, Globe2, Rss,
|
||||
|
||||
// Business
|
||||
Briefcase, Building2, CreditCard, DollarSign, ShoppingCart,
|
||||
ShoppingBag, Package, Receipt, Wallet, PiggyBank,
|
||||
TrendingUp, TrendingDown, BarChart3, PieChart, LineChart,
|
||||
|
||||
// People
|
||||
User, Users, UserPlus, UserCheck, UserCircle, Contact,
|
||||
Heart, ThumbsUp, Star, Award, Trophy, Medal, Crown,
|
||||
|
||||
// Content
|
||||
FileText, File, Folder, BookOpen, Book, Newspaper,
|
||||
Bookmark, Tag, Hash, Quote, Type, AlignLeft,
|
||||
|
||||
// Tech
|
||||
Code, Code2, Terminal, Database, Server, Cloud, Wifi,
|
||||
Smartphone, Monitor, Laptop, Globe as GlobeIcon,
|
||||
Cpu, Zap, Bolt, Plug, Key, Lock, Unlock, Shield,
|
||||
ShieldCheck, Bug, Rocket, Wand2,
|
||||
|
||||
// Layout
|
||||
LayoutDashboard, LayoutGrid, Grid, Layers, Sidebar,
|
||||
PanelLeft, PanelRight, Columns2, Rows2, Table,
|
||||
|
||||
// Time & Calendar
|
||||
Clock, Calendar, CalendarDays, Timer, Hourglass,
|
||||
Sunrise, Sunset, Moon, Sun,
|
||||
|
||||
// Location
|
||||
MapPin, Map, Compass, Navigation, Home, Building2 as BuildingIcon,
|
||||
Flag, Route,
|
||||
|
||||
// Health & Science
|
||||
Heart as HeartIcon, HeartPulse, Activity, Stethoscope,
|
||||
Pill, Syringe, Thermometer, Microscope, Atom, Brain,
|
||||
|
||||
// Nature
|
||||
Leaf, TreePine, Trees, Flower, Sprout, Mountain,
|
||||
Waves, Wind, Snowflake, Sun as SunIcon, Cloud as CloudIcon,
|
||||
|
||||
// Food & Drink
|
||||
Utensils, Coffee, Pizza, Wine, CookingPot, Cherry,
|
||||
Apple, Cake, IceCream,
|
||||
|
||||
// Transport
|
||||
Car, Plane, Train, Bus, Bike, Truck, Ship, Sailboat,
|
||||
|
||||
// Education
|
||||
GraduationCap, School, Library, BookOpen as BookOpenIcon,
|
||||
Lightbulb, Puzzle, Target,
|
||||
|
||||
// Misc
|
||||
Sparkles, Flame, Gift, Ticket, Scissors, Wrench,
|
||||
Hammer, Paintbrush, Palette, Gem, Diamond, Ribbon,
|
||||
PartyPopper, Smile, Frown, AlertTriangle, Info,
|
||||
HelpCircle, CircleAlert, AlertCircle,
|
||||
} from "lucide-react";
|
||||
@@ -0,0 +1,115 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// TygaRun Client — ready-made SaaS APIs for generated sites
|
||||
// ---------------------------------------------------------------------------
|
||||
// Universal client for ALL sdk.tyga.run modules: auth, scheduling (bookings/
|
||||
// eventtypes/availability), crm, ecommerce, billing, notifications, webhooks...
|
||||
// Where Lovable gives you raw tables, tyga.run gives you ready-made APIs.
|
||||
//
|
||||
// This stub ships unconfigured. When the project connects tyga.run, the
|
||||
// builder replaces TYGARUN_CONFIG with real values (like lib/anydb.ts).
|
||||
// Until then isConfigured() is false and hooks fall back to seed data.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TYGARUN_CONFIG = {
|
||||
baseUrl: "__TYGARUN_SDK_URL__", // e.g. https://sdk.tyga.run
|
||||
// Browser-safe `tc_pub_` publishable key (Supabase-anon model): origin-locked,
|
||||
// sandbox-pinned, module-restricted server-side. Safe to ship in public code.
|
||||
publishableKey: "__TYGARUN_ANON_KEY__",
|
||||
sandbox: "__TYGARUN_SANDBOX__", // "true" | "false"
|
||||
};
|
||||
|
||||
export function isConfigured(): boolean {
|
||||
return !TYGARUN_CONFIG.publishableKey.startsWith("__");
|
||||
}
|
||||
|
||||
const TOKEN_KEY = "tygarun_token";
|
||||
|
||||
export function getToken(): string | null {
|
||||
try { return localStorage.getItem(TOKEN_KEY); } catch { return null; }
|
||||
}
|
||||
export function setToken(token: string | null) {
|
||||
try {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||
else localStorage.removeItem(TOKEN_KEY);
|
||||
} catch { /* SSR / private mode */ }
|
||||
}
|
||||
|
||||
export interface TygaRunError extends Error {
|
||||
statusCode?: number;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal call to any tyga.run module endpoint.
|
||||
* tygarun("scheduling", "/bookings", { query: { page: 1 } })
|
||||
* tygarun("auth", "/signin", { method: "POST", body: { email, password } })
|
||||
* tygarun("crm", "/contacts", { method: "POST", body: contact })
|
||||
*/
|
||||
export async function tygarun<T = any>(
|
||||
module: string,
|
||||
endpoint = "/",
|
||||
opts: {
|
||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
body?: any;
|
||||
query?: Record<string, any>;
|
||||
headers?: Record<string, string>;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
if (!isConfigured()) {
|
||||
const err = new Error("tyga.run not connected") as TygaRunError;
|
||||
err.statusCode = 0;
|
||||
throw err;
|
||||
}
|
||||
const { method = "GET", body, query = {}, headers = {} } = opts;
|
||||
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
||||
const url = new URL(`${TYGARUN_CONFIG.baseUrl.replace(/\/+$/, "")}/${module}${path}`);
|
||||
Object.entries(query).forEach(([k, v]) => {
|
||||
if (v !== null && v !== undefined) url.searchParams.append(k, String(v));
|
||||
});
|
||||
|
||||
const token = getToken();
|
||||
const res = await fetch(url.toString(), {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
// Publishable key ALWAYS authenticates the app (never the user).
|
||||
Authorization: `Bearer ${TYGARUN_CONFIG.publishableKey}`,
|
||||
// End-user identity (their JWT from auth.signin) rides separately.
|
||||
...(token ? { "X-User-Token": token } : {}),
|
||||
"X-Sandbox-Mode": TYGARUN_CONFIG.sandbox,
|
||||
...headers,
|
||||
},
|
||||
body: body && method !== "GET" && method !== "DELETE" ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let json: any = {};
|
||||
try { json = text ? JSON.parse(text) : {}; } catch { /* non-JSON error body */ }
|
||||
|
||||
if (!res.ok) {
|
||||
const err = new Error(json.error || json.message || `tyga.run ${module} error ${res.status}`) as TygaRunError;
|
||||
err.statusCode = res.status;
|
||||
err.body = json;
|
||||
throw err;
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
|
||||
// ── Auth convenience (JWT stored for user-scoped calls) ─────────────────────
|
||||
export const auth = {
|
||||
async signup(data: { email: string; password: string; firstName?: string; lastName?: string; [k: string]: any }) {
|
||||
const res = await tygarun<any>("auth", "/signup", { method: "POST", body: data });
|
||||
if (res.token) setToken(res.token);
|
||||
return res;
|
||||
},
|
||||
async signin(email: string, password: string, rememberMe = false) {
|
||||
const res = await tygarun<any>("auth", "/signin", { method: "POST", body: { email, password, rememberMe } });
|
||||
if (res.token) setToken(res.token);
|
||||
return res;
|
||||
},
|
||||
signout() { setToken(null); },
|
||||
isSignedIn(): boolean { return Boolean(getToken()); },
|
||||
};
|
||||
|
||||
export default tygarun;
|
||||
+6
-3
@@ -1,3 +1,6 @@
|
||||
export function cn(...inputs: Array<string | number | null | undefined | false>) {
|
||||
return inputs.filter(Boolean).join(" ");
|
||||
}
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user