stackkit 0.1.7 → 0.1.8

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.
@@ -8,8 +8,6 @@ exports.removeEnvVariables = removeEnvVariables;
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const logger_1 = require("../ui/logger");
11
- const ENV_MARKER_START = "# StackKit:";
12
- const ENV_MARKER_END = "# End StackKit";
13
11
  async function addEnvVariables(projectRoot, variables, options = {}) {
14
12
  const envExamplePath = path_1.default.join(projectRoot, ".env.example");
15
13
  const envPath = path_1.default.join(projectRoot, ".env");
@@ -59,14 +57,11 @@ async function appendToEnvFile(filePath, variables, fileType, options = {}) {
59
57
  if (content && !content.endsWith("\n")) {
60
58
  content += "\n";
61
59
  }
62
- // Add marker and variables
63
- content += "\n";
64
- content += `${ENV_MARKER_START} Added by StackKit\n`;
60
+ // Append variables
65
61
  for (const variable of newVariables) {
66
62
  const value = fileType === "example" ? variable.value || "" : variable.value || "";
67
63
  content += `${variable.key}=${value}\n`;
68
64
  }
69
- content += `${ENV_MARKER_END}\n`;
70
65
  await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
71
66
  await fs_extra_1.default.writeFile(filePath, content, "utf-8");
72
67
  }
@@ -86,23 +81,12 @@ async function removeFromEnvFile(filePath, keys) {
86
81
  const content = await fs_extra_1.default.readFile(filePath, "utf-8");
87
82
  const lines = content.split("\n");
88
83
  const newLines = [];
89
- let inStackKitBlock = false;
90
84
  for (const line of lines) {
91
- if (line.includes(ENV_MARKER_START)) {
92
- inStackKitBlock = true;
93
- continue;
94
- }
95
- if (line.includes(ENV_MARKER_END)) {
96
- inStackKitBlock = false;
97
- continue;
98
- }
99
85
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
100
86
  if (match && keys.includes(match[1])) {
101
87
  continue;
102
88
  }
103
- if (!inStackKitBlock || !line.startsWith("#")) {
104
- newLines.push(line);
105
- }
89
+ newLines.push(line);
106
90
  }
107
91
  while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
108
92
  newLines.pop();
@@ -214,14 +214,14 @@ class AdvancedCodeGenerator {
214
214
  const [, caseValue, caseContent] = caseMatch;
215
215
  const cleanCaseValue = caseValue.trim().replace(/['"]/g, "");
216
216
  if (cleanCaseValue === "default") {
217
- defaultCase = caseContent;
217
+ defaultCase = caseContent.trim();
218
218
  }
219
219
  else if (actualVal === cleanCaseValue) {
220
- result = caseContent;
220
+ result = caseContent.trim();
221
221
  break;
222
222
  }
223
223
  }
224
- return result || defaultCase || "";
224
+ return (result || defaultCase || "").trim();
225
225
  });
226
226
  // Handle variable replacement with advanced expressions
227
227
  content = content.replace(/\{\{([^}]+)\}\}/g, (match, varExpr) => {
@@ -378,6 +378,18 @@ class AdvancedCodeGenerator {
378
378
  !relativePath.startsWith("node_modules/"));
379
379
  },
380
380
  });
381
+ // If template provides a .env.example, create a .env from it when
382
+ // the target project does not already have a .env file.
383
+ try {
384
+ const envExampleSrc = path.join(templatePath, ".env.example");
385
+ const envDest = path.join(outputPath, ".env");
386
+ if ((await fs.pathExists(envExampleSrc)) && !(await fs.pathExists(envDest))) {
387
+ await fs.copy(envExampleSrc, envDest);
388
+ }
389
+ }
390
+ catch {
391
+ // ignore failures here — not critical
392
+ }
381
393
  }
382
394
  }
