turned template into create cli

This commit is contained in:
2025-07-25 01:08:04 +02:00
parent 1b71b472a7
commit 028f287067
85 changed files with 3774 additions and 3292 deletions

336
index.js Executable file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env node
import * as p from '@clack/prompts';
import color from 'picocolors';
import fs from 'fs-extra';
import path from 'path';
import { spawn, spawnSync } from 'node:child_process';
async function runCommand(cmd, args, cwd) {
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args, { cwd, stdio: 'ignore' });
proc.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`Command failed: ${cmd} ${args.join(' ')}`))));
});
}
async function main() {
p.intro(`${color.bgRedBright(color.black(' ✨ Welcome to Lumify Sanity Template! ✨ '))}`);
const project = await p.group(
{
projectName: () =>
p.text({
message: `${color.cyan('🔖 Enter your project name (for display):')}`,
placeholder: 'My Sanity Lumify page',
validate: (v) => (v.length < 2 ? `${color.red('❌ Too short!')}` : undefined),
}),
dirName: ({ results }) => {
const defaultKebab = String(results.projectName)
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return p.text({
message: `${color.cyan('📂 Enter a directory name for your project:')}`,
placeholder: `./${defaultKebab}`,
initialValue: defaultKebab,
validate: (v) => {
if (v.length < 2) return `${color.red('❌ Too short!')}`;
const kebab = String(v)
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const root = path.resolve(process.cwd(), kebab);
if (fs.existsSync(root)) {
return `${color.red('⚠️ Directory already exists:')} ${color.yellow(root)}`;
}
return undefined;
},
});
},
shouldGit: () =>
p.confirm({
message: `${color.green('🌱 Do you want to initialize a git repository?')}`,
initialValue: true,
}),
gitOrg: ({ results }) => {
if (!results.shouldGit) return undefined;
return p.text({
message: `${color.cyan('🐙 Enter the GitHub organization/user to push to (leave empty to skip remote):')}`,
placeholder: 'my-org-or-username',
initialValue: 'lumify-systems',
validate: (v) => (v && !/^([\w-]+)$/.test(v) ? `${color.red('❌ Invalid org/user name!')}` : undefined),
});
},
packageManager: () =>
p.select({
message: `${color.magenta('📦 Which package manager do you want to use?')}`,
options: [
{ value: 'bun', label: `${color.yellow('bun 🥯')}` },
{ value: 'npm', label: `${color.green('npm 📦')}` },
],
initialValue: 'bun',
}),
faviconPath: () =>
p.text({
message: `${color.cyan('🖼️ Path to favicon SVG (leave empty to skip):')}`,
placeholder: './myicon.svg',
}),
extraDomain: () =>
p.text({
message: `${color.cyan('🌍 Enter a domain this will run on later (leave empty to skip):')}`,
placeholder: 'youronlinedomain.com',
validate: () => undefined,
}),
},
{
onCancel: () => {
p.cancel('Operation cancelled.');
process.exit(0);
},
}
);
const kebabName = String(project.dirName)
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const rootDir = path.resolve(process.cwd(), kebabName);
const pmx = project.packageManager === 'bun' ? 'bunx' : 'npx';
const studioDir = path.join(rootDir, 'apps', 'studio');
await p.tasks([
{
title: `${color.yellow('📁 Copying template contents to root...')}`,
task: async () => {
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const templateDir = path.resolve(__dirname, 'template');
await fs.ensureDir(rootDir);
await fs.copy(templateDir, rootDir, { overwrite: true });
return 'Template copied!';
},
},
{
title: `${color.green(`📦 Installing dependencies with ${project.packageManager}...`)}`,
task: async () => {
await runCommand(project.packageManager === 'bun' ? 'bun' : 'npm', ['install'], rootDir);
return 'Dependencies installed successfully';
},
},
{
title: `${color.cyan('🔑 Checking Sanity login status...')}`,
task: async () => {
await fs.ensureDir(studioDir);
let loggedIn = false;
let hasProjects = false;
try {
const result = await new Promise((res, rej) => {
let out = '';
const c = spawn(pmx, ['sanity', 'projects', 'list'], { cwd: studioDir, stdio: ['ignore', 'pipe', 'pipe'] });
c.stdout.on('data', (d) => (out += d));
c.on('exit', (code) => (code === 0 ? res(out) : rej(new Error('fail'))));
});
if (result.includes('id ')) {
loggedIn = true;
hasProjects = result.split('\n').slice(1).some((l) => l.trim());
}
} catch { }
if (loggedIn) {
return hasProjects ? 'Already logged in to Sanity!' : 'Logged in to Sanity (no projects yet).';
} else {
await runCommand(pmx, ['sanity', 'login'], studioDir);
return 'Sanity login complete!';
}
},
},
{
title: `${color.magenta('🛠️ Creating Sanity project...')}`,
task: async () => {
spawnSync(pmx, [
"sanity",
"projects",
"create",
project.projectName
], {
cwd: studioDir,
stdio: "inherit",
encoding: "utf-8"
})
const listProjects = spawnSync(pmx, [
"sanity",
"projects",
"list"
], {
cwd: studioDir,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8"
})
let sanityJson;
if (listProjects.stdout) {
try {
const lines = listProjects.stdout.split(/\r?\n/).filter(l => l.trim() && !l.startsWith('id') && !l.startsWith('─'));
const projectIds = lines.map(line => {
const match = line.match(/^([a-z0-9]{8})\b/);
return match ? match[1] : null;
}).filter(id => !!id);
if (projectIds.length > 0) {
sanityJson = { projectId: projectIds[0] };
}
} catch (e) {
throw new Error("Could not parse Sanity project list output.");
}
}
if (!sanityJson || !sanityJson.projectId) {
throw new Error("Sanity project creation failed.");
}
// Update connection file with project ID
const connPath = path.join(rootDir, "packages/sanity-connection/index.ts");
if (!(await fs.pathExists(connPath))) {
throw new Error(`Connection file not found at ${connPath}`);
}
let connFile = await fs.readFile(connPath, "utf8");
if (!/projectId: "[^"]+"/.test(connFile)) {
throw new Error(`Could not find projectId in ${connPath}. Please ensure the file contains a line like projectId: "${sanityJson.projectId}"`);
}
connFile = connFile.replace(/projectId: "[^"]+"/, `projectId: "${sanityJson.projectId}"`);
await fs.writeFile(connPath, connFile);
return `Sanity project created with ID: ${sanityJson.projectId}`;
},
},
{
title: `${color.green('🔐 Creating Sanity viewer token...')}`,
task: async () => {
if (!(await fs.pathExists(studioDir))) throw new Error(`Studio directory not found at ${studioDir}`);
const addOut = await new Promise((res, rej) => {
let buf = '';
const c = spawn(
pmx,
['sanity', 'tokens', 'add', 'Main Viewer API Token', '--role=viewer', '-y', '--json'],
{ cwd: studioDir, stdio: ['ignore', 'pipe', 'pipe'] }
);
c.stdout.on('data', (d) => (buf += d));
c.on('exit', (code) => (code === 0 ? res(buf) : rej(new Error('token failed'))));
});
const token = JSON.parse(addOut);
const connPath = path.join(rootDir, 'packages/sanity-connection/index.ts');
let content = await fs.readFile(connPath, 'utf8');
content = content
.replace(/publicViewerToken: "[^"]+"/, `publicViewerToken: "${token.key}"`)
.replace(/studioHost: "[^"]+"/, `studioHost: "${kebabName}"`)
.replace(/studioUrl: "[^"]+"/, `studioUrl: "https://${kebabName}.sanity.studio"`);
await fs.writeFile(connPath, content);
return 'Viewer token created and configured!';
},
},
]);
if (project.faviconPath && project.faviconPath.trim()) {
await p.tasks([
{
title: `${color.yellow('🌟 Generating favicon...')}`,
task: async () => {
await runCommand(pmx, ['bun-create-favicon', project.faviconPath, 'packages/ui/favicon/'], rootDir);
await fs.copy(path.join(rootDir, 'packages/ui/favicon/'), path.join(rootDir, 'apps/client/public/'), { overwrite: true });
await fs.copy(path.join(rootDir, 'packages/ui/favicon/'), path.join(rootDir, 'apps/studio/static/'), { overwrite: true });
return 'Favicon generated and copied!';
},
},
]);
}
const corsOrigins = [
'http://localhost:3000',
'https://*.api.sanity.io',
'wss://*.api.sanity.io',
`https://${kebabName}.sanity.studio`,
];
if (project.extraDomain && project.extraDomain.trim()) {
corsOrigins.push(`https://${project.extraDomain.trim()}`);
}
await p.tasks(
corsOrigins.map((origin) => ({
title: `${color.cyan('🌐 Adding Sanity CORS origin:')} ${color.yellow(origin)}`,
task: async () => {
const args = ['sanity', 'cors', 'add', origin, '--yes'];
if (origin === `https://${kebabName}.sanity.studio`) args.push('--credentials');
await runCommand(pmx, args, studioDir);
return `CORS added: ${origin}` + (args.includes('--credentials') ? ' (credentials allowed)' : '');
},
}))
);
if (project.shouldGit) {
await p.tasks([
{
title: `${color.green('🌱 Setting up Git repository...')}`,
task: async () => {
await runCommand('git', ['config', '--global', 'init.defaultBranch', 'main'], process.cwd());
await runCommand('git', ['init'], rootDir);
await runCommand('git', ['add', '.'], rootDir);
await runCommand('git', ['commit', '-m', 'Initiated from Lumify Sanity template :rocket:'], rootDir);
await runCommand('git', ['branch', '-M', 'main'], rootDir);
return 'Git repository initialized with main branch';
},
},
]);
const org = typeof project.gitOrg === 'string' ? project.gitOrg.trim() : '';
if (org) {
const githubUrl = `https://github.com/new?name=${kebabName}&owner=${org}`;
p.note(
`Please create a new GitHub repository named\n\`${kebabName}\` under \`${org}\` at:\n\n${color.cyan(githubUrl)}\n\nThe browser will open for you.`,
'GitHub Setup Required'
);
await runCommand('xdg-open', [githubUrl], process.cwd());
await p.text({
message: 'Press Enter after you have created the GitHub repository to continue...',
placeholder: '',
validate: () => undefined,
});
await p.tasks([
{
title: `${color.magenta('🚀 Pushing to GitHub...')}`,
task: async () => {
await runCommand('git', ['remote', 'add', 'origin', `git@github.com:${org}/${kebabName}.git`], rootDir);
await runCommand('git', ['push', '-u', 'origin', 'main'], rootDir);
return 'Pushed to GitHub successfully!';
},
},
]);
}
}
const pmRun = `${project.packageManager} run dev`;
const pmDeploy = `${project.packageManager} run deploy`;
p.text({
message:
`${color.bgGreen(color.black('✨ Setup complete! ✨'))}\n${color.dim('Need help?')} ${color.underline(color.cyan('https://docs.lumify.systems/'))} ${color.yellow('💡')}`,
});
p.outro(
color.bold(color.cyan('🎉 All done! Your project is ready! 🎉')) + '\n\n' +
color.bold('Next steps:\n') +
` ${color.bold(color.green('cd'))} ${color.cyan(kebabName)}\n` +
` ${color.bold(color.green(pmRun))} ${color.dim('# Start your local dev server')} ${color.yellow('🖥️')}\n` +
` ${color.bold(color.green(pmDeploy))} ${color.dim('# Deploy your Sanity Studio')} ${color.yellow('🚀')}\n\n` +
color.dim('Local development:\n') +
` • App: ${color.cyan('http://localhost:3000')} ${color.yellow('🌐')}\n` +
` • Studio: ${color.cyan('http://localhost:3333')} ${color.yellow('🛠️')}\n\n` +
color.dim('After deploying:\n') +
` • Studio: ${color.cyan(`https://${kebabName}.sanity.studio`)} ${color.yellow('✨')}`
);
process.exit(0);
}
main().catch((err) => {
p.log.error('An error occurred during setup:');
p.log.error(err.message || err);
process.exit(1);
});