@entro314labs/at3-stack-kit
Version:
Upgrade existing projects to AT3 Stack with intelligent migration
1,575 lines (1,490 loc) • 113 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/cli.ts
import {
cancel,
confirm,
intro,
isCancel,
multiselect,
note,
outro,
spinner
} from "@clack/prompts";
import chalk2 from "chalk";
import { program } from "commander";
import { detect as detectPackageManager2 } from "detect-package-manager";
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs-extra";
import { join as join11 } from "path";
// src/detect.ts
import { detect as detectPackageManager } from "detect-package-manager";
import { existsSync, readdirSync, readFileSync } from "fs-extra";
import { join } from "path";
async function detectProjectType(projectPath) {
const packageJsonPath = join(projectPath, "package.json");
if (!existsSync(packageJsonPath)) {
return "unknown";
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const hasAI = deps.ai || deps["@ai-sdk/openai"] || deps["@ai-sdk/anthropic"] || deps["@ai-sdk/google"];
const hasSupabase = deps["@supabase/supabase-js"] || deps["@supabase/ssr"];
const hasNextJS = deps.next;
const hasTailwind = deps.tailwindcss;
const hasTypeScript = deps.typescript || existsSync(join(projectPath, "tsconfig.json"));
if (hasAI && hasSupabase && hasNextJS && (hasTailwind || hasTypeScript)) {
return "ait3e";
}
if (deps.next && deps["@trpc/server"]) {
if (deps.prisma || deps["@prisma/client"] || deps["drizzle-orm"]) {
return "t3";
}
}
if (deps.next) return "nextjs";
if (deps.nuxt || deps["@nuxt/core"]) return "nuxt";
if (deps.vue) return "vue";
if (deps.react) return "react";
if (deps.vite) return "vite";
if (deps.webpack) return "webpack";
return "node";
} catch (error) {
return "unknown";
}
}
async function analyzeProject(projectPath) {
if (!existsSync(projectPath)) {
throw new Error(`Project path does not exist: ${projectPath}`);
}
const packageJsonPath = join(projectPath, "package.json");
if (!existsSync(packageJsonPath)) {
throw new Error("No package.json found. This does not appear to be a Node.js project.");
}
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
let packageManager = "npm";
try {
packageManager = await detectPackageManager({ cwd: projectPath });
} catch {
if (existsSync(join(projectPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
else if (existsSync(join(projectPath, "yarn.lock"))) packageManager = "yarn";
else if (existsSync(join(projectPath, "bun.lockb"))) packageManager = "bun";
}
const dependencies = await analyzeDependencies(packageJson, projectPath);
const configFiles = findConfigFiles(projectPath);
const hasTypeScript = hasTypeScriptSupport(projectPath, dependencies);
const hasNextjs = hasDependency(dependencies, "next");
const hasReact = hasDependency(dependencies, "react");
const hasVue = hasDependency(dependencies, "vue");
const hasTailwind = hasDependency(dependencies, "tailwindcss");
const hasTRPC = detectTRPC(dependencies);
const hasEslint = hasDependency(dependencies, "eslint");
const hasPrettier = hasDependency(dependencies, "prettier");
const hasBiome = hasDependency(dependencies, "@biomejs/biome");
const hasAI = detectAISupport(dependencies);
const hasSupabase = detectSupabase(dependencies, projectPath);
const hasEdgeRuntime = detectEdgeRuntime(projectPath);
const hasVectorDB = hasSupabaseVectorConfig(projectPath);
const hasPWA = detectPWA(dependencies, projectPath);
const hasI18n = detectI18n(dependencies, projectPath);
const hasDrizzle = detectDrizzle(dependencies, projectPath);
const hasPrisma = detectPrisma(dependencies, projectPath);
const authProvider = detectAuthProvider(dependencies, projectPath);
const testing = detectTesting(dependencies);
const hasVitest = testing.unit === "vitest";
const hasPlaywright = testing.e2e === "playwright";
return {
path: projectPath,
type: await detectProjectType(projectPath),
hasNextjs,
hasReact,
hasVue,
hasTypeScript,
hasTailwind,
hasTRPC,
hasSupabase,
hasAI,
hasPWA,
hasI18n,
hasVitest,
hasPlaywright,
hasEslint,
hasPrettier,
hasBiome,
hasEdgeRuntime,
hasVectorDB,
hasDrizzle,
hasPrisma,
authProvider,
testing,
packageManager,
dependencies,
configFiles
};
}
async function analyzeDependencies(packageJson, projectPath) {
const deps = [];
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.dependencies)) {
const info = {
name,
version,
type: "dependency"
};
try {
const installedPkgPath = join(projectPath, "node_modules", name, "package.json");
if (existsSync(installedPkgPath)) {
const installedPkg = JSON.parse(readFileSync(installedPkgPath, "utf-8"));
info.current = installedPkg.version;
}
} catch {
}
deps.push(info);
}
}
if (packageJson.devDependencies) {
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
const info = {
name,
version,
type: "devDependency"
};
try {
const installedPkgPath = join(projectPath, "node_modules", name, "package.json");
if (existsSync(installedPkgPath)) {
const installedPkg = JSON.parse(readFileSync(installedPkgPath, "utf-8"));
info.current = installedPkg.version;
}
} catch {
}
deps.push(info);
}
}
if (packageJson.peerDependencies) {
for (const [name, version] of Object.entries(packageJson.peerDependencies)) {
deps.push({
name,
version,
type: "peerDependency"
});
}
}
return deps;
}
function findConfigFiles(projectPath) {
const configFiles = [];
const commonConfigFiles = [
// TypeScript
"tsconfig.json",
"tsconfig.build.json",
"tsconfig.test.json",
// Next.js
"next.config.js",
"next.config.ts",
"next.config.mjs",
"next-env.d.ts",
// Tailwind
"tailwind.config.js",
"tailwind.config.ts",
"tailwind.config.mjs",
"postcss.config.js",
"postcss.config.mjs",
// Linting
".eslintrc.js",
".eslintrc.json",
".eslintrc.yml",
".eslintrc.yaml",
"eslint.config.js",
"eslint.config.mjs",
".prettierrc",
".prettierrc.js",
".prettierrc.json",
"biome.json",
"biome.jsonc",
// Testing
"vitest.config.ts",
"vitest.config.js",
"vitest.config.mts",
"jest.config.js",
"jest.config.ts",
"playwright.config.ts",
"cypress.config.js",
"cypress.config.ts",
// Build tools
"vite.config.ts",
"vite.config.js",
"webpack.config.js",
"rollup.config.js",
"turbo.json",
// Database
"drizzle.config.ts",
"drizzle.config.js",
"prisma/schema.prisma",
// Environment
".env",
".env.local",
".env.example",
".env.development",
".env.production",
// Other
".gitignore",
"README.md",
"package.json",
"pnpm-workspace.yaml",
"vercel.json",
"netlify.toml"
];
commonConfigFiles.forEach((file) => {
if (existsSync(join(projectPath, file))) {
configFiles.push(file);
}
});
if (existsSync(join(projectPath, "supabase", "config.toml"))) {
configFiles.push("supabase/config.toml");
}
return configFiles;
}
function hasTypeScriptSupport(projectPath, dependencies) {
return existsSync(join(projectPath, "tsconfig.json")) || hasDependency(dependencies, "typescript");
}
function hasDependency(dependencies, name) {
return dependencies.some((dep) => dep.name === name);
}
function detectAISupport(dependencies) {
const aiDeps = [
"ai",
"@ai-sdk/openai",
"@ai-sdk/anthropic",
"@ai-sdk/google",
"@ai-sdk/azure",
"@ai-sdk/mistral",
"@ai-sdk/cohere",
"openai",
"@anthropic-ai/sdk",
"@google/generative-ai",
"langchain",
"@langchain/core",
"llamaindex"
];
return aiDeps.some((dep) => hasDependency(dependencies, dep));
}
function detectSupabase(dependencies, projectPath) {
const hasSupabaseDeps = hasDependency(dependencies, "@supabase/supabase-js") || hasDependency(dependencies, "@supabase/ssr") || hasDependency(dependencies, "@supabase/auth-helpers-nextjs");
const hasSupabaseConfig = existsSync(join(projectPath, "supabase", "config.toml"));
return hasSupabaseDeps || hasSupabaseConfig;
}
function detectEdgeRuntime(projectPath) {
const middlewarePaths = [
join(projectPath, "middleware.ts"),
join(projectPath, "middleware.js"),
join(projectPath, "src/middleware.ts"),
join(projectPath, "src/middleware.js")
];
if (middlewarePaths.some((p) => existsSync(p))) {
return true;
}
const apiPaths = [
join(projectPath, "app/api"),
join(projectPath, "src/app/api"),
join(projectPath, "pages/api")
];
for (const apiPath of apiPaths) {
if (existsSync(apiPath)) {
try {
const files = getAllFiles(apiPath, [".ts", ".js"]);
for (const file of files) {
const content = readFileSync(file, "utf8");
if (content.includes("export const runtime = 'edge'")) {
return true;
}
}
} catch {
}
}
}
return false;
}
function hasSupabaseVectorConfig(projectPath) {
const supabaseMigrationDir = join(projectPath, "supabase", "migrations");
if (!existsSync(supabaseMigrationDir)) return false;
try {
const migrationFiles = readdirSync(supabaseMigrationDir);
return migrationFiles.some((file) => {
if (file.endsWith(".sql")) {
const content = readFileSync(join(supabaseMigrationDir, file), "utf8");
return content.includes("vector") || content.includes("embedding") || content.includes("pgvector");
}
return false;
});
} catch {
return false;
}
}
function detectDrizzle(dependencies, projectPath) {
const hasDrizzleDeps = hasDependency(dependencies, "drizzle-orm") || hasDependency(dependencies, "drizzle-kit");
const hasDrizzleConfig = existsSync(join(projectPath, "drizzle.config.ts")) || existsSync(join(projectPath, "drizzle.config.js"));
return hasDrizzleDeps || hasDrizzleConfig;
}
function detectPrisma(dependencies, projectPath) {
const hasPrismaDeps = hasDependency(dependencies, "prisma") || hasDependency(dependencies, "@prisma/client");
const hasPrismaSchema = existsSync(join(projectPath, "prisma", "schema.prisma"));
return hasPrismaDeps || hasPrismaSchema;
}
function detectAuthProvider(dependencies, projectPath) {
if (hasDependency(dependencies, "@supabase/auth-helpers-nextjs") || hasDependency(dependencies, "@supabase/ssr")) {
const hasAuthConfig = existsSync(join(projectPath, "src/lib/supabase")) || existsSync(join(projectPath, "lib/supabase"));
if (hasAuthConfig) return "supabase";
}
if (hasDependency(dependencies, "@clerk/nextjs") || hasDependency(dependencies, "@clerk/clerk-react")) {
return "clerk";
}
if (hasDependency(dependencies, "better-auth")) {
return "better-auth";
}
if (hasDependency(dependencies, "next-auth") || hasDependency(dependencies, "@auth/core")) {
return "next-auth";
}
if (hasDependency(dependencies, "lucia")) {
return "lucia";
}
return "none";
}
function detectTRPC(dependencies) {
return hasDependency(dependencies, "@trpc/server") || hasDependency(dependencies, "@trpc/client") || hasDependency(dependencies, "@trpc/react-query");
}
function detectPWA(dependencies, projectPath) {
const hasPWADeps = hasDependency(dependencies, "@ducanh2912/next-pwa") || hasDependency(dependencies, "next-pwa") || hasDependency(dependencies, "workbox-webpack-plugin");
const hasManifest = existsSync(join(projectPath, "public", "manifest.json"));
const hasServiceWorker = existsSync(join(projectPath, "public", "sw.js")) || existsSync(join(projectPath, "public", "service-worker.js"));
return hasPWADeps || hasManifest && hasServiceWorker;
}
function detectI18n(dependencies, projectPath) {
const hasI18nDeps = hasDependency(dependencies, "next-intl") || hasDependency(dependencies, "next-i18next") || hasDependency(dependencies, "react-i18next") || hasDependency(dependencies, "i18next");
const hasMessagesDir = existsSync(join(projectPath, "messages")) || existsSync(join(projectPath, "locales")) || existsSync(join(projectPath, "public/locales"));
return hasI18nDeps || hasMessagesDir;
}
function detectTesting(dependencies) {
let unit = "none";
if (hasDependency(dependencies, "vitest")) {
unit = "vitest";
} else if (hasDependency(dependencies, "jest")) {
unit = "jest";
}
let e2e = "none";
if (hasDependency(dependencies, "@playwright/test") || hasDependency(dependencies, "playwright")) {
e2e = "playwright";
} else if (hasDependency(dependencies, "cypress")) {
e2e = "cypress";
}
return { unit, e2e };
}
function getAllFiles(dirPath, extensions) {
const files = [];
try {
const entries = readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...getAllFiles(fullPath, extensions));
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
} catch {
}
return files;
}
function getMissingFeatures(info) {
const missing = [];
if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) missing.push("database");
if (!info.hasAI) missing.push("ai");
if (!info.hasPWA) missing.push("pwa");
if (!info.hasI18n) missing.push("i18n");
if (info.testing.unit === "none") missing.push("testing");
if (!info.hasTailwind) missing.push("tailwind");
if (!info.hasTypeScript) missing.push("typescript");
if (info.authProvider === "none") missing.push("auth");
return missing;
}
function isCompatible(info) {
if (!(info.hasNextjs || info.hasReact)) return false;
if (!["npm", "pnpm", "yarn", "bun"].includes(info.packageManager)) return false;
return true;
}
function getRecommendations(info) {
const recommendations = [];
if (!info.hasTypeScript) {
recommendations.push({
priority: "high",
feature: "typescript",
reason: "TypeScript provides better development experience and type safety"
});
}
if (!info.hasTailwind) {
recommendations.push({
priority: "high",
feature: "tailwind",
reason: "Tailwind CSS is essential for AT3 stack styling"
});
}
if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) {
recommendations.push({
priority: "high",
feature: "database",
reason: "A database solution is needed for most applications"
});
}
if (info.authProvider === "none") {
recommendations.push({
priority: "medium",
feature: "auth",
reason: "Authentication is essential for user management"
});
}
if (!info.hasAI) {
recommendations.push({
priority: "medium",
feature: "ai",
reason: "AI integration is a core feature of AT3 stack"
});
}
if (info.testing.unit === "none") {
recommendations.push({
priority: "low",
feature: "testing",
reason: "Comprehensive testing improves code quality"
});
}
if (!info.hasBiome && (info.hasEslint || info.hasPrettier)) {
recommendations.push({
priority: "low",
feature: "biome",
reason: "Biome provides faster linting and formatting than ESLint/Prettier"
});
}
return recommendations;
}
function getAT3Score(info) {
let score = 0;
const maxScore = 10;
if (info.hasNextjs) score += 1;
if (info.hasTypeScript) score += 1;
if (info.hasTailwind) score += 1;
if (info.hasSupabase || info.hasDrizzle || info.hasPrisma) score += 1;
if (info.authProvider !== "none") score += 1;
if (info.hasAI) score += 2;
if (info.hasEdgeRuntime) score += 1;
if (info.testing.unit !== "none") score += 1;
if (info.hasBiome) score += 1;
const percentage = Math.round(score / maxScore * 100);
let level;
if (percentage === 0) level = "none";
else if (percentage < 30) level = "basic";
else if (percentage < 60) level = "intermediate";
else if (percentage < 90) level = "advanced";
else level = "full";
return { score, maxScore, percentage, level };
}
// src/features/add-ai.ts
import { join as join2 } from "path";
import { ensureDir, pathExists, readFile, writeFile } from "fs-extra";
async function addAI(type, projectPath) {
const srcPath = join2(projectPath, "src");
await ensureDir(srcPath);
const aiPath = join2(srcPath, "lib", "ai");
await ensureDir(aiPath);
if (type === "custom") {
await addCustomAI(aiPath);
}
if (type === "vercel") {
await addVercelAI(aiPath);
}
const apiPath = join2(srcPath, "app", "api");
await ensureDir(apiPath);
await addAPIRoutes(apiPath, type);
await updatePackageJson(projectPath, type);
}
async function addCustomAI(aiPath) {
const clientConfig = `/**
* Custom AI client configuration
*/
export interface AIProvider {
name: string;
baseURL: string;
apiKey: string;
}
export const AI_PROVIDERS = {
openai: {
name: 'OpenAI',
baseURL: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
},
anthropic: {
name: 'Anthropic',
baseURL: 'https://api.anthropic.com',
apiKey: process.env.ANTHROPIC_API_KEY!,
},
google: {
name: 'Google AI',
baseURL: 'https://generativelanguage.googleapis.com',
apiKey: process.env.GOOGLE_AI_API_KEY!,
},
} as const;
export type AIProviderKey = keyof typeof AI_PROVIDERS;
`;
await writeFile(join2(aiPath, "config.ts"), clientConfig);
const client = `/**
* Custom AI client implementation
*/
import { AI_PROVIDERS, type AIProviderKey } from './config.js';
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface CompletionOptions {
model: string;
temperature?: number;
maxTokens?: number;
}
export class AIClient {
constructor(
private provider: AIProviderKey,
private options: CompletionOptions = {}
) {}
async completion(
messages: ChatMessage[],
options: Partial<CompletionOptions> = {}
): Promise<string> {
const config = AI_PROVIDERS[this.provider];
const finalOptions = { ...this.options, ...options };
// Implementation would depend on the provider
// This is a basic structure
const response = await fetch(\`\${config.baseURL}/chat/completions\`, {
method: 'POST',
headers: {
'Authorization': \`Bearer \${config.apiKey}\`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: finalOptions.model || 'gpt-3.5-turbo',
messages,
temperature: finalOptions.temperature || 0.7,
max_tokens: finalOptions.maxTokens || 1000,
}),
});
const data = await response.json();
return data.choices[0]?.message?.content || '';
}
}
`;
await writeFile(join2(aiPath, "client.ts"), client);
}
async function addVercelAI(aiPath) {
const vercelClient = `/**
* Vercel AI SDK integration
*/
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { openai } from '@ai-sdk/openai';
import { generateText, streamText } from 'ai';
export const AI_MODELS = {
'gpt-4-turbo': openai('gpt-4-turbo'),
'gpt-3.5-turbo': openai('gpt-3.5-turbo'),
'claude-3-haiku': anthropic('claude-3-haiku-20240307'),
'claude-3-sonnet': anthropic('claude-3-sonnet-20240229'),
'gemini-pro': google('models/gemini-pro'),
} as const;
export type AIModelKey = keyof typeof AI_MODELS;
export async function generateCompletion(
model: AIModelKey,
prompt: string,
options?: {
temperature?: number;
maxTokens?: number;
}
) {
const { text } = await generateText({
model: AI_MODELS[model],
prompt,
temperature: options?.temperature || 0.7,
maxTokens: options?.maxTokens || 1000,
});
return text;
}
export async function streamCompletion(
model: AIModelKey,
prompt: string,
options?: {
temperature?: number;
maxTokens?: number;
}
) {
const { textStream } = await streamText({
model: AI_MODELS[model],
prompt,
temperature: options?.temperature || 0.7,
maxTokens: options?.maxTokens || 1000,
});
return textStream;
}
`;
await writeFile(join2(aiPath, "vercel-client.ts"), vercelClient);
const hooks = `/**
* React hooks for Vercel AI SDK
*/
'use client';
import { useChat, useCompletion } from 'ai/react';
import { type AIModelKey } from './vercel-client.js';
export function useAIChat(model: AIModelKey = 'gpt-3.5-turbo') {
return useChat({
api: '/api/chat',
body: {
model,
},
});
}
export function useAICompletion(model: AIModelKey = 'gpt-3.5-turbo') {
return useCompletion({
api: '/api/completion',
body: {
model,
},
});
}
`;
await writeFile(join2(aiPath, "vercel-hooks.ts"), hooks);
}
async function addAPIRoutes(apiPath, type) {
if (type === "vercel") {
const chatRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client';
import { streamText } from 'ai';
export async function POST(req: Request) {
const { messages, model = 'gpt-3.5-turbo' } = await req.json();
const result = await streamText({
model: AI_MODELS[model as AIModelKey],
messages,
});
return result.toDataStreamResponse();
}
`;
await ensureDir(join2(apiPath, "chat"));
await writeFile(join2(apiPath, "chat", "route.ts"), chatRoute);
const completionRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client';
import { generateText } from 'ai';
export async function POST(req: Request) {
const { prompt, model = 'gpt-3.5-turbo' } = await req.json();
const { text } = await generateText({
model: AI_MODELS[model as AIModelKey],
prompt,
});
return Response.json({ text });
}
`;
await ensureDir(join2(apiPath, "completion"));
await writeFile(join2(apiPath, "completion", "route.ts"), completionRoute);
}
}
async function updatePackageJson(projectPath, type) {
const packageJsonPath = join2(projectPath, "package.json");
if (!await pathExists(packageJsonPath)) return;
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
if (!packageJson.dependencies) packageJson.dependencies = {};
if (type === "vercel") {
packageJson.dependencies.ai = "^5.0.104";
packageJson.dependencies["@ai-sdk/openai"] = "^2.0.74";
packageJson.dependencies["@ai-sdk/anthropic"] = "^2.0.50";
packageJson.dependencies["@ai-sdk/google"] = "^2.0.44";
}
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
// src/features/add-better-auth.ts
import { readFile as readFile2, writeFile as writeFile2, pathExists as pathExists2, ensureDir as ensureDir2 } from "fs-extra";
import { join as join3 } from "path";
// src/utils/cli-styling.ts
import boxen from "boxen";
import chalk from "chalk";
import figures from "figures";
import gradient from "gradient-string";
var colors = {
primary: chalk.hex("#2563eb"),
// Blue
secondary: chalk.hex("#7c3aed"),
// Purple
success: chalk.green,
error: chalk.red,
warning: chalk.yellow,
info: chalk.blue,
muted: chalk.gray,
accent: chalk.magenta,
ai: chalk.hex("#10b981")
// Emerald for AI features
};
var gradients = {
at3: gradient("#2563eb", "#10b981"),
// Blue to Emerald
ai: gradient("#10b981", "#059669"),
// Emerald gradient
success: gradient("#22c55e", "#16a34a"),
// Green gradient
warning: gradient("#f59e0b", "#d97706"),
// Amber gradient
error: gradient("#ef4444", "#dc2626")
// Red gradient
};
var symbols = {
success: colors.success(figures.tick),
error: colors.error(figures.cross),
warning: colors.warning(figures.warning),
info: colors.info(figures.info),
arrow: colors.muted(figures.arrowRight),
bullet: colors.muted(figures.bullet),
line: colors.muted(figures.line),
ai: colors.ai("\u{1F916}"),
stack: colors.primary("\u{1F4DA}"),
edge: colors.secondary("\u26A1"),
database: colors.info("\u{1F5C4}\uFE0F"),
config: colors.muted("\u2699\uFE0F")
};
var style = {
title: (text) => gradients.at3(text),
subtitle: (text) => colors.secondary(text),
heading: (text) => colors.primary.bold(text),
label: (text) => colors.muted(text),
value: (text) => colors.primary(text),
success: (text) => colors.success(text),
error: (text) => colors.error(text),
warning: (text) => colors.warning(text),
info: (text) => colors.info(text),
muted: (text) => colors.muted(text),
accent: (text) => colors.accent(text),
ai: (text) => colors.ai(text),
code: (text) => chalk.cyan.italic(text),
path: (text) => chalk.dim.underline(text),
command: (text) => chalk.bgBlack.white.bold(` ${text} `)
};
var header = {
main: (title, subtitle) => {
const titleText = gradients.at3.multiline(title);
const subtitleText = subtitle ? `
${colors.muted(subtitle)}` : "";
return `${titleText}${subtitleText}`;
},
section: (title) => `
${colors.primary.bold(title)}
${colors.muted("\u2500".repeat(title.length))}`
};
// src/features/add-better-auth.ts
async function addBetterAuth(projectPath) {
console.log(colors.info("Adding Better Auth..."));
await updatePackageJson2(projectPath);
await addEnvExample(projectPath);
await createAuthFiles(projectPath);
console.log(colors.success("\u2713 Added Better Auth configuration"));
}
async function updatePackageJson2(projectPath) {
const packageJsonPath = join3(projectPath, "package.json");
if (!await pathExists2(packageJsonPath)) return;
const packageJson = JSON.parse(await readFile2(packageJsonPath, "utf-8"));
if (!packageJson.dependencies) packageJson.dependencies = {};
packageJson.dependencies["better-auth"] = "^1.1.0";
await writeFile2(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
async function addEnvExample(projectPath) {
const envExamplePath = join3(projectPath, ".env.example");
let envContent = "";
if (await pathExists2(envExamplePath)) {
envContent = await readFile2(envExamplePath, "utf-8");
}
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `
# Better Auth
BETTER_AUTH_SECRET=your_secret_here
BETTER_AUTH_URL=http://localhost:3000
`;
await writeFile2(envExamplePath, envContent);
}
}
async function createAuthFiles(projectPath) {
const libPath = join3(projectPath, "src", "lib", "auth");
await ensureDir2(libPath);
await writeFile2(join3(libPath, "client.ts"), `import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL
})
`);
await writeFile2(join3(libPath, "auth.ts"), `import { betterAuth } from "better-auth";
// import { db } from "@/db";
// import { drizzleAdapter } from "better-auth/adapters/drizzle";
export const auth = betterAuth({
// adapter: drizzleAdapter(db, {
// provider: "pg",
// }),
emailAndPassword: {
enabled: true
},
// socialProviders: {
// github: {
// clientId: process.env.GITHUB_CLIENT_ID,
// clientSecret: process.env.GITHUB_CLIENT_SECRET,
// }
// },
})
`);
const apiPath = join3(projectPath, "src", "app", "api", "auth", "[...all]");
await ensureDir2(apiPath);
await writeFile2(join3(apiPath, "route.ts"), `import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
`);
}
// src/features/add-clerk.ts
import { readFile as readFile3, writeFile as writeFile3, pathExists as pathExists3 } from "fs-extra";
import { join as join4 } from "path";
async function addClerk(projectPath) {
console.log(colors.info("Adding Clerk authentication..."));
await updatePackageJson3(projectPath);
await addEnvExample2(projectPath);
await addMiddleware(projectPath);
console.log(colors.warning("NOTE: You need to wrap your root layout with <ClerkProvider> manually."));
console.log(colors.success("\u2713 Added Clerk configuration"));
}
async function updatePackageJson3(projectPath) {
const packageJsonPath = join4(projectPath, "package.json");
if (!await pathExists3(packageJsonPath)) return;
const packageJson = JSON.parse(await readFile3(packageJsonPath, "utf-8"));
if (!packageJson.dependencies) packageJson.dependencies = {};
packageJson.dependencies["@clerk/nextjs"] = "^6.9.0";
await writeFile3(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
async function addEnvExample2(projectPath) {
const envExamplePath = join4(projectPath, ".env.example");
let envContent = "";
if (await pathExists3(envExamplePath)) {
envContent = await readFile3(envExamplePath, "utf-8");
}
if (!envContent.includes("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")) {
envContent += `
# Clerk Auth
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
`;
await writeFile3(envExamplePath, envContent);
}
}
async function addMiddleware(projectPath) {
const middlewarePath = join4(projectPath, "src", "middleware.ts");
const content = `import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: ["/((?!.*\\\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
`;
await writeFile3(middlewarePath, content);
}
// src/features/add-drizzle.ts
import { ensureDir as ensureDir3, pathExists as pathExists4, readFile as readFile4, writeFile as writeFile4 } from "fs-extra";
import { join as join5 } from "path";
async function addDrizzle(projectPath) {
const srcPath = join5(projectPath, "src");
await ensureDir3(srcPath);
const dbPath = join5(srcPath, "db");
await ensureDir3(dbPath);
await addDrizzleConfig(projectPath);
await addSchema(dbPath);
await addClient(dbPath);
await updatePackageJson4(projectPath);
await addEnvExample3(projectPath);
console.log("\u2713 Added Drizzle ORM + PostgreSQL support");
}
async function addDrizzleConfig(projectPath) {
const config = `import { defineConfig } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is missing');
}
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL,
},
});
`;
await writeFile4(join5(projectPath, "drizzle.config.ts"), config);
}
async function addSchema(dbPath) {
const schema = `import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name'),
email: text('email').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
`;
await writeFile4(join5(dbPath, "schema.ts"), schema);
}
async function addClient(dbPath) {
const client = `import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL!;
// Disable prefetch as it is not supported for "Transaction" pool mode
const client = postgres(connectionString, { prepare: false });
export const db = drizzle(client, { schema });
`;
await writeFile4(join5(dbPath, "index.ts"), client);
}
async function updatePackageJson4(projectPath) {
const packageJsonPath = join5(projectPath, "package.json");
if (!await pathExists4(packageJsonPath)) return;
const packageJson = JSON.parse(await readFile4(packageJsonPath, "utf-8"));
if (!packageJson.dependencies) packageJson.dependencies = {};
if (!packageJson.devDependencies) packageJson.devDependencies = {};
if (!packageJson.scripts) packageJson.scripts = {};
packageJson.dependencies["drizzle-orm"] = "^0.36.0";
packageJson.dependencies["postgres"] = "^3.4.4";
packageJson.dependencies["dotenv"] = "^16.4.5";
packageJson.devDependencies["drizzle-kit"] = "^0.28.0";
packageJson.devDependencies["pg"] = "^8.13.0";
packageJson.devDependencies["@types/pg"] = "^8.11.10";
packageJson.scripts["db:generate"] = "drizzle-kit generate";
packageJson.scripts["db:migrate"] = "drizzle-kit migrate";
packageJson.scripts["db:push"] = "drizzle-kit push";
packageJson.scripts["db:studio"] = "drizzle-kit studio";
await writeFile4(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
async function addEnvExample3(projectPath) {
const envExamplePath = join5(projectPath, ".env.example");
let envContent = "";
if (await pathExists4(envExamplePath)) {
envContent = await readFile4(envExamplePath, "utf-8");
}
if (!envContent.includes("DATABASE_URL")) {
const dbVars = `
# Database (PostgreSQL)
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
`;
envContent = envContent + dbVars;
await writeFile4(envExamplePath, envContent);
}
}
// src/features/add-i18n.ts
import { ensureDir as ensureDir4, readFile as readFile5, writeFile as writeFile5 } from "fs-extra";
import { join as join6 } from "path";
async function addI18n(projectPath) {
const srcPath = join6(projectPath, "src");
await ensureDir4(srcPath);
const messagesPath = join6(projectPath, "messages");
await ensureDir4(messagesPath);
await addDefaultMessages(messagesPath);
const i18nPath = join6(srcPath, "lib", "i18n");
await ensureDir4(i18nPath);
await addI18nConfig(i18nPath);
await addI18nRequest(i18nPath);
await addI18nNavigation(i18nPath);
await addI18nMiddleware(srcPath);
await addLocaleProvider(i18nPath);
await addLanguageSwitcher(srcPath);
await addLocaleLayout(srcPath);
await updatePackageJson5(projectPath);
await updateNextConfig(projectPath);
console.log("\u2713 Added i18n support (next-intl v4)");
}
async function addDefaultMessages(messagesPath) {
const enMessages = {
Common: {
loading: "Loading...",
error: "An error occurred",
retry: "Try again",
cancel: "Cancel",
save: "Save",
delete: "Delete",
edit: "Edit",
back: "Back",
next: "Next",
previous: "Previous",
search: "Search",
noResults: "No results found"
},
Navigation: {
home: "Home",
about: "About",
contact: "Contact",
dashboard: "Dashboard",
settings: "Settings",
profile: "Profile"
},
Auth: {
signIn: "Sign In",
signOut: "Sign Out",
signUp: "Sign Up",
email: "Email",
password: "Password",
forgotPassword: "Forgot password?",
rememberMe: "Remember me",
noAccount: "Don't have an account?",
hasAccount: "Already have an account?"
},
Index: {
title: "Welcome to AT3 Stack",
description: "Build AI-native applications with edge deployment",
getStarted: "Get Started",
learnMore: "Learn More"
},
Errors: {
notFound: "Page not found",
notFoundDescription: "The page you are looking for does not exist.",
serverError: "Server error",
serverErrorDescription: "Something went wrong on our end.",
goHome: "Go to home"
},
LanguageSwitcher: {
label: "Language",
en: "English",
es: "Espa\xF1ol",
fr: "Fran\xE7ais",
de: "Deutsch",
ja: "\u65E5\u672C\u8A9E",
zh: "\u4E2D\u6587"
}
};
const esMessages = {
Common: {
loading: "Cargando...",
error: "Ocurri\xF3 un error",
retry: "Intentar de nuevo",
cancel: "Cancelar",
save: "Guardar",
delete: "Eliminar",
edit: "Editar",
back: "Atr\xE1s",
next: "Siguiente",
previous: "Anterior",
search: "Buscar",
noResults: "No se encontraron resultados"
},
Navigation: {
home: "Inicio",
about: "Acerca de",
contact: "Contacto",
dashboard: "Panel",
settings: "Configuraci\xF3n",
profile: "Perfil"
},
Auth: {
signIn: "Iniciar sesi\xF3n",
signOut: "Cerrar sesi\xF3n",
signUp: "Registrarse",
email: "Correo electr\xF3nico",
password: "Contrase\xF1a",
forgotPassword: "\xBFOlvidaste tu contrase\xF1a?",
rememberMe: "Recordarme",
noAccount: "\xBFNo tienes una cuenta?",
hasAccount: "\xBFYa tienes una cuenta?"
},
Index: {
title: "Bienvenido a AT3 Stack",
description: "Construye aplicaciones nativas de IA con despliegue en el borde",
getStarted: "Comenzar",
learnMore: "Saber m\xE1s"
},
Errors: {
notFound: "P\xE1gina no encontrada",
notFoundDescription: "La p\xE1gina que buscas no existe.",
serverError: "Error del servidor",
serverErrorDescription: "Algo sali\xF3 mal de nuestro lado.",
goHome: "Ir al inicio"
},
LanguageSwitcher: {
label: "Idioma",
en: "English",
es: "Espa\xF1ol",
fr: "Fran\xE7ais",
de: "Deutsch",
ja: "\u65E5\u672C\u8A9E",
zh: "\u4E2D\u6587"
}
};
await writeFile5(join6(messagesPath, "en.json"), JSON.stringify(enMessages, null, 2));
await writeFile5(join6(messagesPath, "es.json"), JSON.stringify(esMessages, null, 2));
}
async function addI18nConfig(i18nPath) {
const config = `import { getRequestConfig } from 'next-intl/server'
import { hasLocale } from 'next-intl'
import { routing } from './navigation'
export default getRequestConfig(async ({ requestLocale }) => {
// Typically corresponds to the \`[locale]\` segment
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale
return {
locale,
messages: (await import(\`../../../messages/\${locale}.json\`)).default,
}
})
`;
await writeFile5(join6(i18nPath, "config.ts"), config);
}
async function addI18nRequest(i18nPath) {
const request = `import { getRequestConfig } from 'next-intl/server'
import { routing } from './navigation'
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the \`[locale]\` segment
let locale = await requestLocale
// Ensure that the incoming \`locale\` is valid
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(\`../../../messages/\${locale}.json\`)).default,
timeZone: 'UTC',
now: new Date(),
}
})
`;
await writeFile5(join6(i18nPath, "request.ts"), request);
}
async function addI18nNavigation(i18nPath) {
const navigation = `import { createNavigation } from 'next-intl/navigation'
import { defineRouting } from 'next-intl/routing'
export const locales = ['en', 'es'] as const
export const defaultLocale = 'en' as const
export type Locale = (typeof locales)[number]
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: 'as-needed',
})
// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing)
`;
await writeFile5(join6(i18nPath, "navigation.ts"), navigation);
}
async function addI18nMiddleware(srcPath) {
const middleware = `import createMiddleware from 'next-intl/middleware'
import { routing } from './lib/i18n/navigation'
export default createMiddleware(routing)
export const config = {
// Match all pathnames except for
// - ... if they start with \`/api\`, \`/_next\` or \`/_vercel\`
// - ... the ones containing a dot (e.g. \`favicon.ico\`)
matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)'],
}
`;
await writeFile5(join6(srcPath, "middleware.ts"), middleware);
}
async function addLocaleProvider(i18nPath) {
const provider = `'use client'
import { NextIntlClientProvider } from 'next-intl'
import type { ReactNode } from 'react'
interface LocaleProviderProps {
children: ReactNode
locale: string
messages: Record<string, unknown>
timeZone?: string
now?: Date
}
export function LocaleProvider({
children,
locale,
messages,
timeZone = 'UTC',
now,
}: LocaleProviderProps) {
return (
<NextIntlClientProvider
locale={locale}
messages={messages}
timeZone={timeZone}
now={now}
>
{children}
</NextIntlClientProvider>
)
}
`;
await writeFile5(join6(i18nPath, "provider.tsx"), provider);
const index = `export { locales, defaultLocale, routing, type Locale } from './navigation'
export { Link, redirect, usePathname, useRouter, getPathname } from './navigation'
export { LocaleProvider } from './provider'
`;
await writeFile5(join6(i18nPath, "index.ts"), index);
}
async function addLanguageSwitcher(srcPath) {
const componentPath = join6(srcPath, "components", "layout");
await ensureDir4(componentPath);
const switcher = `'use client'
import { useLocale, useTranslations } from 'next-intl'
import { usePathname, useRouter } from '@/lib/i18n/navigation'
import { locales, type Locale } from '@/lib/i18n/navigation'
interface LanguageSwitcherProps {
className?: string
}
export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
const t = useTranslations('LanguageSwitcher')
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const handleChange = (newLocale: string) => {
router.replace(pathname, { locale: newLocale as Locale })
}
return (
<div className={className}>
<label htmlFor="language-select" className="sr-only">
{t('label')}
</label>
<select
id="language-select"
value={locale}
onChange={(e) => handleChange(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
{locales.map((loc) => (
<option key={loc} value={loc}>
{t(loc)}
</option>
))}
</select>
</div>
)
}
/**
* Alternative: Language switcher with buttons
*/
export function LanguageSwitcherButtons({ className }: LanguageSwitcherProps) {
const t = useTranslations('LanguageSwitcher')
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
return (
<div className={\`flex gap-2 \${className}\`}>
{locales.map((loc) => (
<button
key={loc}
onClick={() => router.replace(pathname, { locale: loc })}
disabled={locale === loc}
className={\`px-3 py-1 text-sm rounded-md transition-colors \${
locale === loc
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
}\`}
>
{t(loc)}
</button>
))}
</div>
)
}
`;
await writeFile5(join6(componentPath, "language-switcher.tsx"), switcher);
}
async function addLocaleLayout(srcPath) {
const localePath = join6(srcPath, "app", "[locale]");
await ensureDir4(localePath);
const layout = `import { NextIntlClientProvider, hasLocale } from 'next-intl'
import { getMessages, setRequestLocale } from 'next-intl/server'
import { notFound } from 'next/navigation'
import type { ReactNode } from 'react'
import { routing } from '@/lib/i18n/navigation'
interface LocaleLayoutProps {
children: ReactNode
params: Promise<{ locale: string }>
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}
export default async function LocaleLayout({
children,
params,
}: LocaleLayoutProps) {
const { locale } = await params
// Ensure that the incoming \`locale\` is valid
if (!hasLocale(routing.locales, locale)) {
notFound()
}
// Enable static rendering
setRequestLocale(locale)
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages()
return (
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
)
}
`;
await writeFile5(join6(localePath, "layout.tsx"), layout);
const page = `import { useTranslations } from 'next-intl'
import { setRequestLocale } from 'next-intl/server'
import { Link } from '@/lib/i18n/navigation'
interface HomePageProps {
params: Promise<{ locale: string }>
}
export default async function HomePage({ params }: HomePageProps) {
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
return <HomeContent />
}
function HomeContent() {
const t = useTranslations('Index')
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold mb-4">{t('title')}</h1>
<p className="text-lg text-muted-foreground mb-8">{t('description')}</p>
<div className="flex gap-4">
<Link
href="/dashboard"
className="rounded-md bg-primary px-6 py-3 text-primary-foreground hover:bg-primary/90"
>
{t('getStarted')}
</Link>
<Link
href="/about"
className="rounded-md border border-input bg-background px-6 py-3 hover:bg-accent hover:text-accent-foreground"
>
{t('learnMore')}
</Link>
</div>
</main>
)
}
`;
await writeFile5(join6(localePath, "page.tsx"), page);
const notFound = `import { useTranslations } from 'next-intl'
import { Link } from '@/lib/i18n/navigation'
export default function NotFoundPage() {
const t = useTranslations('Errors')
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold mb-4">{t('notFound')}</h1>
<p className="text-lg text-muted-foreground mb-8">
{t('notFoundDescription')}
</p>
<Link
href="/"
className="rounded-md bg-primary px-6 py-3 text-primary-foreground hover:bg-primary/90"
>
{t('goHome')}
</Link>
</main>
)
}
`;
await writeFile5(join6(localePath, "not-found.tsx"), notFound);
}
async function updatePackageJson5(projectPath) {
const packageJsonPath = join6(projectPath, "package.json");
const packageJson = JSON.parse(await readFile5(packageJsonPath, "utf-8"));
if (!packageJson.dependencies) packageJson.dependencies = {};
packageJson.dependencies["next-intl"] = "^4.5.5";
await writeFile5(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
async function updateNextConfig(projectPath) {
const configPath = join6(projectPath, "next.config.ts");
try {
let configContent = await readFile5(configPath, "utf-8");
if (configContent.includes("withNextIntl") || configContent.includes("next-intl/plugin")) {
return;
}
const importStatement = `import createNextIntlPlugin from 'next-intl/plugin'
`;
const pluginInit = `const withNextIntl = createNextIntlPlugin('./src/lib/i18n/request.ts')
`;
if (!configContent.includes("next-intl/plugin")) {
configContent = importStatement + configContent;
}
const exportDefaultRegex = /export\s+default\s+(\w+);?\s*$/m;
const match = configContent.match(exportDefaultRegex);
if (match) {
const configName = match[1];
configContent = configContent.replace(
exportDefaultRegex,
`${pluginInit}export default withNextIntl(${configName})
`
);
} else {
const inlineExportRegex = /export\s+default\s+({[\s\S]*?})\s*;?\s*$/m;
const inlineMatch = configContent.match(inlineExportRegex);
if (inlineMatch) {
configContent = configContent.replace(
inlineExportRegex,
`const nextConfig = ${inlineMatch[1]}
${pluginInit}export default withNextIntl(nextConfig)
`
);
}
}
await writeFile5(configPath, configContent);
} catch (error) {
console.warn(
"Could not update next.config.ts automatically. Please wrap your config with withNextIntl manually."
);
}
}
// src/features/add-pwa.ts
import { join as join7 } from "path";
import { ensureDir as ensureDir5, readFile as readFile6, writeFile as writeFile6 } from "fs-extra";
async function addPWA(projectPath) {
const srcPath = join7(projectPath, "src");
const publicPath = join7(projectPath, "public");
await ensureDir5(publicPath);
await ensureDir5(join7(publicPath, "icons"));
await addManifest(publicPath);
await addServiceWorker(publicPath);
await addOfflinePage(srcPath);
await addPWAUtils(srcPath);
await addInstallPrompt(srcPath);
await addPWAProvider(srcPath);
await updatePackageJson6(projectPath);
await updateNextConfig2(projectPath);
await updateLayout(srcPath);
console.log("\u2713 PWA support added");
}
async function addManifest(publicPath) {
const manifest = {
name: "AT3 App",
short_name: "AT3",
description: "AT3 Stack Application - AI-native, edge-first",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#6366f1",
orientation: "portrait-primary",
scope: "/",
lang: "en",
categories: ["productivity", "utilities"],
icons: [
{
src: "/icons/icon-72x72.png",
sizes: "72x72",
type: "image/png",
purpose: "any"
},
{
src: "/icons/icon-96x96.png",
sizes: "96x96",
type: "image/png",
purpose: "any"
},
{
src: "/icons/icon-128x128.png",
sizes: "128x128",
type: "image/png",
purpose: "any"
},
{
src: "/icons/icon-144x144.png",
sizes: "144x144",
type: "image/png",
pur