383
395
  processOperationTemplates(operation, context) {
@@ -469,34 +481,107 @@ class AdvancedCodeGenerator {
469
481
  switch (patchOp.type) {
470
482
  case "add-import":
471
483
  if (patchOp.imports) {
484
+ // Process imports and trim any accidental surrounding blank lines
472
485
  const imports = patchOp.imports
473
486
  .map((imp) => this.processTemplate(imp, context))
474
- .join("\n");
475
- // Add imports at the top, after existing imports
487
+ .join("\n")
488
+ .replace(/^\n+/, "")
489
+ .replace(/\n+$/, "");
490
+ // Add imports at the top, after existing imports without introducing
491
+ // extra blank lines.
476
492
  const lines = content.split("\n");
477
- let insertIndex = 0;
493
+ // Find the last import line index
494
+ let lastImportIndex = -1;
478
495
  for (let i = 0; i < lines.length; i++) {
479
- if (lines[i].trim().startsWith("import") || lines[i].trim() === "") {
480
- insertIndex = i + 1;
496
+ if (lines[i].trim().startsWith("import"))
497
+ lastImportIndex = i;
498
+ }
499
+ // Insert right after the last import. If there's a blank line
500
+ // immediately after the imports, overwrite that blank line to
501
+ // avoid introducing an extra empty line above the inserted imports.
502
+ const insertIndex = lastImportIndex === -1 ? 0 : lastImportIndex + 1;
503
+ // Only add imports that don't already exist in the file
504
+ const importLines = imports
505
+ .split("\n")
506
+ .map((l) => l.trim())
507
+ .filter(Boolean);
508
+ const newImportLines = importLines.filter((imp) => !lines.some((ln) => ln.trim() === imp));
509
+ if (newImportLines.length > 0) {
510
+ // Insert imports
511
+ if (insertIndex < lines.length && lines[insertIndex].trim() === "") {
512
+ lines.splice(insertIndex, 1, ...newImportLines);
481
513
  }
482
514
  else {
483
- break;
515
+ lines.splice(insertIndex, 0, ...newImportLines);
516
+ }
517
+ // After insertion, ensure exactly one blank line after the import block
518
+ // Find last import line index again
519
+ let lastIdx = -1;
520
+ for (let i = 0; i < lines.length; i++) {
521
+ if (lines[i].trim().startsWith("import"))
522
+ lastIdx = i;
523
+ }
524
+ const nextIdx = lastIdx + 1;
525
+ if (lastIdx !== -1) {
526
+ // Remove multiple blank lines after imports
527
+ let j = nextIdx;
528
+ while (j < lines.length && lines[j].trim() === "") {
529
+ j++;
530
+ }
531
+ // Ensure exactly one blank line after imports unless imports end at EOF
532
+ if (nextIdx < lines.length) {
533
+ lines.splice(nextIdx, j - nextIdx, "");
534
+ }
484
535
  }
485
536
  }
486
- lines.splice(insertIndex, 0, imports);
487
537
  content = lines.join("\n");
488
538
  }
489
539
  break;
490
540
  case "add-code":
491
- if (patchOp.code && patchOp.after) {
541
+ if (patchOp.code) {
492
542
  const processedCode = this.processTemplate(patchOp.code, context);
493
- const afterPattern = this.processTemplate(patchOp.after, context);
494
- const index = content.indexOf(afterPattern);
495
- if (index !== -1) {
496
- content =
497
- content.slice(0, index + afterPattern.length) +
498
- processedCode +
499
- content.slice(index + afterPattern.length);
543
+ // Skip insertion if the exact code already exists in the file
544
+ const codeTrimmed = processedCode.trim();
545
+ if (codeTrimmed && content.includes(codeTrimmed)) {
546
+ break;
547
+ }
548
+ // Insert after pattern if provided
549
+ if (patchOp.after) {
550
+ const afterPattern = this.processTemplate(patchOp.after, context);
551
+ const index = content.indexOf(afterPattern);
552
+ if (index !== -1) {
553
+ const left = content.slice(0, index + afterPattern.length);
554
+ const right = content.slice(index + afterPattern.length);
555
+ // Normalize code: trim surrounding newlines and ensure single trailing newline
556
+ let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
557
+ // If right already starts with a newline, avoid double-blank by
558
+ // removing trailing newline from codeNormalized so only one newline remains
559
+ const rightStartsWithNewline = right.startsWith("\n");
560
+ if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
561
+ codeNormalized = codeNormalized.replace(/\n+$/, "");
562
+ }
563
+ const leftNeedsNewline = !left.endsWith("\n");
564
+ content = left + (leftNeedsNewline ? "\n" : "") + codeNormalized + right;
565
+ }
566
+ }
567
+ // Insert before pattern if provided
568
+ if (patchOp.before) {
569
+ const beforePattern = this.processTemplate(patchOp.before, context);
570
+ const index = content.indexOf(beforePattern);
571
+ if (index !== -1) {
572
+ const left = content.slice(0, index);
573
+ const right = content.slice(index);
574
+ // Normalize code: trim surrounding newlines and ensure single trailing newline
575
+ let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
576
+ // If right already starts with a newline, avoid double-blank by
577
+ // removing trailing newline from codeNormalized so only one newline remains
578
+ const rightStartsWithNewline = right.startsWith("\n");
579
+ if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
580
+ codeNormalized = codeNormalized.replace(/\n+$/, "");
581
+ }
582
+ const leftNeedsNewline = !left.endsWith("\n");
583
+ content = left + (leftNeedsNewline ? "\n" : "") + codeNormalized + right;
584
+ }
500
585
  }
501
586
  }
502
587
  break;
@@ -546,6 +631,8 @@ class AdvancedCodeGenerator {
546
631
  }
547
632
  }
548
633
  }
634
+ // Normalize excessive blank lines introduced during patching
635
+ content = content.replace(/\n{3,}/g, "\n\n");
549
636
  // Write back the modified content
550
637
  await fs.writeFile(filePath, content, "utf-8");
551
638
  }
@@ -620,8 +707,8 @@ class AdvancedCodeGenerator {
620
707
  if ((type === "framework" && name === selectedModules.framework) ||
621
708
  (type === "database" && name === selectedModules.database) ||
622
709
  (type === "auth" && name === selectedModules.auth)) {
623
- Object.assign(allDeps, generator.dependencies);
624
- Object.assign(allDevDeps, generator.devDependencies);
710
+ // Dependencies and devDependencies are now provided via `add-dependency`
711
+ // operations. Keep merging scripts from generator configs for now.
625
712
  Object.assign(allScripts, generator.scripts);
626
713
  }
627
714
  }
@@ -90,11 +90,24 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
90
90
  }
