create-next-shadcn-kit 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikunj Sonigara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # create-next-shadcn-kit
2
+
3
+ > Create a Next.js app with **shadcn/ui** pre-integrated — zero config.
4
+
5
+ One command, a fresh Next.js project, Tailwind configured, shadcn/ui initialized, and your favorite components already installed. No juggling two CLIs.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npx create-next-shadcn-kit@latest
11
+ ```
12
+
13
+ Or pass a name directly:
14
+
15
+ ```bash
16
+ npx create-next-shadcn-kit my-app
17
+ ```
18
+
19
+ ## What it does
20
+
21
+ 1. Scaffolds a fresh Next.js app via `create-next-app@latest` (App Router, Tailwind, Turbopack).
22
+ 2. Runs `shadcn@latest init` inside the new project.
23
+ 3. Pre-installs the shadcn components you selected.
24
+ 4. Wires up state management — **Redux Toolkit** (default) or **Zustand**, with a sample store and (for Redux) a `<Providers>` wrapper already mounted in `app/layout`.
25
+ 5. (Optional) Sets up **Husky + lint-staged + Prettier** with a pre-commit hook that runs `eslint --fix` and `prettier --write` on staged files.
26
+
27
+ You skip the "install Next → read shadcn docs → run a second init → add components one by one" ritual.
28
+
29
+ ## Options
30
+
31
+ | Flag | Description |
32
+ | --------------------------------------- | ----------------------------------- |
33
+ | `-y, --yes` | Skip prompts, use sensible defaults |
34
+ | `--ts` / `--js` | TypeScript (default) or JavaScript |
35
+ | `--npm` / `--pnpm` / `--yarn` / `--bun` | Pick your package manager |
36
+ | `--no-husky` | Skip Husky + lint-staged setup |
37
+ | `--state=<lib>` | `redux` (default), `zustand`, `none`|
38
+ | `-v, --version` | Print version |
39
+ | `-h, --help` | Show help |
40
+
41
+ ## Examples
42
+
43
+ ```bash
44
+ # Fully interactive
45
+ npx create-next-shadcn-kit
46
+
47
+ # Non-interactive with defaults
48
+ npx create-next-shadcn-kit my-app --yes
49
+
50
+ # Use pnpm
51
+ npx create-next-shadcn-kit my-app --pnpm
52
+ ```
53
+
54
+ ## Requirements
55
+
56
+ - Node.js **18.17+**
57
+ - Network access (to fetch `create-next-app` and `shadcn`)
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ git clone <this-repo>
63
+ cd create-next-shadcn-kit
64
+ npm install
65
+ node bin/index.js test-app --yes
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/index.js';
3
+
4
+ main().catch((err) => {
5
+ console.error();
6
+ console.error(err?.message || err);
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "create-next-shadcn-kit",
3
+ "version": "0.1.0",
4
+ "description": "Create a Next.js app with shadcn/ui pre-integrated — zero config.",
5
+ "keywords": [
6
+ "nextjs",
7
+ "next",
8
+ "shadcn",
9
+ "shadcn-ui",
10
+ "cli",
11
+ "scaffold",
12
+ "starter",
13
+ "create-next-app",
14
+ "tailwind"
15
+ ],
16
+ "author": "Nikunj Sonigara <nikunjsonigara987@gmail.com>",
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "bin": {
20
+ "create-next-shadcn-kit": "bin/index.js"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18.17.0"
30
+ },
31
+ "scripts": {
32
+ "start": "node bin/index.js",
33
+ "test:local": "node bin/index.js test-app --yes"
34
+ },
35
+ "dependencies": {
36
+ "picocolors": "^1.1.1",
37
+ "prompts": "^2.4.2"
38
+ }
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { createRequire } from "node:module";
2
+ import pc from "picocolors";
3
+ import { parseArgs } from "./utils.js";
4
+ import { promptConfig } from "./prompts.js";
5
+ import { scaffold } from "./scaffold.js";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const pkg = require("../package.json");
9
+
10
+ export async function main() {
11
+ const args = parseArgs(process.argv.slice(2));
12
+
13
+ if (args.help) {
14
+ printHelp();
15
+ return;
16
+ }
17
+
18
+ if (args.version) {
19
+ console.log(pkg.version);
20
+ return;
21
+ }
22
+
23
+ assertNodeVersion();
24
+
25
+ console.log();
26
+ console.log(pc.bold(pc.cyan("◆ create-next-shadcn-kit")) + pc.dim(` v${pkg.version} — Next.js + shadcn/ui starter`));
27
+
28
+ const config = await promptConfig(args);
29
+ await scaffold(config);
30
+
31
+ console.log();
32
+ console.log(pc.green("✔ Success!") + " Your project is ready.");
33
+ console.log();
34
+ console.log("Next steps:");
35
+ console.log(pc.cyan(` cd ${config.projectName}`));
36
+ console.log(pc.cyan(` ${devCommand(config.packageManager)}`));
37
+ console.log();
38
+ console.log(pc.dim("Docs: https://ui.shadcn.com"));
39
+ console.log();
40
+ }
41
+
42
+ function devCommand(pm) {
43
+ if (pm === "npm") return "npm run dev";
44
+ if (pm === "yarn") return "yarn dev";
45
+ return `${pm} dev`;
46
+ }
47
+
48
+ function assertNodeVersion() {
49
+ const major = Number(process.versions.node.split(".")[0]);
50
+ if (major < 18) {
51
+ throw new Error(`Node.js 18.17+ is required. You are running ${process.versions.node}.`);
52
+ }
53
+ }
54
+
55
+ function printHelp() {
56
+ console.log(`
57
+ ${pc.bold("create-next-shadcn-kit")} — Create a Next.js app with shadcn/ui pre-integrated.
58
+
59
+ ${pc.bold("Usage:")}
60
+ npx create-next-shadcn-kit [project-name] [options]
61
+
62
+ ${pc.bold("Options:")}
63
+ -y, --yes Skip prompts and use defaults
64
+ --ts, --typescript Use TypeScript (default)
65
+ --js, --javascript Use JavaScript
66
+ --npm Use npm (default)
67
+ --pnpm Use pnpm
68
+ --yarn Use yarn
69
+ --bun Use bun
70
+ --no-husky Skip Husky + lint-staged setup
71
+ --state=<lib> State management: redux (default), zustand, none
72
+ -v, --version Show version
73
+ -h, --help Show this help
74
+
75
+ ${pc.bold("Examples:")}
76
+ npx create-next-shadcn-kit
77
+ npx create-next-shadcn-kit my-app
78
+ npx create-next-shadcn-kit my-app --yes --pnpm
79
+ `);
80
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,156 @@
1
+ import prompts from "prompts";
2
+ import pc from "picocolors";
3
+ import { isValidProjectName } from "./utils.js";
4
+
5
+ const COMPONENT_CHOICES = [
6
+ { title: "button", value: "button", selected: true },
7
+ { title: "card", value: "card", selected: true },
8
+ { title: "input", value: "input", selected: true },
9
+ { title: "label", value: "label", selected: true },
10
+ { title: "form", value: "form", selected: false },
11
+ { title: "dialog", value: "dialog", selected: false },
12
+ { title: "dropdown-menu", value: "dropdown-menu", selected: false },
13
+ { title: "select", value: "select", selected: false },
14
+ { title: "sonner (toast)", value: "sonner", selected: false },
15
+ { title: "tabs", value: "tabs", selected: false },
16
+ { title: "textarea", value: "textarea", selected: false },
17
+ { title: "tooltip", value: "tooltip", selected: false },
18
+ { title: "avatar", value: "avatar", selected: false },
19
+ { title: "badge", value: "badge", selected: false },
20
+ { title: "separator", value: "separator", selected: false },
21
+ { title: "skeleton", value: "skeleton", selected: false },
22
+ { title: "table", value: "table", selected: false },
23
+ ];
24
+
25
+ const onCancel = () => {
26
+ console.log();
27
+ console.log(pc.red("✖") + " Cancelled");
28
+ process.exit(1);
29
+ };
30
+
31
+ export async function promptConfig(args) {
32
+ if (args.yes) {
33
+ return {
34
+ projectName: args.projectName || "my-app",
35
+ typescript: args.typescript ?? true,
36
+ eslint: true,
37
+ srcDir: true,
38
+ importAlias: "@/*",
39
+ packageManager: args.packageManager || "npm",
40
+ husky: args.husky ?? true,
41
+ state: args.state || "redux",
42
+ components: ["button", "card", "input", "label"],
43
+ };
44
+ }
45
+
46
+ const questions = [];
47
+
48
+ if (!args.projectName) {
49
+ questions.push({
50
+ type: "text",
51
+ name: "projectName",
52
+ message: "Project name:",
53
+ initial: "my-app",
54
+ validate: (v) => (isValidProjectName(v) ? true : "Invalid project name"),
55
+ });
56
+ }
57
+
58
+ if (args.typescript === undefined) {
59
+ questions.push({
60
+ type: "toggle",
61
+ name: "typescript",
62
+ message: "Use TypeScript?",
63
+ initial: true,
64
+ active: "yes",
65
+ inactive: "no",
66
+ });
67
+ }
68
+
69
+ questions.push(
70
+ {
71
+ type: "toggle",
72
+ name: "eslint",
73
+ message: "Use ESLint?",
74
+ initial: true,
75
+ active: "yes",
76
+ inactive: "no",
77
+ },
78
+ {
79
+ type: "toggle",
80
+ name: "srcDir",
81
+ message: "Use a `src/` directory?",
82
+ initial: true,
83
+ active: "yes",
84
+ inactive: "no",
85
+ },
86
+ {
87
+ type: "text",
88
+ name: "importAlias",
89
+ message: "Import alias:",
90
+ initial: "@/*",
91
+ },
92
+ );
93
+
94
+ if (!args.packageManager) {
95
+ questions.push({
96
+ type: "select",
97
+ name: "packageManager",
98
+ message: "Package manager:",
99
+ choices: [
100
+ { title: "npm", value: "npm" },
101
+ { title: "pnpm", value: "pnpm" },
102
+ { title: "yarn", value: "yarn" },
103
+ { title: "bun", value: "bun" },
104
+ ],
105
+ initial: 0,
106
+ });
107
+ }
108
+
109
+ if (args.husky === undefined) {
110
+ questions.push({
111
+ type: "toggle",
112
+ name: "husky",
113
+ message: "Set up Husky + lint-staged (pre-commit hooks)?",
114
+ initial: true,
115
+ active: "yes",
116
+ inactive: "no",
117
+ });
118
+ }
119
+
120
+ if (!args.state) {
121
+ questions.push({
122
+ type: "select",
123
+ name: "state",
124
+ message: "State management:",
125
+ choices: [
126
+ { title: "Redux Toolkit", value: "redux" },
127
+ { title: "Zustand", value: "zustand" },
128
+ { title: "None", value: "none" },
129
+ ],
130
+ initial: 0,
131
+ });
132
+ }
133
+
134
+ questions.push({
135
+ type: "multiselect",
136
+ name: "components",
137
+ message: "Pre-install components:",
138
+ choices: COMPONENT_CHOICES,
139
+ hint: "(space to toggle, enter to confirm)",
140
+ instructions: false,
141
+ });
142
+
143
+ const answers = await prompts(questions, { onCancel });
144
+
145
+ return {
146
+ projectName: args.projectName || answers.projectName,
147
+ typescript: args.typescript ?? answers.typescript,
148
+ eslint: answers.eslint,
149
+ srcDir: answers.srcDir,
150
+ importAlias: answers.importAlias || "@/*",
151
+ packageManager: args.packageManager || answers.packageManager,
152
+ husky: args.husky ?? answers.husky ?? true,
153
+ state: args.state || answers.state || "redux",
154
+ components: answers.components || [],
155
+ };
156
+ }
@@ -0,0 +1,382 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import pc from "picocolors";
4
+ import { run } from "./utils.js";
5
+
6
+ export async function scaffold(config) {
7
+ const cwd = process.cwd();
8
+ const projectPath = path.resolve(cwd, config.projectName);
9
+
10
+ if (fs.existsSync(projectPath) && fs.readdirSync(projectPath).length > 0) {
11
+ throw new Error(`Directory "${config.projectName}" already exists and is not empty.`);
12
+ }
13
+
14
+ console.log();
15
+ console.log(pc.cyan("◆") + " Creating Next.js app...");
16
+ console.log();
17
+
18
+ const cnaArgs = [
19
+ "create-next-app@latest",
20
+ config.projectName,
21
+ config.typescript ? "--typescript" : "--javascript",
22
+ config.eslint ? "--eslint" : "--no-eslint",
23
+ "--tailwind",
24
+ "--app",
25
+ config.srcDir ? "--src-dir" : "--no-src-dir",
26
+ "--turbopack",
27
+ `--import-alias=${config.importAlias}`,
28
+ `--use-${config.packageManager}`,
29
+ "--yes",
30
+ ];
31
+
32
+ await run("npx", cnaArgs);
33
+
34
+ console.log();
35
+ console.log(pc.cyan("◆") + " Initializing shadcn/ui...");
36
+ console.log();
37
+
38
+ const shadcnRunner = runnerFor(config.packageManager);
39
+
40
+ await run(shadcnRunner.cmd, [...shadcnRunner.args, "shadcn@latest", "init", "--yes", "--defaults"], { cwd: projectPath });
41
+
42
+ if (config.components.length > 0) {
43
+ console.log();
44
+ console.log(pc.cyan("◆") + ` Adding components: ${pc.dim(config.components.join(", "))}`);
45
+ console.log();
46
+
47
+ await run(shadcnRunner.cmd, [...shadcnRunner.args, "shadcn@latest", "add", ...config.components, "--yes"], { cwd: projectPath });
48
+ }
49
+
50
+ if (config.state === "redux") {
51
+ await setupRedux(projectPath, config);
52
+ } else if (config.state === "zustand") {
53
+ await setupZustand(projectPath, config);
54
+ }
55
+
56
+ if (config.husky) {
57
+ await setupHusky(projectPath, config);
58
+ }
59
+ }
60
+
61
+ function appDirFor(projectPath, config) {
62
+ return config.srcDir ? path.join(projectPath, "src", "app") : path.join(projectPath, "app");
63
+ }
64
+
65
+ function storeDirFor(projectPath, config) {
66
+ return config.srcDir ? path.join(projectPath, "src", "store") : path.join(projectPath, "store");
67
+ }
68
+
69
+ async function setupRedux(projectPath, config) {
70
+ console.log();
71
+ console.log(pc.cyan("◆") + " Setting up Redux Toolkit...");
72
+ console.log();
73
+
74
+ const installer = installerFor(config.packageManager, false);
75
+ await run(installer.cmd, [...installer.args, "@reduxjs/toolkit", "react-redux"], { cwd: projectPath });
76
+
77
+ const storeDir = storeDirFor(projectPath, config);
78
+ fs.mkdirSync(storeDir, { recursive: true });
79
+
80
+ const ts = config.typescript;
81
+ const storeExt = ts ? "ts" : "js";
82
+ const compExt = ts ? "tsx" : "js";
83
+
84
+ const storeIndex = ts
85
+ ? `import { configureStore } from "@reduxjs/toolkit";
86
+ import counterReducer from "./counterSlice";
87
+
88
+ export const store = configureStore({
89
+ reducer: {
90
+ counter: counterReducer,
91
+ },
92
+ });
93
+
94
+ export type RootState = ReturnType<typeof store.getState>;
95
+ export type AppDispatch = typeof store.dispatch;
96
+ `
97
+ : `import { configureStore } from "@reduxjs/toolkit";
98
+ import counterReducer from "./counterSlice";
99
+
100
+ export const store = configureStore({
101
+ reducer: {
102
+ counter: counterReducer,
103
+ },
104
+ });
105
+ `;
106
+ fs.writeFileSync(path.join(storeDir, `index.${storeExt}`), storeIndex);
107
+
108
+ const counterSlice = ts
109
+ ? `import { createSlice, PayloadAction } from "@reduxjs/toolkit";
110
+
111
+ interface CounterState {
112
+ value: number;
113
+ }
114
+
115
+ const initialState: CounterState = { value: 0 };
116
+
117
+ const counterSlice = createSlice({
118
+ name: "counter",
119
+ initialState,
120
+ reducers: {
121
+ increment: (state) => {
122
+ state.value += 1;
123
+ },
124
+ decrement: (state) => {
125
+ state.value -= 1;
126
+ },
127
+ incrementByAmount: (state, action: PayloadAction<number>) => {
128
+ state.value += action.payload;
129
+ },
130
+ },
131
+ });
132
+
133
+ export const { increment, decrement, incrementByAmount } = counterSlice.actions;
134
+ export default counterSlice.reducer;
135
+ `
136
+ : `import { createSlice } from "@reduxjs/toolkit";
137
+
138
+ const initialState = { value: 0 };
139
+
140
+ const counterSlice = createSlice({
141
+ name: "counter",
142
+ initialState,
143
+ reducers: {
144
+ increment: (state) => {
145
+ state.value += 1;
146
+ },
147
+ decrement: (state) => {
148
+ state.value -= 1;
149
+ },
150
+ incrementByAmount: (state, action) => {
151
+ state.value += action.payload;
152
+ },
153
+ },
154
+ });
155
+
156
+ export const { increment, decrement, incrementByAmount } = counterSlice.actions;
157
+ export default counterSlice.reducer;
158
+ `;
159
+ fs.writeFileSync(path.join(storeDir, `counterSlice.${storeExt}`), counterSlice);
160
+
161
+ if (ts) {
162
+ const hooks = `import { useDispatch, useSelector } from "react-redux";
163
+ import type { TypedUseSelectorHook } from "react-redux";
164
+ import type { RootState, AppDispatch } from "./index";
165
+
166
+ export const useAppDispatch: () => AppDispatch = useDispatch;
167
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
168
+ `;
169
+ fs.writeFileSync(path.join(storeDir, "hooks.ts"), hooks);
170
+ }
171
+
172
+ const appDir = appDirFor(projectPath, config);
173
+ const providers = ts
174
+ ? `"use client";
175
+
176
+ import { Provider } from "react-redux";
177
+ import { store } from "@/store";
178
+
179
+ export function Providers({ children }: { children: React.ReactNode }) {
180
+ return <Provider store={store}>{children}</Provider>;
181
+ }
182
+ `
183
+ : `"use client";
184
+
185
+ import { Provider } from "react-redux";
186
+ import { store } from "@/store";
187
+
188
+ export function Providers({ children }) {
189
+ return <Provider store={store}>{children}</Provider>;
190
+ }
191
+ `;
192
+ fs.writeFileSync(path.join(appDir, `providers.${compExt}`), providers);
193
+
194
+ patchLayoutWithProviders(appDir, ts);
195
+ }
196
+
197
+ async function setupZustand(projectPath, config) {
198
+ console.log();
199
+ console.log(pc.cyan("◆") + " Setting up Zustand...");
200
+ console.log();
201
+
202
+ const installer = installerFor(config.packageManager, false);
203
+ await run(installer.cmd, [...installer.args, "zustand"], { cwd: projectPath });
204
+
205
+ const storeDir = storeDirFor(projectPath, config);
206
+ fs.mkdirSync(storeDir, { recursive: true });
207
+
208
+ const ts = config.typescript;
209
+ const ext = ts ? "ts" : "js";
210
+ const contents = ts
211
+ ? `import { create } from "zustand";
212
+
213
+ interface CounterState {
214
+ count: number;
215
+ increment: () => void;
216
+ decrement: () => void;
217
+ reset: () => void;
218
+ }
219
+
220
+ export const useCounterStore = create<CounterState>((set) => ({
221
+ count: 0,
222
+ increment: () => set((s) => ({ count: s.count + 1 })),
223
+ decrement: () => set((s) => ({ count: s.count - 1 })),
224
+ reset: () => set({ count: 0 }),
225
+ }));
226
+ `
227
+ : `import { create } from "zustand";
228
+
229
+ export const useCounterStore = create((set) => ({
230
+ count: 0,
231
+ increment: () => set((s) => ({ count: s.count + 1 })),
232
+ decrement: () => set((s) => ({ count: s.count - 1 })),
233
+ reset: () => set({ count: 0 }),
234
+ }));
235
+ `;
236
+ fs.writeFileSync(path.join(storeDir, `useCounterStore.${ext}`), contents);
237
+ }
238
+
239
+ function patchLayoutWithProviders(appDir, ts) {
240
+ const layoutPath = path.join(appDir, ts ? "layout.tsx" : "layout.js");
241
+ if (!fs.existsSync(layoutPath)) {
242
+ console.log(pc.yellow("⚠") + ` Could not find ${path.basename(layoutPath)} — wrap {children} with <Providers> manually.`);
243
+ return;
244
+ }
245
+
246
+ let layout = fs.readFileSync(layoutPath, "utf8");
247
+ const original = layout;
248
+
249
+ if (!layout.includes(`from "./providers"`)) {
250
+ if (/import\s+["']\.\/globals\.css["'];?/.test(layout)) {
251
+ layout = layout.replace(
252
+ /(import\s+["']\.\/globals\.css["'];?)/,
253
+ `$1\nimport { Providers } from "./providers";`
254
+ );
255
+ } else {
256
+ layout = `import { Providers } from "./providers";\n${layout}`;
257
+ }
258
+ }
259
+
260
+ if (!layout.includes("<Providers>") && layout.includes("{children}")) {
261
+ layout = layout.replace("{children}", "<Providers>{children}</Providers>");
262
+ }
263
+
264
+ if (layout === original) {
265
+ console.log(pc.yellow("⚠") + " Could not auto-wrap layout — please wrap {children} with <Providers> manually.");
266
+ return;
267
+ }
268
+
269
+ fs.writeFileSync(layoutPath, layout);
270
+ }
271
+
272
+ async function setupHusky(projectPath, config) {
273
+ console.log();
274
+ console.log(pc.cyan("◆") + " Setting up Husky + lint-staged + Prettier...");
275
+ console.log();
276
+
277
+ const installer = installerFor(config.packageManager, true);
278
+ await run(
279
+ installer.cmd,
280
+ [...installer.args, "husky@^8", "lint-staged", "prettier"],
281
+ { cwd: projectPath }
282
+ );
283
+
284
+ const pkgPath = path.join(projectPath, "package.json");
285
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
286
+ pkg.scripts = {
287
+ ...pkg.scripts,
288
+ prepare: "husky install",
289
+ format: "prettier --check .",
290
+ "format:fix": "prettier --write .",
291
+ "lint:fix": "eslint --fix",
292
+ ...(config.typescript ? { typecheck: "tsc --noEmit" } : {}),
293
+ };
294
+ delete pkg["lint-staged"];
295
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
296
+
297
+ const runner = runnerFor(config.packageManager);
298
+ await run(runner.cmd, [...runner.args, "husky", "install"], {
299
+ cwd: projectPath,
300
+ });
301
+
302
+ await run("git", ["config", "core.hooksPath", ".husky"], { cwd: projectPath });
303
+
304
+ const huskyDir = path.join(projectPath, ".husky");
305
+ if (!fs.existsSync(huskyDir)) fs.mkdirSync(huskyDir, { recursive: true });
306
+
307
+ const hookCmd = config.typescript
308
+ ? "npx tsc --noEmit && npx lint-staged"
309
+ : "npx lint-staged";
310
+ const preCommit = `#!/usr/bin/env sh
311
+ . "$(dirname -- "$0")/_/husky.sh"
312
+
313
+ ${hookCmd}
314
+ `;
315
+ const preCommitPath = path.join(huskyDir, "pre-commit");
316
+ fs.writeFileSync(preCommitPath, preCommit);
317
+ fs.chmodSync(preCommitPath, 0o755);
318
+
319
+ const jsGlob = config.typescript ? "*.{js,jsx,ts,tsx}" : "*.{js,jsx}";
320
+ const lintStagedConfig = {
321
+ [jsGlob]: ["eslint --fix", "prettier --write"],
322
+ "*.{json,css,scss,md,mdx,yml,yaml,html}": ["prettier --write"],
323
+ };
324
+ fs.writeFileSync(
325
+ path.join(projectPath, ".lintstagedrc.json"),
326
+ JSON.stringify(lintStagedConfig, null, 2) + "\n"
327
+ );
328
+
329
+ const prettierrc = {
330
+ semi: true,
331
+ singleQuote: false,
332
+ tabWidth: 2,
333
+ trailingComma: "es5",
334
+ printWidth: 100,
335
+ arrowParens: "always",
336
+ endOfLine: "lf",
337
+ };
338
+ fs.writeFileSync(path.join(projectPath, ".prettierrc"), JSON.stringify(prettierrc, null, 2) + "\n");
339
+
340
+ const prettierIgnore = [
341
+ "node_modules",
342
+ ".next",
343
+ "out",
344
+ "build",
345
+ "dist",
346
+ "coverage",
347
+ "package-lock.json",
348
+ "pnpm-lock.yaml",
349
+ "yarn.lock",
350
+ "bun.lockb",
351
+ "",
352
+ ].join("\n");
353
+ fs.writeFileSync(path.join(projectPath, ".prettierignore"), prettierIgnore);
354
+ }
355
+
356
+ function installerFor(pm, dev = true) {
357
+ switch (pm) {
358
+ case "pnpm":
359
+ return { cmd: "pnpm", args: dev ? ["add", "-D"] : ["add"] };
360
+ case "yarn":
361
+ return { cmd: "yarn", args: dev ? ["add", "-D"] : ["add"] };
362
+ case "bun":
363
+ return { cmd: "bun", args: dev ? ["add", "-d"] : ["add"] };
364
+ case "npm":
365
+ default:
366
+ return { cmd: "npm", args: dev ? ["install", "-D"] : ["install"] };
367
+ }
368
+ }
369
+
370
+ function runnerFor(pm) {
371
+ switch (pm) {
372
+ case "pnpm":
373
+ return { cmd: "pnpm", args: ["dlx"] };
374
+ case "yarn":
375
+ return { cmd: "yarn", args: ["dlx"] };
376
+ case "bun":
377
+ return { cmd: "bunx", args: [] };
378
+ case "npm":
379
+ default:
380
+ return { cmd: "npx", args: [] };
381
+ }
382
+ }
package/src/utils.js ADDED
@@ -0,0 +1,70 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function parseArgs(argv) {
4
+ const positional = [];
5
+ const flags = {};
6
+
7
+ for (let i = 0; i < argv.length; i++) {
8
+ const arg = argv[i];
9
+ if (arg.startsWith("--")) {
10
+ const eq = arg.indexOf("=");
11
+ if (eq !== -1) {
12
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
13
+ } else {
14
+ const key = arg.slice(2);
15
+ const next = argv[i + 1];
16
+ if (next && !next.startsWith("-")) {
17
+ flags[key] = next;
18
+ i++;
19
+ } else {
20
+ flags[key] = true;
21
+ }
22
+ }
23
+ } else if (arg.startsWith("-") && arg.length > 1) {
24
+ flags[arg.slice(1)] = true;
25
+ } else {
26
+ positional.push(arg);
27
+ }
28
+ }
29
+
30
+ const pm = flags.pnpm ? "pnpm" : flags.yarn ? "yarn" : flags.bun ? "bun" : flags.npm ? "npm" : undefined;
31
+
32
+ let ts;
33
+ if (flags.ts || flags.typescript) ts = true;
34
+ else if (flags.js || flags.javascript) ts = false;
35
+
36
+ const state = ["redux", "zustand", "none"].includes(flags.state) ? flags.state : undefined;
37
+
38
+ return {
39
+ projectName: positional[0],
40
+ yes: Boolean(flags.yes || flags.y),
41
+ help: Boolean(flags.help || flags.h),
42
+ version: Boolean(flags.version || flags.v),
43
+ typescript: ts,
44
+ packageManager: pm,
45
+ husky: flags["no-husky"] ? false : undefined,
46
+ state,
47
+ };
48
+ }
49
+
50
+ export function run(cmd, args, options = {}) {
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn(cmd, args, {
53
+ stdio: "inherit",
54
+ shell: process.platform === "win32",
55
+ ...options,
56
+ });
57
+ child.on("error", reject);
58
+ child.on("close", (code) => {
59
+ if (code === 0) resolve();
60
+ else reject(new Error(`Command failed: ${cmd} ${args.join(" ")} (exit ${code})`));
61
+ });
62
+ });
63
+ }
64
+
65
+ export function isValidProjectName(name) {
66
+ if (!name || typeof name !== "string") return false;
67
+ if (name.length > 214) return false;
68
+ if (name === "." || name === "..") return false;
69
+ return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i.test(name);
70
+ }