stackkit 0.2.2 → 0.2.3

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.
@@ -36,12 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AdvancedCodeGenerator = void 0;
37
37
  const fs = __importStar(require("fs-extra"));
38
38
  const path = __importStar(require("path"));
39
+ const shared_1 = require("../discovery/shared");
39
40
  const package_root_1 = require("../utils/package-root");
40
41
  const generator_utils_1 = require("./generator-utils");
41
42
  class AdvancedCodeGenerator {
42
43
  constructor(frameworkConfig) {
43
44
  this.generators = new Map();
44
45
  this.postInstallCommands = [];
46
+ this.createdFiles = [];
45
47
  this.frameworkConfig = frameworkConfig;
46
48
  }
47
49
  async loadGenerators(modulesPath) {
@@ -290,7 +292,10 @@ class AdvancedCodeGenerator {
290
292
  };
291
293
  // Set default prismaProvider if database is prisma but no provider specified
292
294
  if (selectedModules.database === "prisma" && !context.prismaProvider) {
293
- context.prismaProvider = "postgresql";
295
+ const providers = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
296
+ if (providers && providers.length > 0) {
297
+ context.prismaProvider = providers[0];
298
+ }
294
299
  }
295
300
  // Collect all applicable operations
296
301
  const applicableOperations = [];
@@ -340,6 +345,9 @@ class AdvancedCodeGenerator {
340
345
  await this.generatePackageJson(selectedModules, features, outputPath);
341
346
  return this.postInstallCommands;
342
347
  }
348
+ getCreatedFiles() {
349
+ return this.createdFiles.slice();
350
+ }
343
351
  async executeOperation(operation, context, outputPath) {
344
352
  // Process templates in operation content
345
353
  const processedOperation = this.processOperationTemplates(operation, context);
@@ -408,6 +416,81 @@ class AdvancedCodeGenerator {
408
416
  catch {
409
417
  // ignore
410
418
  }
419
+ // Ensure any top-level dotfiles declared in template.json are created
420
+ try {
421
+ const templateJsonPath = path.join(templatePath, "template.json");
422
+ if (await fs.pathExists(templateJsonPath)) {
423
+ const tpl = await fs.readJson(templateJsonPath);
424
+ if (tpl && Array.isArray(tpl.files)) {
425
+ for (const f of tpl.files) {
426
+ if (typeof f === "string" && f.startsWith(".")) {
427
+ const targetDest = path.join(outputPath, f);
428
+ if (await fs.pathExists(targetDest))
429
+ continue; // already present
430
+ // Special-case: allow creating .gitignore from non-dot fallbacks
431
+ if (f === ".gitignore") {
432
+ const nameWithoutDot = f.slice(1);
433
+ const candidates = [f, nameWithoutDot, `_${nameWithoutDot}`];
434
+ for (const cand of candidates) {
435
+ const src = path.join(templatePath, cand);
436
+ if (await fs.pathExists(src)) {
437
+ await fs.copy(src, targetDest);
438
+ break;
439
+ }
440
+ }
441
+ continue;
442
+ }
443
+ // For other dotfiles (e.g. .env.example), only create if the template
444
+ // actually contains a dotfile source. Don't synthesize from non-dot fallbacks
445
+ // to avoid creating files unintentionally.
446
+ const srcDot = path.join(templatePath, f);
447
+ if (await fs.pathExists(srcDot)) {
448
+ await fs.copy(srcDot, targetDest);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ catch {
456
+ // ignore failures here
457
+ }
458
+ try {
459
+ const templateJsonPath2 = path.join(templatePath, "template.json");
460
+ if (await fs.pathExists(templateJsonPath2)) {
461
+ const tpl = await fs.readJson(templateJsonPath2);
462
+ if (tpl && Array.isArray(tpl.files)) {
463
+ for (const f of tpl.files) {
464
+ if (typeof f === "string" && f.startsWith(".")) {
465
+ const dotDest = path.join(outputPath, f);
466
+ const nameWithoutDot = f.slice(1);
467
+ const nonDot = path.join(outputPath, nameWithoutDot);
468
+ const underscore = path.join(outputPath, `_${nameWithoutDot}`);
469
+ // If dot already exists, remove non-dot fallbacks
470
+ if (await fs.pathExists(dotDest)) {
471
+ if (await fs.pathExists(nonDot)) {
472
+ await fs.remove(nonDot);
473
+ }
474
+ if (await fs.pathExists(underscore)) {
475
+ await fs.remove(underscore);
476
+ }
477
+ continue;
478
+ }
479
+ // If dot doesn't exist but a non-dot fallback was copied, rename it
480
+ if (await fs.pathExists(nonDot)) {
481
+ await fs.move(nonDot, dotDest, { overwrite: true });
482
+ }
483
+ else if (await fs.pathExists(underscore)) {
484
+ await fs.move(underscore, dotDest, { overwrite: true });
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ catch {
492
+ // ignore
493
+ }
411
494
  }
412
495
  }
413
496
  processOperationTemplates(operation, context) {
@@ -481,6 +564,14 @@ class AdvancedCodeGenerator {
481
564
  }
482
565
  // Write destination file
483
566
  await fs.writeFile(destinationPath, content, "utf-8");
567
+ try {
568
+ const rel = path.relative(outputPath, destinationPath);
569
+ if (rel && !this.createdFiles.includes(rel))
570
+ this.createdFiles.push(rel);
571
+ }
572
+ catch {
573
+ // ignore logging failures
574
+ }
484
575
  }
485
576
  async executePatchFile(operation, context, outputPath) {
486
577
  if (!operation.destination)
@@ -755,7 +846,10 @@ class AdvancedCodeGenerator {
755
846
  features,
756
847
  };
757
848
  if (selectedModules.database === "prisma" && !context.prismaProvider) {
758
- context.prismaProvider = "postgresql";
849
+ const providers = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
850
+ if (providers && providers.length > 0) {
851
+ context.prismaProvider = providers[0];
852
+ }
759
853
  }
760
854
  const applicableOperations = [];
761
855
  for (const [key, generator] of this.generators) {
@@ -1,5 +1,5 @@
1
- import type { GeneratorConfig } from "./code-generator";
2
1
  import type { ModuleMetadata } from "../../types";
2
+ import type { GeneratorConfig } from "./code-generator";
3
3
  export declare function mergeModuleIntoGeneratorConfig(config: GeneratorConfig, modulePath: string): Promise<GeneratorConfig>;
4
4
  export declare function mergeGeneratorIntoModuleMetadata(metadata: ModuleMetadata, modulePath: string): Promise<ModuleMetadata>;
5
5
  export declare function locateOperationSource(generatorType: string, generatorName: string, sourceRel: string): string | null;
@@ -78,11 +78,13 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
78
78
  if (generator.operations && Array.isArray(generator.operations)) {
79
79
  for (const operation of generator.operations) {
80
80
  if (operation.type === "add-env" && operation.envVars) {
81
- metadata.envVars = metadata.envVars || [];
81
+ if (!Array.isArray(metadata.envVars))
82
+ metadata.envVars = [];
83
+ const arr = metadata.envVars;
82
84
  for (const [key, value] of Object.entries(operation.envVars)) {
83
- metadata.envVars.push({
85
+ arr.push({
84
86
  key,
85
- value: value,
87
+ value: String(value),
86
88
  description: `Environment variable for ${key}`,
87
89
  required: true,
88
90
  });
@@ -8,35 +8,68 @@ exports.getRouterBasePath = getRouterBasePath;
8
8
  exports.getLibPath = getLibPath;
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const installed_detection_1 = require("../discovery/installed-detection");
12
+ const package_root_1 = require("../utils/package-root");
11
13
  async function detectProjectInfo(targetDir) {
12
14
  const packageJsonPath = path_1.default.join(targetDir, "package.json");
13
15
  if (!(await fs_extra_1.default.pathExists(packageJsonPath))) {
14
16
  throw new Error("No package.json found. This does not appear to be a Node.js project.");
15
17
  }
16
18
  const packageJson = await fs_extra_1.default.readJSON(packageJsonPath);
17
- // Detect framework
18
- const isNextJs = packageJson.dependencies?.next || packageJson.devDependencies?.next;
19
- const isExpress = packageJson.dependencies?.express || packageJson.devDependencies?.express;
20
- const isReact = packageJson.dependencies?.react || packageJson.devDependencies?.react;
21
- const isVite = packageJson.dependencies?.vite || packageJson.devDependencies?.vite;
22
- let framework;
23
- if (isNextJs) {
24
- framework = "nextjs";
25
- }
26
- else if (isExpress) {
27
- framework = "express";
28
- }
29
- else if (isReact && isVite) {
30
- framework = "react";
19
+ // Detect framework by matching available templates' characteristic files
20
+ // Framework is dynamic and driven by templates; keep as string for discovery
21
+ let framework = "unknown";
22
+ try {
23
+ const templatesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "templates");
24
+ if (await fs_extra_1.default.pathExists(templatesDir)) {
25
+ const dirs = await fs_extra_1.default.readdir(templatesDir);
26
+ let bestMatch = null;
27
+ for (const d of dirs) {
28
+ const tplPath = path_1.default.join(templatesDir, d, "template.json");
29
+ if (!(await fs_extra_1.default.pathExists(tplPath)))
30
+ continue;
31
+ try {
32
+ const tpl = await fs_extra_1.default.readJSON(tplPath);
33
+ const files = Array.isArray(tpl.files) ? tpl.files : [];
34
+ let score = 0;
35
+ for (const f of files) {
36
+ const candidate = path_1.default.join(targetDir, f.replace(/\\/g, "/"));
37
+ if (await fs_extra_1.default.pathExists(candidate))
38
+ score++;
39
+ }
40
+ if (!bestMatch || score > bestMatch.score)
41
+ bestMatch = { name: d, score };
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ }
47
+ if (bestMatch && bestMatch.score > 0) {
48
+ // Use the template folder name as the framework identifier
49
+ framework = bestMatch.name;
50
+ }
51
+ }
31
52
  }
32
- else if (isReact) {
33
- framework = "react";
53
+ catch {
54
+ // fall back to dependency heuristics below
34
55
  }
35
- else {
36
- framework = "unknown";
56
+ // Fallback: simple dependency-based detection
57
+ if (framework === "unknown") {
58
+ const isNextJs = packageJson.dependencies?.next || packageJson.devDependencies?.next;
59
+ const isExpress = packageJson.dependencies?.express || packageJson.devDependencies?.express;
60
+ const isReact = packageJson.dependencies?.react || packageJson.devDependencies?.react;
61
+ const isVite = packageJson.dependencies?.vite || packageJson.devDependencies?.vite;
62
+ if (isNextJs)
63
+ framework = "nextjs";
64
+ else if (isExpress)
65
+ framework = "express";
66
+ else if (isReact && isVite)
67
+ framework = "react";
68
+ else if (isReact)
69
+ framework = "react";
37
70
  }
38
71
  if (framework === "unknown") {
39
- throw new Error("Only Next.js, Express, and React projects are currently supported.");
72
+ throw new Error("Unsupported project type or unable to detect framework from templates.");
40
73
  }
41
74
  // Detect router type (only for Next.js)
42
75
  let router = "unknown";
@@ -79,16 +112,13 @@ async function detectProjectInfo(targetDir) {
79
112
  else if (bunLockExists) {
80
113
  packageManager = "bun";
81
114
  }
82
- // Check for existing integrations
83
- const hasAuth = !!(packageJson.dependencies?.["next-auth"] ||
84
- packageJson.dependencies?.["better-auth"] ||
85
- packageJson.dependencies?.["@auth/core"] ||
86
- packageJson.dependencies?.["@kinde-oss/kinde-auth-nextjs"]);
87
- const hasPrisma = !!(packageJson.dependencies?.["@prisma/client"] || packageJson.devDependencies?.["prisma"]);
88
- const hasDatabase = hasPrisma ||
89
- !!(packageJson.dependencies?.["mongoose"] ||
90
- packageJson.dependencies?.["typeorm"] ||
91
- packageJson.dependencies?.["drizzle-orm"]);
115
+ // Detect installed modules by comparing project dependencies against
116
+ // declared dependencies in `modules/*/generator.json` and `module.json`.
117
+ const detectedAuth = await (0, installed_detection_1.detectAuthModules)(packageJson);
118
+ const detectedDbs = await (0, installed_detection_1.detectDatabaseModules)(packageJson);
119
+ const hasAuth = detectedAuth.length > 0;
120
+ const hasPrisma = detectedDbs.includes("prisma");
121
+ const hasDatabase = hasPrisma || detectedDbs.length > 0;
92
122
  return {
93
123
  framework,
94
124
  router,
@@ -7,32 +7,36 @@ export interface TemplateMetadata {
7
7
  features: string[];
8
8
  }
9
9
  export interface ModuleMetadata {
10
- name: string;
11
- displayName: string;
12
- description: string;
13
- category: "auth" | "database" | "ui" | "other";
14
- supportedFrameworks: string[];
15
- dependencies: {
10
+ name?: string;
11
+ displayName?: string;
12
+ description?: string;
13
+ category?: string;
14
+ provider?: string;
15
+ supportedFrameworks?: string[];
16
+ frameworkConfigs?: Record<string, unknown>;
17
+ dependencies?: Record<string, unknown> | {
16
18
  common?: Record<string, string>;
17
19
  providers?: Record<string, Record<string, string>>;
18
- } | Record<string, string>;
19
- devDependencies?: {
20
+ };
21
+ devDependencies?: Record<string, unknown> | {
20
22
  common?: Record<string, string>;
21
23
  providers?: Record<string, Record<string, string>>;
22
- } | Record<string, string>;
23
- envVars: EnvVar[];
24
- patches: ModulePatch[];
24
+ };
25
+ envVars?: EnvVar[] | Record<string, string>;
26
+ patches?: ModulePatch[];
25
27
  frameworkPatches?: Record<string, {
26
28
  [file: string]: {
27
29
  merge?: Record<string, unknown>;
28
30
  };
29
31
  }>;
30
32
  postInstall?: string[];
31
- frameworkConfigs?: Record<string, {
32
- dependencies?: Record<string, string>;
33
- devDependencies?: Record<string, string>;
34
- patches?: ModulePatch[];
35
- }>;
33
+ compatibility?: {
34
+ databases?: string[];
35
+ auth?: string[];
36
+ languages?: string[];
37
+ };
38
+ files?: string[];
39
+ scripts?: Record<string, string>;
36
40
  }
37
41
  export interface EnvVar {
38
42
  key: string;
@@ -64,7 +68,7 @@ export interface ModifyJsonPatch extends ModulePatch {
64
68
  }[];
65
69
  }
66
70
  export interface ProjectInfo {
67
- framework: "nextjs" | "express" | "react" | "unknown";
71
+ framework: string;
68
72
  router: "app" | "pages" | "unknown";
69
73
  language: "ts" | "js";
70
74
  packageManager: "npm" | "yarn" | "pnpm" | "bun";
@@ -1,4 +1,4 @@
1
- import { betterAuth, env } from "better-auth";
1
+ import { betterAuth } from "better-auth";
2
2
  import { sendEmail } from "./email/email-service";
3
3
  import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from "./email/email-templates";
4
4
  {{#switch database}}
@@ -32,7 +32,7 @@ return betterAuth({
32
32
  {{/switch}}
33
33
  baseURL: process.env.BETTER_AUTH_URL,
34
34
  secret: process.env.BETTER_AUTH_SECRET,
35
- trustedOrigins: [process.env.APP_URL],
35
+ trustedOrigins: [process.env.APP_URL!],
36
36
  user: {
37
37
  additionalFields: {
38
38
  role: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackkit",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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": {
@@ -25,8 +25,8 @@
25
25
  "scripts": {
26
26
  "dev": "tsc --watch",
27
27
  "build": "npm run clean && tsc && npm run copy-assets",
28
- "copy-assets": "cp -r ../../templates . && cp -r ../../modules .",
29
- "clean": "rm -rf dist templates modules",
28
+ "copy-assets": "node ./scripts/copy-assets.js",
29
+ "clean": "node ./scripts/clean.js",
30
30
  "typecheck": "tsc --noEmit",
31
31
  "lint": "eslint src --ext .ts",
32
32
  "lint:fix": "eslint src --ext .ts --fix",
@@ -52,6 +52,9 @@
52
52
  ],
53
53
  "author": "Tariqul Islam",
54
54
  "license": "MIT",
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
55
58
  "dependencies": {
56
59
  "chalk": "^4.1.2",
57
60
  "commander": "^12.0.0",
@@ -0,0 +1,2 @@
1
+ PORT=3000
2
+ NODE_ENV=development
@@ -0,0 +1,23 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+
7
+ # Environment
8
+ .env*
9
+ !.env.example
10
+
11
+ # Logs
12
+ logs/
13
+ *.log
14
+ npm-debug.log*
15
+ yarn-debug.log*
16
+ yarn-error.log*
17
+
18
+ # OS
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # TypeScript
23
+ *.tsbuildinfo
@@ -0,0 +1 @@
1
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
@@ -0,0 +1,42 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files
34
+ .env*
35
+ !.env.example
36
+
37
+ # vercel
38
+ .vercel
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts
@@ -0,0 +1 @@
1
+ VITE_API_URL=http://localhost:3000
@@ -0,0 +1,44 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ # Dependencies
11
+ node_modules
12
+
13
+ # Build outputs
14
+ dist
15
+ dist-ssr
16
+ build
17
+ *.local
18
+
19
+ # Environment
20
+ .env*
21
+ !.env.example
22
+
23
+ # Editor directories and files
24
+ .vscode/*
25
+ !.vscode/extensions.json
26
+ !.vscode/settings.json
27
+ .idea
28
+ .DS_Store
29
+ *.suo
30
+ *.ntvs*
31
+ *.njsproj
32
+ *.sln
33
+ *.sw?
34
+
35
+ # Testing
36
+ coverage
37
+ *.lcov
38
+ .nyc_output
39
+
40
+ # Misc
41
+ .cache
42
+ .turbo
43
+ .vercel
44
+ .netlify
@@ -0,0 +1,4 @@
1
+ dist
2
+ node_modules
3
+ coverage
4
+ *.log
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "all",
4
+ "singleQuote": false,
5
+ "printWidth": 100,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "arrowParens": "always"
9
+ }
@@ -1,3 +1,3 @@
1
1
  export function cn(...classes: (string | boolean | undefined | null)[]): string {
2
2
  return classes.filter(Boolean).join(" ");
3
- }
3
+ }