create-stackit 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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/bin/index.js +64 -0
  4. package/package.json +43 -0
  5. package/src/choices.js +63 -0
  6. package/src/generator.js +133 -0
  7. package/src/injectors/addons.js +311 -0
  8. package/src/injectors/structure.js +51 -0
  9. package/src/pm.js +94 -0
  10. package/src/prompts.js +179 -0
  11. package/src/utils.js +18 -0
  12. package/templates/next-js-redux-mui/README.md +15 -0
  13. package/templates/next-js-redux-mui/app/layout.jsx +12 -0
  14. package/templates/next-js-redux-mui/app/page.jsx +15 -0
  15. package/templates/next-js-redux-mui/app/providers.jsx +16 -0
  16. package/templates/next-js-redux-mui/package.json +22 -0
  17. package/templates/next-js-redux-mui/src/store/counterSlice.js +13 -0
  18. package/templates/next-js-redux-mui/src/store/store.js +8 -0
  19. package/templates/next-js-redux-mui/src/theme.js +9 -0
  20. package/templates/next-js-redux-shadcn/README.md +15 -0
  21. package/templates/next-js-redux-shadcn/app/globals.css +3 -0
  22. package/templates/next-js-redux-shadcn/app/layout.jsx +13 -0
  23. package/templates/next-js-redux-shadcn/app/page.jsx +15 -0
  24. package/templates/next-js-redux-shadcn/app/providers.jsx +11 -0
  25. package/templates/next-js-redux-shadcn/package.json +23 -0
  26. package/templates/next-js-redux-shadcn/src/store/counterSlice.js +13 -0
  27. package/templates/next-js-redux-shadcn/src/store/store.js +8 -0
  28. package/templates/next-js-zustand-mui/README.md +15 -0
  29. package/templates/next-js-zustand-mui/app/layout.jsx +16 -0
  30. package/templates/next-js-zustand-mui/app/page.jsx +18 -0
  31. package/templates/next-js-zustand-mui/package.json +21 -0
  32. package/templates/next-js-zustand-mui/src/theme.js +9 -0
  33. package/templates/next-js-zustand-shadcn/README.md +15 -0
  34. package/templates/next-js-zustand-shadcn/app/globals.css +3 -0
  35. package/templates/next-js-zustand-shadcn/app/layout.jsx +10 -0
  36. package/templates/next-js-zustand-shadcn/app/page.jsx +18 -0
  37. package/templates/next-js-zustand-shadcn/package.json +22 -0
  38. package/templates/next-ts-redux-mui/README.md +15 -0
  39. package/templates/next-ts-redux-mui/app/layout.tsx +14 -0
  40. package/templates/next-ts-redux-mui/app/page.tsx +15 -0
  41. package/templates/next-ts-redux-mui/app/providers.tsx +16 -0
  42. package/templates/next-ts-redux-mui/package.json +27 -0
  43. package/templates/next-ts-redux-mui/src/store/counterSlice.ts +16 -0
  44. package/templates/next-ts-redux-mui/src/store/store.ts +11 -0
  45. package/templates/next-ts-redux-mui/src/theme.ts +9 -0
  46. package/templates/next-ts-redux-shadcn/README.md +15 -0
  47. package/templates/next-ts-redux-shadcn/app/globals.css +3 -0
  48. package/templates/next-ts-redux-shadcn/app/layout.tsx +15 -0
  49. package/templates/next-ts-redux-shadcn/app/page.tsx +15 -0
  50. package/templates/next-ts-redux-shadcn/app/providers.tsx +11 -0
  51. package/templates/next-ts-redux-shadcn/package.json +27 -0
  52. package/templates/next-ts-redux-shadcn/src/store/counterSlice.ts +16 -0
  53. package/templates/next-ts-redux-shadcn/src/store/store.ts +11 -0
  54. package/templates/next-ts-zustand-mui/README.md +15 -0
  55. package/templates/next-ts-zustand-mui/app/layout.tsx +18 -0
  56. package/templates/next-ts-zustand-mui/app/page.tsx +18 -0
  57. package/templates/next-ts-zustand-mui/package.json +26 -0
  58. package/templates/next-ts-zustand-mui/src/theme.ts +9 -0
  59. package/templates/next-ts-zustand-shadcn/README.md +15 -0
  60. package/templates/next-ts-zustand-shadcn/app/globals.css +3 -0
  61. package/templates/next-ts-zustand-shadcn/app/layout.tsx +12 -0
  62. package/templates/next-ts-zustand-shadcn/app/page.tsx +18 -0
  63. package/templates/next-ts-zustand-shadcn/package.json +26 -0
  64. package/templates/vite-js-redux-mui/README.md +15 -0
  65. package/templates/vite-js-redux-mui/package.json +24 -0
  66. package/templates/vite-js-redux-mui/src/App.jsx +14 -0
  67. package/templates/vite-js-redux-mui/src/main.jsx +18 -0
  68. package/templates/vite-js-redux-mui/src/store/counterSlice.js +13 -0
  69. package/templates/vite-js-redux-mui/src/store/store.js +8 -0
  70. package/templates/vite-js-redux-mui/src/theme.js +9 -0
  71. package/templates/vite-js-redux-shadcn/README.md +15 -0
  72. package/templates/vite-js-redux-shadcn/package.json +24 -0
  73. package/templates/vite-js-redux-shadcn/src/App.jsx +14 -0
  74. package/templates/vite-js-redux-shadcn/src/index.css +3 -0
  75. package/templates/vite-js-redux-shadcn/src/main.jsx +14 -0
  76. package/templates/vite-js-redux-shadcn/src/store/counterSlice.js +13 -0
  77. package/templates/vite-js-redux-shadcn/src/store/store.js +8 -0
  78. package/templates/vite-js-zustand-mui/README.md +15 -0
  79. package/templates/vite-js-zustand-mui/package.json +23 -0
  80. package/templates/vite-js-zustand-mui/src/App.jsx +17 -0
  81. package/templates/vite-js-zustand-mui/src/main.jsx +14 -0
  82. package/templates/vite-js-zustand-mui/src/theme.js +9 -0
  83. package/templates/vite-js-zustand-shadcn/README.md +15 -0
  84. package/templates/vite-js-zustand-shadcn/package.json +23 -0
  85. package/templates/vite-js-zustand-shadcn/src/App.jsx +17 -0
  86. package/templates/vite-js-zustand-shadcn/src/index.css +3 -0
  87. package/templates/vite-js-zustand-shadcn/src/main.jsx +10 -0
  88. package/templates/vite-ts-redux-mui/README.md +15 -0
  89. package/templates/vite-ts-redux-mui/package.json +27 -0
  90. package/templates/vite-ts-redux-mui/src/App.tsx +14 -0
  91. package/templates/vite-ts-redux-mui/src/main.tsx +18 -0
  92. package/templates/vite-ts-redux-mui/src/store/counterSlice.ts +16 -0
  93. package/templates/vite-ts-redux-mui/src/store/store.ts +11 -0
  94. package/templates/vite-ts-redux-mui/src/theme.ts +9 -0
  95. package/templates/vite-ts-redux-shadcn/README.md +15 -0
  96. package/templates/vite-ts-redux-shadcn/package.json +27 -0
  97. package/templates/vite-ts-redux-shadcn/src/App.tsx +14 -0
  98. package/templates/vite-ts-redux-shadcn/src/index.css +3 -0
  99. package/templates/vite-ts-redux-shadcn/src/main.tsx +14 -0
  100. package/templates/vite-ts-redux-shadcn/src/store/counterSlice.ts +16 -0
  101. package/templates/vite-ts-redux-shadcn/src/store/store.ts +11 -0
  102. package/templates/vite-ts-zustand-mui/README.md +15 -0
  103. package/templates/vite-ts-zustand-mui/package.json +26 -0
  104. package/templates/vite-ts-zustand-mui/src/App.tsx +17 -0
  105. package/templates/vite-ts-zustand-mui/src/main.tsx +14 -0
  106. package/templates/vite-ts-zustand-mui/src/theme.ts +9 -0
  107. package/templates/vite-ts-zustand-shadcn/README.md +15 -0
  108. package/templates/vite-ts-zustand-shadcn/package.json +23 -0
  109. package/templates/vite-ts-zustand-shadcn/src/main.tsx +2 -0