91
91
  }
92
92
  }
93
- if (generator.dependencies) {
94
- metadata.dependencies = { ...metadata.dependencies, ...generator.dependencies };
95
- }
96
- if (generator.devDependencies) {
97
- metadata.devDependencies = { ...metadata.devDependencies, ...generator.devDependencies };
93
+ // Collect dependencies/devDependencies from add-dependency operations
94
+ if (generator.operations && Array.isArray(generator.operations)) {
95
+ for (const operation of generator.operations) {
96
+ if (operation.type === "add-dependency") {
97
+ if (operation.dependencies) {
98
+ metadata.dependencies = {
99
+ ...metadata.dependencies,
100
+ ...operation.dependencies,
101
+ };
102
+ }
103
+ if (operation.devDependencies) {
104
+ metadata.devDependencies = {
105
+ ...metadata.devDependencies,
106
+ ...operation.devDependencies,
107
+ };
108
+ }
109
+ }
110
+ }
98
111
  }
99
112
  if (generator.postInstall && Array.isArray(generator.postInstall)) {
100
113
  metadata.postInstall = metadata.postInstall || [];
@@ -34,8 +34,7 @@
34
34
  "condition": { "database": "prisma" },
35
35
  "dependencies": {
36
36
  "@auth/prisma-adapter": "^0.5.0"
37
- },
38
- "devDependencies": {}
37
+ }
39
38
  },
40
39
  {
41
40
  "type": "add-env",
@@ -44,11 +43,12 @@
44
43
  "AUTH_GOOGLE_ID": "",
45
44
  "AUTH_GOOGLE_SECRET": ""
46
45
  }
46
+ },
47
+ {
48
+ "type": "add-dependency",
49
+ "dependencies": {
50
+ "next-auth": "^5.0.0-beta.30"
51
+ }
47
52
  }
48
- ],
49
- "dependencies": {
50
- "next-auth": "^5.0.0-beta.30"
51
- },
52
- "devDependencies": {},
53
- "scripts": {}
53
+ ]
54
54
  }
@@ -1,4 +1,4 @@
1
- import { betterAuth } from "better-auth";
1
+ import { betterAuth, env } from "better-auth";
2
2
  import { sendEmail } from "./email/email-service";
3
3
  import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from "./email/email-templates";
