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

3260
bun.lock

File diff suppressed because it is too large Load Diff

17
debug.js Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env node
import * as p from '@clack/prompts';
async function demoSpinner() {
await p.tasks([
{
title: 'Running demo task',
task: async () => {
await new Promise((r) => setTimeout(r, 1500));
return 'Demo completed!';
},
},
]);
}
demoSpinner().catch(console.error);

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);
});

View File

@@ -1,26 +1,24 @@
{
"name": "web",
"name": "lumify-sanity-template",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"deploy": "turbo run deploy",
"generate": "turbo run generate",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
"description": "Bun create template for Lumify web/studio monorepo setup.",
"bin": {
"create-lumify-template": "./index.js"
},
"devDependencies": {
"prettier": "^3.6.2",
"turbo": "^2.5.4",
"typescript": "5.8.3"
},
"engines": {
"bun": ">=1.2.12"
},
"packageManager": "bun@1.2.12",
"workspaces": [
"apps/*",
"packages/*"
]
"main": "index.js",
"keywords": [
"bun",
"create",
"template",
"lumify",
"sanity"
],
"type": "module",
"dependencies": {
"@clack/prompts": "^0.11.0",
"@types/bun": "^1.2.19",
"colorette": "^2.0.20",
"fs-extra": "^11.3.0"
}
}

38
template/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem

View File

@@ -16,30 +16,30 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-slot": "^1.2.3",
"@sanity/client": "^7.8.1",
"@repo/sanity-connection": "*",
"@repo/typescript-config": "*",
"@repo/ui": "*",
"@sanity/client": "^7.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.6",
"framer-motion": "^12.23.9",
"lucide-react": "^0.525.0",
"next": "15.3.5",
"next-sanity": "^9.12.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.4.3",
"next-sanity": "^10.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@repo/typescript-config": "*",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.5",
"tailwindcss": "^4",
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^24.1.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"eslint": "^9.31.0",
"eslint-config-next": "15.4.3",
"tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.5",
"typescript": "^5"
"typescript": "^5.8.3"
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "website",
"name": "studio",
"private": true,
"version": "1.0.0",
"main": "package.json",
@@ -16,23 +16,23 @@
"sanity"
],
"dependencies": {
"@repo/ui": "*",
"@repo/sanity-connection": "*",
"@repo/ui": "*",
"@sanity/document-internationalization": "^3.3.3",
"@sanity/vision": "^3.99.0",
"@sanity/vision": "^4.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sanity": "^3.99.0",
"sanity": "^4.1.1",
"sanity-plugin-link-field": "^1.4.0",
"sanity-plugin-media": "^3.0.4",
"sanity-plugin-seo": "^1.3.0",
"sanity-plugin-simpler-color-input": "^3.1.0",
"sanity-plugin-seo": "^1.3.1",
"sanity-plugin-simpler-color-input": "^3.1.1",
"styled-components": "^6.1.19"
},
"devDependencies": {
"@sanity/eslint-config-studio": "^5.0.2",
"@types/react": "^19.1.8",
"eslint": "^9.30.1",
"eslint": "^9.31.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
},

3294
template/bun.lock Normal file

File diff suppressed because it is too large Load Diff

31
template/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "web",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"deploy": "turbo run deploy",
"generate": "turbo run generate",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
},
"devDependencies": {
"prettier": "^3.6.2",
"turbo": "^2.5.5",
"typescript": "5.8.3"
},
"engines": {
"bun": ">=1.2.12"
},
"packageManager": "bun@1.2.12",
"workspaces": [
"apps/*",
"packages/*"
],
"dependencies": {
"@clack/prompts": "^0.11.0",
"colorette": "^2.0.20",
"fs-extra": "^11.3.0"
}
}

View File

@@ -4,6 +4,6 @@ export const sanityConnection = {
studioHost: "vaporvee",
studioUrl: "https://vaporvee.sanity.studio", // normaly https://<studioHost>.sanity.studio
projectId: "ax04yw0e",
previewUrl: "https://vaporvee.vercel.app",
previewUrl: "http://localhost:3000",
dataset: "production", // leave as "production" for the main dataset
};

View File