@@ -0,0 +1,311 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { logger } from '../utils.js';
4
+
5
+ /**
6
+ * Each addon is a self-contained injector. Register it in ADDON_REGISTRY.
7
+ * An injector receives { targetDir, framework, state, ui, pkg } and mutates pkg
8
+ * (returns updated pkg) and writes any files it needs.
9
+ */
10
+
11
+ const ADDON_REGISTRY = {
12
+ 'tanstack-query': injectTanstackQuery,
13
+ axios: injectAxios,
14
+ forms: injectForms,
15
+ toast: injectToast,
16
+ testing: injectTesting,
17
+ lint: injectLint,
18
+ hooks: injectHooks,
19
+ auth: injectAuth,
20
+ };
21
+
22
+ export async function applyAddons({ targetDir, framework, state, ui, addons }) {
23
+ if (!addons || addons.length === 0) return;
24
+
25
+ const pkgPath = path.join(targetDir, 'package.json');
26
+ let pkg = await fs.readJson(pkgPath);
27
+
28
+ for (const addon of addons) {
29
+ const fn = ADDON_REGISTRY[addon];
30
+ if (!fn) {
31
+ logger.warn(` ↳ Unknown addon: ${addon}, skipping`);
32
+ continue;
33
+ }
34
+ pkg = (await fn({ targetDir, framework, state, ui, pkg })) || pkg;
35
+ logger.dim(` ✓ injected: ${addon}`);
36
+ }
37
+
38
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
39
+ }
40
+
41
+ // ───────────────────────────────────────────────
42
+ // Injectors
43
+ // ───────────────────────────────────────────────
44
+
45
+ async function injectTanstackQuery({ targetDir, pkg }) {
46
+ pkg.dependencies = {
47
+ ...pkg.dependencies,
48
+ '@tanstack/react-query': '^5.59.0',
49
+ '@tanstack/react-query-devtools': '^5.59.0',
50
+ };
51
+
52
+ const file = path.join(targetDir, 'src/lib/react-query.ts');
53
+ await fs.outputFile(
54
+ file,
55
+ `import { QueryClient } from '@tanstack/react-query';
56
+
57
+ export const queryClient = new QueryClient({
58
+ defaultOptions: {
59
+ queries: {
60
+ retry: 1,
61
+ refetchOnWindowFocus: false,
62
+ staleTime: 60_000,
63
+ },
64
+ },
65
+ });
66
+ `
67
+ );
68
+
69
+ await fs.outputFile(
70
+ path.join(targetDir, 'src/hooks/api/useApiQuery.ts'),
71
+ `import { useQuery, type UseQueryOptions, type QueryKey } from '@tanstack/react-query';
72
+
73
+ export function useApiQuery<TData = unknown, TError = Error>(
74
+ key: QueryKey,
75
+ fn: () => Promise<TData>,
76
+ options?: Omit<UseQueryOptions<TData, TError>, 'queryKey' | 'queryFn'>
77
+ ) {
78
+ return useQuery<TData, TError>({
79
+ queryKey: key,
80
+ queryFn: fn,
81
+ ...options,
82
+ });
83
+ }
84
+ `
85
+ );
86
+
87
+ await fs.outputFile(
88
+ path.join(targetDir, 'src/hooks/api/useApiMutation.ts'),
89
+ `import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
90
+
91
+ export function useApiMutation<TData = unknown, TVariables = void, TError = Error>(
92
+ fn: (vars: TVariables) => Promise<TData>,
93
+ options?: Omit<UseMutationOptions<TData, TError, TVariables>, 'mutationFn'>
94
+ ) {
95
+ return useMutation<TData, TError, TVariables>({
96
+ mutationFn: fn,
97
+ ...options,
98
+ });
99
+ }
100
+ `
101
+ );
102
+
103
+ return pkg;
104
+ }
105
+
106
+ async function injectAxios({ targetDir, pkg }) {
107
+ pkg.dependencies = { ...pkg.dependencies, axios: '^1.7.7' };
108
+
109
+ await fs.outputFile(
110
+ path.join(targetDir, 'src/lib/api-client.ts'),
111
+ `import axios, { AxiosError } from 'axios';
112
+
113
+ const baseURL =
114
+ (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_URL) ||
115
+ process.env.NEXT_PUBLIC_API_URL ||
116
+ '/api';
117
+
118
+ export const api = axios.create({
119
+ baseURL,
120
+ withCredentials: true,
121
+ timeout: 30_000,
122
+ });
123
+
124
+ api.interceptors.request.use((config) => {
125
+ const token =
126
+ typeof window !== 'undefined' ? window.localStorage.getItem('token') : null;
127
+ if (token) config.headers.Authorization = \`Bearer \${token}\`;
128
+ return config;
129
+ });
130
+
131
+ api.interceptors.response.use(
132
+ (res) => res,
133
+ (error: AxiosError<{ message?: string }>) => {
134
+ const message =
135
+ error.response?.data?.message ||
136
+ error.message ||
137
+ 'Something went wrong';
138
+
139
+ if (typeof window !== 'undefined') {
140
+ window.dispatchEvent(new CustomEvent('api-error', { detail: message }));
141
+ }
142
+ return Promise.reject(error);
143
+ }
144
+ );
145
+ `
146
+ );
147
+ return pkg;
148
+ }
149
+
150
+ async function injectForms({ pkg }) {
151
+ pkg.dependencies = {
152
+ ...pkg.dependencies,
153
+ 'react-hook-form': '^7.53.0',
154
+ zod: '^3.23.8',
155
+ '@hookform/resolvers': '^3.9.0',
156
+ };
157
+ return pkg;
158
+ }
159
+
160
+ async function injectToast({ targetDir, pkg }) {
161
+ pkg.dependencies = { ...pkg.dependencies, 'react-hot-toast': '^2.4.1' };
162
+
163
+ await fs.outputFile(
164
+ path.join(targetDir, 'src/components/ui/AppToaster.tsx'),
165
+ `import { useEffect } from 'react';
166
+ import { Toaster, toast } from 'react-hot-toast';
167
+
168
+ export function AppToaster() {
169
+ useEffect(() => {
170
+ const onApiError = (e: Event) => {
171
+ const detail = (e as CustomEvent<string>).detail;
172
+ toast.error(detail || 'Something went wrong');
173
+ };
174
+ window.addEventListener('api-error', onApiError);
175
+ return () => window.removeEventListener('api-error', onApiError);
176
+ }, []);
177
+
178
+ return <Toaster position="top-right" />;
179
+ }
180
+ `
181
+ );
182
+ return pkg;
183
+ }
184
+
185
+ async function injectTesting({ pkg }) {
186
+ pkg.devDependencies = {
187
+ ...pkg.devDependencies,
188
+ vitest: '^2.1.1',
189
+ '@testing-library/react': '^16.0.1',
190
+ '@testing-library/jest-dom': '^6.5.0',
191
+ '@testing-library/user-event': '^14.5.2',
192
+ jsdom: '^25.0.1',
193
+ };
194
+ pkg.scripts = {
195
+ ...pkg.scripts,
196
+ test: 'vitest',
197
+ 'test:ui': 'vitest --ui',
198
+ };
199
+ return pkg;
200
+ }
201
+
202
+ async function injectLint({ pkg }) {
203
+ pkg.devDependencies = {
204
+ ...pkg.devDependencies,
205
+ eslint: '^9.12.0',
206
+ prettier: '^3.3.3',
207
+ 'eslint-config-prettier': '^9.1.0',
208
+ };
209
+ pkg.scripts = {
210
+ ...pkg.scripts,
211
+ lint: 'eslint .',
212
+ format: 'prettier --write .',
213
+ };
214
+ return pkg;
215
+ }
216
+
217
+ async function injectHooks({ targetDir }) {
218
+ // Just write the hook files; no deps needed.
219
+ const hooksDir = path.join(targetDir, 'src/hooks/core');
220
+ await fs.outputFile(
221
+ path.join(hooksDir, 'useDebounce.ts'),
222
+ `import { useEffect, useState } from 'react';
223
+
224
+ export function useDebounce<T>(value: T, delay = 500): T {
225
+ const [debounced, setDebounced] = useState(value);
226
+ useEffect(() => {
227
+ const t = setTimeout(() => setDebounced(value), delay);
228
+ return () => clearTimeout(t);
229
+ }, [value, delay]);
230
+ return debounced;
231
+ }
232
+ `
233
+ );
234
+
235
+ await fs.outputFile(
236
+ path.join(hooksDir, 'useThrottle.ts'),
237
+ `import { useCallback, useRef } from 'react';
238
+
239
+ export function useThrottle<TArgs extends unknown[]>(
240
+ fn: (...args: TArgs) => void,
241
+ delay = 300
242
+ ) {
243
+ const last = useRef(0);
244
+ return useCallback(
245
+ (...args: TArgs) => {
246
+ const now = Date.now();
247
+ if (now - last.current >= delay) {
248
+ last.current = now;
249
+ fn(...args);
250
+ }
251
+ },
252
+ [fn, delay]
253
+ );
254
+ }
255
+ `
256
+ );
257
+
258
+ await fs.outputFile(
259
+ path.join(hooksDir, 'useToggle.ts'),
260
+ `import { useCallback, useState } from 'react';
261
+
262
+ export function useToggle(initial = false) {
263
+ const [value, setValue] = useState(initial);
264
+ const toggle = useCallback(() => setValue((v) => !v), []);
265
+ return [value, toggle, setValue] as const;
266
+ }
267
+ `
268
+ );
269
+
270
+ await fs.outputFile(
271
+ path.join(hooksDir, 'usePrevious.ts'),
272
+ `import { useEffect, useRef } from 'react';
273
+
274
+ export function usePrevious<T>(value: T): T | undefined {
275
+ const ref = useRef<T | undefined>(undefined);
276
+ useEffect(() => {
277
+ ref.current = value;
278
+ }, [value]);
279
+ return ref.current;
280
+ }
281
+ `
282
+ );
283
+
284
+ await fs.outputFile(
285
+ path.join(targetDir, 'src/hooks/data/normalize.ts'),
286
+ `export function normalizeArray<T extends Record<string, any>>(
287
+ arr: T[],
288
+ key: keyof T = 'id' as keyof T
289
+ ): Record<string, T> {
290
+ return arr.reduce<Record<string, T>>((acc, item) => {
291
+ acc[String(item[key])] = item;
292
+ return acc;
293
+ }, {});
294
+ }
295
+ `
296
+ );
297
+ }
298
+
299
+ async function injectAuth({ targetDir, pkg }) {
300
+ // Stub — real implementation depends on chosen state lib.
301
+ // Leaving a placeholder file so the user has a starting point.
302
+ await fs.outputFile(
303
+ path.join(targetDir, 'src/features/auth/README.md'),
304
+ `# Auth feature
305
+
306
+ This is a scaffolded auth module. Wire up your endpoints in \`api/\`,
307
+ state in \`store/\`, and protected routes per your framework.
308
+ `
309
+ );
310
+ return pkg;
311
+ }
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+
4
+ /**
5
+ * Templates ship with `modular` structure by default.
6
+ * If user picks `atomic`, we restructure components/ into atoms/molecules/organisms.
7
+ * Otherwise we just ensure the modular skeleton exists.
8
+ */
9
+ export async function applyStructure({ targetDir, framework, structure }) {
10
+ const srcDir = path.join(targetDir, 'src');
11
+
12
+ if (structure === 'atomic') {
13
+ const atomicBase = path.join(srcDir, 'components');
14
+ await fs.ensureDir(path.join(atomicBase, 'atoms'));
15
+ await fs.ensureDir(path.join(atomicBase, 'molecules'));
16
+ await fs.ensureDir(path.join(atomicBase, 'organisms'));
17
+ await fs.ensureDir(path.join(atomicBase, 'templates'));
18
+ await fs.outputFile(
19
+ path.join(atomicBase, 'README.md'),
20
+ `# Atomic structure
21
+
22
+ - \`atoms/\` — smallest building blocks (Button, Input, Label)
23
+ - \`molecules/\` — small groupings (SearchBar = Input + Button)
24
+ - \`organisms/\` — complex sections (Header, ProductList)
25
+ - \`templates/\` — page-level layouts (no real data)
26
+ `
27
+ );
28
+ return;
29
+ }
30
+
31
+ // modular (default)
32
+ await fs.ensureDir(path.join(srcDir, 'features'));
33
+ await fs.ensureDir(path.join(srcDir, 'components/ui'));
34
+ await fs.ensureDir(path.join(srcDir, 'components/shared'));
35
+ await fs.outputFile(
36
+ path.join(srcDir, 'features/README.md'),
37
+ `# Feature modules
38
+
39
+ Each feature is self-contained:
40
+
41
+ \`\`\`
42
+ features/auth/
43
+ api/ — service calls
44
+ components/ — feature-only components
45
+ hooks/ — feature-only hooks
46
+ store/ — feature state slice
47
+ types.ts
48
+ \`\`\`
49
+ `
50
+ );
51
+ }
package/src/pm.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Package manager detection + helpers.
3
+ *
4
+ * Detection logic mirrors what create-vite / create-next-app do:
5
+ * read process.env.npm_config_user_agent which looks like:
6
+ * "npm/10.5.0 node/v20.11.0 darwin x64"
7
+ * "yarn/1.22.22 npm/? node/v20.11.0 darwin x64"
8
+ * "pnpm/9.12.0 npm/? node/v20.11.0 darwin x64"
9
+ *
10
+ * This is set by whichever PM invoked the CLI (e.g. `pnpm create stackit`).
11
+ */
12
+
13
+ export const SUPPORTED_PMS = ['npm', 'yarn', 'pnpm'];
14
+
15
+ /**
16
+ * Detect the PM that invoked this CLI. Returns null if not detected
17
+ * or if the detected PM isn't in our supported list.
18
+ */
19
+ export function detectPackageManager() {
20
+ const userAgent = process.env.npm_config_user_agent;
21
+ if (!userAgent) return null;
22
+
23
+ // Take just the first token: "pnpm/9.12.0" → "pnpm"
24
+ const name = userAgent.split(' ')[0]?.split('/')[0];
25
+ if (!name) return null;
26
+
27
+ return SUPPORTED_PMS.includes(name) ? name : null;
28
+ }
29
+
30
+ /**
31
+ * Get install args for a given PM. All three accept bare invocation
32
+ * but we pass the explicit subcommand for clarity in logs.
33
+ */
34
+ export function getInstallCommand(pm) {
35
+ switch (pm) {
36
+ case 'yarn':
37
+ return { cmd: 'yarn', args: [] }; // yarn alone = install
38
+ case 'pnpm':
39
+ return { cmd: 'pnpm', args: ['install'] };
40
+ case 'npm':
41
+ default:
42
+ return { cmd: 'npm', args: ['install'] };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get the user-facing run command for a script.
48
+ * e.g. runScript('pnpm', 'dev') → 'pnpm dev'
49
+ * runScript('npm', 'dev') → 'npm run dev'
50
+ */
51
+ export function runScript(pm, script) {
52
+ switch (pm) {
53
+ case 'yarn':
54
+ return `yarn ${script}`;
55
+ case 'pnpm':
56
+ return `pnpm ${script}`;
57
+ case 'npm':
58
+ default:
59
+ return `npm run ${script}`;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get the install command as a single user-facing string,
65
+ * for README and "next steps" messaging.
66
+ */
67
+ export function installCommandString(pm) {
68
+ switch (pm) {
69
+ case 'yarn':
70
+ return 'yarn';
71
+ case 'pnpm':
72
+ return 'pnpm install';
73
+ case 'npm':
74
+ default:
75
+ return 'npm install';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The packageManager field value for package.json.
81
+ * Used by Corepack to enforce PM consistency across a team.
82
+ * Versions are minimums; users can bump as needed.
83
+ */
84
+ export function getPackageManagerField(pm) {
85
+ switch (pm) {
86
+ case 'yarn':
87
+ return 'yarn@1.22.22';
88
+ case 'pnpm':
89
+ return 'pnpm@9.12.0';
90
+ case 'npm':
91
+ default:
92
+ return 'npm@10.8.0';
93
+ }
94
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,179 @@
1
+ import prompts from 'prompts';
2
+ import path from 'node:path';
3
+ import fs from 'fs-extra';
4
+ import validateProjectName from 'validate-npm-package-name';
5
+ import chalk from 'chalk';
6
+ import { CHOICES } from './choices.js';
7
+ import {SUPPORTED_PMS, detectPackageManager} from './pm.js'
8
+ import {logger} from './utils.js'
9
+
10
+ const onCancel = () => {
11
+ const err = new Error('User cancelled');
12
+ err.isCancelled = true;
13
+ throw err;
14
+ };
15
+
16
+ function validateName(name) {
17
+ if (!name || !name.trim()) return 'Project name is required';
18
+ const result = validateProjectName(name);
19
+ if (!result.validForNewPackages) {
20
+ const issues = [...(result.errors || []), ...(result.warnings || [])];
21
+ return issues.join(', ') || 'Invalid project name';
22
+ }
23
+ return true;
24
+ }
25
+
26
+ async function confirmOverwrite(targetDir) {
27
+ if (!fs.existsSync(targetDir)) return true;
28
+ const files = fs.readdirSync(targetDir);
29
+ if (files.length === 0) return true;
30
+
31
+ const { overwrite } = await prompts(
32
+ {
33
+ type: 'confirm',
34
+ name: 'overwrite',
35
+ message: chalk.yellow(
36
+ `Directory "${path.basename(targetDir)}" is not empty. Overwrite?`
37
+ ),
38
+ initial: false,
39
+ },
40
+ { onCancel }
41
+ );
42
+
43
+ return overwrite;
44
+ }
45
+
46
+ export async function runPrompts(initial) {
47
+ const questions = [];
48
+
49
+ if (!initial.projectName) {
50
+ questions.push({
51
+ type: 'text',
52
+ name: 'projectName',
53
+ message: 'Project name:',
54
+ initial: 'my-stackit-app',
55
+ validate: validateName,
56
+ });
57
+ }
58
+
59
+ if (!initial.framework) {
60
+ questions.push({
61
+ type: 'select',
62
+ name: 'framework',
63
+ message: 'Framework?',
64
+ choices: CHOICES.framework,
65
+ initial: 0,
66
+ });
67
+ }
68
+
69
+ if (!initial.language) {
70
+ questions.push({
71
+ type: 'select',
72
+ name: 'language',
73
+ message: 'Language?',
74
+ choices: CHOICES.language,
75
+ initial: 0,
76
+ });
77
+ }
78
+
79
+ if (!initial.state) {
80
+ questions.push({
81
+ type: 'select',
82
+ name: 'state',
83
+ message: 'State management?',
84
+ choices: CHOICES.state,
85
+ initial: 0,
86
+ });
87
+ }
88
+
89
+ if (!initial.ui) {
90
+ questions.push({
91
+ type: 'select',
92
+ name: 'ui',
93
+ message: 'UI library?',
94
+ choices: CHOICES.ui,
95
+ initial: 0,
96
+ });
97
+ }
98
+
99
+ if (!initial.structure) {
100
+ questions.push({
101
+ type: 'select',
102
+ name: 'structure',
103
+ message: 'Folder structure?',
104
+ choices: CHOICES.structure,
105
+ initial: 1, // modular default — better for real apps
106
+ });
107
+ }
108
+
109
+
110
+ let resolvedPm = null;
111
+ let pmSource = null;
112
+
113
+ if(initial.packageManager){
114
+ if(!SUPPORTED_PMS.includes(initial.packageManager)){
115
+ throw new Error(
116
+ `Unsupported package manager: "${initial.packageManager}". ` +
117
+ `Supported: ${SUPPORTED_PMS.join(', ')}`
118
+ )
119
+ }
120
+ resolvedPm = initial.packageManager;
121
+ pmSource = 'flag'
122
+ }else{
123
+ const detected = detectPackageManager();
124
+ if(detected){
125
+ resolvedPm= detected;
126
+ pmSource='detected';
127
+ logger.dim(` ↳ detected package manager: ${chalk.cyan(detected)}`);
128
+ }else{
129
+ questions.push({
130
+ type: 'select',
131
+ name: 'packageManager',
132
+ message: 'Package Manager',
133
+ choices: CHOICES.packageManager,
134
+ })
135
+ }
136
+ }
137
+
138
+ // Always-asked toggles
139
+ questions.push(
140
+ {
141
+ type: 'multiselect',
142
+ name: 'addons',
143
+ message: 'Pick add-ons (space to toggle, enter to confirm):',
144
+ choices: CHOICES.addons,
145
+ hint: '- Space to select, Enter to submit',
146
+ instructions: false,
147
+ },
148
+ {
149
+ type: 'confirm',
150
+ name: 'initGit',
151
+ message: 'Initialize a git repository?',
152
+ initial: true,
153
+ }
154
+ );
155
+
156
+ const answered = await prompts(questions, { onCancel });
157
+ const finalAnswers = { ...initial, ...answered };
158
+
159
+ // Validate even if name came from CLI
160
+ const nameCheck = validateName(finalAnswers.projectName);
161
+ if (nameCheck !== true) {
162
+ throw new Error(`Invalid project name: ${nameCheck}`);
163
+ }
164
+
165
+ // Compute target dir + overwrite confirmation
166
+ const targetDir = path.resolve(process.cwd(), finalAnswers.projectName);
167
+ const ok = await confirmOverwrite(targetDir);
168
+ if (!ok) onCancel();
169
+
170
+ return {
171
+ ...finalAnswers,
172
+ targetDir,
173
+ templateKey: buildTemplateKey(finalAnswers),
174
+ };
175
+ }
176
+
177
+ function buildTemplateKey({ framework, language, state, ui }) {
178
+ return `${framework}-${language ?? 'ts'}-${state}-${ui}`;
179
+ }
package/src/utils.js ADDED
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const logger = {
4
+ info: (msg) => console.log(chalk.cyan(msg)),
5
+ success: (msg) => console.log(chalk.green(msg)),
6
+ warn: (msg) => console.log(chalk.yellow(msg)),
7
+ error: (msg) => console.log(chalk.red(msg)),
8
+ dim: (msg) => console.log(chalk.dim(msg)),
9
+ };
10
+
11
+ export function printBanner() {
12
+ console.log();
13
+ console.log(chalk.bold.magenta(' ╭───────────────────────────────╮'));
14
+ console.log(chalk.bold.magenta(' │ ') + chalk.bold.white('create-stackit') + chalk.bold.magenta(' v0.1.0 │'));
15
+ console.log(chalk.bold.magenta(' │ ') + chalk.dim('pragmatic react starters ') + chalk.bold.magenta('│'));
16
+ console.log(chalk.bold.magenta(' ╰───────────────────────────────╯'));
17
+ console.log();
18
+ }
@@ -0,0 +1,15 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Generated with [create-stackit](https://github.com/your-handle/create-stackit).
4
+
5
+ ## Stack
6
+ - Next.js + JavaScript
7
+ - Redux Toolkit
8
+ - Material UI
9
+
10
+ ## Scripts
11
+ ```bash
12
+ npm run dev # start dev server
13
+ npm run build # production build
14
+ npm run start # start production server
15
+ ```
@@ -0,0 +1,12 @@
1
+ import React from 'react'
2
+ import { Providers } from './providers'
3
+
4
+ export default function RootLayout({ children }) {
5
+ return (
6
+ <html lang="en">
7
+ <body>
8
+ <Providers>{children}</Providers>
9
+ </body>
10
+ </html>
11
+ )
12
+ }
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+ import { useDispatch, useSelector } from 'react-redux'
3
+ import { increment } from '../src/store/counterSlice'
4
+
5
+ export default function Home() {
6
+ const count = useSelector((state) => state.counter.value)
7
+ const dispatch = useDispatch()
8
+ return (
9
+ <main>
10
+ <h1>create-stackit</h1>
11
+ <p>Count: {count}</p>
12
+ <button onClick={() => dispatch(increment())}>Increment</button>
13
+ </main>
14
+ )
15
+ }