better-ts-stack 1.1.4 → 2.0.1

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -82
  3. package/dist/args.js +90 -0
  4. package/dist/builder/buildPlanner.js +71 -0
  5. package/dist/builder/configGenerator.js +19 -10
  6. package/dist/builder/dependencyInstaller.js +8 -15
  7. package/dist/builder/fileProcessor.js +47 -44
  8. package/dist/builder/gitInitializer.js +21 -35
  9. package/dist/builder/index.js +33 -91
  10. package/dist/builder/moduleSelector.js +11 -16
  11. package/dist/builder/templateContext.js +6 -27
  12. package/dist/index.js +47 -21
  13. package/dist/modules/registry.js +4 -6
  14. package/dist/output/nextSteps.js +10 -20
  15. package/dist/prompts/index.js +22 -59
  16. package/dist/prompts/stack.js +85 -71
  17. package/dist/types/index.js +47 -41
  18. package/dist/ui/index.js +18 -0
  19. package/dist/ui/messages.js +124 -0
  20. package/dist/ui/prompts.js +70 -0
  21. package/dist/ui/theme.js +23 -0
  22. package/dist/validators/index.js +15 -40
  23. package/package.json +71 -66
  24. package/templates/backend/express/.prettierrc +2 -2
  25. package/templates/backend/express/config.json +40 -44
  26. package/templates/backend/express/src/{index.ts → index.ts.hbs} +75 -76
  27. package/templates/backend/nestjs/.prettierignore +52 -0
  28. package/templates/backend/nestjs/.prettierrc +16 -0
  29. package/templates/backend/nestjs/config.json +41 -0
  30. package/templates/backend/nestjs/eslint.config.mjs +40 -0
  31. package/templates/backend/nestjs/gitignore +36 -0
  32. package/templates/backend/nestjs/nest-cli.json +9 -0
  33. package/templates/backend/nestjs/src/app.module.ts.hbs +34 -0
  34. package/templates/backend/nestjs/src/health/health.controller.ts +12 -0
  35. package/templates/backend/nestjs/src/main.ts +23 -0
  36. package/templates/backend/nestjs/tsconfig.eslint.json +9 -0
  37. package/templates/backend/nestjs/tsconfig.json +25 -0
  38. package/templates/frontend/nextjs/app/layout.tsx +13 -13
  39. package/templates/frontend/nextjs/app/page.tsx.hbs +97 -97
  40. package/templates/frontend/nextjs/components/ui/button.tsx +51 -51
  41. package/templates/frontend/nextjs/components/ui/card.tsx +60 -60
  42. package/templates/frontend/nextjs/components/ui/field.tsx +67 -67
  43. package/templates/frontend/nextjs/components/ui/input.tsx +18 -18
  44. package/templates/frontend/nextjs/config.json +37 -38
  45. package/templates/frontend/nextjs/eslint.config.mjs +11 -11
  46. package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
  47. package/templates/frontend/tanstack/config.json +45 -48
  48. package/templates/modules/auth/express/config.json +20 -26
  49. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +43 -57
  50. package/templates/modules/auth/nestjs/config.json +20 -0
  51. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -0
  52. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -0
  53. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -0
  54. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -0
  55. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -0
  56. package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -0
  57. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -0
  58. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -0
  59. package/templates/modules/auth/nextjs/config.json +18 -31
  60. package/templates/modules/auth/tanstack/config.json +2 -16
  61. package/templates/modules/docker/Dockerfile.hbs +135 -133
  62. package/templates/modules/docker/config.json +15 -19
  63. package/templates/modules/drizzle/express/config.json +26 -27
  64. package/templates/modules/drizzle/nestjs/config.json +26 -0
  65. package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -0
  66. package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -0
  67. package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -0
  68. package/templates/modules/drizzle/nextjs/config.json +26 -29
  69. package/templates/modules/drizzle/tanstack/config.json +5 -5
  70. package/templates/modules/mongoose/express/config.json +15 -16
  71. package/templates/modules/mongoose/nestjs/config.json +15 -0
  72. package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -0
  73. package/templates/modules/mongoose/nextjs/config.json +15 -16
  74. package/templates/modules/mongoose/tanstack/config.json +16 -17
  75. package/templates/modules/prisma/express/config.json +27 -28
  76. package/templates/modules/prisma/nestjs/config.json +27 -0
  77. package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -0
  78. package/templates/modules/prisma/nestjs/prisma.config.ts +12 -0
  79. package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -0
  80. package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -0
  81. package/templates/modules/prisma/nextjs/config.json +28 -29
  82. package/templates/modules/prisma/tanstack/config.json +5 -6
  83. package/dist/builder/configGenerator.d.ts +0 -7
  84. package/dist/builder/configGenerator.d.ts.map +0 -1
  85. package/dist/builder/configGenerator.js.map +0 -1
  86. package/dist/builder/dependencyInstaller.d.ts +0 -3
  87. package/dist/builder/dependencyInstaller.d.ts.map +0 -1
  88. package/dist/builder/dependencyInstaller.js.map +0 -1
  89. package/dist/builder/fileProcessor.d.ts +0 -6
  90. package/dist/builder/fileProcessor.d.ts.map +0 -1
  91. package/dist/builder/fileProcessor.js.map +0 -1
  92. package/dist/builder/gitInitializer.d.ts +0 -2
  93. package/dist/builder/gitInitializer.d.ts.map +0 -1
  94. package/dist/builder/gitInitializer.js.map +0 -1
  95. package/dist/builder/index.d.ts +0 -3
  96. package/dist/builder/index.d.ts.map +0 -1
  97. package/dist/builder/index.js.map +0 -1
  98. package/dist/builder/moduleSelector.d.ts +0 -9
  99. package/dist/builder/moduleSelector.d.ts.map +0 -1
  100. package/dist/builder/moduleSelector.js.map +0 -1
  101. package/dist/builder/templateContext.d.ts +0 -21
  102. package/dist/builder/templateContext.d.ts.map +0 -1
  103. package/dist/builder/templateContext.js.map +0 -1
  104. package/dist/index.d.ts +0 -2
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js.map +0 -1
  107. package/dist/intro.d.ts +0 -2
  108. package/dist/intro.d.ts.map +0 -1
  109. package/dist/intro.js +0 -27
  110. package/dist/intro.js.map +0 -1
  111. package/dist/modules/registry.d.ts +0 -6
  112. package/dist/modules/registry.d.ts.map +0 -1
  113. package/dist/modules/registry.js.map +0 -1
  114. package/dist/output/nextSteps.d.ts +0 -4
  115. package/dist/output/nextSteps.d.ts.map +0 -1
  116. package/dist/output/nextSteps.js.map +0 -1
  117. package/dist/prompts/index.d.ts +0 -4
  118. package/dist/prompts/index.d.ts.map +0 -1
  119. package/dist/prompts/index.js.map +0 -1
  120. package/dist/prompts/stack.d.ts +0 -3
  121. package/dist/prompts/stack.d.ts.map +0 -1
  122. package/dist/prompts/stack.js.map +0 -1
  123. package/dist/types/index.d.ts +0 -159
  124. package/dist/types/index.d.ts.map +0 -1
  125. package/dist/types/index.js.map +0 -1
  126. package/dist/validators/index.d.ts +0 -5
  127. package/dist/validators/index.d.ts.map +0 -1
  128. package/dist/validators/index.js.map +0 -1
  129. package/templates/backend/express/.eslintrc.js +0 -24