4
4
  {{#switch database}}
@@ -13,6 +13,12 @@ import { mongodbAdapter } from "better-auth/adapters/mongodb";
13
13
  {{/switch}}
14
14
 
15
15
  export async function initAuth() {
16
+ {{#if database == 'mongoose'}}
17
+ const mongooseInstance = await mongoose();
18
+ const client = mongooseInstance.connection.getClient();
19
+ const db = client.db();
20
+ {{/if}}
21
+
16
22
  return betterAuth({
17
23
  {{#switch database}}
18
24
  {{#case prisma}}
@@ -21,12 +27,10 @@ return betterAuth({
21
27
  }),
22
28
  {{/case}}
23
29
  {{#case mongoose}}
24
- const mongooseInstance = await mongoose();
25
- const client = mongooseInstance.connection.getClient();
26
- const db = client.db();
27
30
  database: mongodbAdapter(db, { client }),
28
31
  {{/case}}
29
32
  {{/switch}}
33
+ secret: env.BETTER_AUTH_SECRET,
30
34
  user: {
31
35
  additionalFields: {
32
36
  role: {
@@ -1,7 +1,7 @@
1
1
  import nodemailer from "nodemailer";
2
2
 
3
3
  // Create email transporter
4
- const transporter = nodemailer.createTransporter({
4
+ const transporter = nodemailer.createTransport({
5
5
  host: process.env.EMAIL_HOST,
6
6
  port: parseInt(process.env.EMAIL_PORT || "587"),
7
7
  secure: process.env.EMAIL_PORT === "465",
@@ -27,7 +27,6 @@ export const sendEmail = async ({ to, subject, text, html }: {
27
27
  html,
28
28
  });
29
29
  } catch (error) {
30
- // eslint-disable-next-line no-console
31
30
  console.error("Email sending failed:", error);
32
31
  throw error;
33
32
  }
@@ -51,7 +51,7 @@
51
51
  "operations": [
52
52
  {
53
53
  "type": "add-import",
54
- "imports": ["import { initAuth } from \"@/lib/auth\";"]
54
+ "imports": ["import { initAuth } from \"../lib/auth\";"]
55
55
  },
56
56
  {
57
57
  "type": "add-code",
@@ -67,7 +67,7 @@
67
67
  "operations": [
68
68
  {
69
69
  "type": "add-import",
70
- "imports": ["import { auth } from \"@/lib/auth\";",
70
+ "imports": ["import { auth } from \"../lib/auth\";",
71
71
  "import { toNodeHandler } from \"better-auth/node\";"]
72
72
  },
73
73
  {
@@ -97,12 +97,13 @@
97
97
  "EMAIL_USER": "",
98
98
  "EMAIL_PASS": "",
99
99
  "EMAIL_FROM": "noreply@yourapp.com"
100
+ }
101
+ },
102
+ {
103
+ "type": "add-dependency",
104
+ "dependencies": {
105
+ "better-auth": "^1.4.12"
100
106
  }
101
107
  }
102
- ],
103
- "dependencies": {
104
- "better-auth": "^1.4.12"
105
- },
106
- "devDependencies": {},
107
- "scripts": {}
108
+ ]
108
109
  }
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "type": "create-file",
8
8
  "source": "lib/mongoose.ts",
9
- "destination": "src/lib/mongoose.ts"
9
+ "destination": "lib/mongoose.ts"
10
10
  },
11
11
  {
12
12
  "type": "create-file",
@@ -20,7 +20,7 @@
20
20
  "operations": [
21
21
  {
22
22
  "type": "add-import",
23
- "imports": ["import { mongoose } from \"@/lib/mongoose\";"]
23
+ "imports": ["import { mongoose } from \"../lib/mongoose\";"]
24
24
  },
25
25
  {
26
26
  "type": "add-code",
@@ -34,10 +34,12 @@
34
34
  "envVars": {
35
35
  "MONGODB_URI": "mongodb://localhost:27017/database_name"
36
36
  }
37
+ },
38
+ {
39
+ "type": "add-dependency",
40
+ "dependencies": {
41
+ "mongoose": "^8.8.0"
42
+ }
37
43
  }
38
- ],
39
- "dependencies": {
40
- "mongoose": "^8.8.0"
41
- },
42
- "devDependencies": {}
44
+ ]
43
45
  }
@@ -91,17 +91,22 @@
91
91
  "envVars": {
92
92
  "DATABASE_URL": "mongodb://localhost:27017/database_name"
93
93
  }
94
+ },
95
+ {
96
+ "type": "add-dependency",
97
+ "dependencies": {
98
+ "dotenv": "^17.2.3"
99
+ }
100
+ },
101
+ {
102
+ "type": "add-script",
103
+ "scripts": {
104
+ "db:generate": "npx prisma generate",
105
+ "db:push": "npx prisma db push",
106
+ "db:seed": "tsx prisma/seed.ts",
107
+ "db:migrate": "npx prisma migrate dev",
108
+ "db:studio": "npx prisma studio"
109
+ }
94
110
  }
95
- ],
96
- "dependencies": {
97
- "dotenv": "^17.2.3"
98
- },
99
- "devDependencies": {},
100
- "scripts": {
101
- "db:generate": "npx prisma generate",
102
- "db:push": "npx prisma db push",
103
- "db:seed": "tsx prisma/seed.ts",
104
- "db:migrate": "npx prisma migrate dev",
105
- "db:studio": "npx prisma studio"
106
- }
111
+ ]
107
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackkit",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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": {