create-lovstudio-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/dist/index.mjs +384 -0
  3. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # create-lovstudio-app
2
+
3
+ Create a new Lovstudio Next.js project with one command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-lovstudio-app my-app
9
+ ```
10
+
11
+ Or with your preferred package manager:
12
+
13
+ ```bash
14
+ pnpm create lovstudio-app my-app
15
+ yarn create lovstudio-app my-app
16
+ bun create lovstudio-app my-app
17
+ ```
18
+
19
+ ## Features
20
+
21
+ The CLI creates a project based on the [Lovstudio Next.js template](https://github.com/lovstudio/lovweb) with:
22
+
23
+ - ⚡️ **Next.js 15** with App Router
24
+ - 🔒 **Supabase Auth** with cookie-based sessions
25
+ - 🗄️ **DrizzleORM** with PostgreSQL
26
+ - 🎨 **Tailwind CSS v4** + shadcn/ui
27
+ - 🌐 **i18n** with next-intl (Chinese/English)
28
+ - 📊 **Monitoring** with Sentry + PostHog
29
+ - 🛡️ **Security** with Arcjet
30
+
31
+ ## Options
32
+
33
+ ```bash
34
+ create-lovstudio-app [project-name] [options]
35
+
36
+ Options:
37
+ --skip-install Skip dependency installation
38
+ --use-npm Use npm as package manager
39
+ --use-pnpm Use pnpm as package manager
40
+ --use-yarn Use yarn as package manager
41
+ --use-bun Use bun as package manager
42
+ -h, --help Display help
43
+ -v, --version Display version
44
+ ```
45
+
46
+ ## Interactive Mode
47
+
48
+ When you run `create-lovstudio-app`, it will prompt you to:
49
+
50
+ 1. Enter project name
51
+ 2. Select features to include:
52
+ - Supabase Auth (default: yes)
53
+ - i18n support (default: yes)
54
+ - Storybook (default: no)
55
+ - Monitoring (default: yes)
56
+ 3. Choose package manager
57
+ 4. Initialize git repository
58
+
59
+ ## License
60
+
61
+ MIT
package/dist/index.mjs ADDED
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { cac } from "cac";
5
+
6
+ // src/create.ts
7
+ import { execSync as execSync2 } from "child_process";
8
+ import fs2 from "fs";
9
+ import path2 from "path";
10
+ import ora from "ora";
11
+ import pc3 from "picocolors";
12
+
13
+ // src/prompts.ts
14
+ import prompts from "prompts";
15
+ import pc from "picocolors";
16
+
17
+ // src/utils/package-manager.ts
18
+ import { execSync } from "child_process";
19
+ function detectPackageManager() {
20
+ const userAgent = process.env.npm_config_user_agent || "";
21
+ if (userAgent.startsWith("pnpm"))
22
+ return "pnpm";
23
+ if (userAgent.startsWith("yarn"))
24
+ return "yarn";
25
+ if (userAgent.startsWith("bun"))
26
+ return "bun";
27
+ return "npm";
28
+ }
29
+ function isPackageManagerInstalled(pm) {
30
+ try {
31
+ execSync(`${pm} --version`, { stdio: "ignore" });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function getInstallCommand(pm) {
38
+ switch (pm) {
39
+ case "pnpm":
40
+ return "pnpm install";
41
+ case "yarn":
42
+ return "yarn";
43
+ case "bun":
44
+ return "bun install";
45
+ default:
46
+ return "npm install";
47
+ }
48
+ }
49
+
50
+ // src/prompts.ts
51
+ async function promptProjectConfig(defaultName) {
52
+ const detectedPm = detectPackageManager();
53
+ const response = await prompts(
54
+ [
55
+ {
56
+ type: "text",
57
+ name: "projectName",
58
+ message: "Project name:",
59
+ initial: defaultName || "my-lovstudio-app",
60
+ validate: (value) => {
61
+ if (!value)
62
+ return "Project name is required";
63
+ if (!/^[a-z0-9-_]+$/i.test(value))
64
+ return "Project name can only contain letters, numbers, hyphens and underscores";
65
+ return true;
66
+ }
67
+ },
68
+ {
69
+ type: "multiselect",
70
+ name: "features",
71
+ message: "Select features:",
72
+ instructions: pc.dim("(space to select, enter to confirm)"),
73
+ choices: [
74
+ { title: "Supabase Auth", value: "supabase", selected: true },
75
+ { title: "i18n (Chinese/English)", value: "i18n", selected: true },
76
+ { title: "Storybook", value: "storybook", selected: false },
77
+ { title: "Monitoring (Sentry + PostHog)", value: "monitoring", selected: true }
78
+ ]
79
+ },
80
+ {
81
+ type: "select",
82
+ name: "packageManager",
83
+ message: "Package manager:",
84
+ choices: [
85
+ { title: `pnpm ${detectedPm === "pnpm" ? pc.dim("(detected)") : pc.dim("(recommended)")}`, value: "pnpm" },
86
+ { title: `npm ${detectedPm === "npm" ? pc.dim("(detected)") : ""}`, value: "npm" },
87
+ { title: `yarn ${detectedPm === "yarn" ? pc.dim("(detected)") : ""}`, value: "yarn" },
88
+ { title: `bun ${detectedPm === "bun" ? pc.dim("(detected)") : ""}`, value: "bun" }
89
+ ],
90
+ initial: detectedPm === "pnpm" ? 0 : detectedPm === "npm" ? 1 : detectedPm === "yarn" ? 2 : 3
91
+ },
92
+ {
93
+ type: "confirm",
94
+ name: "initGit",
95
+ message: "Initialize a git repository?",
96
+ initial: true
97
+ }
98
+ ],
99
+ {
100
+ onCancel: () => {
101
+ console.log(pc.red("\nOperation cancelled"));
102
+ process.exit(0);
103
+ }
104
+ }
105
+ );
106
+ if (!isPackageManagerInstalled(response.packageManager)) {
107
+ console.log(pc.yellow(`
108
+ \u26A0 ${response.packageManager} is not installed. Falling back to npm.`));
109
+ response.packageManager = "npm";
110
+ }
111
+ const featureSet = new Set(response.features || []);
112
+ return {
113
+ projectName: response.projectName,
114
+ features: {
115
+ supabase: featureSet.has("supabase"),
116
+ i18n: featureSet.has("i18n"),
117
+ storybook: featureSet.has("storybook"),
118
+ monitoring: featureSet.has("monitoring")
119
+ },
120
+ packageManager: response.packageManager,
121
+ initGit: response.initGit
122
+ };
123
+ }
124
+
125
+ // src/template.ts
126
+ import { downloadTemplate } from "giget";
127
+ var TEMPLATE_REPO = "github:lovstudio/lovweb";
128
+ async function download(dest, _config) {
129
+ await downloadTemplate(TEMPLATE_REPO, {
130
+ dir: dest,
131
+ force: true
132
+ // Can specify ref: 'v1.0.0' to lock version
133
+ });
134
+ }
135
+
136
+ // src/postprocess.ts
137
+ import fs from "fs";
138
+ import path from "path";
139
+ var ALWAYS_REMOVE = [
140
+ "cli",
141
+ ".changeset",
142
+ ".github",
143
+ "CHANGELOG.md",
144
+ ".coderabbit.yaml",
145
+ "crowdin.yml",
146
+ "codecov.yml",
147
+ "checkly.config.ts",
148
+ "unlighthouse.config.ts",
149
+ ".claude",
150
+ "CLAUDE.md",
151
+ "AGENTS.md"
152
+ ];
153
+ var STORYBOOK_FILES = [
154
+ ".storybook"
155
+ ];
156
+ var STORYBOOK_PATTERNS = [".stories.tsx", ".stories.ts"];
157
+ var I18N_FILES = [
158
+ "src/locales",
159
+ "crowdin.yml"
160
+ ];
161
+ var MONITORING_FILES = [
162
+ "src/instrumentation.ts",
163
+ "src/instrumentation-client.ts",
164
+ "sentry.client.config.ts",
165
+ "sentry.server.config.ts",
166
+ "sentry.edge.config.ts"
167
+ ];
168
+ async function processTemplate(projectPath, config) {
169
+ for (const file of ALWAYS_REMOVE) {
170
+ await removeIfExists(path.join(projectPath, file));
171
+ }
172
+ if (!config.features.storybook) {
173
+ for (const file of STORYBOOK_FILES) {
174
+ await removeIfExists(path.join(projectPath, file));
175
+ }
176
+ await removeMatchingFiles(projectPath, STORYBOOK_PATTERNS);
177
+ }
178
+ if (!config.features.i18n) {
179
+ for (const file of I18N_FILES) {
180
+ await removeIfExists(path.join(projectPath, file));
181
+ }
182
+ }
183
+ if (!config.features.monitoring) {
184
+ for (const file of MONITORING_FILES) {
185
+ await removeIfExists(path.join(projectPath, file));
186
+ }
187
+ }
188
+ await updatePackageJson(projectPath, config);
189
+ await ensureEnvExample(projectPath, config);
190
+ }
191
+ async function removeIfExists(filePath) {
192
+ try {
193
+ const stat = await fs.promises.stat(filePath);
194
+ if (stat.isDirectory()) {
195
+ await fs.promises.rm(filePath, { recursive: true, force: true });
196
+ } else {
197
+ await fs.promises.unlink(filePath);
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ async function removeMatchingFiles(dir, patterns) {
203
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
204
+ for (const entry of entries) {
205
+ const fullPath = path.join(dir, entry.name);
206
+ if (entry.isDirectory()) {
207
+ if (entry.name === "node_modules" || entry.name === ".git")
208
+ continue;
209
+ await removeMatchingFiles(fullPath, patterns);
210
+ } else if (patterns.some((pattern) => entry.name.endsWith(pattern))) {
211
+ await fs.promises.unlink(fullPath);
212
+ }
213
+ }
214
+ }
215
+ async function updatePackageJson(projectPath, config) {
216
+ const pkgPath = path.join(projectPath, "package.json");
217
+ const pkg = JSON.parse(await fs.promises.readFile(pkgPath, "utf-8"));
218
+ pkg.name = config.projectName;
219
+ pkg.version = "0.1.0";
220
+ pkg.private = true;
221
+ delete pkg.repository;
222
+ delete pkg.bugs;
223
+ delete pkg.homepage;
224
+ if (!config.features.storybook) {
225
+ delete pkg.scripts?.storybook;
226
+ delete pkg.scripts?.["build-storybook"];
227
+ delete pkg.devDependencies?.["@chromatic-com/storybook"];
228
+ delete pkg.devDependencies?.["@storybook/addon-essentials"];
229
+ delete pkg.devDependencies?.["@storybook/addon-interactions"];
230
+ delete pkg.devDependencies?.["@storybook/addon-onboarding"];
231
+ delete pkg.devDependencies?.["@storybook/blocks"];
232
+ delete pkg.devDependencies?.["@storybook/nextjs"];
233
+ delete pkg.devDependencies?.["@storybook/react"];
234
+ delete pkg.devDependencies?.["@storybook/test"];
235
+ delete pkg.devDependencies?.storybook;
236
+ }
237
+ if (!config.features.monitoring) {
238
+ delete pkg.dependencies?.["@sentry/nextjs"];
239
+ delete pkg.dependencies?.["posthog-js"];
240
+ delete pkg.dependencies?.["@logtape/logtape"];
241
+ delete pkg.dependencies?.["@logtape/otel"];
242
+ delete pkg.dependencies?.["@better-stack/browser-sdk"];
243
+ }
244
+ if (config.packageManager !== "pnpm") {
245
+ delete pkg.packageManager;
246
+ }
247
+ await fs.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
248
+ }
249
+ async function ensureEnvExample(projectPath, config) {
250
+ const envExamplePath = path.join(projectPath, ".env.example");
251
+ let envContent = `# Database
252
+ DATABASE_URL=
253
+
254
+ # Supabase
255
+ NEXT_PUBLIC_SUPABASE_URL=
256
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
257
+ `;
258
+ if (config.features.monitoring) {
259
+ envContent += `
260
+ # Monitoring (optional)
261
+ SENTRY_DSN=
262
+ NEXT_PUBLIC_POSTHOG_KEY=
263
+ NEXT_PUBLIC_POSTHOG_HOST=
264
+ BETTER_STACK_SOURCE_TOKEN=
265
+ `;
266
+ }
267
+ envContent += `
268
+ # Security (optional)
269
+ ARCJET_KEY=
270
+ `;
271
+ await fs.promises.writeFile(envExamplePath, envContent);
272
+ }
273
+
274
+ // src/utils/logger.ts
275
+ import pc2 from "picocolors";
276
+ var logger = {
277
+ info: (msg) => console.log(pc2.cyan(msg)),
278
+ success: (msg) => console.log(pc2.green(`\u2714 ${msg}`)),
279
+ warn: (msg) => console.log(pc2.yellow(`\u26A0 ${msg}`)),
280
+ error: (msg) => console.log(pc2.red(`\u2716 ${msg}`)),
281
+ log: (msg) => console.log(msg),
282
+ blank: () => console.log()
283
+ };
284
+ function banner() {
285
+ console.log();
286
+ console.log(pc2.cyan("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
287
+ console.log(pc2.cyan("\u2502") + pc2.bold(" \u{1F3E0} Create Lovstudio App ") + pc2.cyan("\u2502"));
288
+ console.log(pc2.cyan("\u2502") + pc2.dim(" Next.js 15 + TypeScript ") + pc2.cyan("\u2502"));
289
+ console.log(pc2.cyan("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
290
+ console.log();
291
+ }
292
+ function printNextSteps(projectName, packageManager) {
293
+ const pmRun = packageManager === "npm" ? "npm run" : packageManager;
294
+ console.log();
295
+ console.log(pc2.green("\u{1F389} Success!") + ` Created ${pc2.bold(projectName)}`);
296
+ console.log();
297
+ console.log(pc2.dim("Next steps:"));
298
+ console.log();
299
+ console.log(` ${pc2.cyan("cd")} ${projectName}`);
300
+ console.log(` ${pc2.cyan("cp")} .env.example .env.local`);
301
+ console.log(` ${pc2.cyan(pmRun)} dev`);
302
+ console.log();
303
+ console.log(pc2.dim("\u{1F4DA} Documentation: https://github.com/lovstudio/lovweb"));
304
+ console.log();
305
+ }
306
+
307
+ // src/create.ts
308
+ async function create(projectName, options) {
309
+ banner();
310
+ const config = await promptProjectConfig(projectName);
311
+ if (!config)
312
+ return;
313
+ if (options?.useNpm)
314
+ config.packageManager = "npm";
315
+ if (options?.usePnpm)
316
+ config.packageManager = "pnpm";
317
+ if (options?.useYarn)
318
+ config.packageManager = "yarn";
319
+ if (options?.useBun)
320
+ config.packageManager = "bun";
321
+ const projectPath = path2.resolve(process.cwd(), config.projectName);
322
+ if (fs2.existsSync(projectPath)) {
323
+ const files = fs2.readdirSync(projectPath);
324
+ if (files.length > 0) {
325
+ logger.error(`Directory ${pc3.bold(config.projectName)} is not empty.`);
326
+ process.exit(1);
327
+ }
328
+ }
329
+ console.log();
330
+ const downloadSpinner = ora("Downloading template...").start();
331
+ try {
332
+ await download(projectPath, config);
333
+ downloadSpinner.succeed("Downloaded template");
334
+ } catch (error) {
335
+ downloadSpinner.fail("Failed to download template");
336
+ logger.error(error instanceof Error ? error.message : String(error));
337
+ process.exit(1);
338
+ }
339
+ const processSpinner = ora("Processing template...").start();
340
+ try {
341
+ await processTemplate(projectPath, config);
342
+ processSpinner.succeed("Processed template");
343
+ } catch (error) {
344
+ processSpinner.fail("Failed to process template");
345
+ logger.error(error instanceof Error ? error.message : String(error));
346
+ process.exit(1);
347
+ }
348
+ if (!options?.skipInstall) {
349
+ const installSpinner = ora(`Installing dependencies with ${config.packageManager}...`).start();
350
+ try {
351
+ const installCmd = getInstallCommand(config.packageManager);
352
+ execSync2(installCmd, {
353
+ cwd: projectPath,
354
+ stdio: "pipe"
355
+ });
356
+ installSpinner.succeed("Installed dependencies");
357
+ } catch (error) {
358
+ installSpinner.fail("Failed to install dependencies");
359
+ logger.warn("You can try installing manually later.");
360
+ }
361
+ }
362
+ if (config.initGit) {
363
+ const gitSpinner = ora("Initializing git repository...").start();
364
+ try {
365
+ execSync2("git init", { cwd: projectPath, stdio: "pipe" });
366
+ execSync2("git add -A", { cwd: projectPath, stdio: "pipe" });
367
+ execSync2('git commit -m "Initial commit from create-lovstudio-app"', {
368
+ cwd: projectPath,
369
+ stdio: "pipe"
370
+ });
371
+ gitSpinner.succeed("Initialized git repository");
372
+ } catch {
373
+ gitSpinner.fail("Failed to initialize git repository");
374
+ }
375
+ }
376
+ printNextSteps(config.projectName, config.packageManager);
377
+ }
378
+
379
+ // src/index.ts
380
+ var cli = cac("create-lovstudio-app");
381
+ cli.command("[project-name]", "Create a new Lovstudio Next.js project").option("--skip-install", "Skip dependency installation").option("--use-npm", "Use npm as package manager").option("--use-pnpm", "Use pnpm as package manager").option("--use-yarn", "Use yarn as package manager").option("--use-bun", "Use bun as package manager").action(create);
382
+ cli.help();
383
+ cli.version("1.0.0");
384
+ cli.parse();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "create-lovstudio-app",
3
+ "version": "1.0.0",
4
+ "description": "Create a new Lovstudio Next.js project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-lovstudio-app": "./dist/index.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch",
15
+ "release": "pnpm build && npm publish",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "keywords": [
19
+ "create",
20
+ "lovstudio",
21
+ "nextjs",
22
+ "template",
23
+ "boilerplate",
24
+ "supabase",
25
+ "typescript"
26
+ ],
27
+ "author": "Lovstudio",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/lovstudio/lovweb.git",
32
+ "directory": "cli"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/lovstudio/lovweb/issues"
36
+ },
37
+ "homepage": "https://github.com/lovstudio/lovweb#readme",
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "dependencies": {
42
+ "cac": "^6.7.14",
43
+ "giget": "^1.2.3",
44
+ "ora": "^8.1.1",
45
+ "picocolors": "^1.1.1",
46
+ "prompts": "^2.4.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.2",
50
+ "@types/prompts": "^2.4.9",
51
+ "tsup": "^8.3.5",
52
+ "typescript": "^5.7.2"
53
+ }
54
+ }