@@ -1,77 +1,91 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ask = ask;
6
4
  exports.collectStackChoices = collectStackChoices;
7
- const prompts_1 = require("@clack/prompts");
8
- const consola_1 = __importDefault(require("consola"));
5
+ const core_1 = require("@clack/core");
9
6
  const types_1 = require("../types");
10
- async function collectStackChoices(applicationType) {
11
- return (0, prompts_1.group)({
12
- framework: async () => {
13
- if (applicationType === "backend") {
14
- while (true) {
15
- const selection = await (0, prompts_1.select)({
16
- message: "Select a backend framework:",
17
- options: types_1.backendFrameworkOptions,
18
- initialValue: "express",
19
- });
20
- if ((0, prompts_1.isCancel)(selection) || selection !== "nestjs")
21
- return selection;
22
- consola_1.default.warn("This framework is coming soon! Please select another option.");
23
- }
24
- }
25
- return (0, prompts_1.select)({
26
- message: "Select a frontend framework:",
27
- options: types_1.frontendFrameworkOptions,
28
- initialValue: "nextjs",
29
- });
30
- },
31
- databaseType: () => (0, prompts_1.select)({
32
- message: "Select a database:",
33
- options: types_1.databaseTypeOptions,
34
- initialValue: "none",
35
- }),
36
- orm: async ({ results }) => {
37
- if (results.databaseType === "none")
38
- return "none";
39
- const options = results.databaseType === "mongodb"
40
- ? types_1.mongodbOrmOptions
41
- : types_1.postgresqlOrmOptions;
42
- return (0, prompts_1.select)({
43
- message: `Select an ORM for ${results.databaseType}:`,
44
- options,
45
- initialValue: options[0].value,
46
- });
47
- },
48
- packageManager: () => (0, prompts_1.select)({
49
- message: "Select a package manager:",
50
- options: types_1.packageManagerOptions,
51
- initialValue: "npm",
52
- }),
53
- useDocker: () => (0, prompts_1.confirm)({ message: "Use Docker?", initialValue: false }),
54
- useAuth: async ({ results }) => {
55
- if (applicationType === "fullstack" && results.databaseType === "none")
56
- return false;
57
- return (0, prompts_1.select)({
58
- message: applicationType === "fullstack"
59
- ? "Add Better Auth?"
60
- : "Add authentication?",
61
- options: types_1.authOptions,
62
- initialValue: false,
63
- });
64
- },
65
- initGit: () => (0, prompts_1.confirm)({
66
- message: "Initialize a git repository?",
67
- initialValue: true,
68
- }),
69
- installDeps: () => (0, prompts_1.confirm)({ message: "Install dependencies now?", initialValue: false }),
70
- }, {
71
- onCancel: () => {
72
- (0, prompts_1.cancel)("Operation cancelled.");
73
- process.exit(0);
74
- },
7
+ const ui_1 = require("../ui");
8
+ // Returns the preset value when there is one, otherwise asks the user
9
+ async function ask(preset, prompt) {
10
+ if (preset !== undefined)
11
+ return preset;
12
+ // Without a terminal a prompt reads end-of-input and looks like a cancel
13
+ if (!process.stdin.isTTY) {
14
+ throw (0, types_1.buildError)("Cannot ask questions without an interactive terminal. Pass --yes, or give every option as a flag (see --help)", "NOT_INTERACTIVE");
15
+ }
16
+ const answer = await prompt();
17
+ if ((0, core_1.isCancel)(answer)) {
18
+ (0, ui_1.info)("Cancelled. Nothing was created.");
19
+ process.exit(0);
20
+ }
21
+ return answer;
22
+ }
23
+ async function askFramework() {
24
+ const applicationType = await (0, ui_1.select)({
25
+ message: "Application type",
26
+ options: types_1.applicationTypeOptions,
27
+ initialValue: "backend",
28
+ });
29
+ if ((0, core_1.isCancel)(applicationType))
30
+ return applicationType;
31
+ return applicationType === "backend"
32
+ ? (0, ui_1.select)({
33
+ message: "Backend framework",
34
+ options: types_1.backendFrameworkOptions,
35
+ initialValue: "express",
36
+ })
37
+ : (0, ui_1.select)({
38
+ message: "Frontend framework",
39
+ options: types_1.frontendFrameworkOptions,
40
+ initialValue: "nextjs",
41
+ });
42
+ }
43
+ async function askDatabase() {
44
+ const system = await (0, ui_1.select)({
45
+ message: "Database",
46
+ options: types_1.databaseSystemOptions,
47
+ initialValue: "none",
75
48
  });
49
+ if ((0, core_1.isCancel)(system) || system === "none")
50
+ return system;
51
+ const options = system === "mongodb" ? types_1.mongodbOrmOptions : types_1.postgresqlOrmOptions;
52
+ return (0, ui_1.select)({
53
+ message: "ORM",
54
+ options,
55
+ initialValue: options[0].value,
56
+ });
57
+ }
58
+ // Collects the stack choices, prompting only for those missing from the preset
59
+ async function collectStackChoices(preset) {
60
+ const framework = await ask(preset.framework, askFramework);
61
+ const database = await ask(preset.database, askDatabase);
62
+ const packageManager = await ask(preset.packageManager, () => (0, ui_1.select)({
63
+ message: "Package manager",
64
+ options: types_1.packageManagerOptions,
65
+ initialValue: "npm",
66
+ }));
67
+ const useDocker = await ask(preset.useDocker, () => (0, ui_1.confirm)({ message: "Add Docker?", initialValue: false }));
68
+ // Better Auth stores users and sessions, so full-stack auth needs a database
69
+ const isFullstack = (0, types_1.getApplicationType)(framework) === "fullstack";
70
+ const isAuthAvailable = !isFullstack || database !== "none";
71
+ if (preset.useAuth && !isAuthAvailable) {
72
+ throw (0, types_1.buildError)(`Auth for ${framework} needs a database. Add --database prisma, drizzle, or mongoose`, "INVALID_ARGUMENT");
73
+ }
74
+ const useAuth = isAuthAvailable
75
+ ? await ask(preset.useAuth, () => (0, ui_1.confirm)({
76
+ message: isFullstack ? "Add Better Auth?" : "Add JWT authentication?",
77
+ initialValue: false,
78
+ }))
79
+ : false;
80
+ const initGit = await ask(preset.initGit, () => (0, ui_1.confirm)({ message: "Initialize a git repository?", initialValue: true }));
81
+ const installDeps = await ask(preset.installDeps, () => (0, ui_1.confirm)({ message: "Install dependencies now?", initialValue: false }));
82
+ return {
83
+ framework,
84
+ database,
85
+ packageManager,
86
+ useDocker,
87
+ useAuth,
88
+ initGit,
89
+ installDeps,
90
+ };
76
91
  }
77
- //# sourceMappingURL=stack.js.map
@@ -1,77 +1,83 @@
1
1
  "use strict";
2
2
  // Core type definitions for the CLI builder
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.authOptions = exports.packageManagerOptions = exports.databaseOptions = exports.postgresqlOrmOptions = exports.mongodbOrmOptions = exports.databaseTypeOptions = exports.frontendFrameworkOptions = exports.backendFrameworkOptions = exports.applicationTypeOptions = void 0;
5
- exports.deriveDatabase = deriveDatabase;
4
+ exports.packageManagerOptions = exports.postgresqlOrmOptions = exports.mongodbOrmOptions = exports.databaseSystemOptions = exports.frontendFrameworkOptions = exports.backendFrameworkOptions = exports.applicationTypeOptions = exports.DEFAULT_PORT = exports.DEFAULT_CHOICES = void 0;
6
5
  exports.buildError = buildError;
7
- /**
8
- * Derives the database option from database type and ORM selection.
9
- * Returns "none" if either is "none", otherwise returns the ORM which
10
- * must be a valid DatabaseOption (prisma, mongoose, or drizzle).
11
- */
12
- function deriveDatabase(databaseType, orm) {
13
- if (databaseType === "none" || orm === "none") {
14
- return "none";
15
- }
16
- // At this point, orm must be one of: prisma, mongoose, drizzle
17
- // which are all valid DatabaseOption values
18
- return orm;
19
- }
6
+ exports.isBuildError = isBuildError;
7
+ exports.getApplicationType = getApplicationType;
8
+ // Used for every choice not given as a flag when --yes is passed
9
+ exports.DEFAULT_CHOICES = {
10
+ framework: "express",
11
+ database: "none",
12
+ useDocker: false,
13
+ useAuth: false,
14
+ packageManager: "npm",
15
+ initGit: true,
16
+ installDeps: false,
17
+ };
18
+ // Port the generated server listens on
19
+ exports.DEFAULT_PORT = 3000;
20
20
  // Unified error creation function
21
- function buildError(errorOrMessage, code, context, exitCode = 1) {
21
+ function buildError(errorOrMessage, code, context) {
22
22
  const message = errorOrMessage instanceof Error
23
23
  ? errorOrMessage.message
24
24
  : String(errorOrMessage);
25
25
  const fullMessage = context ? `${context}: ${message}` : message;
26
26
  const error = new Error(fullMessage);
27
- return Object.assign(error, { code, exitCode });
27
+ return Object.assign(error, { code });
28
+ }
29
+ // True for errors created by buildError(), whose message is meant for the user
30
+ function isBuildError(error) {
31
+ return error instanceof Error && "code" in error;
28
32
  }
29
33
  // Type-safe option definitions
30
34
  exports.applicationTypeOptions = [
31
- { value: "backend", label: "Backend API (Express/NestJS)" },
32
- { value: "fullstack", label: "Full-stack App (Next.js/TanStack)" },
35
+ {
36
+ value: "backend",
37
+ label: "Backend API",
38
+ hint: "Express or NestJS",
39
+ },
40
+ {
41
+ value: "fullstack",
42
+ label: "Full-stack app",
43
+ hint: "Next.js or TanStack Start",
44
+ },
33
45
  ];
34
46
  exports.backendFrameworkOptions = [
35
47
  { value: "express", label: "Express" },
36
- { value: "nestjs", label: "NestJS", hint: "(Coming Soon)" },
48
+ { value: "nestjs", label: "NestJS" },
37
49
  ];
38
50
  exports.frontendFrameworkOptions = [
39
- { value: "nextjs", label: "Next.js 16 (App Router)" },
51
+ { value: "nextjs", label: "Next.js 16", hint: "App Router" },
40
52
  {
41
53
  value: "tanstack",
42
- label: "TanStack Start (React)",
43
- hint: "(Release Candidate)",
54
+ label: "TanStack Start",
55
+ hint: "React, release candidate",
44
56
  },
45
57
  ];
46
- // Database type options (what database system)
47
- exports.databaseTypeOptions = [
48
- { value: "none", label: "None (Skip database setup)" },
58
+ // Database system options; the ORM is chosen in a second step
59
+ exports.databaseSystemOptions = [
60
+ { value: "none", label: "None", hint: "skip database setup" },
49
61
  { value: "mongodb", label: "MongoDB" },
50
62
  { value: "postgresql", label: "PostgreSQL" },
51
63
  ];
52
64
  // ORM options for MongoDB
53
65
  exports.mongodbOrmOptions = [
54
- { value: "mongoose", label: "Mongoose (MongoDB ODM)" },
66
+ { value: "mongoose", label: "Mongoose", hint: "MongoDB ODM" },
55
67
  ];
56
68
  // ORM options for PostgreSQL
57
69
  exports.postgresqlOrmOptions = [
58
- { value: "prisma", label: "Prisma (Type-safe ORM)" },
59
- { value: "drizzle", label: "Drizzle (Lightweight ORM)" },
60
- ];
61
- // Legacy database options (for backward compatibility)
62
- exports.databaseOptions = [
63
- { value: "none", label: "None (Skip database setup)" },
64
- { value: "prisma", label: "Prisma (Type-safe ORM)" },
65
- { value: "mongoose", label: "Mongoose (Standard MongoDB ODM)" },
66
- { value: "drizzle", label: "Drizzle (Lightweight ORM)" },
70
+ { value: "prisma", label: "Prisma", hint: "type-safe ORM" },
71
+ { value: "drizzle", label: "Drizzle", hint: "lightweight ORM" },
67
72
  ];
68
73
  exports.packageManagerOptions = [
69
74
  { value: "npm", label: "npm" },
70
75
  { value: "pnpm", label: "pnpm" },
71
76
  { value: "bun", label: "bun" },
72
77
  ];
73
- exports.authOptions = [
74
- { value: false, label: "No" },
75
- { value: true, label: "Yes" },
76
- ];
77
- //# sourceMappingURL=index.js.map
78
+ // Backend frameworks produce an API server, the others a full-stack app
79
+ function getApplicationType(framework) {
80
+ return exports.backendFrameworkOptions.some((option) => option.value === framework)
81
+ ? "backend"
82
+ : "fullstack";
83
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./messages"), exports);
18
+ __exportStar(require("./prompts"), exports);
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.error = exports.warn = exports.info = exports.success = void 0;
7
+ exports.showBanner = showBanner;
8
+ exports.showSummary = showSummary;
9
+ exports.showPlan = showPlan;
10
+ exports.showNextSteps = showNextSteps;
11
+ // Everything the CLI prints outside of prompts
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const node_process_1 = require("node:process");
14
+ const types_1 = require("../types");
15
+ const theme_1 = require("./theme");
16
+ // "better-ts-stack" in the figlet Big font
17
+ const WORDMARK = [
18
+ " _ _ _ _ _ _",
19
+ " | | | | | | | | | | | |",
20
+ " | |__ ___| |_| |_ ___ _ __ ______| |_ ___ ______ ___| |_ __ _ ___| | __",
21
+ " | '_ \\ / _ \\ __| __/ _ \\ '__|______| __/ __|______/ __| __/ _` |/ __| |/ /",
22
+ " | |_) | __/ |_| || __/ | | |_\\__ \\ \\__ \\ || (_| | (__| <",
23
+ " |_.__/ \\___|\\__|\\__\\___|_| \\__|___/ |___/\\__\\__,_|\\___|_|\\_\\",
24
+ ];
25
+ function write(line = "") {
26
+ process.stdout.write(`${line}\n`);
27
+ }
28
+ const success = (message) => write(`${(0, theme_1.lime)("✓")} ${message}`);
29
+ exports.success = success;
30
+ const info = (message) => write((0, theme_1.dim)(`· ${message}`));
31
+ exports.info = info;
32
+ const warn = (message) => write(`${(0, theme_1.amber)("!")} ${message}`);
33
+ exports.warn = warn;
34
+ const error = (message) => write(`${(0, theme_1.red)("×")} ${message}`);
35
+ exports.error = error;
36
+ const WORDMARK_WIDTH = Math.max(...WORDMARK.map((line) => line.length));
37
+ function greeting() {
38
+ const hour = new Date().getHours();
39
+ if (hour < 12)
40
+ return "Good morning.";
41
+ if (hour < 18)
42
+ return "Good afternoon.";
43
+ return "Good evening.";
44
+ }
45
+ // Opens the session with the wordmark, tagline, and key hints. An interactive
46
+ // session starts on a cleared screen; console.clear() does nothing when piped
47
+ function showBanner(version, isInteractive) {
48
+ if (isInteractive)
49
+ console.clear();
50
+ write();
51
+ if ((process.stdout.columns ?? 80) > WORDMARK_WIDTH) {
52
+ for (const line of WORDMARK)
53
+ write((0, theme_1.lime)(line));
54
+ }
55
+ else {
56
+ write((0, theme_1.lime)((0, theme_1.bold)("better-ts-stack")));
57
+ }
58
+ write();
59
+ write((0, theme_1.lavender)((0, theme_1.bold)("TypeScript projects, ready to run.")));
60
+ if (isInteractive) {
61
+ write((0, theme_1.dim)(greeting()));
62
+ write((0, theme_1.lime)((0, theme_1.bold)("Answer a few questions to get started.")));
63
+ write((0, theme_1.dim)(`v${version} · ↑↓ to move · enter to confirm · ctrl+c to cancel`));
64
+ }
65
+ else {
66
+ write((0, theme_1.dim)(`v${version}`));
67
+ }
68
+ write();
69
+ }
70
+ // Displays a summary of what will be built
71
+ function showSummary(config, targetDir) {
72
+ const stack = [
73
+ config.framework,
74
+ config.database !== "none" && config.database,
75
+ config.useAuth && "auth",
76
+ config.useDocker && "docker",
77
+ ];
78
+ const setup = [
79
+ config.packageManager,
80
+ config.initGit && "git",
81
+ config.installDeps && "install dependencies",
82
+ ];
83
+ write(` ${(0, theme_1.amber)((0, theme_1.bold)(config.projectName))}`);
84
+ write(` ${(0, theme_1.pink)("Stack")} ${stack.filter(Boolean).join(" · ")}`);
85
+ write(` ${(0, theme_1.pink)("Setup")} ${setup.filter(Boolean).join(" · ")}`);
86
+ write(` ${(0, theme_1.pink)("Path ")} ${node_path_1.default.relative((0, node_process_1.cwd)(), targetDir) || "."}`);
87
+ write();
88
+ }
89
+ // Prints a titled list, or nothing when the list is empty
90
+ function showSection(title, lines) {
91
+ if (lines.length === 0)
92
+ return;
93
+ write(` ${(0, theme_1.amber)((0, theme_1.bold)(title))} ${(0, theme_1.dim)(`(${lines.length})`)}`);
94
+ for (const line of lines)
95
+ write(` ${line}`);
96
+ write();
97
+ }
98
+ // Formats name/value pairs as two aligned columns
99
+ function toColumns(entries) {
100
+ const width = Math.max(0, ...Object.keys(entries).map((key) => key.length));
101
+ return Object.entries(entries).map(([key, value]) => `${key.padEnd(width)} ${(0, theme_1.dim)(value)}`);
102
+ }
103
+ // Shows everything a build would produce, for --dry-run
104
+ function showPlan(plan) {
105
+ showSection("Files", plan.files);
106
+ showSection("Dependencies", toColumns(plan.dependencies));
107
+ showSection("Dev dependencies", toColumns(plan.devDependencies));
108
+ showSection("Overrides", toColumns(plan.overrides));
109
+ showSection("Scripts", toColumns(plan.scripts));
110
+ showSection("Environment variables", Object.entries(plan.envVars).map(([key, value]) => `${key}=${(0, theme_1.dim)(value)}`));
111
+ showSection("Commands", plan.commands.map(theme_1.lime));
112
+ }
113
+ // Shows the numbered next steps, with the commands to run highlighted
114
+ function showNextSteps(steps) {
115
+ write();
116
+ write(` ${(0, theme_1.amber)((0, theme_1.bold)("Next steps"))}`);
117
+ steps.forEach((step, index) => {
118
+ const isCommand = /^(cd|npm|pnpm|bun) /.test(step);
119
+ write(` ${(0, theme_1.dim)(`${index + 1}.`)} ${isCommand ? (0, theme_1.lime)(step) : step}`);
120
+ });
121
+ write();
122
+ write(` ${(0, theme_1.dim)(`Your app will run at http://localhost:${types_1.DEFAULT_PORT}`)}`);
123
+ write();
124
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.text = text;
4
+ exports.select = select;
5
+ exports.confirm = confirm;
6
+ // Prompts built on @clack/core, which handles the keyboard while the render
7
+ // functions below draw each state
8
+ const core_1 = require("@clack/core");
9
+ const theme_1 = require("./theme");
10
+ // A prompt waiting for input: "$ Project name"
11
+ const question = (message) => `${(0, theme_1.lime)("$")} ${(0, theme_1.bold)(message)}`;
12
+ // An answered prompt, collapsed to one line: "✓ Project name my-app". The
13
+ // trailing newline leaves a blank line before the next prompt
14
+ const answered = (message, answer) => `${(0, theme_1.lime)("✓")} ${(0, theme_1.dim)(message)} ${answer}\n`;
15
+ const cancelled = (message) => `${(0, theme_1.red)("×")} ${(0, theme_1.dim)(message)}`;
16
+ function text(options) {
17
+ const { message, placeholder } = options;
18
+ return new core_1.TextPrompt({
19
+ validate: options.validate,
20
+ render() {
21
+ if (this.state === "submit")
22
+ return answered(message, this.value ?? "");
23
+ if (this.state === "cancel")
24
+ return cancelled(message);
25
+ // The inverse first character stands in for the cursor on an empty input
26
+ const input = this.userInput
27
+ ? this.userInputWithCursor
28
+ : (0, theme_1.inverse)(placeholder[0]) + (0, theme_1.dim)(placeholder.slice(1));
29
+ const error = this.state === "error" ? `\n ${(0, theme_1.red)(this.error)}` : "";
30
+ return `${question(message)}\n ${input}${error}\n`;
31
+ },
32
+ }).prompt();
33
+ }
34
+ function select(options) {
35
+ const { message } = options;
36
+ return new core_1.SelectPrompt({
37
+ options: options.options,
38
+ initialValue: options.initialValue,
39
+ render() {
40
+ if (this.state === "submit")
41
+ return answered(message, this.options[this.cursor].label);
42
+ if (this.state === "cancel")
43
+ return cancelled(message);
44
+ const rows = this.options.map((option, index) => {
45
+ const hint = option.hint ? ` ${(0, theme_1.dim)(option.hint)}` : "";
46
+ return index === this.cursor
47
+ ? ` ${(0, theme_1.lime)("›")} ${option.label}${hint}`
48
+ : ` ${(0, theme_1.dim)(option.label)}`;
49
+ });
50
+ return `${question(message)}\n${rows.join("\n")}\n`;
51
+ },
52
+ }).prompt();
53
+ }
54
+ function confirm(options) {
55
+ const { message } = options;
56
+ return new core_1.ConfirmPrompt({
57
+ active: "Yes",
58
+ inactive: "No",
59
+ initialValue: options.initialValue,
60
+ render() {
61
+ if (this.state === "submit")
62
+ return answered(message, this.value ? "Yes" : "No");
63
+ if (this.state === "cancel")
64
+ return cancelled(message);
65
+ // Both choices keep their column; only the marker and color move
66
+ const choice = (label, isActive) => isActive ? (0, theme_1.lime)(`› ${label}`) : (0, theme_1.dim)(` ${label}`);
67
+ return `${question(message)}\n ${choice("Yes", this.value === true)} ${choice("No", this.value === false)}\n`;
68
+ },
69
+ }).prompt();
70
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inverse = exports.dim = exports.bold = exports.red = exports.pink = exports.lime = exports.lavender = exports.amber = void 0;
4
+ // Colors and text styles shared by every piece of CLI output
5
+ const node_util_1 = require("node:util");
6
+ // Plain text is used when the output is piped, NO_COLOR is set, or the
7
+ // terminal has fewer than 256 colors
8
+ const hasColors = process.stdout.isTTY && process.stdout.hasColors(256);
9
+ // Wraps text in a color from the 256-color palette
10
+ function color(code) {
11
+ return (text) => hasColors ? `\x1b[38;5;${code}m${text}\x1b[39m` : text;
12
+ }
13
+ exports.amber = color(214); // Brand: wordmark and headings
14
+ exports.lavender = color(183); // Tagline
15
+ exports.lime = color(148); // Prompts, success, and commands to run
16
+ exports.pink = color(211); // Labels
17
+ exports.red = color(203); // Errors
18
+ const bold = (text) => (0, node_util_1.styleText)("bold", text);
19
+ exports.bold = bold;
20
+ const dim = (text) => (0, node_util_1.styleText)("dim", text);
21
+ exports.dim = dim;
22
+ const inverse = (text) => (0, node_util_1.styleText)("inverse", text);
23
+ exports.inverse = inverse;
@@ -1,43 +1,14 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.projectNameSchema = void 0;
37
7
  exports.validateProjectName = validateProjectName;
8
+ exports.validateProjectPath = validateProjectPath;
38
9
  exports.validateDirectoryEmpty = validateDirectoryEmpty;
39
- const fs = __importStar(require("fs-extra"));
40
- const path = __importStar(require("path"));
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const node_path_1 = __importDefault(require("node:path"));
41
12
  const zod_1 = require("zod");
42
13
  // Validates project name: lowercase letters, numbers, and hyphens
43
14
  exports.projectNameSchema = zod_1.z
@@ -56,18 +27,22 @@ function validateProjectName(name) {
56
27
  const firstError = result.error.issues[0];
57
28
  return firstError.message;
58
29
  }
30
+ // Checks a project path such as "my-app", "apps/api", or "."; the last folder
31
+ // name becomes the package name
32
+ function validateProjectPath(projectPath) {
33
+ return validateProjectName(node_path_1.default.basename(node_path_1.default.resolve(projectPath)));
34
+ }
59
35
  // Ensures a directory is empty or does not exist before starting build
60
36
  async function validateDirectoryEmpty(dirPath) {
61
- const exists = await fs.pathExists(dirPath);
37
+ const exists = await fs_extra_1.default.pathExists(dirPath);
62
38
  if (!exists)
63
39
  return null;
64
- const stats = await fs.stat(dirPath);
40
+ const stats = await fs_extra_1.default.stat(dirPath);
65
41
  if (!stats.isDirectory()) {
66
42
  return `Path "${dirPath}" exists but is not a directory`;
67
43
  }
68
- const files = await fs.readdir(dirPath);
44
+ const files = await fs_extra_1.default.readdir(dirPath);
69
45
  if (files.length === 0)
70
46
  return null;
71
- return `Directory "${path.basename(dirPath)}" already exists and is not empty`;
47
+ return `Directory "${node_path_1.default.basename(dirPath)}" already exists and is not empty`;
72
48
  }
73
- //# sourceMappingURL=index.js.map