stackkit 0.3.2 → 0.3.4

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.
@@ -66,30 +66,62 @@ async function detectPackageManager(cwd) {
66
66
  async function installDependencies(cwd, pm, maxRetries = 2) {
67
67
  const args = ["install"];
68
68
  const stdio = "pipe";
69
+ const packageInstallTimeout = Number(process.env.STACKKIT_INSTALL_TIMEOUT_MS) || constants_1.TIMEOUTS.PACKAGE_INSTALL;
70
+ const envFallback = process.env.STACKKIT_FALLBACK_PMS
71
+ ? process.env.STACKKIT_FALLBACK_PMS.split(",")
72
+ .map((s) => s.trim())
73
+ .filter(Boolean)
74
+ : [];
75
+ const defaultOrder = ["pnpm", "npm", "yarn", "bun"];
76
+ const preferredOrder = Array.from(new Set([pm, ...envFallback, ...defaultOrder].map((s) => s)));
69
77
  let lastError = null;
70
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
78
+ async function isAvailable(pmCheck) {
71
79
  try {
72
- if (attempt > 0) {
73
- logger_1.logger.debug(`Retry attempt ${attempt} for installing dependencies`);
74
- await new Promise((resolve) => setTimeout(resolve, constants_1.TIMEOUTS.RETRY_DELAY_BASE * attempt));
75
- }
76
- await (0, execa_1.default)(pm, args, { cwd, stdio, timeout: constants_1.TIMEOUTS.PACKAGE_INSTALL });
77
- return;
80
+ await (0, execa_1.default)(pmCheck, ["--version"], { cwd, stdio: "pipe", timeout: 2000 });
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ for (const pmCandidate of preferredOrder) {
88
+ if (!["pnpm", "npm", "yarn", "bun"].includes(pmCandidate))
89
+ continue;
90
+ const available = await isAvailable(pmCandidate);
91
+ if (!available) {
92
+ logger_1.logger.debug(`${pmCandidate} not found on PATH, skipping`);
93
+ continue;
78
94
  }
79
- catch (error) {
80
- lastError = error;
81
- logger_1.logger.debug(`Installation attempt ${attempt + 1} failed: ${lastError.message}`);
82
- const err = error;
83
- const errorMsg = err.message || "";
84
- if (errorMsg.includes("ECONNRESET") ||
85
- errorMsg.includes("ETIMEDOUT") ||
86
- errorMsg.includes("ENOTFOUND")) {
87
- continue;
95
+ logger_1.logger.debug(`Attempting install with ${pmCandidate}`);
96
+ let succeeded = false;
97
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
98
+ try {
99
+ if (attempt > 0) {
100
+ logger_1.logger.debug(`Retry attempt ${attempt} for ${pmCandidate}`);
101
+ await new Promise((resolve) => setTimeout(resolve, constants_1.TIMEOUTS.RETRY_DELAY_BASE * attempt));
102
+ }
103
+ await (0, execa_1.default)(pmCandidate, args, { cwd, stdio, timeout: packageInstallTimeout });
104
+ succeeded = true;
105
+ break;
106
+ }
107
+ catch (error) {
108
+ lastError = error;
109
+ logger_1.logger.debug(`Installation attempt ${attempt + 1} with ${pmCandidate} failed: ${lastError.message}`);
110
+ const err = error;
111
+ const errorMsg = err.message || "";
112
+ if (errorMsg.includes("ECONNRESET") ||
113
+ errorMsg.includes("ETIMEDOUT") ||
114
+ errorMsg.includes("ENOTFOUND")) {
115
+ continue;
116
+ }
117
+ break;
88
118
  }
89
- throw error;
90
119
  }
120
+ if (succeeded)
121
+ return;
122
+ logger_1.logger.debug(`${pmCandidate} failed after retries, trying next fallback`);
91
123
  }
92
- throw new Error(`Failed to install dependencies after ${maxRetries + 1} attempts: ${lastError?.message || "Unknown error"}`);
124
+ throw new Error(`Failed to install dependencies after trying fallback package managers: ${lastError?.message || "Unknown error"}`);
93
125
  }
94
126
  async function addDependencies(cwd, pm, packages, dev = false) {
95
127
  if (packages.length === 0) {
@@ -1,91 +1,24 @@
1
1
  <!doctype html>
2
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
2
+ <html lang="en">
3
3
 
4
4
  <head>
5
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
5
+ <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
8
- <title>
9
- <%= appName || "Your App" %> - Continue with Google
10
- </title>
7
+ <title>Continue Sign-in</title>
11
8
  </head>
12
9
 
13
- <body style="margin:0; padding:0; background-color:#f3f4f6;">
14
- <div style="display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;">
15
- Continue your Google sign-in securely.
16
- </div>
17
- <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
18
- style="background-color:#f3f4f6; padding:24px 12px;">
19
- <tr>
20
- <td align="center">
21
- <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
22
- style="max-width:600px; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
23
- <tr>
24
- <td style="padding:28px 24px 16px 24px; font-family:Arial, Helvetica, sans-serif; color:#111827;">
25
- <h1 style="margin:0; font-size:22px; line-height:30px; font-weight:700;">Continue with
26
- Google</h1>
27
- <p style="margin:14px 0 0; font-size:15px; line-height:24px; color:#374151;">
28
- Hi <%= userName || "there" %>,
29
- </p>
30
- <p style="margin:10px 0 0; font-size:15px; line-height:24px; color:#374151;">
31
- We received a request to continue authentication with Google for your <strong>
32
- <%= appName || "account" %>
33
- </strong>.
34
- </p>
35
- </td>
36
- </tr>
10
+ <body style="font-family: Arial, Helvetica, sans-serif; padding: 24px;">
11
+ <p>Redirecting to <%= provider %> login...</p>
37
12
 
38
- <tr>
39
- <td style="padding:0 24px 24px 24px; font-family:Arial, Helvetica, sans-serif;">
40
- <table role="presentation" cellspacing="0" cellpadding="0" border="0">
41
- <tr>
42
- <td align="center" bgcolor="#111827" style="border-radius:8px;">
43
- <a href="<%= redirectUrl %>" target="_blank"
44
- style="display:inline-block; padding:12px 22px; font-size:15px; line-height:20px; font-weight:600; color:#ffffff; text-decoration:none;">
45
- Continue Sign-in
46
- </a>
47
- </td>
48
- </tr>
49
- </table>
13
+ <form id="social-login-form" method="POST" action="<%= signInEndpoint %>">
14
+ <input type="hidden" name="provider" value="<%= provider %>" />
15
+ <input type="hidden" name="callbackURL" value="<%= callbackURL %>" />
16
+ <button type="submit">Continue</button>
17
+ </form>
50
18
 
51
- <p
52
- style="margin:14px 0 0; font-size:13px; line-height:20px; color:#6b7280; word-break:break-word;">
53
- Button not working? Copy and paste this URL into your browser:<br />
54
- <a href="<%= redirectUrl %>" target="_blank"
55
- style="color:#2563eb; text-decoration:underline;">
56
- <%= redirectUrl %>
57
- </a>
58
- </p>
59
- </td>
60
- </tr>
61
-
62
- <tr>
63
- <td style="padding:0 24px 24px 24px; font-family:Arial, Helvetica, sans-serif; color:#4b5563;">
64
- <p style="margin:0; font-size:14px; line-height:22px;">
65
- If you didn’t request this, you can safely ignore this email. No changes will be made
66
- unless you continue.
67
- </p>
68
- </td>
69
- </tr>
70
-
71
- <tr>
72
- <td
73
- style="padding:16px 24px 24px 24px; border-top:1px solid #e5e7eb; font-family:Arial, Helvetica, sans-serif; color:#6b7280;">
74
- <p style="margin:0; font-size:12px; line-height:18px;">
75
- Need help? Contact us at
76
- <a href="mailto:<%= supportEmail || " support@example.com" %>" style="color:#2563eb;
77
- text-decoration:underline;"><%= supportEmail || "support@example.com" %></a>.
78
- </p>
79
- <p style="margin:6px 0 0; font-size:12px; line-height:18px;">
80
- © <%= year || new Date().getFullYear() %>
81
- <%= appName || "Your App" %>. All rights reserved.
82
- </p>
83
- </td>
84
- </tr>
85
- </table>
86
- </td>
87
- </tr>
88
- </table>
89
- </body>
19
+ <script>
20
+ document.getElementById("social-login-form")?.submit();
21
+ </script>
22
+ </body>
90
23
 
91
24
  </html>
@@ -1,3 +1,5 @@
1
+ import { Role } from "@prisma/client";
2
+
1
3
  declare global {
2
4
  namespace Express {
3
5
  interface Request {
@@ -5,7 +7,7 @@ declare global {
5
7
  id: string;
6
8
  name: string;
7
9
  email: string;
8
- role: string;
10
+ role: Role;
9
11
  };
10
12
  }
11
13
  }
@@ -8,9 +8,12 @@ dotenv.config({ path: path.join(process.cwd(), ".env") });
8
8
  {{/if}}
9
9
 
10
10
  interface EnvConfig {
11
- APP_URL?: string;
11
+ APP_URL: string;
12
+ {{#if framework == "nextjs"}}
13
+ API_URL: string;
14
+ {{/if}}
12
15
  DATABASE_URL: string;
13
- FRONTEND_URL?: string;
16
+ FRONTEND_URL: string;
14
17
  BETTER_AUTH_URL: string;
15
18
  BETTER_AUTH_SECRET: string;
16
19
  GOOGLE_CLIENT_ID: string;
@@ -40,6 +43,9 @@ interface EnvConfig {
40
43
  const loadEnvVars = (): EnvConfig => {
41
44
  const requiredEnvVars = [
42
45
  "APP_URL",
46
+ {{#if framework == "nextjs"}}
47
+ "API_URL",
48
+ {{/if}}
43
49
  "DATABASE_URL",
44
50
  "FRONTEND_URL",
45
51
  "BETTER_AUTH_URL",
@@ -83,6 +89,9 @@ const loadEnvVars = (): EnvConfig => {
83
89
 
84
90
  return {
85
91
  APP_URL: process.env.APP_URL as string,
92
+ {{#if framework == "nextjs"}}
93
+ API_URL: process.env.API_URL as string,
94
+ {{/if}}
86
95
  DATABASE_URL: process.env.DATABASE_URL as string,
87
96
  FRONTEND_URL: process.env.FRONTEND_URL as string,
88
97
  BETTER_AUTH_URL: process.env.BETTER_AUTH_URL as string,
@@ -9,7 +9,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
9
9
  {{/if}}
10
10
  {{#if combo == "prisma:nextjs"}}
11
11
  import { Role, UserStatus } from "@prisma/client";
12
- import { sendEmail } from "../service/email/email-service";
12
+ import { sendEmail } from "@/lib/utils/email";
13
13
  import { prisma } from "../database/prisma";
14
14
  import { envVars } from "@/lib/env";
15
15
  import { prismaAdapter } from "better-auth/adapters/prisma";
@@ -48,7 +48,10 @@ export const auth = betterAuth({
48
48
  baseURL: envVars.BETTER_AUTH_URL,
49
49
  secret: envVars.BETTER_AUTH_SECRET,
50
50
  trustedOrigins: [
51
- envVars.FRONTEND_URL || envVars.BETTER_AUTH_URL || "http://localhost:3000",
51
+ envVars.APP_URL!,
52
+ envVars.FRONTEND_URL!,
53
+ envVars.BETTER_AUTH_URL!,
54
+ "http://localhost:3000",
52
55
  ],
53
56
  emailAndPassword: {
54
57
  enabled: true,
@@ -65,6 +65,10 @@ export const sendEmail = async ({
65
65
  })),
66
66
  });
67
67
  } catch {
68
- throw new AppError(status.INTERNAL_SERVER_ERROR, "Failed to send email");
68
+ {{#if framework == "express"}}
69
+ throw new AppError(status.INTERNAL_SERVER_ERROR, `Failed to send email to ${to}`);
70
+ {{else}}
71
+ throw new Error(`Failed to send email to ${to}`);
72
+ {{/if}}
69
73
  }
70
74
  };
@@ -1,5 +1,5 @@
1
- import { PrismaClient } from "@prisma/client";
2
1
  import 'dotenv/config';
2
+ import { PrismaClient } from "@prisma/client";
3
3
 
4
4
  const globalForPrisma = globalThis as unknown as {
5
5
  prisma: PrismaClient | undefined
@@ -1,6 +1,5 @@
1
1
  generator client {
2
2
  provider = "prisma-client-js"
3
- output = "../src/generated/prisma"
4
3
  }
5
4
 
6
5
  datasource db {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackkit",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Production-ready CLI to create and extend JavaScript or TypeScript apps with modular stacks.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,21 +35,6 @@ npm run dev
35
35
  - `npm run start` - Start production server
36
36
  - `npm run lint` - Run linter
37
37
 
38
- ## Project Structure
39
-
40
- ```
41
- app/
42
- ├── globals.css # Global styles
43
- ├── layout.tsx # Root layout
44
- ├── page.tsx # Home page
45
- └── api/ # API routes
46
-
47
- lib/
48
- └── utils.ts # Utility functions
49
-
50
- public/ # Static assets
51
- ```
52
-
53
38
  ## Recommended Folder & File Structure
54
39
 
55
40
  ```text
@@ -12,16 +12,8 @@ const api = axios.create({
12
12
  });
13
13
 
14
14
  api.interceptors.request.use(
15
- (config) => {
16
- const token = localStorage.getItem("auth_token");
17
- if (token) {
18
- (config.headers as Record<string, string>).Authorization = `Bearer ${token}`;
19
- }
20
- return config;
21
- },
22
- (error: AxiosError) => {
23
- return Promise.reject(error);
24
- },
15
+ (config) => config,
16
+ (error: AxiosError) => Promise.reject(error),
25
17
  );
26
18
 
27
19
  api.interceptors.response.use(
@@ -48,19 +48,6 @@ VITE_API_URL=http://localhost:3000/api
48
48
  VITE_APP_NAME=My App
49
49
  ```
50
50
 
51
- ## Project Structure
52
-
53
- ```
54
- src/
55
- ├── api/ # API client
56
- ├── components/ # UI components
57
- ├── hooks/ # Custom hooks
58
- ├── lib/ # Utilities
59
- ├── pages/ # Route pages
60
- ├── types/ # TypeScript types
61
- └── utils/ # Helper functions
62
- ```
63
-
64
51
  ## Recommended Folder & File Structure
65
52
 
66
53
  ```text
@@ -3,7 +3,7 @@ import axios, { AxiosError } from "axios";
3
3
  import toast from "react-hot-toast";
4
4
 
5
5
  const api = axios.create({
6
- baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000/api",
6
+ baseURL: import.meta.env.VITE_API_URL || "http://localhost:5000/api",
7
7
  timeout: 10000,
8
8
  headers: {
9
9
  "Content-Type": "application/json",
@@ -11,16 +11,8 @@ const api = axios.create({
11
11
  });
12
12
 
13
13
  api.interceptors.request.use(
14
- (config) => {
15
- const token = localStorage.getItem("auth_token");
16
- if (token) {
17
- (config.headers as Record<string, string>).Authorization = `Bearer ${token}`;
18
- }
19
- return config;
20
- },
21
- (error: AxiosError) => {
22
- return Promise.reject(error);
23
- },
14
+ (config) => config,
15
+ (error: AxiosError) => Promise.reject(error),
24
16
  );
25
17
 
26
18
  api.interceptors.response.use(