Skip to content
Merged
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
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { generateProject } from './src/generate.js';
import { addAction } from './src/commands/add.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -384,4 +385,10 @@ program
.option('-t, --template <template>', 'Show details for a specific template')
.action(listAction);

program
.command('add')
.description('Add a feature plugin to an existing Opusify project')
.argument('<plugin>', 'Plugin to add (auth, dark-mode, analytics)')
.action(addAction);

program.parse();
84 changes: 84 additions & 0 deletions src/commands/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
import { execSync } from 'child_process';
import { addAuth } from '../plugins/auth.js';
import { addDarkMode } from '../plugins/dark-mode.js';
import { addAnalytics } from '../plugins/analytics.js';

const PLUGINS = {
auth: { label: 'Authentication (NextAuth.js)', fn: addAuth },
'dark-mode': { label: 'Dark Mode Toggle', fn: addDarkMode },
analytics: { label: 'Analytics (Vercel Analytics)', fn: addAnalytics },
};

export async function addAction(plugin) {
const cwd = process.cwd();
const configPath = path.join(cwd, 'opusify.config.json');

// Check if we're in an Opusify project
if (!fs.existsSync(configPath)) {
console.log(chalk.red('\n✖ Not an Opusify project.'));
console.log(chalk.gray(' No opusify.config.json found in the current directory.'));
console.log(chalk.gray(' Run this command from inside a project generated by Opusify.\n'));
process.exit(1);
}

// Validate plugin name
if (!plugin || !PLUGINS[plugin]) {
console.log(chalk.red(`\n✖ Unknown plugin: "${plugin || ''}"`));
console.log(chalk.cyan('\n Available plugins:'));
for (const [key, val] of Object.entries(PLUGINS)) {
console.log(chalk.white(` • ${key}`) + chalk.gray(` — ${val.label}`));
}
console.log('');
process.exit(1);
}

// Read project config
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
console.log('');
console.log(chalk.blue.bold(` Adding ${PLUGINS[plugin].label} to ${config.projectName}...`));
console.log('');

// Run the plugin
const { files, dependencies, devDependencies } = PLUGINS[plugin].fn(cwd, config);

// Write files
for (const [filePath, content] of Object.entries(files)) {
const fullPath = path.join(cwd, filePath);
const dir = path.dirname(fullPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(fullPath, content);
console.log(chalk.green(` ✔ Created ${filePath}`));
}

// Install dependencies
const allDeps = { ...dependencies, ...devDependencies };
if (Object.keys(allDeps).length > 0) {
// Update package.json
const pkgPath = path.join(cwd, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

if (dependencies && Object.keys(dependencies).length > 0) {
pkg.dependencies = { ...pkg.dependencies, ...dependencies };
}
if (devDependencies && Object.keys(devDependencies).length > 0) {
pkg.devDependencies = { ...pkg.devDependencies, ...devDependencies };
}
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));

const spinner = ora('Installing new dependencies...').start();
try {
execSync('npm install', { cwd, stdio: 'pipe' });
spinner.succeed('Dependencies installed!');
} catch (err) {
spinner.fail('Could not install dependencies. Run npm install manually.');
}
}

console.log('');
console.log(chalk.green.bold(` ✔ ${PLUGINS[plugin].label} added successfully!`));
console.log('');
}
159 changes: 159 additions & 0 deletions src/plugins/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
export function addAnalytics(cwd, config) {
const isNextjs = config.architecture === 'nextjs-monolith' || config.architecture === 'nextjs-turborepo';
const isVite = config.architecture === 'vite-react';

const files = {};
const dependencies = {};

if (isNextjs) {
// Vercel Analytics component
files['components/Analytics.tsx'] = `'use client';

import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function AnalyticsProvider() {
return (
<>
<Analytics />
<SpeedInsights />
</>
);
}
`;

// Instructions file
files['ANALYTICS.md'] = `# Analytics Setup

Vercel Analytics and Speed Insights have been added to your project.

## Usage

Import the \`AnalyticsProvider\` component in your root layout:

\`\`\`tsx
// app/layout.tsx
import AnalyticsProvider from '@/components/Analytics';

export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<AnalyticsProvider />
</body>
</html>
);
}
\`\`\`

## Deployment

Analytics data will automatically appear in your Vercel dashboard once deployed.
For local development, analytics events are logged to the console.

## Alternative: Plausible Analytics

If you prefer Plausible (privacy-focused, self-hostable), replace the Analytics component with:

\`\`\`tsx
// components/Analytics.tsx
import Script from 'next/script';

export default function AnalyticsProvider() {
return (
<Script
defer
data-domain="yourdomain.com"
src="https://plausible.io/js/script.js"
/>
);
}
\`\`\`
`;

dependencies['@vercel/analytics'] = '^1.3.1';
dependencies['@vercel/speed-insights'] = '^1.0.12';

} else if (isVite) {
// For Vite, use a simple Plausible/GA script approach
files['src/components/Analytics.tsx'] = `import { useEffect } from 'react';

/**
* Analytics component for Vite SPA.
* Replace the placeholder domain with your actual domain.
*
* For Plausible: Set VITE_PLAUSIBLE_DOMAIN in your .env
* For Google Analytics: Set VITE_GA_ID in your .env
*/
export default function Analytics() {
useEffect(() => {
// Track page views on route change
const trackPageView = () => {
if (typeof window !== 'undefined' && (window as any).plausible) {
(window as any).plausible('pageview');
}
};

trackPageView();
window.addEventListener('popstate', trackPageView);
return () => window.removeEventListener('popstate', trackPageView);
}, []);

return null;
}
`;

files['ANALYTICS.md'] = `# Analytics Setup

A lightweight analytics component has been added to your Vite project.

## Option 1: Plausible Analytics

Add this script tag to your \`index.html\` \`<head>\`:

\`\`\`html
<script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js"></script>
\`\`\`

## Option 2: Google Analytics

Add your GA4 measurement ID to \`.env\`:

\`\`\`
VITE_GA_ID=G-XXXXXXXXXX
\`\`\`

Then add the GA script to \`index.html\`:

\`\`\`html
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
\`\`\`

## Usage

Import the Analytics component in your App.tsx:

\`\`\`tsx
import Analytics from './components/Analytics';

function App() {
return (
<>
<Analytics />
{/* rest of your app */}
</>
);
}
\`\`\`
`;
}

return { files, dependencies, devDependencies: {} };
}
Loading
Loading