gazan-init 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -93,7 +93,7 @@ GAZAN asks its questions in order, then generates the project:
93
93
  ```text
94
94
  ┌ GAZAN — backend project initializer
95
95
 
96
- ◇ Project name
96
+ ◇ Project name/path
97
97
  │ my-api
98
98
 
99
99
  ◇ Which module system do you want?
@@ -156,7 +156,73 @@ Next steps:
156
156
  ```
157
157
 
158
158
  If the target directory already has content in it (beyond a stray `.git`), GAZAN asks before
159
- touching anything — see [Existing directories](#existing-directories).
159
+ touching anything — see **Existing directories** under [Troubleshooting](#troubleshooting).
160
+
161
+ ## Project name and output path
162
+
163
+ The first prompt (`Project name/path`) answers two questions at once: **where** the project is
164
+ written, and **what it's called**. Both are derived safely with Node's `path` APIs — never by
165
+ concatenating strings — so every form below behaves predictably:
166
+
167
+ | You type | Where GAZAN generates | `package.json` `name` |
168
+ |---|---|---|
169
+ | `my-api` | `<cwd>/my-api/` (created) | `my-api` |
170
+ | `.` | **directly inside the current directory** — nothing nested | basename of the current directory |
171
+ | `./backend` | `<cwd>/backend/` | `backend` |
172
+ | `../backend` | one directory up from `<cwd>`, named `backend` | `backend` |
173
+ | `/abs/path/to/api` | that exact absolute path | `api` |
174
+
175
+ **The project name always comes from the resolved output directory's basename, not the literal
176
+ text you typed.** This is what makes `.` work correctly:
177
+
178
+ ```bash
179
+ mkdir my-api
180
+ cd my-api
181
+ gazan init
182
+ ```
183
+
184
+ ```text
185
+ Project name/path: .
186
+ ```
187
+
188
+ generates directly into `my-api/`:
189
+
190
+ ```text
191
+ my-api/
192
+ ├── src/
193
+ ├── package.json # { "name": "my-api", ... }
194
+ ├── README.md
195
+ ├── .env.example
196
+ └── ...
197
+ ```
198
+
199
+ GAZAN never creates `my-api/./`, `my-api/my-api/`, or any other nested duplicate — `.` resolves
200
+ to `process.cwd()` itself (via `path.resolve`), and the *directory* GAZAN is about to write into
201
+ is what names the project, not the string that happened to be typed. This also means a directory
202
+ name that isn't a valid npm package name (uppercase letters, spaces, dots — e.g. a folder called
203
+ `My Api`) is automatically sanitized into a valid `package.json` name (`my-api`) without changing
204
+ the actual folder name on disk or the human-readable title used in the generated `README.md`.
205
+
206
+ There is currently no separate "application name" prompt distinct from the output path — if you
207
+ need `package.json`'s `name` to differ from the directory GAZAN writes into, rename the directory
208
+ (or edit `name` in the generated `package.json`) after generation.
209
+
210
+ ### Current-directory safety
211
+
212
+ Selecting `.` does not bypass GAZAN's normal existing-directory safety check (see **Existing
213
+ directories** under [Troubleshooting](#troubleshooting)) — it applies identically whether the
214
+ target is a brand-new folder or the directory you're already standing in:
215
+
216
+ ```text
217
+ ? Current directory is not empty.
218
+ ❯ Cancel
219
+ Continue (files may be overwritten)
220
+ Use another directory
221
+ ```
222
+
223
+ Nothing is overwritten until you explicitly choose "Continue," and generation itself always
224
+ happens in a temporary directory first, only copied into place once it fully succeeds — so a
225
+ mid-generation failure never leaves your current directory partially written.
160
226
 
161
227
  ## Generated project structure
162
228
 
@@ -238,6 +304,55 @@ Controllers and services are classes. A controller is constructed with a service
238
304
  the database. Routes stay thin — they wire a validator and a controller method together and
239
305
  nothing else.
240
306
 
307
+ ## How GAZAN works
308
+
309
+ Step by step, in the order the CLI actually executes them:
310
+
311
+ 1. You run `gazan init` (or `npx gazan-init init` / `node bin/gazan.js init` from a clone).
312
+ 2. GAZAN checks your Node.js version first and exits with a clear message if it's below 18.
313
+ 3. You answer the prompts, in order: **project name/path** → **module system** (CJS/MJS) →
314
+ **language** (JS/TS) → **module aliases** (on/off) → **database** → **architecture** (MVC/HMVC)
315
+ → **use `src/`?** → **Socket.IO?** → **BullMQ?** → **Redis for rate limiting?** (skipped
316
+ automatically if BullMQ is enabled, since BullMQ already requires Redis) → **authentication**
317
+ (enabled, then which methods — multi-select) → **do you have an `entity.json`?**
318
+ 4. If you answered yes to `entity.json`, GAZAN asks for its path and validates it immediately,
319
+ right there in the prompt flow — an invalid file gives you a chance to fix the path and retry
320
+ before anything else happens.
321
+ 5. GAZAN resolves your project name/path answer into an output directory (see
322
+ [Project name and output path](#project-name-and-output-path)) and checks it for existing
323
+ content, asking before touching a non-empty directory.
324
+ 6. Every answer is normalized into one configuration object — the single shape every generator
325
+ reads from (generators never see raw prompt answers).
326
+ 7. If an `entity.json` path was given, it's read and validated again here, against the actual
327
+ selected database (some rules, like MongoDB's primary-key restriction, depend on which database
328
+ you picked).
329
+ 8. GAZAN generates the whole project into a temporary directory — not your real target directory
330
+ yet.
331
+ 9. The base folder skeleton, `package.json`, `.gitignore`, and (TypeScript only) `tsconfig.json`
332
+ are written.
333
+ 10. Environment validation, error classes, and graceful-shutdown scaffolding are generated.
334
+ 11. Security middleware is generated — rate limiting, centralized error/not-found handlers, and
335
+ the Zod request-validation middleware (Helmet/CORS are wired directly into `app.js`).
336
+ 12. Database artifacts are generated for the selected backend — a Prisma schema, a Mongoose
337
+ connection module, or a native MongoDB client — or nothing at all for "No database" (see
338
+ [Database support](#database-support)).
339
+ 13. Redis, BullMQ, and Socket.IO scaffolding are generated, each strictly only if you selected it.
340
+ 14. Authentication helpers and middleware are generated for each method you selected.
341
+ 15. If you provided an `entity.json`, its models are turned into real code: Mongoose model files
342
+ (Prisma's models already live in the one `schema.prisma` written in step 12), Zod validators,
343
+ services, controllers, and routes — then the aggregate routes file and the `app`/`server`
344
+ entry files are (re)generated to wire everything together.
345
+ 16. Module aliases are generated — `tsconfig.json` paths, `jsconfig.json`, `module-alias` config,
346
+ or the ESM loader, depending on your language/module-system combination — only if aliases are
347
+ enabled.
348
+ 17. The generated project's own `README.md` is written last, reflecting exactly what was produced
349
+ (see [Generated project README](#generated-project-structure)).
350
+ 18. Only once every step above succeeds is the temporary directory copied into your real target
351
+ directory. A failure at any point leaves that directory exactly as it was before you ran
352
+ `gazan init`.
353
+ 19. GAZAN prints a checklist of what it did, any warnings worth reading (e.g. the OAuth stub, or
354
+ the MJS+JS experimental loader notice), and the exact next commands to run.
355
+
241
356
  ## `entity.json`
242
357
 
243
358
  An optional JSON file you can point GAZAN at during `init`. When provided, it becomes the source
@@ -256,6 +371,72 @@ models[1].relations.author.model:
256
371
  unknown model 'Userr'. Did you mean 'User'?
257
372
  ```
258
373
 
374
+ ### Model definition
375
+
376
+ Every entry in `models` needs a `name` and a non-empty `fields` object:
377
+
378
+ ```json
379
+ { "name": "User", "fields": {} }
380
+ ```
381
+
382
+ `name`:
383
+
384
+ - **Required**, a string.
385
+ - **Must be a valid identifier** — letters and digits only, starting with a letter
386
+ (`/^[A-Za-z][A-Za-z0-9]*$/`). This is what actually blocks path traversal and unsafe filesystem
387
+ paths — a name like `../User` or `User/../../etc` fails this regex before it can ever reach a
388
+ filename (see [Invalid examples](#invalid-examples)).
389
+ - **Cannot be a JS reserved word** (`class`, `constructor`, `function`, `this`, `export`, …) or —
390
+ for PostgreSQL/Prisma specifically — a Prisma schema keyword (`model`, `datasource`, `generator`,
391
+ `enum`, `type`, `view`).
392
+ - **Must be unique** across the file.
393
+
394
+ **Naming conventions** — GAZAN derives every generated identifier from `name` using the same
395
+ casing rules everywhere, so `User`, `BlogPost`, and `UserProfile` normalize consistently:
396
+
397
+ | `name` | Prisma model / class | camelCase (var/service instance) | kebab-case (file names) | snake_case (table) | Table name (pluralized) | Route path (pluralized) |
398
+ |---|---|---|---|---|---|---|
399
+ | `User` | `User` | `user` | `user` | `user` | `users` | `/api/users` |
400
+ | `BlogPost` | `BlogPost` | `blogPost` | `blog-post` | `blog_post` | `blog_posts` | `/api/blog-posts` |
401
+ | `UserProfile` | `UserProfile` | `userProfile` | `user-profile` | `user_profile` | `user_profiles` | `/api/user-profiles` |
402
+
403
+ Generated file names always follow the kebab-case form: `blog-post.service.ts`,
404
+ `blog-post.controller.ts`, etc. Override the table/collection name with `tableName` if you don't
405
+ want the auto-pluralized default.
406
+
407
+ ### Providing entity.json
408
+
409
+ GAZAN asks for this during `init`, after the authentication prompt:
410
+
411
+ ```text
412
+ ◇ Do you have an entity.json file?
413
+ │ Yes
414
+
415
+ ◇ Enter entity.json path
416
+ │ ./entity.json
417
+ ```
418
+
419
+ - **Both relative and absolute paths work.** A relative path (`./entity.json`, `entity.json`,
420
+ `../shared/entity.json`) is resolved against the directory you ran `gazan init` from
421
+ (`path.resolve(process.cwd(), yourInput)`), same as the project name/path prompt.
422
+ - **It's parsed and validated immediately, in the prompt itself** — not deferred to generation
423
+ time. GAZAN reads the file, `JSON.parse`s it, runs it through the schema (structural) and then
424
+ semantic (cross-field, cross-model) validators, and reports success or a detailed error right
425
+ there.
426
+ - **If the file doesn't exist**, you get `No file exists at: <resolved absolute path>` and are
427
+ asked whether to try a different path.
428
+ - **If it exists but isn't valid JSON**, you get `entity.json is not valid JSON` plus the
429
+ underlying `JSON.parse` error message.
430
+ - **If it's valid JSON but fails schema or semantic validation**, you get every failing field
431
+ listed with its exact path and reason (see [Invalid examples](#invalid-examples)) — never a
432
+ single generic "invalid entity.json".
433
+ - **Retrying** — on any validation failure you're asked "Try a different path?"; declining
434
+ continues `init` with no `entity.json` (generic CRUD scaffolding, no entities) rather than
435
+ aborting the whole run.
436
+ - A validated `entity.json` is re-parsed once more right before generation, against the database
437
+ you actually selected — some rules (MongoDB's primary-key restriction, for example) depend on
438
+ which database is in play, not just the file's own contents.
439
+
259
440
  ### Basic example
260
441
 
261
442
  ```json
@@ -360,26 +541,60 @@ against both Prisma and Mongoose.
360
541
 
361
542
  ### Field types
362
543
 
363
- | Type | Description | PostgreSQL (Prisma) | MongoDB (Mongoose) |
364
- |---|---|---|---|
365
- | `string` | Short text | `String` | `String` |
366
- | `text` | Long text | `String` | `String` |
367
- | `number` | Generic number | `Float` | `Number` |
368
- | `integer` | Whole number | `Int` | `Number` |
369
- | `float` | Floating point | `Float` | `Number` |
370
- | `boolean` | True/false | `Boolean` | `Boolean` |
371
- | `date` | Calendar date | `DateTime` | `Date` |
372
- | `datetime` | Date + time | `DateTime` | `Date` |
373
- | `uuid` | UUID string | `String` (`@default(uuid())` when `default: "uuid"`) | `String` |
374
- | `json` | Arbitrary JSON | `Json` | `Mixed` |
375
- | `enum` | Fixed set of values (needs `values`) | Generated Prisma `enum` | `String` with a schema-level enum validator |
376
- | `decimal` | Fixed-precision number | `Decimal` | `Decimal128` |
377
- | `bigint` | Large integer | `BigInt` | `Mixed` (Mongoose has no dedicated bigint type) |
378
-
379
- **MongoDB primary keys must be `uuid` or omitted.** Every model's primary key is routed through
380
- Mongo's native `_id` rather than a redundant parallel field; GAZAN generates a UUID string default
381
- for it. A primary key of any other type (or `autoIncrement`, which Mongo has no native equivalent
382
- for) is rejected at validation time rather than silently approximated.
544
+ `entity.json` supports exactly 13 field types this list comes straight from the parser's schema
545
+ (`FIELD_TYPES` in `src/parser/entity/schema.js`); any other string is rejected before generation
546
+ even starts (see [Invalid examples](#invalid-examples)).
547
+
548
+ | Type | Description | PostgreSQL (Prisma) | MongoDB (Mongoose) | MongoDB (native driver) |
549
+ |---|---|---|---|---|
550
+ | `string` | Short text | `String` | `String` | — |
551
+ | `text` | Long text | `String` | `String` | — |
552
+ | `number` | Generic number | `Float` | `Number` | — |
553
+ | `integer` | Whole number | `Int` | `Number` | — |
554
+ | `float` | Floating point | `Float` | `Number` | |
555
+ | `boolean` | True/false | `Boolean` | `Boolean` | — |
556
+ | `date` | Calendar date | `DateTime` | `Date` | |
557
+ | `datetime` | Date + time | `DateTime` | `Date` | — |
558
+ | `uuid` | UUID string | `String` (`@default(uuid())` when `default: "uuid"`) | `String` | |
559
+ | `json` | Arbitrary JSON | `Json` | `Mixed` | — |
560
+ | `enum` | Fixed set of values (needs `values`) | Generated Prisma `enum` | `String` with a schema-level enum validator | — |
561
+ | `decimal` | Fixed-precision number | `Decimal` | `Decimal128` | |
562
+ | `bigint` | Large integer | `BigInt` | `Mixed` (Mongoose has no dedicated bigint type) | — |
563
+
564
+ **The native MongoDB driver has no schema layer at all.** `entity.json` is still validated
565
+ identically regardless of which MongoDB mode you pick, but the native-driver generator doesn't
566
+ emit a per-type mapping, model file, or field definitions — the driver returns/accepts plain JS
567
+ objects, and the generated services (see [Entity-generated application code](#entity-generated-application-code))
568
+ just read/write whatever shape you give them. Field-level constraints (`required`, `unique`,
569
+ `min`/`max`, `length`, `enum` values, …) are enforced by the generated **Zod validator** at the
570
+ route boundary either way — that part doesn't depend on the database backend.
571
+
572
+ ### Primary keys
573
+
574
+ Every model has exactly one primary key — either declared explicitly with `primaryKey: true`, or
575
+ synthesized automatically if you don't declare one:
576
+
577
+ ```json
578
+ "id": { "type": "uuid", "primaryKey": true, "default": "uuid" }
579
+ ```
580
+
581
+ - **At most one field per model may set `primaryKey: true`** — a second one is rejected
582
+ (`model 'X' declares more than one primaryKey field`).
583
+ - **A primary key field cannot be `nullable: true`** — rejected with
584
+ `primary key field 'id' cannot be nullable`.
585
+ - **If no field declares `primaryKey: true`**, GAZAN synthesizes one for you: a field named `id`,
586
+ `type: "uuid"`, `default: "uuid"`, `required: true` — the same shape as the explicit example
587
+ above. You never need to declare a primary key by hand unless you want a different type
588
+ (PostgreSQL only) or a different name.
589
+ - **PostgreSQL/Prisma** — any field type is allowed as a primary key; `autoIncrement: true` is
590
+ valid on `integer`/`bigint` primary keys (`@default(autoincrement())`), and `type: "uuid"` with
591
+ `default: "uuid"` generates `@default(uuid())`.
592
+ - **MongoDB (Mongoose or native)** — the primary key must be `type: "uuid"` **or omitted
593
+ entirely** (falling back to the synthesized UUID `id` above). Every Mongo primary key is routed
594
+ through Mongo's native `_id` field itself, rather than a redundant parallel `id` column, with a
595
+ generated UUID-string default. Any other declared type — and `autoIncrement`, which Mongo has no
596
+ native equivalent for — is rejected at validation time rather than silently approximated (see
597
+ [Invalid examples](#invalid-examples)).
383
598
 
384
599
  ### Field attributes
385
600
 
@@ -396,9 +611,37 @@ for) is rejected at validation time rather than silently approximated.
396
611
  | `min` / `max` | Numeric bounds, enforced in the generated Zod validator (and Mongoose `min`/`max`). |
397
612
  | `values` | Required for `type: "enum"` — the list of allowed values. |
398
613
 
614
+ ### Required vs nullable
615
+
616
+ These are two different questions, and `entity.json` keeps them separate:
617
+
618
+ - **`required`** — must the caller supply this field when creating a record? It's enforced by the
619
+ generated **Zod validator** at the route boundary (`.optional()` is appended to the field's Zod
620
+ expression when `required` is `false`) — this is a request-validation concern.
621
+ - **`nullable`** — may the *stored value* be `null` in the database? In Prisma, a nullable,
622
+ non-required field becomes an optional scalar (`String?`); a `required` field is always
623
+ non-optional in the schema regardless of `nullable`. In Mongoose, `required: true` sets
624
+ `required: true` on the schema path; `nullable` has no separate Mongoose keyword (Mongoose paths
625
+ are nullable by default unless `required`), so it exists in `entity.json` mainly for
626
+ documentation and for the Zod validator's `.nullable()`.
627
+ - **`required: true` and `nullable: true` are mutually exclusive** — declaring both is rejected at
628
+ validation time: `field cannot be both 'required: true' and 'nullable: true' — required implies
629
+ non-null`.
630
+ - **A `primaryKey` field can never be `nullable: true`** (see [Primary keys](#primary-keys)) —
631
+ rejected regardless of its `required` value.
632
+ - **`default` does not change `required`/`nullable` semantics** — a field with a `default` can
633
+ still be declared `required: true` (the default only applies when the field is omitted from the
634
+ input the *application* passes to the database layer, not at the `entity.json`/Zod level, which
635
+ still requires the caller to supply it unless you also set `required: false`).
636
+
399
637
  ### Relations
400
638
 
401
- Four relation types, declared under a model's `relations` object:
639
+ Four relation types, declared under a model's `relations` object (`RELATION_TYPES` in
640
+ `src/parser/entity/schema.js`): `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`.
641
+
642
+ #### `belongsTo`
643
+
644
+ This model holds the foreign key column/field.
402
645
 
403
646
  ```json
404
647
  {
@@ -416,32 +659,500 @@ Four relation types, declared under a model's `relations` object:
416
659
  }
417
660
  ```
418
661
 
419
- - **`belongsTo`** this model holds the foreign key (`foreignKey` required).
420
- - **`hasMany`** / **`hasOne`**the *other* model holds the foreign key (`foreignKey` required).
421
- You only need to declare this when you want to name or customize the reverse side yourself —
422
- GAZAN auto-derives a reverse array field for any `belongsTo` that doesn't have one, including
423
- disambiguating multiple relations to the same model and self-relations.
424
- - **`belongsToMany`** many-to-many (`through` required, naming the join). **Both models must
425
- declare it, with a matching `through` value** a one-sided declaration is rejected at
426
- validation time rather than silently generating a broken schema.
662
+ 1. **What it means** `Post.authorId` points at a `User`.
663
+ 2. **Required JSON structure**`type: "belongsTo"`, `model` (target model name), `foreignKey`
664
+ (required the name of a real field already declared on *this* model).
665
+ 3. **Foreign key behavior** the `foreignKey` field's declared type must match the target model's
666
+ primary key type, or generation is rejected (`foreignKey 'authorId' has type 'X' but
667
+ 'User.id' (its primary key) has type 'Y'`).
668
+ 4. **Nullable behavior** the generated relation field's optionality mirrors the FK scalar's:
669
+ if `authorId` is `required: true` and not `nullable`, Prisma emits `author User @relation(...)`
670
+ (non-optional); if the FK is optional/nullable, it emits `author User? @relation(...)`. Prisma
671
+ rejects a required relation object backed by a nullable FK, so GAZAN always keeps them in sync.
672
+ 5. **Reverse relation behavior** — see `hasMany`/`hasOne` below; you don't have to declare the
673
+ reverse side yourself.
674
+ 6. **Database output** — Prisma: a scalar FK column plus a `@relation(...)` object field. Mongoose:
675
+ a `String` field with `ref: "<Target>"` (see [Field types](#field-types) — every Mongo FK is a
676
+ `String` because every Mongo primary key is a `String` `_id`).
677
+ 7. **Invalid configurations** — missing `foreignKey` (`relations of type 'belongsTo' require a
678
+ 'foreignKey'`); `foreignKey` naming a field that doesn't exist on this model; `model` naming a
679
+ model that doesn't exist (with a "did you mean" suggestion); FK/target-PK type mismatch.
680
+
681
+ #### `hasMany` / `hasOne`
682
+
683
+ The *other* model holds the foreign key. You only need to declare either explicitly when you want
684
+ to **name or customize the reverse side yourself** — GAZAN auto-derives a reverse field for any
685
+ `belongsTo` elsewhere in the schema that doesn't already have one, including disambiguating
686
+ multiple relations to the same model (see `Post.author`/`Post.editor` in the
687
+ [realistic example](#realistic-example)) and self-relations.
688
+
689
+ ```json
690
+ {
691
+ "models": [
692
+ {
693
+ "name": "User",
694
+ "fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
695
+ "relations": {
696
+ "profile": { "type": "hasOne", "model": "Profile", "foreignKey": "userId" }
697
+ }
698
+ },
699
+ {
700
+ "name": "Profile",
701
+ "fields": {
702
+ "id": { "type": "uuid", "primaryKey": true, "default": "uuid" },
703
+ "userId": { "type": "uuid", "required": true, "unique": true }
704
+ },
705
+ "relations": {
706
+ "user": { "type": "belongsTo", "model": "User", "foreignKey": "userId" }
707
+ }
708
+ }
709
+ ]
710
+ }
711
+ ```
712
+
713
+ 1. **What it means** — `hasMany` is the one-to-many reverse side (a `User` `hasMany` `Post`);
714
+ `hasOne` is the one-to-one reverse side (a `User` `hasOne` `Profile`), typically paired with a
715
+ `unique` FK on the other side (as above).
716
+ 2. **Required JSON structure** — `type`, `model` (target model name), `foreignKey` (required — the
717
+ name of a field that must exist on the *target* model, not this one).
718
+ 3. **Foreign key behavior** — `foreignKey` must exist on the related model
719
+ (`foreignKey 'userId' does not exist on related model 'Profile'` otherwise).
720
+ 4. **Nullable behavior** — `hasOne` always generates an optional relation field (`profile
721
+ Profile? @relation(...)` in Prisma) since the related row may not exist yet; `hasMany` generates
722
+ a (possibly empty) array field, which has no nullability concept.
723
+ 5. **Reverse relation behavior** — this *is* the reverse side of a `belongsTo`. If you don't
724
+ declare it, GAZAN still generates one automatically for you (a `[]` field in Prisma named after
725
+ the pluralized owning model, disambiguated with `As<RelationName>` when more than one
726
+ `belongsTo` points at the same target — e.g. `postsAsAuthor` / `postsAsEditor`). Declare it
727
+ explicitly only to pick your own field name.
728
+ 6. **Database output** — Prisma: `hasMany` → `<name> <Target>[] @relation(...)`; `hasOne` →
729
+ `<name> <Target>? @relation(...)`. Mongoose: a **virtual** (`schema.virtual(name, { ref, localField: "_id", foreignField, justOne })`) — `justOne: true` for `hasOne`, `false` for `hasMany` —
730
+ not a stored field, so it must be `.populate()`d to read.
731
+ 7. **Invalid configurations** — `foreignKey` missing, or naming a field absent from the target
732
+ model.
733
+
734
+ #### `belongsToMany`
735
+
736
+ Many-to-many, naming a join via `through`.
737
+
738
+ ```json
739
+ {
740
+ "models": [
741
+ {
742
+ "name": "Post",
743
+ "fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
744
+ "relations": {
745
+ "tags": { "type": "belongsToMany", "model": "Tag", "through": "PostTag" }
746
+ }
747
+ },
748
+ {
749
+ "name": "Tag",
750
+ "fields": { "id": { "type": "uuid", "primaryKey": true, "default": "uuid" } },
751
+ "relations": {
752
+ "posts": { "type": "belongsToMany", "model": "Post", "through": "PostTag" }
753
+ }
754
+ }
755
+ ]
756
+ }
757
+ ```
758
+
759
+ ```text
760
+ User
761
+
762
+ Role
763
+ ```
764
+
765
+ is the same shape: **both sides must declare `belongsToMany`, with the same `through` value.**
766
+
767
+ 1. **What it means** — an implicit many-to-many join, with no extra fields on the join itself.
768
+ 2. **Required JSON structure** — `type: "belongsToMany"`, `model`, `through` (required — the join
769
+ table/collection name both sides must agree on).
770
+ 3. **Foreign key behavior** — none; there's no scalar FK field for a many-to-many, only the array
771
+ relation itself.
772
+ 4. **Nullable behavior** — not applicable; both sides are array fields.
773
+ 5. **Reverse relation behavior** — **not automatic**, unlike `belongsTo`/`hasMany`/`hasOne`. A
774
+ one-sided `belongsToMany` (declared on `Post` pointing at `Tag`, but not the reverse on `Tag`)
775
+ is rejected: `many-to-many relation 'tags' has no matching belongsToMany declared on 'Tag' back
776
+ to 'Post'`. A mismatched `through` value between the two sides is also rejected: `through
777
+ 'PostTag' does not match 'Tag.relations.posts.through' ('PostTags') — both sides of a
778
+ many-to-many must agree`.
779
+ 6. **Database output** — Prisma: an implicit many-to-many join table (`<name> <Target>[]
780
+ @relation("<through>")` on both sides — Prisma manages the join table itself). Mongoose: a plain
781
+ array-of-refs field on both sides (`type: [String], ref: "<Target>"`), no separate join
782
+ document.
783
+ 7. **Invalid configurations** — one-sided declaration (no matching reverse); mismatched `through`
784
+ values between the two sides; `through` omitted (`relations of type 'belongsToMany' require a
785
+ 'through' join model/table name`).
427
786
 
428
787
  **Many-to-many is schema-only.** `belongsToMany` is correctly represented in the generated Prisma
429
- schema (implicit join table) and Mongoose model (array of refs on both sides), but the generated
430
- CRUD `create`/`update` endpoints do not read or write it — Prisma's nested-write shape
431
- (`connect: [...]`) and Mongoose's plain array differ enough that GAZAN doesn't attempt a one-size
432
- implementation. Manage join-table writes through your own service code.
788
+ schema and Mongoose model as shown above, but the generated CRUD `create`/`update` endpoints do
789
+ not read or write it — Prisma's nested-write shape (`connect: [...]`) and Mongoose's plain array
790
+ differ enough that GAZAN doesn't attempt a one-size implementation. Manage join-table writes
791
+ through your own service code.
792
+
793
+ ### What entity.json supports
794
+
795
+ - Multiple models, each with its own fields and relations
796
+ - All 13 primitive field types (see [Field types](#field-types))
797
+ - UUID and (Postgres-only) auto-incrementing primary keys, or a synthesized default UUID `id`
798
+ - Enums (`type: "enum"` + `values`)
799
+ - Defaults, including the `"uuid"` and `"now"` generation sentinels
800
+ - `required` and `nullable` field flags (independently — see [Required vs nullable](#required-vs-nullable))
801
+ - Unique fields (`unique: true`)
802
+ - Single-column indexes (`index: true`)
803
+ - String length limits (`length`) and numeric/string bounds (`min`/`max`)
804
+ - Custom table/collection names per model (`tableName`)
805
+ - Opting a model out of timestamps (`timestamps: false`) or out of CRUD generation entirely
806
+ (`crud: false` — the model is still validated and, for Mongoose, still gets a model file, but no
807
+ service/controller/route/validator is generated for it)
808
+ - All four relation types: `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`
809
+ - Self-relations (a model relating to itself — see the realistic fixtures in `tests/fixtures/`)
810
+ - Multiple distinct relations between the same two models (auto-disambiguated reverse fields)
811
+ - Automatic sensitive-field detection (`password`/`secret`/`hash`-like field names are stripped
812
+ from generated API responses — see [Authentication](#authentication))
813
+
814
+ ### What entity.json does NOT support
815
+
816
+ - **Composite (multi-field) unique constraints or indexes** — every `unique`/`index` is
817
+ single-column; `entity.json` has no syntax for `@@unique([a, b])`-style composite constraints.
818
+ - **Non-UUID MongoDB primary keys, or MongoDB `autoIncrement`** — rejected at validation time (see
819
+ [Primary keys](#primary-keys)).
820
+ - **A one-sided `belongsToMany`** — both sides must declare it (see above).
821
+ - **Automatic many-to-many CRUD writes** — the relation is schema-correct, but generated
822
+ create/update endpoints don't read or write the join (see above).
823
+ - **Arbitrary ORM-specific syntax** — no raw Prisma attributes, no raw Mongoose schema options
824
+ beyond what's listed in [Field attributes](#field-attributes); anything not modeled by the
825
+ schema is rejected by `.strict()` Zod validation (`entity.json failed schema validation`), not
826
+ silently ignored.
827
+ - **Application business logic** — no computed fields, hooks, custom SQL, or arbitrary
828
+ server-side logic. `entity.json` only describes data shape and relations.
829
+ - **Controller or service logic beyond generic CRUD** — you get `create`/`findAll`/`findById`/
830
+ `update`/`delete`; anything more custom is written by hand in the generated service/controller.
831
+ - **Per-model route protection** — `entity.json` has no field for "require auth on this model's
832
+ routes." Wire `middlewares/auth.js` into the generated route file yourself if needed.
833
+ - **Field-level renames between `entity.json` and the database column/field name** — the JSON key
834
+ is the field name everywhere (Prisma column, Mongoose path, Zod key).
835
+
836
+ ### Invalid examples
837
+
838
+ Every example below is rejected **before any files are generated** — `entity.json` errors are
839
+ caught at validation time, not partway through a generation run.
840
+
841
+ **Unsafe/invalid model name** (also rejects path-traversal-shaped names):
842
+
843
+ ```json
844
+ { "models": [ { "name": "../User", "fields": { "id": { "type": "uuid", "primaryKey": true } } } ] }
845
+ ```
846
+
847
+ ```text
848
+ models[0].name:
849
+ model name must be a valid identifier (letters/digits, starting with a letter): '../User'
850
+ ```
851
+
852
+ **Unsupported field type:**
853
+
854
+ ```json
855
+ { "models": [ { "name": "User", "fields": { "id": { "type": "unsupported-type" } } } ] }
856
+ ```
857
+
858
+ ```text
859
+ models[0].fields.id.type:
860
+ type must be one of: string, text, number, integer, float, boolean, date, datetime, uuid, json, enum, decimal, bigint
861
+ ```
862
+
863
+ **Unknown relation target** (with a "did you mean" suggestion when one is close enough):
864
+
865
+ ```json
866
+ {
867
+ "models": [
868
+ { "name": "User", "fields": { "id": { "type": "uuid", "primaryKey": true } } },
869
+ {
870
+ "name": "Post",
871
+ "fields": { "authorId": { "type": "uuid", "required": true } },
872
+ "relations": { "author": { "type": "belongsTo", "model": "Userr", "foreignKey": "authorId" } }
873
+ }
874
+ ]
875
+ }
876
+ ```
877
+
878
+ ```text
879
+ models[1].relations.author.model:
880
+ unknown model 'Userr'. Did you mean 'User'?
881
+ ```
882
+
883
+ **One-sided many-to-many:**
884
+
885
+ ```json
886
+ {
887
+ "models": [
888
+ { "name": "Post", "fields": { "id": { "type": "uuid", "primaryKey": true } }, "relations": { "tags": { "type": "belongsToMany", "model": "Tag", "through": "PostTag" } } },
889
+ { "name": "Tag", "fields": { "id": { "type": "uuid", "primaryKey": true } } }
890
+ ]
891
+ }
892
+ ```
893
+
894
+ ```text
895
+ models[0].relations.tags:
896
+ many-to-many relation 'tags' has no matching belongsToMany declared on 'Tag' back to 'Post'
897
+ ```
898
+
899
+ **Database-specific: non-UUID MongoDB primary key** (only rejected when the selected database is
900
+ MongoDB — the very same file is valid for PostgreSQL):
901
+
902
+ ```json
903
+ { "models": [ { "name": "User", "fields": { "id": { "type": "integer", "primaryKey": true, "autoIncrement": true } } } ] }
904
+ ```
905
+
906
+ ```text
907
+ models[0].fields.id.autoIncrement:
908
+ autoIncrement is not supported for MongoDB — it has no native auto-increment primary key
909
+ ```
910
+
911
+ **`required` and `nullable` together:**
912
+
913
+ ```json
914
+ { "models": [ { "name": "User", "fields": { "bio": { "type": "text", "required": true, "nullable": true } } } ] }
915
+ ```
916
+
917
+ ```text
918
+ models[0].fields.bio:
919
+ field cannot be both 'required: true' and 'nullable: true' — required implies non-null
920
+ ```
433
921
 
434
922
  ## Database support
435
923
 
436
- | Mode | Notes |
924
+ GAZAN generates **different, mutually exclusive** database artifacts depending on what you pick —
925
+ never a mix of two backends' files in the same project.
926
+
927
+ ### entity.json → generated output
928
+
929
+ ```text
930
+ entity.json
931
+
932
+ ├── Entity Parser (schema + semantic validation)
933
+
934
+ ├── Normalized Entity Model
935
+
936
+ ├── Database Generator (exactly one of the three runs, per your selection)
937
+ │ ├── Prisma → prisma/schema.prisma
938
+ │ ├── Mongoose → models/<model>.model.{js,ts} (one file per model)
939
+ │ └── Native Mongo → no per-model files (schemaless — see below)
940
+
941
+ └── Application Generator (skipped for a model with "crud": false)
942
+ ├── Validators → validators/<model>.validator.{js,ts} (Zod)
943
+ ├── Services → services/<model>.service.{js,ts} (talks to the DB)
944
+ ├── Controllers → controllers/<model>.controller.{js,ts}
945
+ └── Routes → routes/<model>.routes.{js,ts}
946
+ ```
947
+
948
+ The same `entity.json` is transformed into whichever database representation you selected — the
949
+ Entity Parser and Normalized Entity Model are identical either way; only the Database Generator
950
+ step (and, for Mongoose, whether a per-model file exists at all) differs.
951
+
952
+ ### Database output matrix
953
+
954
+ | Selected database | Generated schema/model output |
437
955
  |---|---|
438
- | PostgreSQL + Prisma | `prisma/schema.prisma` generated from `entity.json` (or a starter model if none is given, so `prisma generate` works immediately). |
439
- | MongoDB + Mongoose | Models under `src/models/`, `_id`-based primary keys (see above). |
440
- | MongoDB (native driver) | No Mongoose — a thin `MongoClient` wrapper with a duplicate-connect guard. |
441
- | None | No database code, no DB dependency, no `DATABASE_URL`/`MONGODB_URI` requirement. |
956
+ | PostgreSQL + Prisma | `prisma/schema.prisma` |
957
+ | MongoDB + Mongoose | `models/*.model.{js,ts}` (one file per model — `src/models/` when using `src/`) |
958
+ | MongoDB (native driver) | No per-model files — a native `MongoClient` config only (`configs/db/index.{js,ts}`) |
959
+ | No database | No database artifacts at all |
960
+
961
+ ### PostgreSQL + Prisma
962
+
963
+ Generates exactly one file, from `entity.json`:
964
+
965
+ ```text
966
+ prisma/
967
+ └── schema.prisma
968
+ ```
969
+
970
+ No Mongoose models and no native-Mongo config are ever generated alongside it. For this
971
+ `entity.json`:
972
+
973
+ ```json
974
+ {
975
+ "models": [
976
+ {
977
+ "name": "User",
978
+ "fields": {
979
+ "id": { "type": "uuid", "primaryKey": true, "default": "uuid" },
980
+ "name": { "type": "string", "required": true },
981
+ "email": { "type": "string", "required": true, "unique": true },
982
+ "age": { "type": "integer" }
983
+ }
984
+ }
985
+ ]
986
+ }
987
+ ```
988
+
989
+ GAZAN generates:
990
+
991
+ ```prisma
992
+ generator client {
993
+ provider = "prisma-client-js"
994
+ }
995
+
996
+ datasource db {
997
+ provider = "postgresql"
998
+ url = env("DATABASE_URL")
999
+ }
1000
+
1001
+ model User {
1002
+ id String @id @default(uuid())
1003
+ name String
1004
+ email String @unique
1005
+ age Int?
1006
+ createdAt DateTime @default(now())
1007
+ updatedAt DateTime @updatedAt
1008
+
1009
+ @@map("users")
1010
+ }
1011
+ ```
1012
+
1013
+ If no `entity.json` is provided, GAZAN still writes a valid starter `prisma/schema.prisma` (one
1014
+ `Example` model) so `prisma generate`/`prisma migrate` work immediately without hand-editing.
1015
+
1016
+ ### MongoDB + Mongoose
1017
+
1018
+ Generates one model file per model declared in `entity.json`, under `models/` (`src/models/` when
1019
+ using `src/`):
1020
+
1021
+ ```text
1022
+ src/
1023
+ └── models/
1024
+ └── user.model.js # (or .ts — matches your selected language)
1025
+ ```
1026
+
1027
+ The **same** `User` model above produces:
1028
+
1029
+ ```js
1030
+ const mongoose = require("mongoose");
1031
+ const { randomUUID } = require("node:crypto");
1032
+
1033
+ const userSchema = new mongoose.Schema(
1034
+ {
1035
+ _id: { type: String, default: () => randomUUID() },
1036
+ name: { type: String, required: true },
1037
+ email: { type: String, required: true, unique: true },
1038
+ age: { type: Number },
1039
+ },
1040
+ {
1041
+ timestamps: true,
1042
+ collection: "users",
1043
+ }
1044
+ );
1045
+ userSchema.set("toJSON", { virtuals: true });
1046
+ const User = mongoose.model("User", userSchema);
1047
+
1048
+ module.exports = User;
1049
+ ```
1050
+
1051
+ No `prisma/schema.prisma` is ever generated for this configuration. If no `entity.json` is
1052
+ provided, no model files are generated at all — only the shared `configs/db/index.{js,ts}`
1053
+ Mongoose connection module — since there's nothing to model yet.
1054
+
1055
+ ### MongoDB (native driver)
1056
+
1057
+ The lightest-weight database option: **no schema, no per-model files at all.** GAZAN generates a
1058
+ single shared client module —
1059
+
1060
+ ```text
1061
+ src/
1062
+ └── configs/
1063
+ └── db/
1064
+ └── index.js # MongoClient wrapper: connect(), getDb(), disconnect()
1065
+ ```
1066
+
1067
+ — and, if `entity.json` was provided, generic CRUD **services** per model that call
1068
+ `getDb().collection("<table>")` directly and pass plain JS objects through (see
1069
+ [Entity-generated application code](#entity-generated-application-code)). There is no Mongoose
1070
+ schema layer to generate, so field types/constraints from `entity.json` are enforced only by the
1071
+ generated Zod validator at the route boundary, not by a database-level schema. No Prisma schema
1072
+ and no Mongoose models are ever generated for this configuration.
1073
+
1074
+ ### No database
1075
+
1076
+ Selecting "None" generates **zero** database-specific artifacts:
1077
+
1078
+ - No `prisma/` directory, no `models/` directory, no `configs/db/`.
1079
+ - No `@prisma/client`, `prisma`, `mongoose`, or `mongodb` dependency in `package.json`.
1080
+ - No `DATABASE_URL`/`MONGODB_URI` in `.env`/`.env.example`, and no corresponding entry in the
1081
+ generated env validator.
1082
+ - If `entity.json` was still provided, generated services fall back to an in-memory `Map`-backed
1083
+ store per model — enough to exercise the full validator → controller → route → service pipeline
1084
+ end-to-end without any real database, but data does not persist across restarts.
1085
+
1086
+ ### Entity-generated application code
1087
+
1088
+ Whether or not a database is selected, providing `entity.json` generates the same **four** kinds
1089
+ of application files per model (skipped for a model with `"crud": false`) — this doesn't change
1090
+ based on database backend, only *where* the files live changes with architecture:
1091
+
1092
+ **MVC** — flat, shared folders, one file per model per folder:
1093
+
1094
+ ```text
1095
+ src/
1096
+ ├── controllers/
1097
+ │ └── user.controller.js
1098
+ ├── services/
1099
+ │ └── user.service.js
1100
+ ├── routes/
1101
+ │ └── user.routes.js
1102
+ │ └── index.js # aggregates every model's router under /api/<pluralized-name>
1103
+ └── validators/
1104
+ └── user.validator.js
1105
+ ```
1106
+
1107
+ **HMVC** — grouped per model instead of by layer:
1108
+
1109
+ ```text
1110
+ src/
1111
+ └── modules/
1112
+ └── user/
1113
+ ├── controllers/
1114
+ │ └── user.controller.js
1115
+ ├── routes/
1116
+ │ └── user.routes.js
1117
+ ├── services/
1118
+ │ └── user.service.js
1119
+ └── validators/
1120
+ └── user.validator.js
1121
+ ```
1122
+
1123
+ File naming is always `<model.kebabName>.<kind>.<ext>` (e.g. `blog-post.controller.ts` for a
1124
+ model named `BlogPost`) — see [Model naming](#model-definition) below.
442
1125
 
443
1126
  ## Authentication
444
1127
 
1128
+ The authentication prompt is **multi-select** — you can enable any combination of methods in one
1129
+ run, not just one:
1130
+
1131
+ ```text
1132
+ ◇ Do you need authentication?
1133
+ │ Yes
1134
+
1135
+ ◇ Which authentication methods?
1136
+ │ ◻ Email/password
1137
+ │ ◼ JWT
1138
+ │ ◼ Refresh tokens
1139
+ │ ◻ OAuth
1140
+ ```
1141
+
1142
+ Internally, your selection normalizes to:
1143
+
1144
+ ```json
1145
+ {
1146
+ "authentication": {
1147
+ "enabled": true,
1148
+ "methods": ["jwt", "refresh-token"]
1149
+ }
1150
+ }
1151
+ ```
1152
+
1153
+ (`enabled` is automatically `false` if you decline the prompt, or if every method gets
1154
+ deselected — an empty selection is equivalent to not enabling authentication at all.)
1155
+
445
1156
  Selected independently — only what you pick is generated:
446
1157
 
447
1158
  | Method | What's generated |
@@ -669,7 +1380,16 @@ The matrix covers, in combination: CJS/MJS, JS/TS, MVC/HMVC, `src`/no-`src`, Pos
669
1380
  MongoDB+Mongoose, MongoDB (native), no database, Redis (with and without BullMQ), BullMQ,
670
1381
  Socket.IO, each authentication method in isolation, module aliases (across every module
671
1382
  system/language combination, MVC and HMVC), and `entity.json` schemas including self-relations,
672
- dual foreign keys to the same model, many-to-many, and a six-model realistic schema.
1383
+ dual foreign keys to the same model, `hasOne`, many-to-many, and a six-model realistic schema.
1384
+
1385
+ Before generating a single project, two fast unit-style checks run first (no `npm install`, so
1386
+ they fail fast): the **entity parser checks** — a valid fixture, an inline invalid-schema object,
1387
+ and dedicated fixtures for `hasOne`, an invalid field type, an invalid (path-traversal-shaped)
1388
+ model name, and an invalid relation (unknown target model) each assert the exact error produced —
1389
+ and the **current-directory checks** — generating with `.`, `./backend`, and `../backend` and
1390
+ asserting the output lands exactly where expected, with no nested duplicate directory and a
1391
+ package name derived from the resolved directory. The "no database" case additionally asserts the
1392
+ *absence* of `prisma/`, `models/`, `configs/db/`, and any database dependency or env var.
673
1393
 
674
1394
  ## Troubleshooting
675
1395
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gazan-init",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Interactive CLI that generates production-ready Node.js backend projects — architecture, database, auth, Redis, BullMQ, Socket.IO, and module aliases, all conditional on what you actually select.",
5
5
  "keywords": [
6
6
  "cli",
@@ -32,13 +32,19 @@ function generateAtomically(targetDir, config, entityModel) {
32
32
  }
33
33
 
34
34
  async function resolveTargetDir(projectName) {
35
+ // path.resolve (never raw string concatenation) already gives '.', './backend', '../backend',
36
+ // an absolute path, and a plain name ("my-app") exactly the semantics each one should have:
37
+ // '.' resolves to process.cwd() itself, so GAZAN generates directly INTO the current directory
38
+ // rather than nesting a new folder inside it.
35
39
  let targetDir = path.resolve(process.cwd(), projectName);
36
40
 
37
41
  for (;;) {
38
42
  if (isDirEmpty(targetDir)) return targetDir;
39
43
 
44
+ const isCwd = targetDir === process.cwd();
45
+ const label = isCwd ? "Current directory" : `Directory "${path.relative(process.cwd(), targetDir) || targetDir}"`;
40
46
  const choice = await p.select({
41
- message: `Directory "${path.relative(process.cwd(), targetDir) || targetDir}" is not empty.`,
47
+ message: `${label} is not empty.`,
42
48
  options: [
43
49
  { value: "cancel", label: "Cancel" },
44
50
  { value: "continue", label: "Continue (files may be overwritten)" },
@@ -108,7 +114,8 @@ async function runInit() {
108
114
  process.exit(1);
109
115
  }
110
116
 
111
- const relTarget = path.relative(process.cwd(), targetDir) || ".";
117
+ const relTarget = path.relative(process.cwd(), targetDir);
118
+ const cdStep = relTarget ? [` cd ${relTarget}`] : []; // generated directly into the current directory — no cd needed
112
119
 
113
120
  p.outro(
114
121
  [
@@ -116,7 +123,7 @@ async function runInit() {
116
123
  "",
117
124
  "Next steps:",
118
125
  "",
119
- ` cd ${relTarget}`,
126
+ ...cdStep,
120
127
  ` npm install`,
121
128
  ` npm run dev`,
122
129
  ].join("\n")
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
 
3
+ const path = require("path");
4
+ const { toPackageName } = require("../utils/strings");
5
+
3
6
  /**
4
7
  * Turns raw prompt answers into the single normalized configuration object
5
8
  * that every generator consumes. Generators must never read prompt answers
@@ -21,8 +24,15 @@ function normalizeConfig(answers) {
21
24
  const rateLimitStrategy = bullMQ ? "redis" : answers.rateLimitStrategy || "memory";
22
25
  const needsRedis = bullMQ || rateLimitStrategy === "redis";
23
26
 
27
+ // The displayed/used project name always comes from the resolved output directory's basename —
28
+ // not the raw prompt answer — so "." (current directory), "./backend", and "my-app" all name
29
+ // the project after where it actually lands instead of after whatever string was typed.
30
+ const projectName = deriveProjectName(answers.targetDir, answers.projectName);
31
+ const packageName = toPackageName(projectName);
32
+
24
33
  return {
25
- projectName: answers.projectName,
34
+ projectName,
35
+ packageName,
26
36
  targetDir: answers.targetDir,
27
37
  moduleSystem,
28
38
  language,
@@ -40,6 +50,14 @@ function normalizeConfig(answers) {
40
50
  };
41
51
  }
42
52
 
53
+ function deriveProjectName(targetDir, rawInput) {
54
+ if (targetDir) {
55
+ const base = path.basename(path.resolve(targetDir));
56
+ if (base) return base;
57
+ }
58
+ return rawInput;
59
+ }
60
+
43
61
  function normalizeDatabase(db) {
44
62
  if (!db || db.type === "none") {
45
63
  return { type: "none", orm: null };
@@ -82,7 +82,7 @@ function buildPackageJson(config) {
82
82
  }
83
83
 
84
84
  const pkg = {
85
- name: config.projectName,
85
+ name: config.packageName || config.projectName,
86
86
  version: "0.1.0",
87
87
  private: true,
88
88
  type: config.moduleSystem === "mjs" ? "module" : "commonjs",
@@ -21,14 +21,24 @@ async function runPrompts(defaults = {}) {
21
21
  p.intro("GAZAN — backend project initializer");
22
22
 
23
23
  const projectName = await ask(p.text, {
24
- message: "Project name",
25
- placeholder: "my-app",
24
+ message: "Project name/path",
25
+ placeholder: "my-app (or '.' for the current directory)",
26
26
  initialValue: defaults.projectName,
27
27
  validate: (value) => {
28
- if (!value) return "Project name is required";
29
- if (!/^[a-z0-9][a-z0-9-_]*$/i.test(value)) return "Use letters, digits, - and _ only";
28
+ if (!value || !value.trim()) return "Project name/path is required";
29
+ const trimmed = value.trim();
30
+ // '.', './relative', '../relative', and absolute paths are all valid OUTPUT PATHS — they're
31
+ // resolved with Node's `path` APIs (never string concatenation) and are not further
32
+ // restricted here; existing-directory safety is enforced later, once the path is resolved.
33
+ if (trimmed === "." || trimmed.startsWith("./") || trimmed.startsWith("../") || path.isAbsolute(trimmed)) {
34
+ return undefined;
35
+ }
36
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(trimmed)) {
37
+ return "Use letters, digits, - and _ only, or a path like '.', './backend', '../backend'";
38
+ }
30
39
  },
31
40
  });
41
+ const projectNamePath = projectName.trim();
32
42
 
33
43
  const moduleSystem = await ask(p.select, {
34
44
  message: "Which module system do you want?",
@@ -129,7 +139,7 @@ async function runPrompts(defaults = {}) {
129
139
  p.outro("Configuration collected.");
130
140
 
131
141
  return {
132
- projectName,
142
+ projectName: projectNamePath,
133
143
  moduleSystem,
134
144
  language,
135
145
  aliasesEnabled,
@@ -98,6 +98,23 @@ function didYouMean(target, candidates) {
98
98
  return bestDistance <= threshold ? best : null;
99
99
  }
100
100
 
101
+ // npm forbids uppercase, most punctuation, and a leading '.' or '_' in package names — this
102
+ // mirrors those rules so a directory-derived name (e.g. from `.` or `./My Api`) always produces
103
+ // a valid package.json "name" instead of failing `npm install` with an opaque error.
104
+ const NPM_NAME_MAX_LENGTH = 214;
105
+
106
+ function toPackageName(name) {
107
+ let out = String(name || "")
108
+ .trim()
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9._~-]+/g, "-")
111
+ .replace(/^[._-]+/, "")
112
+ .replace(/-+/g, "-")
113
+ .replace(/-+$/, "");
114
+ if (!out) out = "app";
115
+ return out.slice(0, NPM_NAME_MAX_LENGTH);
116
+ }
117
+
101
118
  module.exports = {
102
119
  toPascalCase,
103
120
  toCamelCase,
@@ -107,4 +124,5 @@ module.exports = {
107
124
  pluralize,
108
125
  levenshtein,
109
126
  didYouMean,
127
+ toPackageName,
110
128
  };