@famgia/omnify-cli 0.0.85 → 0.0.87

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/dist/cli.js CHANGED
@@ -440,11 +440,14 @@ async function resolveConfig(userConfig, configPath2) {
440
440
  migrationsPath: userConfig.output?.laravel?.migrationsPath ?? "database/migrations"
441
441
  };
442
442
  const laravel = buildLaravelConfig(laravelConfig, userConfig.output?.laravel);
443
+ const tsConfig = userConfig.output?.typescript;
443
444
  const typescript = {
444
- path: userConfig.output?.typescript?.path ?? "types",
445
- singleFile: userConfig.output?.typescript?.singleFile ?? true,
446
- generateEnums: userConfig.output?.typescript?.generateEnums ?? true,
447
- generateRelationships: userConfig.output?.typescript?.generateRelationships ?? true
445
+ path: tsConfig?.path ?? "types",
446
+ schemasDir: tsConfig?.schemasDir ?? "schemas",
447
+ enumDir: tsConfig?.enumDir ?? "enum",
448
+ singleFile: tsConfig?.singleFile ?? true,
449
+ generateEnums: tsConfig?.generateEnums ?? true,
450
+ generateRelationships: tsConfig?.generateRelationships ?? true
448
451
  };
449
452
  const result = {
450
453
  schemasDir: userConfig.schemasDir ?? "./schemas",
@@ -676,8 +679,8 @@ function registerDiffCommand(program2) {
676
679
  }
677
680
 
678
681
  // src/commands/generate.ts
679
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readdirSync } from "fs";
680
- import { resolve as resolve6, dirname as dirname4 } from "path";
682
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
683
+ import { resolve as resolve6, dirname as dirname4, relative } from "path";
681
684
  import {
682
685
  loadSchemas as loadSchemas3,
683
686
  validateSchemas as validateSchemas3,
@@ -701,485 +704,34 @@ import {
701
704
  generateFactories,
702
705
  getFactoryPath
703
706
  } from "@famgia/omnify-laravel";
704
- import { generateTypeScript } from "@famgia/omnify-typescript";
707
+ import { generateTypeScript, copyStubs, generateAIGuides as generateTypescriptAIGuides, shouldGenerateAIGuides as shouldGenerateTypescriptAIGuides } from "@famgia/omnify-typescript";
705
708
 
706
709
  // src/guides/index.ts
707
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
710
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
708
711
  import { resolve as resolve5 } from "path";
709
712
  var CLAUDE_MD = `## Omnify
710
713
 
711
714
  This project uses Omnify for schema-driven code generation.
712
715
 
713
716
  **Documentation**: \`.claude/omnify/\`
714
- - \`schema-guide.md\` - Schema format and property types
715
- - \`laravel-guide.md\` - Laravel generator (if installed)
716
- - \`typescript-guide.md\` - TypeScript generator (if installed)
717
- - \`japan-guide.md\` - Japan plugin types (if installed)
717
+ - \`guides/omnify/\` - Omnify tool docs (schema syntax, config)
718
+ - \`guides/laravel/\` - Laravel implementation guides
719
+ - \`guides/react/\` - React implementation guides
720
+ - \`checklists/\` - Development checklists
721
+ - \`workflows/\` - Workflow guides
722
+ - \`agents/\` - AI agent prompts
718
723
 
719
724
  **Commands**:
720
725
  - \`npx omnify generate\` - Generate code from schemas
721
726
  - \`npx omnify validate\` - Validate schemas
722
727
  `;
723
- var SCHEMA_GUIDE = `# Omnify Schema Guide
724
-
725
- ## Schema File Format
726
-
727
- Schemas are YAML files defining data models. Each file represents one entity.
728
-
729
- \`\`\`yaml
730
- name: User
731
- displayName:
732
- ja: \u30E6\u30FC\u30B6\u30FC
733
- en: User
734
- kind: object
735
-
736
- properties:
737
- email:
738
- type: Email
739
- unique: true
740
- name:
741
- type: String
742
- bio:
743
- type: Text
744
- nullable: true
745
-
746
- options:
747
- timestamps: true
748
- softDelete: true
749
- \`\`\`
750
-
751
- ## Property Types
752
-
753
- ### String Types
754
- - \`String\` - VARCHAR(255)
755
- - \`Text\` - TEXT
756
- - \`LongText\` - LONGTEXT
757
- - \`Email\` - VARCHAR(255) for email addresses
758
- - \`Password\` - VARCHAR(255), hidden in serialization
759
-
760
- ### Numeric Types
761
- - \`Int\` - INTEGER
762
- - \`BigInt\` - BIGINT
763
- - \`TinyInt\` - TINYINT
764
- - \`Float\` - DOUBLE
765
- - \`Decimal\` - DECIMAL(precision, scale)
766
-
767
- ### Date/Time Types
768
- - \`Date\` - DATE
769
- - \`Time\` - TIME
770
- - \`DateTime\` - DATETIME
771
- - \`Timestamp\` - TIMESTAMP
772
-
773
- ### Other Types
774
- - \`Boolean\` - BOOLEAN
775
- - \`Json\` - JSON
776
- - \`Enum\` - ENUM with values
777
- - \`EnumRef\` - Reference to enum schema
778
-
779
- ## Property Options
780
-
781
- \`\`\`yaml
782
- propertyName:
783
- type: String
784
- nullable: true # Can be NULL
785
- unique: true # Unique constraint
786
- default: "value" # Default value
787
- length: 100 # VARCHAR length
788
- hidden: true # Hide from JSON output
789
- fillable: false # Exclude from mass assignment
790
- \`\`\`
791
-
792
- ## Associations
793
-
794
- \`\`\`yaml
795
- # Many-to-One (belongsTo)
796
- author:
797
- type: Association
798
- relation: ManyToOne
799
- target: User
800
- onDelete: CASCADE
801
-
802
- # One-to-Many (hasMany)
803
- posts:
804
- type: Association
805
- relation: OneToMany
806
- target: Post
807
-
808
- # Many-to-Many (belongsToMany)
809
- tags:
810
- type: Association
811
- relation: ManyToMany
812
- target: Tag
813
- joinTable: post_tags
814
-
815
- # Polymorphic
816
- commentable:
817
- type: Association
818
- relation: MorphTo
819
- \`\`\`
820
-
821
- ## Indexes
822
-
823
- \`\`\`yaml
824
- indexes:
825
- - columns: [status, published_at]
826
- - columns: [email]
827
- unique: true
828
- \`\`\`
829
-
830
- ## Schema Options
831
-
832
- \`\`\`yaml
833
- options:
834
- timestamps: true # Add created_at, updated_at
835
- softDelete: true # Add deleted_at
836
- idType: BigInt # Primary key type (Int, BigInt, Uuid)
837
- \`\`\`
838
- `;
839
- var LARAVEL_GUIDE = `# Laravel Generator Guide
840
-
841
- ## Generated Files
842
-
843
- ### Migrations
844
- Located in \`database/migrations/omnify/\`
845
- - Auto-generated from schema changes
846
- - Handles column additions, modifications, removals
847
- - Preserves manual migrations outside omnify folder
848
-
849
- ### Models
850
- Two-tier model structure:
851
- - \`app/Models/OmnifyBase/*BaseModel.php\` - Auto-generated, DO NOT EDIT
852
- - \`app/Models/*.php\` - User models, extend base models, safe to customize
853
-
854
- ### Factories
855
- Located in \`database/factories/\`
856
- - Generated once, safe to customize
857
- - Uses appropriate Faker methods for each type
858
-
859
- ## Model Features
860
-
861
- ### Fillable
862
- All schema properties are mass-assignable by default.
863
- Use \`fillable: false\` to exclude.
864
-
865
- ### Hidden
866
- Use \`hidden: true\` to exclude from JSON/array output.
867
-
868
- \`\`\`yaml
869
- password:
870
- type: Password
871
- hidden: true
872
- \`\`\`
873
-
874
- ### Casts
875
- Auto-generated based on property types:
876
- - \`Boolean\` \u2192 \`'boolean'\`
877
- - \`Json\` \u2192 \`'array'\`
878
- - \`Timestamp\` \u2192 \`'datetime'\`
879
-
880
- ### Relationships
881
- Generated from Association properties:
882
- - \`ManyToOne\` \u2192 \`belongsTo()\`
883
- - \`OneToMany\` \u2192 \`hasMany()\`
884
- - \`ManyToMany\` \u2192 \`belongsToMany()\`
885
- - \`MorphTo\` \u2192 \`morphTo()\`
886
- - \`MorphMany\` \u2192 \`morphMany()\`
887
-
888
- ## Commands
889
-
890
- \`\`\`bash
891
- # Generate migrations and models
892
- npx omnify generate
893
-
894
- # Force regeneration
895
- npx omnify generate --force
896
-
897
- # Validate schemas
898
- npx omnify validate
899
- \`\`\`
900
- `;
901
- var TYPESCRIPT_GUIDE = `# TypeScript Generator Guide
902
-
903
- ## Generated Types
904
-
905
- Types are generated in the configured output directory.
906
-
907
- ### Interface Generation
908
-
909
- Each schema generates a TypeScript interface:
910
-
911
- \`\`\`typescript
912
- export interface User {
913
- id: number;
914
- email: string;
915
- name: string;
916
- bio: string | null;
917
- created_at: string;
918
- updated_at: string;
919
- }
920
- \`\`\`
921
-
922
- ### Type Mappings
923
-
924
- | Omnify Type | TypeScript Type |
925
- |-------------|-----------------|
926
- | String, Text | string |
927
- | Int, BigInt | number |
928
- | Float, Decimal | number |
929
- | Boolean | boolean |
930
- | Date, DateTime | string |
931
- | Json | Record<string, unknown> |
932
- | Enum | union of literals |
933
-
934
- ### Nullable Types
935
-
936
- Nullable properties become \`T | null\`:
937
-
938
- \`\`\`typescript
939
- bio: string | null;
940
- \`\`\`
941
-
942
- ### Associations
943
-
944
- Associations generate optional relation properties:
945
-
946
- \`\`\`typescript
947
- export interface Post {
948
- id: number;
949
- title: string;
950
- author_id: number;
951
- author?: User; // Optional relation
952
- comments?: Comment[]; // Optional array relation
953
- }
954
- \`\`\`
955
- `;
956
- var JAPAN_GUIDE = `# Japan Plugin Types Guide
957
-
958
- This project uses \`@famgia/omnify-japan\` plugin which provides Japan-specific types.
959
-
960
- ## Available Types
961
-
962
- ### Simple Types
963
-
964
- #### JapanesePhone
965
- Japanese phone number format (e.g., \`090-1234-5678\`, \`03-1234-5678\`)
966
- - SQL: \`VARCHAR(15)\`
967
- - Accepts with or without hyphens
968
-
969
- \`\`\`yaml
970
- phone:
971
- type: JapanesePhone
972
- \`\`\`
973
-
974
- #### JapanesePostalCode
975
- Japanese postal code format (e.g., \`123-4567\`)
976
- - SQL: \`VARCHAR(8)\`
977
- - Accepts with or without hyphen
978
-
979
- \`\`\`yaml
980
- postal_code:
981
- type: JapanesePostalCode
982
- nullable: true
983
- \`\`\`
984
-
985
- ### Compound Types
986
-
987
- Compound types expand into multiple database columns automatically.
988
-
989
- #### JapaneseName
990
- Japanese name with kanji and kana variants.
991
-
992
- **Expands to 4 columns:**
993
- - \`{property}_lastname\` - VARCHAR(50) - Family name (\u59D3)
994
- - \`{property}_firstname\` - VARCHAR(50) - Given name (\u540D)
995
- - \`{property}_kana_lastname\` - VARCHAR(100) - Family name in katakana
996
- - \`{property}_kana_firstname\` - VARCHAR(100) - Given name in katakana
997
-
998
- **Accessors generated:**
999
- - \`{property}_full_name\` - "\u59D3 \u540D" (space-separated)
1000
- - \`{property}_full_name_kana\` - "\u30BB\u30A4 \u30E1\u30A4" (space-separated)
1001
-
1002
- \`\`\`yaml
1003
- name:
1004
- type: JapaneseName
1005
- displayName:
1006
- ja: \u6C0F\u540D
1007
- en: Full Name
1008
- # Per-field overrides
1009
- fields:
1010
- KanaLastname:
1011
- nullable: true
1012
- hidden: true
1013
- KanaFirstname:
1014
- nullable: true
1015
- hidden: true
1016
- \`\`\`
1017
-
1018
- #### JapaneseAddress
1019
- Japanese address with postal code and prefecture ID.
1020
-
1021
- **Expands to 5 columns:**
1022
- - \`{property}_postal_code\` - VARCHAR(8) - Postal code (\u90F5\u4FBF\u756A\u53F7)
1023
- - \`{property}_prefecture_id\` - TINYINT UNSIGNED - Prefecture ID 1-47 (\u90FD\u9053\u5E9C\u770C)
1024
- - \`{property}_address1\` - VARCHAR(255) - City/Ward (\u5E02\u533A\u753A\u6751)
1025
- - \`{property}_address2\` - VARCHAR(255) - Street address (\u4E01\u76EE\u756A\u5730\u53F7)
1026
- - \`{property}_address3\` - VARCHAR(255) NULLABLE - Building name (\u30D3\u30EB\u30FB\u30DE\u30F3\u30B7\u30E7\u30F3\u540D)
1027
-
1028
- **Accessors generated:**
1029
- - \`{property}_full_address\` - Concatenation of address1 + address2 + address3
1030
-
1031
- \`\`\`yaml
1032
- address:
1033
- type: JapaneseAddress
1034
- displayName:
1035
- ja: \u4F4F\u6240
1036
- en: Address
1037
- fields:
1038
- Address3:
1039
- nullable: true
1040
- \`\`\`
1041
-
1042
- **Prefecture IDs (JIS X 0401):**
1043
- | ID | Prefecture | ID | Prefecture | ID | Prefecture |
1044
- |----|-----------|----|-----------|----|-----------|
1045
- | 1 | \u5317\u6D77\u9053 | 17 | \u77F3\u5DDD\u770C | 33 | \u5CA1\u5C71\u770C |
1046
- | 2 | \u9752\u68EE\u770C | 18 | \u798F\u4E95\u770C | 34 | \u5E83\u5CF6\u770C |
1047
- | 3 | \u5CA9\u624B\u770C | 19 | \u5C71\u68A8\u770C | 35 | \u5C71\u53E3\u770C |
1048
- | 4 | \u5BAE\u57CE\u770C | 20 | \u9577\u91CE\u770C | 36 | \u5FB3\u5CF6\u770C |
1049
- | 5 | \u79CB\u7530\u770C | 21 | \u5C90\u961C\u770C | 37 | \u9999\u5DDD\u770C |
1050
- | 6 | \u5C71\u5F62\u770C | 22 | \u9759\u5CA1\u770C | 38 | \u611B\u5A9B\u770C |
1051
- | 7 | \u798F\u5CF6\u770C | 23 | \u611B\u77E5\u770C | 39 | \u9AD8\u77E5\u770C |
1052
- | 8 | \u8328\u57CE\u770C | 24 | \u4E09\u91CD\u770C | 40 | \u798F\u5CA1\u770C |
1053
- | 9 | \u6803\u6728\u770C | 25 | \u6ECB\u8CC0\u770C | 41 | \u4F50\u8CC0\u770C |
1054
- | 10 | \u7FA4\u99AC\u770C | 26 | \u4EAC\u90FD\u5E9C | 42 | \u9577\u5D0E\u770C |
1055
- | 11 | \u57FC\u7389\u770C | 27 | \u5927\u962A\u5E9C | 43 | \u718A\u672C\u770C |
1056
- | 12 | \u5343\u8449\u770C | 28 | \u5175\u5EAB\u770C | 44 | \u5927\u5206\u770C |
1057
- | 13 | \u6771\u4EAC\u90FD | 29 | \u5948\u826F\u770C | 45 | \u5BAE\u5D0E\u770C |
1058
- | 14 | \u795E\u5948\u5DDD\u770C | 30 | \u548C\u6B4C\u5C71\u770C | 46 | \u9E7F\u5150\u5CF6\u770C |
1059
- | 15 | \u65B0\u6F5F\u770C | 31 | \u9CE5\u53D6\u770C | 47 | \u6C96\u7E04\u770C |
1060
- | 16 | \u5BCC\u5C71\u770C | 32 | \u5CF6\u6839\u770C | | |
1061
-
1062
- #### JapaneseBankAccount
1063
- Japanese bank account information.
1064
-
1065
- **Expands to 5 columns:**
1066
- - \`{property}_bank_code\` - VARCHAR(4) - Bank code (\u9280\u884C\u30B3\u30FC\u30C9)
1067
- - \`{property}_branch_code\` - VARCHAR(3) - Branch code (\u652F\u5E97\u30B3\u30FC\u30C9)
1068
- - \`{property}_account_type\` - ENUM - Account type: 1=\u666E\u901A, 2=\u5F53\u5EA7, 4=\u8CAF\u84C4
1069
- - \`{property}_account_number\` - VARCHAR(7) - Account number (\u53E3\u5EA7\u756A\u53F7)
1070
- - \`{property}_account_holder\` - VARCHAR(100) - Account holder name (\u53E3\u5EA7\u540D\u7FA9)
1071
-
1072
- \`\`\`yaml
1073
- bank_account:
1074
- type: JapaneseBankAccount
1075
- \`\`\`
1076
-
1077
- ## Per-field Overrides
1078
-
1079
- All compound types support per-field overrides:
1080
-
1081
- \`\`\`yaml
1082
- name:
1083
- type: JapaneseName
1084
- fields:
1085
- Lastname:
1086
- length: 100 # Override default VARCHAR length
1087
- Firstname:
1088
- length: 100
1089
- KanaLastname:
1090
- length: 200
1091
- nullable: true
1092
- hidden: true
1093
- KanaFirstname:
1094
- length: 200
1095
- nullable: true
1096
- hidden: true
1097
- \`\`\`
1098
-
1099
- **Available overrides:**
1100
- - \`length\` - VARCHAR length (override default)
1101
- - \`nullable\` - Whether the field can be NULL
1102
- - \`hidden\` - Exclude from JSON/array output
1103
- - \`fillable\` - Control mass assignment
1104
-
1105
- ## Factory Examples
1106
-
1107
- \`\`\`php
1108
- $faker = fake('ja_JP');
1109
-
1110
- return [
1111
- // JapaneseName
1112
- 'name_lastname' => $faker->lastName(),
1113
- 'name_firstname' => $faker->firstName(),
1114
- 'name_kana_lastname' => $faker->lastKanaName(),
1115
- 'name_kana_firstname' => $faker->firstKanaName(),
1116
-
1117
- // JapanesePhone
1118
- 'phone' => $faker->phoneNumber(),
1119
-
1120
- // JapanesePostalCode
1121
- 'postal_code' => $faker->postcode(),
1122
-
1123
- // JapaneseAddress
1124
- 'address_postal_code' => $faker->postcode(),
1125
- 'address_prefecture_id' => $faker->numberBetween(1, 47),
1126
- 'address_address1' => $faker->city(),
1127
- 'address_address2' => $faker->streetAddress(),
1128
- 'address_address3' => $faker->optional(0.5)->secondaryAddress(),
1129
- ];
1130
- \`\`\`
1131
-
1132
- ## Model Accessors
1133
-
1134
- \`\`\`php
1135
- // JapaneseName accessors
1136
- $customer->name_full_name; // "\u7530\u4E2D \u592A\u90CE"
1137
- $customer->name_full_name_kana; // "\u30BF\u30CA\u30AB \u30BF\u30ED\u30A6"
1138
-
1139
- // JapaneseAddress accessor
1140
- $customer->address_full_address; // "\u5343\u4EE3\u7530\u533A\u4E38\u306E\u51851-1-1\u30D3\u30EB5F"
1141
- \`\`\`
1142
- `;
1143
- function isJapanPlugin(plugin) {
1144
- return plugin.name === "@famgia/omnify-japan";
1145
- }
1146
- function isLaravelPlugin(plugin) {
1147
- return plugin.name === "@famgia/omnify-laravel";
1148
- }
1149
- function isTypeScriptPlugin(plugin) {
1150
- return plugin.name === "@famgia/omnify-typescript";
1151
- }
1152
- function generateAIGuides(rootDir, plugins) {
1153
- const guidesDir = resolve5(rootDir, ".claude/omnify");
728
+ function generateAIGuides(rootDir, _plugins) {
1154
729
  let filesWritten = 0;
1155
- if (!existsSync3(guidesDir)) {
1156
- mkdirSync2(guidesDir, { recursive: true });
1157
- }
1158
730
  const claudeMdPath = resolve5(rootDir, "CLAUDE.md");
1159
731
  if (!existsSync3(claudeMdPath)) {
1160
732
  writeFileSync2(claudeMdPath, CLAUDE_MD);
1161
733
  filesWritten++;
1162
734
  }
1163
- const schemaGuidePath = resolve5(guidesDir, "schema-guide.md");
1164
- writeFileSync2(schemaGuidePath, SCHEMA_GUIDE);
1165
- filesWritten++;
1166
- for (const plugin of plugins) {
1167
- if (isLaravelPlugin(plugin)) {
1168
- const laravelGuidePath = resolve5(guidesDir, "laravel-guide.md");
1169
- writeFileSync2(laravelGuidePath, LARAVEL_GUIDE);
1170
- filesWritten++;
1171
- }
1172
- if (isTypeScriptPlugin(plugin)) {
1173
- const tsGuidePath = resolve5(guidesDir, "typescript-guide.md");
1174
- writeFileSync2(tsGuidePath, TYPESCRIPT_GUIDE);
1175
- filesWritten++;
1176
- }
1177
- if (isJapanPlugin(plugin)) {
1178
- const japanGuidePath = resolve5(guidesDir, "japan-guide.md");
1179
- writeFileSync2(japanGuidePath, JAPAN_GUIDE);
1180
- filesWritten++;
1181
- }
1182
- }
1183
735
  return filesWritten;
1184
736
  }
1185
737
 
@@ -1385,7 +937,7 @@ function writeGeneratorOutputs(outputs, rootDir) {
1385
937
  const filePath = resolve6(rootDir, output.path);
1386
938
  const dir = dirname4(filePath);
1387
939
  if (!existsSync4(dir)) {
1388
- mkdirSync3(dir, { recursive: true });
940
+ mkdirSync2(dir, { recursive: true });
1389
941
  logger.debug(`Created directory: ${dir}`);
1390
942
  }
1391
943
  if (output.skipIfExists && existsSync4(filePath)) {
@@ -1438,11 +990,19 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1438
990
  }
1439
991
  }
1440
992
  }
993
+ const pluginEnumsMap = /* @__PURE__ */ new Map();
994
+ for (const plugin of config.plugins) {
995
+ if (plugin.enums) {
996
+ for (const enumDef of plugin.enums) {
997
+ pluginEnumsMap.set(enumDef.name, enumDef);
998
+ }
999
+ }
1000
+ }
1441
1001
  if (!options.typesOnly && config.output.laravel) {
1442
1002
  logger.step("Generating Laravel migrations...");
1443
1003
  const migrationsDir = resolve6(rootDir, config.output.laravel.migrationsPath);
1444
1004
  if (!existsSync4(migrationsDir)) {
1445
- mkdirSync3(migrationsDir, { recursive: true });
1005
+ mkdirSync2(migrationsDir, { recursive: true });
1446
1006
  logger.debug(`Created directory: ${migrationsDir}`);
1447
1007
  }
1448
1008
  const addedSchemaNames = new Set(
@@ -1487,10 +1047,10 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1487
1047
  const modelsDir = resolve6(rootDir, modelsPath);
1488
1048
  const baseModelsDir = resolve6(rootDir, baseModelsPath);
1489
1049
  if (!existsSync4(modelsDir)) {
1490
- mkdirSync3(modelsDir, { recursive: true });
1050
+ mkdirSync2(modelsDir, { recursive: true });
1491
1051
  }
1492
1052
  if (!existsSync4(baseModelsDir)) {
1493
- mkdirSync3(baseModelsDir, { recursive: true });
1053
+ mkdirSync2(baseModelsDir, { recursive: true });
1494
1054
  }
1495
1055
  const providersPath = config.output.laravel.providersPath ?? "app/Providers";
1496
1056
  const models = generateModels(schemas, {
@@ -1503,7 +1063,7 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1503
1063
  const filePath = resolve6(rootDir, getModelPath(model));
1504
1064
  const fileDir = dirname4(filePath);
1505
1065
  if (!existsSync4(fileDir)) {
1506
- mkdirSync3(fileDir, { recursive: true });
1066
+ mkdirSync2(fileDir, { recursive: true });
1507
1067
  }
1508
1068
  if (!model.overwrite && existsSync4(filePath)) {
1509
1069
  logger.debug(`Skipped (exists): ${getModelPath(model)}`);
@@ -1520,7 +1080,7 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1520
1080
  const factoriesPath = config.output.laravel.factoriesPath;
1521
1081
  const factoriesDir = resolve6(rootDir, factoriesPath);
1522
1082
  if (!existsSync4(factoriesDir)) {
1523
- mkdirSync3(factoriesDir, { recursive: true });
1083
+ mkdirSync2(factoriesDir, { recursive: true });
1524
1084
  }
1525
1085
  const factories = generateFactories(schemas, {
1526
1086
  factoryPath: factoriesPath
@@ -1529,7 +1089,7 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1529
1089
  const filePath = resolve6(rootDir, getFactoryPath(factory));
1530
1090
  const fileDir = dirname4(filePath);
1531
1091
  if (!existsSync4(fileDir)) {
1532
- mkdirSync3(fileDir, { recursive: true });
1092
+ mkdirSync2(fileDir, { recursive: true });
1533
1093
  }
1534
1094
  if (!factory.overwrite && existsSync4(filePath)) {
1535
1095
  logger.debug(`Skipped (exists): ${getFactoryPath(factory)}`);
@@ -1543,25 +1103,35 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1543
1103
  }
1544
1104
  if (!options.migrationsOnly && config.output.typescript) {
1545
1105
  logger.step("Generating TypeScript types...");
1546
- const typesDir = resolve6(rootDir, config.output.typescript.path);
1547
- if (!existsSync4(typesDir)) {
1548
- mkdirSync3(typesDir, { recursive: true });
1549
- logger.debug(`Created directory: ${typesDir}`);
1106
+ const tsConfig = config.output.typescript;
1107
+ const basePath = resolve6(rootDir, tsConfig.path);
1108
+ const schemasDir = resolve6(basePath, tsConfig.schemasDir ?? "schemas");
1109
+ const enumDir = resolve6(basePath, tsConfig.enumDir ?? "enum");
1110
+ const enumImportPrefix = relative(schemasDir, enumDir).replace(/\\/g, "/");
1111
+ if (!existsSync4(schemasDir)) {
1112
+ mkdirSync2(schemasDir, { recursive: true });
1113
+ logger.debug(`Created directory: ${schemasDir}`);
1114
+ }
1115
+ if (!existsSync4(enumDir)) {
1116
+ mkdirSync2(enumDir, { recursive: true });
1117
+ logger.debug(`Created directory: ${enumDir}`);
1550
1118
  }
1551
1119
  const isMultiLocale = config.locale && config.locale.locales && config.locale.locales.length > 1;
1552
- const typescriptConfig = config.output.typescript;
1553
1120
  const typeFiles = generateTypeScript(schemas, {
1554
1121
  customTypes: customTypesMap,
1122
+ pluginEnums: pluginEnumsMap,
1555
1123
  localeConfig: config.locale,
1556
1124
  multiLocale: isMultiLocale,
1557
- generateRules: typescriptConfig.generateRules ?? true,
1558
- validationTemplates: typescriptConfig.validationTemplates
1125
+ generateRules: tsConfig.generateRules ?? true,
1126
+ validationTemplates: tsConfig.validationTemplates,
1127
+ enumImportPrefix
1559
1128
  });
1560
1129
  for (const file of typeFiles) {
1561
- const filePath = resolve6(typesDir, file.filePath);
1130
+ const outputDir = file.category === "enum" ? enumDir : schemasDir;
1131
+ const filePath = resolve6(outputDir, file.filePath);
1562
1132
  const fileDir = dirname4(filePath);
1563
1133
  if (!existsSync4(fileDir)) {
1564
- mkdirSync3(fileDir, { recursive: true });
1134
+ mkdirSync2(fileDir, { recursive: true });
1565
1135
  }
1566
1136
  if (!file.overwrite && existsSync4(filePath)) {
1567
1137
  logger.debug(`Skipped (exists): ${file.filePath}`);
@@ -1572,6 +1142,13 @@ function runDirectGeneration(schemas, config, rootDir, options, changes) {
1572
1142
  typesGenerated++;
1573
1143
  }
1574
1144
  logger.success(`Generated ${typesGenerated} TypeScript file(s)`);
1145
+ const stubsResult = copyStubs({
1146
+ targetDir: basePath,
1147
+ skipIfExists: true
1148
+ });
1149
+ if (stubsResult.copied.length > 0) {
1150
+ logger.success(`Generated ${stubsResult.copied.length} React stub(s)`);
1151
+ }
1575
1152
  }
1576
1153
  return { migrations: migrationsGenerated, types: typesGenerated, models: modelsGenerated, factories: factoriesGenerated };
1577
1154
  }
@@ -1618,7 +1195,9 @@ async function runGenerate(options) {
1618
1195
  const v2Lock = existingLock && isLockFileV2(existingLock) ? existingLock : null;
1619
1196
  const comparison = compareSchemasDeep(currentSnapshots, v2Lock);
1620
1197
  const skipMigrations = !comparison.hasChanges && !options.force;
1621
- if (skipMigrations && !config.output.laravel?.modelsPath) {
1198
+ const pluginsHaveGenerators = config.plugins.some((p) => p.generators && p.generators.length > 0);
1199
+ const hasTypescriptOutput = !!config.output.typescript;
1200
+ if (skipMigrations && !config.output.laravel?.modelsPath && !pluginsHaveGenerators && !hasTypescriptOutput) {
1622
1201
  logger.success("No changes to generate");
1623
1202
  return;
1624
1203
  }
@@ -1641,6 +1220,14 @@ async function runGenerate(options) {
1641
1220
  }
1642
1221
  }
1643
1222
  }
1223
+ const pluginEnumsMap = /* @__PURE__ */ new Map();
1224
+ for (const plugin of config.plugins) {
1225
+ if (plugin.enums) {
1226
+ for (const enumDef of plugin.enums) {
1227
+ pluginEnumsMap.set(enumDef.name, enumDef);
1228
+ }
1229
+ }
1230
+ }
1644
1231
  if (usePlugins) {
1645
1232
  logger.step("Running plugin generators...");
1646
1233
  const counts = await runPluginGeneration(
@@ -1669,25 +1256,35 @@ async function runGenerate(options) {
1669
1256
  }
1670
1257
  if (!options.migrationsOnly && config.output.typescript && typesGenerated === 0) {
1671
1258
  logger.step("Generating TypeScript types...");
1672
- const typesDir = resolve6(rootDir, config.output.typescript.path);
1673
- if (!existsSync4(typesDir)) {
1674
- mkdirSync3(typesDir, { recursive: true });
1675
- logger.debug(`Created directory: ${typesDir}`);
1259
+ const tsConfig2 = config.output.typescript;
1260
+ const basePath2 = resolve6(rootDir, tsConfig2.path);
1261
+ const schemasDir2 = resolve6(basePath2, tsConfig2.schemasDir ?? "schemas");
1262
+ const enumDir2 = resolve6(basePath2, tsConfig2.enumDir ?? "enum");
1263
+ const enumImportPrefix2 = relative(schemasDir2, enumDir2).replace(/\\/g, "/");
1264
+ if (!existsSync4(schemasDir2)) {
1265
+ mkdirSync2(schemasDir2, { recursive: true });
1266
+ logger.debug(`Created directory: ${schemasDir2}`);
1267
+ }
1268
+ if (!existsSync4(enumDir2)) {
1269
+ mkdirSync2(enumDir2, { recursive: true });
1270
+ logger.debug(`Created directory: ${enumDir2}`);
1676
1271
  }
1677
1272
  const isMultiLocale = config.locale && config.locale.locales && config.locale.locales.length > 1;
1678
- const typescriptConfig = config.output.typescript;
1679
1273
  const typeFiles = generateTypeScript(schemas, {
1680
1274
  customTypes: customTypesMap,
1275
+ pluginEnums: pluginEnumsMap,
1681
1276
  localeConfig: config.locale,
1682
1277
  multiLocale: isMultiLocale,
1683
- generateRules: typescriptConfig.generateRules ?? true,
1684
- validationTemplates: typescriptConfig.validationTemplates
1278
+ generateRules: tsConfig2.generateRules ?? true,
1279
+ validationTemplates: tsConfig2.validationTemplates,
1280
+ enumImportPrefix: enumImportPrefix2
1685
1281
  });
1686
1282
  for (const file of typeFiles) {
1687
- const filePath = resolve6(typesDir, file.filePath);
1283
+ const outputDir2 = file.category === "enum" ? enumDir2 : schemasDir2;
1284
+ const filePath = resolve6(outputDir2, file.filePath);
1688
1285
  const fileDir = dirname4(filePath);
1689
1286
  if (!existsSync4(fileDir)) {
1690
- mkdirSync3(fileDir, { recursive: true });
1287
+ mkdirSync2(fileDir, { recursive: true });
1691
1288
  }
1692
1289
  if (!file.overwrite && existsSync4(filePath)) {
1693
1290
  logger.debug(`Skipped (exists): ${file.filePath}`);
@@ -1698,6 +1295,22 @@ async function runGenerate(options) {
1698
1295
  typesGenerated++;
1699
1296
  }
1700
1297
  logger.success(`Generated ${typesGenerated} TypeScript file(s)`);
1298
+ const stubsResult2 = copyStubs({
1299
+ targetDir: basePath2,
1300
+ skipIfExists: true
1301
+ });
1302
+ if (stubsResult2.copied.length > 0) {
1303
+ logger.success(`Generated ${stubsResult2.copied.length} React stub(s)`);
1304
+ }
1305
+ if (shouldGenerateTypescriptAIGuides(rootDir)) {
1306
+ const tsAIResult = generateTypescriptAIGuides(rootDir, {
1307
+ typescriptPath: tsConfig2.path
1308
+ });
1309
+ const tsClaudeTotal = tsAIResult.claudeGuides + tsAIResult.claudeChecklists;
1310
+ if (tsClaudeTotal > 0 || tsAIResult.cursorRules > 0) {
1311
+ logger.debug(`Generated ${tsClaudeTotal} React Claude files, ${tsAIResult.cursorRules} Cursor rules`);
1312
+ }
1313
+ }
1701
1314
  }
1702
1315
  } else {
1703
1316
  const counts = runDirectGeneration(schemas, config, rootDir, options, comparison.changes);
@@ -1882,6 +1495,8 @@ async function runReset(options) {
1882
1495
  const typescriptPath = config.output.typescript?.path;
1883
1496
  const tsBasePath = typescriptPath ? resolve7(rootDir, typescriptPath) : null;
1884
1497
  const commonTsPaths = [
1498
+ "resources/ts/omnify",
1499
+ "resources/ts/omnify/schemas",
1885
1500
  "resources/ts/types/models",
1886
1501
  "frontend/src/types/model",
1887
1502
  "frontend/src/types/models",
@@ -1898,14 +1513,14 @@ async function runReset(options) {
1898
1513
  }
1899
1514
  }
1900
1515
  if (foundTsPath && existsSync5(foundTsPath)) {
1901
- const autoGeneratedDirs = ["base", "enum", "rules"];
1516
+ const autoGeneratedDirs = ["base", "enum", "rules", "hooks", "lib"];
1902
1517
  for (const subDir of autoGeneratedDirs) {
1903
1518
  const subPath = join(foundTsPath, subDir);
1904
1519
  if (existsSync5(subPath)) {
1905
1520
  paths.push({ name: `TypeScript ${subDir}`, path: subPath, type: "dir" });
1906
1521
  }
1907
1522
  }
1908
- const autoGeneratedFiles = ["common.ts", "index.ts"];
1523
+ const autoGeneratedFiles = ["common.ts", "index.ts", "i18n.ts"];
1909
1524
  for (const fileName of autoGeneratedFiles) {
1910
1525
  const filePath = join(foundTsPath, fileName);
1911
1526
  if (existsSync5(filePath)) {