create-onion-lasagna-app 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 (3) hide show
  1. package/README.md +319 -0
  2. package/dist/index.js +662 -0
  3. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # create-onion-lasagna-app
2
+
3
+ Scaffold new onion-lasagna projects with a single command.
4
+
5
+ ```bash
6
+ bunx create-onion-lasagna-app my-app
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Interactive mode (recommended)
13
+ bunx create-onion-lasagna-app
14
+
15
+ # With project name
16
+ bunx create-onion-lasagna-app my-app
17
+
18
+ # Skip prompts with defaults
19
+ bunx create-onion-lasagna-app my-app --yes
20
+
21
+ # Full customization
22
+ bunx create-onion-lasagna-app my-app --structure simple -s simple-clean -v zod -f hono --use-pnpm
23
+ ```
24
+
25
+ ## How It Works
26
+
27
+ ```mermaid
28
+ flowchart LR
29
+ A[Run CLI] --> B{Interactive?}
30
+ B -->|Yes| C[Prompts]
31
+ B -->|No| D[Use Flags]
32
+ C --> E[Clone Starter]
33
+ D --> E
34
+ E --> F[Inject Dependencies]
35
+ F --> G[Create Config]
36
+ G --> H{Install?}
37
+ H -->|Yes| I[Install with PM]
38
+ H -->|No| J{Git?}
39
+ I --> J
40
+ J -->|Yes| K[git init + commit]
41
+ J -->|No| L[Done]
42
+ K --> L
43
+ ```
44
+
45
+ ## Options
46
+
47
+ | Flag | Alias | Description | Default |
48
+ | -------------- | ----- | -------------------------------------------------- | ----------- |
49
+ | `--structure` | - | Project structure: `simple`, `modules` | `simple` |
50
+ | `--starter` | `-s` | Starter template (filtered by structure) | Auto |
51
+ | `--validator` | `-v` | Validation: `zod`, `valibot`, `arktype`, `typebox` | `zod` |
52
+ | `--framework` | `-f` | Framework: `hono`, `elysia`, `fastify` | `hono` |
53
+ | `--use-bun` | - | Use bun package manager | Auto-detect |
54
+ | `--use-npm` | - | Use npm package manager | - |
55
+ | `--use-yarn` | - | Use yarn package manager | - |
56
+ | `--use-pnpm` | - | Use pnpm package manager | - |
57
+ | `--skip-git` | `-g` | Skip git initialization | `false` |
58
+ | `--no-install` | - | Skip dependency installation | `false` |
59
+ | `--dry-run` | `-d` | Preview what would be created | - |
60
+ | `--yes` | `-y` | Skip prompts, use defaults | - |
61
+ | `--version` | `-V` | Show version number | - |
62
+ | `--help` | `-h` | Show help | - |
63
+
64
+ ## Package Manager
65
+
66
+ The CLI auto-detects your package manager based on how you invoke it:
67
+
68
+ ```bash
69
+ bunx create-onion-lasagna-app my-app # Uses bun
70
+ npx create-onion-lasagna-app my-app # Uses npm
71
+ pnpm create onion-lasagna-app my-app # Uses pnpm
72
+ yarn create onion-lasagna-app my-app # Uses yarn
73
+ ```
74
+
75
+ Override with explicit flags:
76
+
77
+ ```bash
78
+ bunx create-onion-lasagna-app my-app --use-pnpm
79
+ npx create-onion-lasagna-app my-app --use-bun
80
+ ```
81
+
82
+ ## Git Initialization
83
+
84
+ By default, the CLI initializes a git repository with an initial commit:
85
+
86
+ ```bash
87
+ # Default: git init + initial commit
88
+ bunx create-onion-lasagna-app my-app
89
+
90
+ # Skip git initialization
91
+ bunx create-onion-lasagna-app my-app --skip-git
92
+ ```
93
+
94
+ ## Dry Run Mode
95
+
96
+ Preview what would be created without making any changes:
97
+
98
+ ```bash
99
+ bunx create-onion-lasagna-app my-app --dry-run
100
+ ```
101
+
102
+ Output includes:
103
+
104
+ - Project configuration summary
105
+ - Files that would be created
106
+ - Actions that would be performed
107
+
108
+ ```
109
+ DRY RUN No changes will be made.
110
+
111
+ Project Configuration:
112
+ ──────────────────────────────────────────────────
113
+ Name: my-app
114
+ Directory: /path/to/my-app
115
+ Structure: simple
116
+ Starter: simple-clean
117
+ ...
118
+
119
+ Files that would be created:
120
+ ──────────────────────────────────────────────────
121
+ + my-app/
122
+ + my-app/package.json
123
+ + my-app/.onion-lasagna.json
124
+ ...
125
+ ```
126
+
127
+ ## Project Name Validation
128
+
129
+ Project names follow npm package naming conventions:
130
+
131
+ | Rule | Invalid | Suggestion |
132
+ | --------------------------- | -------------- | ----------------- |
133
+ | Lowercase only | `MyApp` | `myapp` |
134
+ | No spaces | `my app` | `my-app` |
135
+ | No leading numbers | `123-app` | `app-123-app` |
136
+ | No leading dots/underscores | `_myapp` | `myapp` |
137
+ | No reserved names | `node_modules` | `my-node_modules` |
138
+ | Max 214 characters | (too long) | (truncated) |
139
+
140
+ Invalid names are caught early with helpful suggestions.
141
+
142
+ ## Directory Conflict Handling
143
+
144
+ If the target directory exists and isn't empty, interactive mode offers:
145
+
146
+ ```
147
+ ? Directory "my-app" already exists and is not empty.
148
+ How would you like to proceed?
149
+ ○ Overwrite - Remove existing files and continue
150
+ ○ Choose a different name
151
+ ○ Cancel
152
+ ```
153
+
154
+ In non-interactive mode (`--yes`), existing non-empty directories cause an error.
155
+
156
+ ## Structures & Starters
157
+
158
+ ```mermaid
159
+ graph TD
160
+ subgraph Structures
161
+ S[simple] --> SC[simple-clean]
162
+ M[modules] --> MC[modules-clean]
163
+ end
164
+ ```
165
+
166
+ ### Simple Structure
167
+
168
+ Flat structure for small to medium projects.
169
+
170
+ | Starter | Description |
171
+ | -------------- | ----------------------------- |
172
+ | `simple-clean` | Minimal setup, ready to build |
173
+
174
+ ```
175
+ my-app/
176
+ ├── packages/
177
+ │ └── backend/
178
+ │ ├── bounded-contexts/
179
+ │ │ └── example/
180
+ │ ├── orchestrations/
181
+ │ └── shared/
182
+ ├── .onion-lasagna.json
183
+ └── package.json
184
+ ```
185
+
186
+ ### Modules Structure
187
+
188
+ Module-based structure for large enterprise projects.
189
+
190
+ | Starter | Description |
191
+ | --------------- | ----------------------------- |
192
+ | `modules-clean` | Minimal setup, ready to build |
193
+
194
+ ```
195
+ my-app/
196
+ ├── packages/
197
+ │ ├── backend-modules/
198
+ │ │ ├── user-management/
199
+ │ │ ├── billing/
200
+ │ │ └── notifications/
201
+ │ └── backend-orchestrations/
202
+ ├── .onion-lasagna.json
203
+ └── package.json
204
+ ```
205
+
206
+ ## Smart Starter Filtering
207
+
208
+ The CLI automatically filters starters based on your selected structure:
209
+
210
+ ```bash
211
+ # Only shows simple-* starters
212
+ bunx create-onion-lasagna-app my-app --structure simple
213
+
214
+ # Only shows modules-* starters
215
+ bunx create-onion-lasagna-app my-app --structure modules
216
+ ```
217
+
218
+ If an incompatible starter is specified, the CLI will error:
219
+
220
+ ```bash
221
+ # Error: Starter "modules-clean" is not compatible with structure "simple"
222
+ bunx create-onion-lasagna-app my-app --structure simple -s modules-clean
223
+ ```
224
+
225
+ ## Validators
226
+
227
+ ```mermaid
228
+ graph TD
229
+ subgraph Validators
230
+ Z[Zod] -->|Most Popular| V[Validation]
231
+ VB[Valibot] -->|Smallest Bundle| V
232
+ A[ArkType] -->|Fastest Runtime| V
233
+ T[TypeBox] -->|JSON Schema| V
234
+ end
235
+ ```
236
+
237
+ | Library | Best For |
238
+ | ----------- | -------------------------------------------------- |
239
+ | **Zod** | TypeScript-first, great inference, large ecosystem |
240
+ | **Valibot** | Bundle size critical apps, tree-shakeable |
241
+ | **ArkType** | Performance critical, complex schemas |
242
+ | **TypeBox** | JSON Schema compatibility, OpenAPI |
243
+
244
+ ## Frameworks
245
+
246
+ | Framework | Runtime | Best For |
247
+ | ----------- | --------------------------- | ------------------------------------------- |
248
+ | **Hono** | Any (Node, Bun, Deno, Edge) | Universal deployment |
249
+ | **Elysia** | Bun | Maximum performance, end-to-end type safety |
250
+ | **Fastify** | Node | Enterprise, large plugin ecosystem |
251
+
252
+ ## Generated Files
253
+
254
+ After scaffolding, you'll find:
255
+
256
+ | File | Purpose |
257
+ | ------------------------------- | ------------------------------------------------------------------------- |
258
+ | `.onion-lasagna.json` | Project config (structure, starter, validator, framework, packageManager) |
259
+ | `.git/` | Initialized git repository with initial commit |
260
+ | `packages/backend/.env` | Environment variables |
261
+ | `packages/backend/.env.example` | Environment template |
262
+
263
+ ## Examples
264
+
265
+ ```bash
266
+ # Simple API with Hono + Zod (defaults)
267
+ bunx create-onion-lasagna-app api --yes
268
+
269
+ # Enterprise monolith with Fastify + Valibot + pnpm
270
+ bunx create-onion-lasagna-app platform --structure modules -v valibot -f fastify --use-pnpm
271
+
272
+ # High-performance Bun app with Elysia + ArkType, no git
273
+ bunx create-onion-lasagna-app service -v arktype -f elysia --skip-git
274
+
275
+ # CI/CD: npm, no install, no git
276
+ npx create-onion-lasagna-app test-app --yes --no-install --skip-git
277
+ ```
278
+
279
+ ## After Scaffolding
280
+
281
+ ```bash
282
+ cd my-app
283
+ bun run dev # Start development server
284
+ bun run build # Build for production
285
+ bun run test # Run tests
286
+ ```
287
+
288
+ Post-install instructions adapt to your selected package manager:
289
+
290
+ ```bash
291
+ # If you used --use-pnpm
292
+ cd my-app
293
+ pnpm dev
294
+ ```
295
+
296
+ ## Configuration
297
+
298
+ The `.onion-lasagna.json` file stores your project settings:
299
+
300
+ ```json
301
+ {
302
+ "structure": "simple",
303
+ "starter": "simple-clean",
304
+ "validator": "zod",
305
+ "framework": "hono",
306
+ "packageManager": "bun",
307
+ "createdAt": "2024-01-15T10:30:00.000Z"
308
+ }
309
+ ```
310
+
311
+ This config is used by `onion-lasagna-cli` for code generation.
312
+
313
+ ## Adding New Starters
314
+
315
+ New starters can be added to either structure. The naming convention is:
316
+
317
+ - `{structure}-{name}` (e.g., `simple-clean`, `modules-clean`)
318
+
319
+ The CLI will automatically pick them up and show them when the matching structure is selected.
package/dist/index.js ADDED
@@ -0,0 +1,662 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+ import * as p from "@clack/prompts";
7
+ import pc from "picocolors";
8
+
9
+ // src/scaffold.ts
10
+ import { execSync } from "child_process";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import degit from "degit";
14
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
15
+ "node_modules",
16
+ "favicon.ico",
17
+ "package.json",
18
+ "package-lock.json",
19
+ "yarn.lock",
20
+ "pnpm-lock.yaml",
21
+ "bun.lockb",
22
+ ".git",
23
+ ".gitignore",
24
+ ".env",
25
+ "src",
26
+ "dist",
27
+ "build",
28
+ "test",
29
+ "tests",
30
+ "lib",
31
+ "bin",
32
+ "npm",
33
+ "npx",
34
+ "node"
35
+ ]);
36
+ function validateProjectName(name) {
37
+ if (!name || name.trim().length === 0) {
38
+ return { valid: false, error: "Project name cannot be empty" };
39
+ }
40
+ if (name.length > 214) {
41
+ return {
42
+ valid: false,
43
+ error: "Project name must be 214 characters or fewer",
44
+ suggestion: name.slice(0, 50)
45
+ };
46
+ }
47
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
48
+ return {
49
+ valid: false,
50
+ error: `"${name}" is a reserved name`,
51
+ suggestion: `my-${name}`
52
+ };
53
+ }
54
+ if (name.startsWith(".") || name.startsWith("_")) {
55
+ return {
56
+ valid: false,
57
+ error: "Project name cannot start with a dot or underscore",
58
+ suggestion: name.replace(/^[._]+/, "")
59
+ };
60
+ }
61
+ if (/^[0-9]/.test(name)) {
62
+ return {
63
+ valid: false,
64
+ error: "Project name cannot start with a number",
65
+ suggestion: `app-${name}`
66
+ };
67
+ }
68
+ if (/[A-Z]/.test(name)) {
69
+ return {
70
+ valid: false,
71
+ error: "Project name must be lowercase",
72
+ suggestion: name.toLowerCase()
73
+ };
74
+ }
75
+ if (/\s/.test(name)) {
76
+ return {
77
+ valid: false,
78
+ error: "Project name cannot contain spaces",
79
+ suggestion: name.replace(/\s+/g, "-")
80
+ };
81
+ }
82
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
83
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^[._-]+/, "").replace(/-+/g, "-");
84
+ return {
85
+ valid: false,
86
+ error: "Project name can only contain lowercase letters, numbers, hyphens, and underscores",
87
+ suggestion: sanitized || "my-app"
88
+ };
89
+ }
90
+ if (/[._-]$/.test(name)) {
91
+ return {
92
+ valid: false,
93
+ error: "Project name cannot end with a dot, underscore, or hyphen",
94
+ suggestion: name.replace(/[._-]+$/, "")
95
+ };
96
+ }
97
+ return { valid: true };
98
+ }
99
+ var STARTERS = {
100
+ "simple-clean": {
101
+ structure: "simple",
102
+ label: "Clean",
103
+ hint: "Minimal setup, ready to build",
104
+ repoPath: "simple-clean-starter"
105
+ },
106
+ "modules-clean": {
107
+ structure: "modules",
108
+ label: "Clean",
109
+ hint: "Minimal setup, ready to build",
110
+ repoPath: "modules-clean-starter"
111
+ }
112
+ };
113
+ var REPO = "Cosmneo/onion-lasagna";
114
+ var VALIDATOR_PACKAGES = {
115
+ zod: "zod",
116
+ valibot: "valibot",
117
+ arktype: "arktype",
118
+ typebox: "@sinclair/typebox"
119
+ };
120
+ var FRAMEWORK_PACKAGES = {
121
+ hono: ["hono"],
122
+ elysia: ["elysia"],
123
+ fastify: ["fastify"]
124
+ };
125
+ var INSTALL_COMMANDS = {
126
+ npm: "npm install",
127
+ yarn: "yarn",
128
+ pnpm: "pnpm install",
129
+ bun: "bun install"
130
+ };
131
+ function initGitRepository(targetDir) {
132
+ try {
133
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
134
+ execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
135
+ execSync('git commit -m "Initial commit from create-onion-lasagna-app"', {
136
+ cwd: targetDir,
137
+ stdio: "ignore"
138
+ });
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ async function scaffold(options) {
145
+ const { name, structure, starter, validator, framework, packageManager, install, skipGit } = options;
146
+ const targetDir = path.resolve(process.cwd(), name);
147
+ if (fs.existsSync(targetDir)) {
148
+ const files = fs.readdirSync(targetDir);
149
+ if (files.length > 0) {
150
+ throw new Error(`Directory "${name}" is not empty`);
151
+ }
152
+ }
153
+ const starterConfig = STARTERS[starter];
154
+ if (!starterConfig) {
155
+ throw new Error(`Unknown starter: ${starter}`);
156
+ }
157
+ if (starterConfig.structure !== structure) {
158
+ throw new Error(`Starter "${starter}" is not compatible with structure "${structure}"`);
159
+ }
160
+ const starterPath = `${REPO}/starters/${starterConfig.repoPath}`;
161
+ const emitter = degit(starterPath, {
162
+ cache: false,
163
+ force: true,
164
+ verbose: false
165
+ });
166
+ await emitter.clone(targetDir);
167
+ const rootPackageJsonPath = path.join(targetDir, "package.json");
168
+ const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf-8"));
169
+ rootPackageJson.name = name;
170
+ fs.writeFileSync(rootPackageJsonPath, JSON.stringify(rootPackageJson, null, 2) + "\n");
171
+ const backendPackageJsonPath = path.join(targetDir, "packages", "backend", "package.json");
172
+ if (fs.existsSync(backendPackageJsonPath)) {
173
+ const backendPackageJson = JSON.parse(fs.readFileSync(backendPackageJsonPath, "utf-8"));
174
+ backendPackageJson.dependencies = backendPackageJson.dependencies || {};
175
+ backendPackageJson.dependencies[VALIDATOR_PACKAGES[validator]] = "latest";
176
+ for (const pkg of FRAMEWORK_PACKAGES[framework]) {
177
+ backendPackageJson.dependencies[pkg] = "latest";
178
+ }
179
+ fs.writeFileSync(backendPackageJsonPath, JSON.stringify(backendPackageJson, null, 2) + "\n");
180
+ }
181
+ const envExamplePath = path.join(targetDir, "packages", "backend", ".env.example");
182
+ const envContent = `# Environment variables
183
+ NODE_ENV=development
184
+ PORT=3000
185
+ `;
186
+ fs.writeFileSync(envExamplePath, envContent);
187
+ const envPath = path.join(targetDir, "packages", "backend", ".env");
188
+ fs.writeFileSync(envPath, envContent);
189
+ const configPath = path.join(targetDir, ".onion-lasagna.json");
190
+ const config = {
191
+ structure,
192
+ starter,
193
+ validator,
194
+ framework,
195
+ packageManager,
196
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
197
+ };
198
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
199
+ if (install) {
200
+ const installCmd = INSTALL_COMMANDS[packageManager];
201
+ execSync(installCmd, {
202
+ cwd: targetDir,
203
+ stdio: "ignore"
204
+ });
205
+ }
206
+ if (!skipGit) {
207
+ initGitRepository(targetDir);
208
+ }
209
+ }
210
+
211
+ // src/index.ts
212
+ var VERSION = "0.1.0";
213
+ function setupSignalHandlers() {
214
+ const cleanup = () => {
215
+ process.stdout.write("\x1B[?25h");
216
+ console.log("\n");
217
+ p.cancel("Operation cancelled.");
218
+ process.exit(0);
219
+ };
220
+ process.on("SIGINT", cleanup);
221
+ process.on("SIGTERM", cleanup);
222
+ }
223
+ function detectPackageManager() {
224
+ const userAgent = process.env.npm_config_user_agent || "";
225
+ if (userAgent.startsWith("yarn")) return "yarn";
226
+ if (userAgent.startsWith("pnpm")) return "pnpm";
227
+ if (userAgent.startsWith("bun")) return "bun";
228
+ if (userAgent.startsWith("npm")) return "npm";
229
+ return "bun";
230
+ }
231
+ function parseArgs(args) {
232
+ const result = {};
233
+ for (let i = 0; i < args.length; i++) {
234
+ const arg = args[i];
235
+ if (arg === "--help" || arg === "-h") {
236
+ result.help = true;
237
+ } else if (arg === "--version" || arg === "-V") {
238
+ result.version = true;
239
+ } else if (arg === "--yes" || arg === "-y") {
240
+ result.yes = true;
241
+ } else if (arg === "--no-install") {
242
+ result.install = false;
243
+ } else if (arg === "--skip-git" || arg === "-g") {
244
+ result.skipGit = true;
245
+ } else if (arg === "--dry-run" || arg === "-d") {
246
+ result.dryRun = true;
247
+ } else if (arg === "--use-npm") {
248
+ result.packageManager = "npm";
249
+ } else if (arg === "--use-yarn") {
250
+ result.packageManager = "yarn";
251
+ } else if (arg === "--use-pnpm") {
252
+ result.packageManager = "pnpm";
253
+ } else if (arg === "--use-bun") {
254
+ result.packageManager = "bun";
255
+ } else if (arg === "--structure") {
256
+ result.structure = args[++i];
257
+ } else if (arg === "--starter" || arg === "-s") {
258
+ result.starter = args[++i];
259
+ } else if (arg === "--validator" || arg === "-v") {
260
+ result.validator = args[++i];
261
+ } else if (arg === "--framework" || arg === "-f") {
262
+ result.framework = args[++i];
263
+ } else if (!arg?.startsWith("-") && !result.name) {
264
+ result.name = arg;
265
+ }
266
+ }
267
+ return result;
268
+ }
269
+ function showHelp() {
270
+ const simpleStarters = Object.entries(STARTERS).filter(([, s]) => s.structure === "simple").map(([key]) => key);
271
+ const modulesStarters = Object.entries(STARTERS).filter(([, s]) => s.structure === "modules").map(([key]) => key);
272
+ console.log(`
273
+ ${pc.bold("create-onion-lasagna-app")} ${pc.dim(`v${VERSION}`)} - Scaffold a new onion-lasagna project
274
+
275
+ ${pc.bold("Usage:")}
276
+ create-onion-lasagna-app [project-name] [options]
277
+
278
+ ${pc.bold("Options:")}
279
+ --structure <type> Project structure: simple, modules (default: simple)
280
+ -s, --starter <name> Starter template (filtered by structure)
281
+ -v, --validator <lib> Validation library: zod, valibot, arktype, typebox (default: zod)
282
+ -f, --framework <fw> Web framework: hono, elysia, fastify (default: hono)
283
+
284
+ --use-bun Use bun package manager (default)
285
+ --use-npm Use npm package manager
286
+ --use-yarn Use yarn package manager
287
+ --use-pnpm Use pnpm package manager
288
+
289
+ -g, --skip-git Skip git initialization
290
+ --no-install Skip dependency installation
291
+ -d, --dry-run Show what would be created without making changes
292
+ -y, --yes Skip prompts and use defaults
293
+
294
+ -V, --version Show version number
295
+ -h, --help Show this help message
296
+
297
+ ${pc.bold("Starters:")}
298
+ ${pc.dim("simple structure:")} ${simpleStarters.join(", ")}
299
+ ${pc.dim("modules structure:")} ${modulesStarters.join(", ")}
300
+
301
+ ${pc.bold("Examples:")}
302
+ create-onion-lasagna-app my-app
303
+ create-onion-lasagna-app my-app --use-pnpm --skip-git
304
+ create-onion-lasagna-app my-app --structure modules -s modules-clean
305
+ create-onion-lasagna-app my-app --dry-run
306
+ create-onion-lasagna-app my-app --yes
307
+ `);
308
+ }
309
+ function getStartersForStructure(structure) {
310
+ return Object.entries(STARTERS).filter(([, s]) => s.structure === structure).map(([key, s]) => ({
311
+ value: key,
312
+ label: s.label,
313
+ hint: s.hint
314
+ }));
315
+ }
316
+ function getDefaultStarterForStructure(structure) {
317
+ const starters = Object.entries(STARTERS).filter(([, s]) => s.structure === structure);
318
+ return starters[0]?.[0];
319
+ }
320
+ function getRunCommand(pm) {
321
+ return pm === "npm" ? "npm run" : pm;
322
+ }
323
+ function getInstallCommand(pm) {
324
+ return pm === "yarn" ? "yarn" : `${pm} install`;
325
+ }
326
+ function showDryRunOutput(options) {
327
+ const targetDir = path2.resolve(process.cwd(), options.name);
328
+ console.log(`
329
+ ${pc.bgYellow(pc.black(" DRY RUN "))} No changes will be made.
330
+ `);
331
+ console.log(`${pc.bold("Project Configuration:")}`);
332
+ console.log(pc.dim("\u2500".repeat(50)));
333
+ console.log(` ${pc.cyan("Name:")} ${options.name}`);
334
+ console.log(` ${pc.cyan("Directory:")} ${targetDir}`);
335
+ console.log(` ${pc.cyan("Structure:")} ${options.structure}`);
336
+ console.log(` ${pc.cyan("Starter:")} ${options.starter}`);
337
+ console.log(` ${pc.cyan("Validator:")} ${options.validator}`);
338
+ console.log(` ${pc.cyan("Framework:")} ${options.framework}`);
339
+ console.log(` ${pc.cyan("Package Manager:")} ${options.packageManager}`);
340
+ console.log(` ${pc.cyan("Install deps:")} ${options.install ? "yes" : "no"}`);
341
+ console.log(` ${pc.cyan("Git init:")} ${options.skipGit ? "no" : "yes"}`);
342
+ console.log(`
343
+ ${pc.bold("Files that would be created:")}`);
344
+ console.log(pc.dim("\u2500".repeat(50)));
345
+ console.log(` ${pc.green("+")} ${options.name}/`);
346
+ console.log(` ${pc.green("+")} ${options.name}/package.json`);
347
+ console.log(` ${pc.green("+")} ${options.name}/.onion-lasagna.json`);
348
+ console.log(` ${pc.green("+")} ${options.name}/packages/backend/`);
349
+ console.log(` ${pc.green("+")} ${options.name}/packages/backend/package.json`);
350
+ console.log(` ${pc.green("+")} ${options.name}/packages/backend/.env`);
351
+ console.log(` ${pc.green("+")} ${options.name}/packages/backend/.env.example`);
352
+ if (!options.skipGit) {
353
+ console.log(` ${pc.green("+")} ${options.name}/.git/`);
354
+ }
355
+ console.log(`
356
+ ${pc.bold("Actions that would be performed:")}`);
357
+ console.log(pc.dim("\u2500".repeat(50)));
358
+ console.log(` ${pc.blue("1.")} Clone starter template from GitHub`);
359
+ console.log(` ${pc.blue("2.")} Update package.json with project name`);
360
+ console.log(` ${pc.blue("3.")} Add ${options.validator} and ${options.framework} dependencies`);
361
+ console.log(` ${pc.blue("4.")} Create environment files`);
362
+ console.log(` ${pc.blue("5.")} Create .onion-lasagna.json config`);
363
+ if (options.install) {
364
+ console.log(` ${pc.blue("6.")} Run ${getInstallCommand(options.packageManager)}`);
365
+ }
366
+ if (!options.skipGit) {
367
+ console.log(` ${pc.blue(options.install ? "7." : "6.")} Initialize git repository`);
368
+ }
369
+ console.log(`
370
+ ${pc.dim("Run without --dry-run to create the project.")}
371
+ `);
372
+ }
373
+ function showRichPostInstall(options) {
374
+ const { name, packageManager, install, skipGit } = options;
375
+ const run = getRunCommand(packageManager);
376
+ console.log(
377
+ `
378
+ ${pc.green("Success!")} Created ${pc.bold(name)} at ${pc.dim(path2.resolve(process.cwd(), name))}`
379
+ );
380
+ console.log(`
381
+ ${pc.bold("Inside that directory, you can run:")}`);
382
+ console.log(pc.dim("\u2500".repeat(50)));
383
+ console.log(`
384
+ ${pc.cyan(`${run} dev`)}`);
385
+ console.log(` ${pc.dim("Start the development server")}`);
386
+ console.log(`
387
+ ${pc.cyan(`${run} build`)}`);
388
+ console.log(` ${pc.dim("Build for production")}`);
389
+ console.log(`
390
+ ${pc.cyan(`${run} test`)}`);
391
+ console.log(` ${pc.dim("Run tests")}`);
392
+ console.log(`
393
+ ${pc.cyan(`${run} lint`)}`);
394
+ console.log(` ${pc.dim("Check for linting errors")}`);
395
+ console.log(`
396
+ ${pc.bold("We suggest you begin by typing:")}`);
397
+ console.log(pc.dim("\u2500".repeat(50)));
398
+ console.log(`
399
+ ${pc.cyan(`cd ${name}`)}`);
400
+ if (!install) {
401
+ console.log(` ${pc.cyan(getInstallCommand(packageManager))}`);
402
+ }
403
+ console.log(` ${pc.cyan(`${run} dev`)}`);
404
+ if (!skipGit) {
405
+ console.log(`
406
+ ${pc.dim("A git repository has been initialized with an initial commit.")}`);
407
+ }
408
+ console.log("");
409
+ }
410
+ async function checkDirectoryConflict(name) {
411
+ const targetDir = path2.resolve(process.cwd(), name);
412
+ if (!fs2.existsSync(targetDir)) {
413
+ return "overwrite";
414
+ }
415
+ const files = fs2.readdirSync(targetDir);
416
+ if (files.length === 0) {
417
+ return "overwrite";
418
+ }
419
+ p.log.warn(`Directory ${pc.cyan(name)} already exists and is not empty.`);
420
+ const action = await p.select({
421
+ message: "How would you like to proceed?",
422
+ options: [
423
+ { value: "overwrite", label: "Overwrite", hint: "Remove existing files and continue" },
424
+ { value: "new-name", label: "Choose a different name", hint: "Enter a new project name" },
425
+ { value: "cancel", label: "Cancel", hint: "Abort the operation" }
426
+ ]
427
+ });
428
+ if (p.isCancel(action)) {
429
+ return "cancel";
430
+ }
431
+ return action;
432
+ }
433
+ function removeDirectory(dir) {
434
+ if (fs2.existsSync(dir)) {
435
+ fs2.rmSync(dir, { recursive: true, force: true });
436
+ }
437
+ }
438
+ async function main() {
439
+ setupSignalHandlers();
440
+ const args = parseArgs(process.argv.slice(2));
441
+ if (args.version) {
442
+ console.log(`create-onion-lasagna-app v${VERSION}`);
443
+ process.exit(0);
444
+ }
445
+ if (args.help) {
446
+ showHelp();
447
+ process.exit(0);
448
+ }
449
+ const detectedPm = detectPackageManager();
450
+ if (args.yes || args.name && args.structure && args.starter && args.validator && args.framework) {
451
+ const structure = args.structure || "simple";
452
+ const starter = args.starter || getDefaultStarterForStructure(structure);
453
+ const packageManager2 = args.packageManager || detectedPm;
454
+ const nameValidation = validateProjectName(args.name || "my-onion-app");
455
+ if (!nameValidation.valid) {
456
+ console.error(pc.red(`Invalid project name: ${nameValidation.error}`));
457
+ if (nameValidation.suggestion) {
458
+ console.error(pc.dim(`Suggestion: ${nameValidation.suggestion}`));
459
+ }
460
+ process.exit(1);
461
+ }
462
+ const starterConfig = STARTERS[starter];
463
+ if (starterConfig && starterConfig.structure !== structure) {
464
+ console.error(pc.red(`Starter "${starter}" is not compatible with structure "${structure}"`));
465
+ console.error(
466
+ pc.dim(
467
+ `Available starters for ${structure}: ${getStartersForStructure(structure).map((s2) => s2.value).join(", ")}`
468
+ )
469
+ );
470
+ process.exit(1);
471
+ }
472
+ const options = {
473
+ name: args.name || "my-onion-app",
474
+ structure,
475
+ starter,
476
+ validator: args.validator || "zod",
477
+ framework: args.framework || "hono",
478
+ packageManager: packageManager2,
479
+ install: args.install !== false,
480
+ skipGit: args.skipGit ?? false
481
+ };
482
+ if (args.dryRun) {
483
+ showDryRunOutput(options);
484
+ process.exit(0);
485
+ }
486
+ const targetDir = path2.resolve(process.cwd(), options.name);
487
+ if (fs2.existsSync(targetDir)) {
488
+ const files = fs2.readdirSync(targetDir);
489
+ if (files.length > 0) {
490
+ console.error(pc.red(`Directory "${options.name}" already exists and is not empty.`));
491
+ console.error(pc.dim("Use interactive mode or choose a different name."));
492
+ process.exit(1);
493
+ }
494
+ }
495
+ console.log(pc.cyan(`
496
+ Creating ${options.name}...
497
+ `));
498
+ try {
499
+ await scaffold(options);
500
+ showRichPostInstall(options);
501
+ } catch (error) {
502
+ console.error(pc.red(error instanceof Error ? error.message : String(error)));
503
+ process.exit(1);
504
+ }
505
+ return;
506
+ }
507
+ console.clear();
508
+ p.intro(pc.bgCyan(pc.black(` create-onion-lasagna-app v${VERSION} `)));
509
+ let projectName = args.name || "my-onion-app";
510
+ let needsNewName = false;
511
+ if (args.name) {
512
+ const nameValidation = validateProjectName(args.name);
513
+ if (!nameValidation.valid) {
514
+ p.log.warn(`Invalid project name: ${nameValidation.error}`);
515
+ if (nameValidation.suggestion) {
516
+ p.log.info(`Suggestion: ${nameValidation.suggestion}`);
517
+ }
518
+ needsNewName = true;
519
+ }
520
+ }
521
+ if (!needsNewName && projectName) {
522
+ const conflictAction = await checkDirectoryConflict(projectName);
523
+ if (conflictAction === "cancel") {
524
+ p.cancel("Operation cancelled.");
525
+ process.exit(0);
526
+ } else if (conflictAction === "overwrite") {
527
+ const targetDir = path2.resolve(process.cwd(), projectName);
528
+ if (fs2.existsSync(targetDir) && fs2.readdirSync(targetDir).length > 0) {
529
+ removeDirectory(targetDir);
530
+ p.log.info(`Removed existing directory: ${projectName}`);
531
+ }
532
+ } else if (conflictAction === "new-name") {
533
+ needsNewName = true;
534
+ }
535
+ }
536
+ const project = await p.group(
537
+ {
538
+ name: () => {
539
+ if (!needsNewName && args.name) {
540
+ return Promise.resolve(args.name);
541
+ }
542
+ return p.text({
543
+ message: "Project name",
544
+ placeholder: "my-onion-app",
545
+ defaultValue: needsNewName ? "" : projectName,
546
+ validate: (value) => {
547
+ if (!value) return "Project name is required";
548
+ const validation = validateProjectName(value);
549
+ if (!validation.valid) {
550
+ return validation.error + (validation.suggestion ? ` (try: ${validation.suggestion})` : "");
551
+ }
552
+ const targetDir = path2.resolve(process.cwd(), value);
553
+ if (fs2.existsSync(targetDir) && fs2.readdirSync(targetDir).length > 0) {
554
+ return `Directory "${value}" already exists and is not empty`;
555
+ }
556
+ }
557
+ });
558
+ },
559
+ structure: () => p.select({
560
+ message: "Select project structure",
561
+ initialValue: args.structure,
562
+ options: [
563
+ {
564
+ value: "simple",
565
+ label: "Simple",
566
+ hint: "Flat structure, great for small to medium projects"
567
+ },
568
+ {
569
+ value: "modules",
570
+ label: "Modules",
571
+ hint: "Module-based structure for large enterprise projects"
572
+ }
573
+ ]
574
+ }),
575
+ starter: ({ results }) => {
576
+ const structure = results.structure;
577
+ const starters = getStartersForStructure(structure);
578
+ if (starters.length === 1) {
579
+ return Promise.resolve(starters[0].value);
580
+ }
581
+ return p.select({
582
+ message: "Select a starter template",
583
+ initialValue: args.starter,
584
+ options: starters
585
+ });
586
+ },
587
+ validator: () => p.select({
588
+ message: "Select a validation library",
589
+ initialValue: args.validator,
590
+ options: [
591
+ { value: "zod", label: "Zod", hint: "Most popular, great TypeScript inference" },
592
+ { value: "valibot", label: "Valibot", hint: "Smallest bundle size" },
593
+ { value: "arktype", label: "ArkType", hint: "Fastest runtime validation" },
594
+ { value: "typebox", label: "TypeBox", hint: "JSON Schema compatible" }
595
+ ]
596
+ }),
597
+ framework: () => p.select({
598
+ message: "Select a web framework",
599
+ initialValue: args.framework,
600
+ options: [
601
+ { value: "hono", label: "Hono", hint: "Fast, lightweight, works everywhere" },
602
+ { value: "elysia", label: "Elysia", hint: "Bun-optimized, end-to-end type safety" },
603
+ { value: "fastify", label: "Fastify", hint: "Mature, plugin ecosystem" }
604
+ ]
605
+ }),
606
+ packageManager: () => p.select({
607
+ message: "Select a package manager",
608
+ initialValue: args.packageManager || detectedPm,
609
+ options: [
610
+ { value: "bun", label: "bun", hint: "Fast, all-in-one toolkit" },
611
+ { value: "pnpm", label: "pnpm", hint: "Fast, disk space efficient" },
612
+ { value: "npm", label: "npm", hint: "Node.js default package manager" },
613
+ { value: "yarn", label: "yarn", hint: "Classic alternative to npm" }
614
+ ]
615
+ }),
616
+ install: () => p.confirm({
617
+ message: "Install dependencies?",
618
+ initialValue: args.install !== false
619
+ })
620
+ },
621
+ {
622
+ onCancel: () => {
623
+ p.cancel("Operation cancelled.");
624
+ process.exit(0);
625
+ }
626
+ }
627
+ );
628
+ const packageManager = project.packageManager;
629
+ const scaffoldOptions = {
630
+ name: project.name,
631
+ structure: project.structure,
632
+ starter: project.starter,
633
+ validator: project.validator,
634
+ framework: project.framework,
635
+ packageManager,
636
+ install: project.install,
637
+ skipGit: args.skipGit ?? false
638
+ };
639
+ if (args.dryRun) {
640
+ showDryRunOutput(scaffoldOptions);
641
+ p.outro(pc.dim("Dry run complete. No changes were made."));
642
+ process.exit(0);
643
+ }
644
+ const s = p.spinner();
645
+ try {
646
+ s.start("Scaffolding project...");
647
+ await scaffold(scaffoldOptions);
648
+ s.stop("Project scaffolded!");
649
+ showRichPostInstall({
650
+ name: project.name,
651
+ packageManager,
652
+ install: project.install,
653
+ skipGit: args.skipGit ?? false
654
+ });
655
+ p.outro(pc.green("Happy coding!"));
656
+ } catch (error) {
657
+ s.stop("Failed to scaffold project");
658
+ p.log.error(error instanceof Error ? error.message : String(error));
659
+ process.exit(1);
660
+ }
661
+ }
662
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "create-onion-lasagna-app",
3
+ "version": "0.1.0",
4
+ "description": "CLI to scaffold new onion-lasagna projects",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Cosmneo",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Cosmneo/onion-lasagna.git",
11
+ "directory": "packages/create-onion-lasagna-app"
12
+ },
13
+ "homepage": "https://github.com/Cosmneo/onion-lasagna/tree/main/packages/create-onion-lasagna-app#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/Cosmneo/onion-lasagna/issues"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "scaffold",
20
+ "onion-architecture",
21
+ "hexagonal-architecture",
22
+ "ddd",
23
+ "create-app",
24
+ "boilerplate",
25
+ "starter",
26
+ "typescript"
27
+ ],
28
+ "bin": {
29
+ "create-onion-lasagna-app": "./dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "start": "bun run src/index.ts"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.9.1",
41
+ "degit": "^2.8.4",
42
+ "picocolors": "^1.1.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/degit": "^2.8.6",
46
+ "@types/node": "^22.10.2",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^5.8.3"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }