@tailor-platform/erp-kit 0.2.2 → 0.3.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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +158 -62
  3. package/dist/cli.mjs +344 -215
  4. package/package.json +3 -2
  5. package/skills/erp-kit-app-1-requirements/SKILL.md +19 -8
  6. package/skills/erp-kit-app-2-requirements-review/SKILL.md +5 -4
  7. package/skills/erp-kit-app-2-requirements-review/references/best-practices-check.md +6 -1
  8. package/skills/erp-kit-app-2-requirements-review/references/boundary-consistency-check.md +6 -1
  9. package/skills/erp-kit-app-3-plan/SKILL.md +4 -7
  10. package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +1 -19
  11. package/skills/erp-kit-app-4-plan-review/SKILL.md +1 -10
  12. package/skills/erp-kit-app-5-impl-backend/SKILL.md +1 -8
  13. package/skills/erp-kit-app-shared/SKILL.md +15 -0
  14. package/skills/erp-kit-app-shared/references/link-format-reference.md +13 -0
  15. package/skills/erp-kit-app-shared/references/naming-conventions.md +21 -0
  16. package/skills/erp-kit-app-shared/references/resolver-classification.md +23 -0
  17. package/skills/erp-kit-app-shared/references/schema-constraints.md +25 -0
  18. package/skills/erp-kit-module-1-requirements/SKILL.md +1 -1
  19. package/skills/erp-kit-module-1-requirements/references/feature-doc.md +1 -1
  20. package/skills/erp-kit-module-3-plan/SKILL.md +5 -5
  21. package/skills/erp-kit-module-3-plan/references/naming.md +15 -1
  22. package/skills/erp-kit-module-5-impl/SKILL.md +12 -10
  23. package/skills/erp-kit-module-5-impl/references/generated-code.md +2 -2
  24. package/skills/erp-kit-module-6-impl-review/SKILL.md +1 -1
  25. package/skills/erp-kit-module-6-impl-review/references/error-implementation-parity.md +1 -1
  26. package/skills/erp-kit-module-6-impl-review/references/errors.md +1 -1
  27. package/skills/erp-kit-module-shared/references/errors.md +1 -1
  28. package/skills/erp-kit-module-shared/references/queries.md +1 -1
  29. package/skills/erp-kit-module-shared/references/structure.md +1 -1
  30. package/skills/erp-kit-update/SKILL.md +2 -2
  31. package/src/commands/app/index.ts +57 -24
  32. package/src/commands/generate-doc.test.ts +63 -0
  33. package/src/commands/generate-doc.ts +98 -0
  34. package/src/commands/init-module.test.ts +43 -0
  35. package/src/commands/init-module.ts +74 -0
  36. package/src/commands/module/generate.ts +33 -13
  37. package/src/commands/module/index.ts +18 -28
  38. package/src/{commands/scaffold.test.ts → generator/generate-code-boilerplate.test.ts} +19 -89
  39. package/src/generator/generate-code.test.ts +24 -0
  40. package/src/generator/generate-code.ts +101 -4
  41. package/src/integration.test.ts +2 -2
  42. package/templates/scaffold/app/backend/package.json +4 -4
  43. package/templates/scaffold/app/frontend/package.json +10 -10
  44. package/templates/workflows/erp-kit-check.yml +2 -2
  45. package/src/commands/scaffold.ts +0 -176
package/dist/cli.mjs CHANGED
@@ -430,6 +430,60 @@ async function runCheck(config, cwd) {
430
430
  return results.some((code) => code !== 0) ? 1 : 0;
431
431
  }
432
432
  //#endregion
433
+ //#region src/commands/init-module.ts
434
+ function runInitModule(name, dir) {
435
+ const moduleDir = path.resolve(dir, name);
436
+ if (fs.existsSync(moduleDir)) {
437
+ console.error(`Directory already exists: ${moduleDir}`);
438
+ return 1;
439
+ }
440
+ fs.mkdirSync(moduleDir, { recursive: true });
441
+ return 0;
442
+ }
443
+ async function runInitModuleWithReadme(name, dir, cwd) {
444
+ const initResult = runInitModule(name, dir);
445
+ if (initResult !== 0) return initResult;
446
+ const moduleDir = path.resolve(cwd, dir, name);
447
+ const readmePath = path.join(moduleDir, "README.md");
448
+ const schemaPath = MODULE_SCHEMAS.module;
449
+ const { exitCode, stdout, stderr } = await runMdschema([
450
+ "generate",
451
+ "--schema",
452
+ schemaPath,
453
+ "--output",
454
+ readmePath
455
+ ], cwd);
456
+ if (stdout.trim()) console.log(stdout);
457
+ if (stderr.trim()) console.error(stderr);
458
+ return exitCode;
459
+ }
460
+ function runInitApp(name, dir) {
461
+ const appDir = path.resolve(dir, name);
462
+ if (fs.existsSync(appDir)) {
463
+ console.error(`Directory already exists: ${appDir}`);
464
+ return 1;
465
+ }
466
+ fs.mkdirSync(appDir, { recursive: true });
467
+ return 0;
468
+ }
469
+ async function runInitAppWithReadme(name, dir, cwd) {
470
+ const initResult = runInitApp(name, dir);
471
+ if (initResult !== 0) return initResult;
472
+ const appDir = path.resolve(cwd, dir, name);
473
+ const readmePath = path.join(appDir, "README.md");
474
+ const schemaPath = APP_COMPOSE_SCHEMAS.app;
475
+ const { exitCode, stdout, stderr } = await runMdschema([
476
+ "generate",
477
+ "--schema",
478
+ schemaPath,
479
+ "--output",
480
+ readmePath
481
+ ], cwd);
482
+ if (stdout.trim()) console.log(stdout);
483
+ if (stderr.trim()) console.error(stderr);
484
+ return exitCode;
485
+ }
486
+ //#endregion
433
487
  //#region src/commands/parse-doc-test-cases.ts
434
488
  function isHeading$1(node) {
435
489
  return node.type === "heading";
@@ -655,158 +709,6 @@ function formatSyncCheckReport(result) {
655
709
  return lines.join("\n");
656
710
  }
657
711
  //#endregion
658
- //#region src/commands/scaffold.ts
659
- const MODULE_TYPES = [
660
- "module",
661
- "feature",
662
- "command",
663
- "model",
664
- "query"
665
- ];
666
- const APP_TYPES = [
667
- "app",
668
- "actors",
669
- "business-flow",
670
- "story",
671
- "screen",
672
- "resolver"
673
- ];
674
- [...MODULE_TYPES, ...APP_TYPES];
675
- const MODULE_DIR_MAP = {
676
- feature: "docs/features",
677
- command: "docs/commands",
678
- model: "docs/models",
679
- query: "docs/queries"
680
- };
681
- const APP_DIR_MAP = {
682
- actors: "docs/actors",
683
- "business-flow": "docs/business-flow",
684
- screen: "docs/screen",
685
- resolver: "docs/resolver"
686
- };
687
- function resolveScaffoldPath(type, parentName, name, root) {
688
- if (type === "module" || type === "app") return path.join(root, parentName, "README.md");
689
- if (!name) throw new Error(`Name is required for scaffold type "${type}"`);
690
- if (type === "business-flow") return path.join(root, parentName, "docs/business-flow", name, "README.md");
691
- if (type === "story") {
692
- const parts = name.split("/");
693
- if (parts.length !== 2) throw new Error(`Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`);
694
- return path.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
695
- }
696
- if (MODULE_DIR_MAP[type]) return path.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
697
- if (APP_DIR_MAP[type]) return path.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
698
- throw new Error(`Unknown scaffold type: ${type}`);
699
- }
700
- async function runScaffold(type, parentName, name, root, cwd) {
701
- const outputPath = resolveScaffoldPath(type, parentName, name, root);
702
- const absoluteOutput = path.resolve(cwd, outputPath);
703
- if (fs.existsSync(absoluteOutput)) {
704
- console.error(`File already exists: ${outputPath}`);
705
- return 1;
706
- }
707
- const schemaPath = ALL_SCHEMAS[type];
708
- if (!schemaPath) {
709
- console.error(`No schema found for type: ${type}`);
710
- return 2;
711
- }
712
- try {
713
- fs.mkdirSync(path.dirname(absoluteOutput), { recursive: true });
714
- } catch (err) {
715
- console.error(`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`);
716
- return 1;
717
- }
718
- const { exitCode, stdout, stderr } = await runMdschema([
719
- "generate",
720
- "--schema",
721
- schemaPath,
722
- "--output",
723
- absoluteOutput
724
- ], cwd);
725
- if (stdout.trim()) console.log(stdout);
726
- if (stderr.trim()) console.error(stderr);
727
- if (exitCode !== 0) return exitCode;
728
- if (type === "module") scaffoldModuleSrc(path.dirname(absoluteOutput), parentName);
729
- if (type === "app") scaffoldAppSrc(path.dirname(absoluteOutput), parentName);
730
- return exitCode;
731
- }
732
- function copyTemplateDir(srcDir, destDir, replacements, placeholderFiles) {
733
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
734
- const srcPath = path.join(srcDir, entry.name);
735
- const destName = entry.name === "__dot__gitignore" ? ".gitignore" : entry.name;
736
- const destPath = path.join(destDir, destName);
737
- if (entry.isDirectory()) {
738
- fs.mkdirSync(destPath, { recursive: true });
739
- copyTemplateDir(srcPath, destPath, replacements, placeholderFiles);
740
- } else {
741
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
742
- if (placeholderFiles.has(entry.name)) {
743
- let content = fs.readFileSync(srcPath, "utf-8");
744
- for (const [from, to] of Object.entries(replacements)) content = content.replaceAll(from, to);
745
- fs.writeFileSync(destPath, content);
746
- } else fs.copyFileSync(srcPath, destPath);
747
- }
748
- }
749
- }
750
- function scaffoldModuleSrc(moduleDir, moduleName) {
751
- copyTemplateDir(path.join(PACKAGE_ROOT, "templates", "scaffold", "module"), moduleDir, { "template-module": moduleName }, new Set(["permissions.ts", "tailor.config.ts"]));
752
- }
753
- function scaffoldAppSrc(appDir, appName) {
754
- const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "app");
755
- const erpKitVersion = readErpKitVersion();
756
- copyTemplateDir(templateDir, appDir, {
757
- "template-app-frontend": `${appName}-frontend`,
758
- "template-app-backend": appName,
759
- "template-app": appName,
760
- "\"workspace:*\"": `"${erpKitVersion}"`
761
- }, new Set([
762
- "package.json",
763
- "tailor.config.ts",
764
- "index.html"
765
- ]));
766
- }
767
- //#endregion
768
- //#region src/commands/module/list.ts
769
- const MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
770
- const EXCLUDED_DIRS = new Set(["shared", "testing"]);
771
- function countFiles(dir, pattern, exclusions) {
772
- if (!existsSync(dir)) return 0;
773
- return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f))).length;
774
- }
775
- function listModules() {
776
- if (!existsSync(MODULES_DIR)) return [];
777
- return readdirSync(MODULES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name)).map((d) => {
778
- const modDir = join(MODULES_DIR, d.name);
779
- return {
780
- name: d.name,
781
- commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
782
- models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
783
- features: countFiles(join(modDir, "docs", "features"), /\.md$/, [])
784
- };
785
- }).sort((a, b) => a.name.localeCompare(b.name));
786
- }
787
- function formatModuleList(modules) {
788
- if (modules.length === 0) return "No modules found.";
789
- const lines = [];
790
- lines.push(chalk.bold("Modules:\n"));
791
- const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
792
- for (const mod of modules) {
793
- const counts = [
794
- `${mod.commands} commands`,
795
- `${mod.models} models`,
796
- `${mod.features} features`
797
- ].join(", ");
798
- lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
799
- }
800
- lines.push("");
801
- lines.push(`${modules.length} modules`);
802
- return lines.join("\n");
803
- }
804
- function runModuleList() {
805
- const modules = listModules();
806
- console.log(formatModuleList(modules));
807
- return 0;
808
- }
809
- //#endregion
810
712
  //#region src/generator/parse-command-doc.ts
811
713
  function parseCommandDoc(fileName, markdown) {
812
714
  const commandName = fileName.charAt(0).toLowerCase() + fileName.slice(1);
@@ -995,6 +897,18 @@ describe("${doc.commandName}", () => {
995
897
  });
996
898
  `;
997
899
  }
900
+ function generateDbStub(modelName) {
901
+ return `import { db, unsafeAllowAllGqlPermission, unsafeAllowAllTypePermission } from "@tailor-platform/sdk";
902
+
903
+ export const ${modelName} = db
904
+ .type("${modelName.charAt(0).toUpperCase() + modelName.slice(1)}", {
905
+ // TODO: define fields
906
+ ...db.fields.timestamps(),
907
+ })
908
+ .permission(unsafeAllowAllTypePermission)
909
+ .gqlPermission(unsafeAllowAllGqlPermission);
910
+ `;
911
+ }
998
912
  function generatePermissions(moduleName, commandNames) {
999
913
  return `// @generated — do not edit
1000
914
  import { definePermissions } from "../../../shared";
@@ -1004,18 +918,63 @@ ${[...commandNames].sort().map((name) => ` "${name}",`).join("\n")}
1004
918
  ] as const);
1005
919
  `;
1006
920
  }
921
+ function copyTemplateDir(srcDir, destDir, replacements, placeholderFiles) {
922
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
923
+ const srcPath = path.join(srcDir, entry.name);
924
+ const destName = entry.name === "__dot__gitignore" ? ".gitignore" : entry.name;
925
+ const destPath = path.join(destDir, destName);
926
+ if (entry.isDirectory()) {
927
+ fs.mkdirSync(destPath, { recursive: true });
928
+ copyTemplateDir(srcPath, destPath, replacements, placeholderFiles);
929
+ } else {
930
+ if (fs.existsSync(destPath)) continue;
931
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
932
+ if (placeholderFiles.has(entry.name)) {
933
+ let content = fs.readFileSync(srcPath, "utf-8");
934
+ for (const [from, to] of Object.entries(replacements)) content = content.replaceAll(from, to);
935
+ fs.writeFileSync(destPath, content);
936
+ } else fs.copyFileSync(srcPath, destPath);
937
+ }
938
+ }
939
+ }
940
+ function scaffoldModuleBoilerplate(moduleDir, moduleName) {
941
+ copyTemplateDir(path.join(PACKAGE_ROOT, "templates", "scaffold", "module"), moduleDir, { "template-module": moduleName }, new Set(["permissions.ts", "tailor.config.ts"]));
942
+ }
943
+ function scaffoldAppBoilerplate(appDir, appName) {
944
+ const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "app");
945
+ const erpKitVersion = readErpKitVersion();
946
+ copyTemplateDir(templateDir, appDir, {
947
+ "template-app-frontend": `${appName}-frontend`,
948
+ "template-app-backend": appName,
949
+ "template-app": appName,
950
+ "\"workspace:*\"": `"${erpKitVersion}"`
951
+ }, new Set([
952
+ "package.json",
953
+ "tailor.config.ts",
954
+ "index.html"
955
+ ]));
956
+ }
957
+ function runGenerateAppCode(appPath) {
958
+ const appName = path.basename(appPath);
959
+ scaffoldAppBoilerplate(appPath, appName);
960
+ console.log(`Generated boilerplate for ${appName}`);
961
+ return 0;
962
+ }
1007
963
  function runGenerateCode(modulePath, moduleName) {
964
+ scaffoldModuleBoilerplate(modulePath, moduleName);
1008
965
  const docsDir = path.join(modulePath, "docs", "commands");
1009
966
  const libDir = path.join(modulePath, "lib");
1010
967
  const commandDir = path.join(modulePath, "command");
1011
968
  if (!fs.existsSync(docsDir)) {
1012
- console.error(`No docs/commands/ directory found at ${docsDir}`);
1013
- return 1;
969
+ console.log(`No docs/commands/ directory found skipping code generation`);
970
+ console.log(`Generated boilerplate for ${moduleName}`);
971
+ return 0;
1014
972
  }
1015
973
  const mdFiles = fs.readdirSync(docsDir).filter((f) => f.endsWith(".md"));
1016
974
  if (mdFiles.length === 0) {
1017
- console.error(`No command docs found in ${docsDir}`);
1018
- return 1;
975
+ console.log(`No command docs found skipping code generation`);
976
+ console.log(`Generated boilerplate for ${moduleName}`);
977
+ return 0;
1019
978
  }
1020
979
  const parsedDocs = [];
1021
980
  for (const file of mdFiles) {
@@ -1090,41 +1049,185 @@ function runGenerateCode(modulePath, moduleName) {
1090
1049
  }
1091
1050
  }
1092
1051
  }
1052
+ const modelDocsDir = path.join(modulePath, "docs", "models");
1053
+ if (fs.existsSync(modelDocsDir)) {
1054
+ const modelFiles = fs.readdirSync(modelDocsDir).filter((f) => f.endsWith(".md"));
1055
+ if (modelFiles.length > 0) {
1056
+ const dbDir = path.join(modulePath, "db");
1057
+ fs.mkdirSync(dbDir, { recursive: true });
1058
+ for (const file of modelFiles) {
1059
+ const modelName = path.basename(file, ".md");
1060
+ const camelName = modelName.charAt(0).toLowerCase() + modelName.slice(1);
1061
+ const dbFile = path.join(dbDir, `${camelName}.ts`);
1062
+ if (!fs.existsSync(dbFile)) {
1063
+ fs.writeFileSync(dbFile, generateDbStub(camelName));
1064
+ console.log(` scaffolded ${path.relative(modulePath, dbFile)}`);
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1093
1069
  console.log(`Generated ${generated} file(s) for ${moduleName}`);
1094
1070
  return 0;
1095
1071
  }
1096
1072
  //#endregion
1073
+ //#region src/commands/generate-doc.ts
1074
+ const MODULE_DOC_TYPES = [
1075
+ "feature",
1076
+ "command",
1077
+ "model",
1078
+ "query"
1079
+ ];
1080
+ const APP_DOC_TYPES = [
1081
+ "actors",
1082
+ "business-flow",
1083
+ "story",
1084
+ "screen",
1085
+ "resolver"
1086
+ ];
1087
+ [...MODULE_DOC_TYPES, ...APP_DOC_TYPES];
1088
+ const MODULE_DIR_MAP = {
1089
+ feature: "docs/features",
1090
+ command: "docs/commands",
1091
+ model: "docs/models",
1092
+ query: "docs/queries"
1093
+ };
1094
+ const APP_DIR_MAP = {
1095
+ actors: "docs/actors",
1096
+ "business-flow": "docs/business-flow",
1097
+ screen: "docs/screen",
1098
+ resolver: "docs/resolver"
1099
+ };
1100
+ function resolveDocPath(type, name, modulePath) {
1101
+ if (!name) throw new Error(`Name is required for doc type "${type}"`);
1102
+ if (type === "business-flow") return path.join(modulePath, "docs/business-flow", name, "README.md");
1103
+ if (type === "story") {
1104
+ const parts = name.split("/");
1105
+ if (parts.length !== 2) throw new Error(`Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`);
1106
+ return path.join(modulePath, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
1107
+ }
1108
+ if (MODULE_DIR_MAP[type]) return path.join(modulePath, MODULE_DIR_MAP[type], `${name}.md`);
1109
+ if (APP_DIR_MAP[type]) return path.join(modulePath, APP_DIR_MAP[type], `${name}.md`);
1110
+ throw new Error(`Unknown doc type: ${type}`);
1111
+ }
1112
+ async function runGenerateDoc(type, name, modulePath, cwd) {
1113
+ const outputPath = resolveDocPath(type, name, modulePath);
1114
+ const absoluteOutput = path.resolve(cwd, outputPath);
1115
+ if (fs.existsSync(absoluteOutput)) {
1116
+ console.error(`File already exists: ${outputPath}`);
1117
+ return 1;
1118
+ }
1119
+ const schemaPath = ALL_SCHEMAS[type];
1120
+ if (!schemaPath) {
1121
+ console.error(`No schema found for type: ${type}`);
1122
+ return 2;
1123
+ }
1124
+ try {
1125
+ fs.mkdirSync(path.dirname(absoluteOutput), { recursive: true });
1126
+ } catch (err) {
1127
+ console.error(`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`);
1128
+ return 1;
1129
+ }
1130
+ const { exitCode, stdout, stderr } = await runMdschema([
1131
+ "generate",
1132
+ "--schema",
1133
+ schemaPath,
1134
+ "--output",
1135
+ absoluteOutput
1136
+ ], cwd);
1137
+ if (stdout.trim()) console.log(stdout);
1138
+ if (stderr.trim()) console.error(stderr);
1139
+ return exitCode;
1140
+ }
1141
+ //#endregion
1097
1142
  //#region src/commands/module/generate.ts
1098
1143
  const cwd$3 = process.cwd();
1099
- const generateCommand = defineCommand({
1144
+ const pathArgs$2 = z.object({ path: arg(z.string(), {
1145
+ alias: "p",
1146
+ description: "Path to the module directory"
1147
+ }) });
1148
+ const generateCommand$1 = defineCommand({
1100
1149
  name: "generate",
1101
- description: "Generate code from model definitions and docs",
1102
- subCommands: { code: defineCommand({
1103
- name: "code",
1104
- description: "Generate errors, permissions, command shells, and query shells from docs",
1105
- args: z.object({
1106
- root: arg(z.string(), {
1107
- alias: "r",
1108
- description: "Path to modules directory"
1150
+ description: "Generate docs and code for a module",
1151
+ subCommands: {
1152
+ doc: defineCommand({
1153
+ name: "doc",
1154
+ description: "Create a documentation file from a schema template",
1155
+ args: pathArgs$2.extend({
1156
+ type: arg(z.enum(MODULE_DOC_TYPES), {
1157
+ positional: true,
1158
+ description: `Doc type (${MODULE_DOC_TYPES.join(", ")})`
1159
+ }),
1160
+ name: arg(z.string().optional(), {
1161
+ positional: true,
1162
+ description: "Item name (required for all types)"
1163
+ })
1109
1164
  }),
1110
- module: arg(z.string(), {
1111
- positional: true,
1112
- description: "Module name (e.g., primitives, item-management)"
1113
- })
1165
+ run: async (args) => {
1166
+ const exitCode = await runGenerateDoc(args.type, args.name, args.path, cwd$3);
1167
+ process.exit(exitCode);
1168
+ }
1114
1169
  }),
1115
- run: (args) => {
1116
- const modulePath = path.resolve(cwd$3, args.root, args.module);
1117
- console.log(`Generating code for ${args.module}...`);
1118
- const exitCode = runGenerateCode(modulePath, args.module);
1119
- process.exit(exitCode);
1120
- }
1121
- }) }
1170
+ code: defineCommand({
1171
+ name: "code",
1172
+ description: "Generate boilerplate, implementation stubs, and generated code from docs",
1173
+ args: pathArgs$2,
1174
+ run: (args) => {
1175
+ const modulePath = path.resolve(cwd$3, args.path);
1176
+ const moduleName = path.basename(modulePath);
1177
+ console.log(`Generating code for ${moduleName}...`);
1178
+ const exitCode = runGenerateCode(modulePath, moduleName);
1179
+ process.exit(exitCode);
1180
+ }
1181
+ })
1182
+ }
1122
1183
  });
1123
1184
  //#endregion
1185
+ //#region src/commands/module/list.ts
1186
+ const MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
1187
+ const EXCLUDED_DIRS = new Set(["shared", "testing"]);
1188
+ function countFiles(dir, pattern, exclusions) {
1189
+ if (!existsSync(dir)) return 0;
1190
+ return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f))).length;
1191
+ }
1192
+ function listModules() {
1193
+ if (!existsSync(MODULES_DIR)) return [];
1194
+ return readdirSync(MODULES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name)).map((d) => {
1195
+ const modDir = join(MODULES_DIR, d.name);
1196
+ return {
1197
+ name: d.name,
1198
+ commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
1199
+ models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
1200
+ features: countFiles(join(modDir, "docs", "features"), /\.md$/, [])
1201
+ };
1202
+ }).sort((a, b) => a.name.localeCompare(b.name));
1203
+ }
1204
+ function formatModuleList(modules) {
1205
+ if (modules.length === 0) return "No modules found.";
1206
+ const lines = [];
1207
+ lines.push(chalk.bold("Modules:\n"));
1208
+ const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
1209
+ for (const mod of modules) {
1210
+ const counts = [
1211
+ `${mod.commands} commands`,
1212
+ `${mod.models} models`,
1213
+ `${mod.features} features`
1214
+ ].join(", ");
1215
+ lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
1216
+ }
1217
+ lines.push("");
1218
+ lines.push(`${modules.length} modules`);
1219
+ return lines.join("\n");
1220
+ }
1221
+ function runModuleList() {
1222
+ const modules = listModules();
1223
+ console.log(formatModuleList(modules));
1224
+ return 0;
1225
+ }
1226
+ //#endregion
1124
1227
  //#region src/commands/module/index.ts
1125
1228
  const cwd$2 = process.cwd();
1126
- const rootArgs$1 = z.object({ root: arg(z.string(), {
1127
- alias: "r",
1229
+ const pathArgs$1 = z.object({ path: arg(z.string(), {
1230
+ alias: "p",
1128
1231
  description: "Path to modules directory"
1129
1232
  }) });
1130
1233
  const listCommand = defineCommand({
@@ -1138,41 +1241,37 @@ const listCommand = defineCommand({
1138
1241
  const checkCommand$1 = defineCommand({
1139
1242
  name: "check",
1140
1243
  description: "Validate module docs against schemas",
1141
- args: rootArgs$1,
1244
+ args: pathArgs$1,
1142
1245
  run: async (args) => {
1143
- const exitCode = await runCheck({ modulesRoot: args.root }, cwd$2);
1246
+ const exitCode = await runCheck({ modulesRoot: args.path }, cwd$2);
1144
1247
  process.exit(exitCode);
1145
1248
  }
1146
1249
  });
1147
1250
  const syncCheckCommand$1 = defineCommand({
1148
1251
  name: "sync-check",
1149
1252
  description: "Validate source <-> doc correspondence",
1150
- args: rootArgs$1,
1253
+ args: pathArgs$1,
1151
1254
  run: async (args) => {
1152
- const result = await runSyncCheck({ modulesRoot: args.root }, cwd$2);
1255
+ const result = await runSyncCheck({ modulesRoot: args.path }, cwd$2);
1153
1256
  console.log(formatSyncCheckReport(result));
1154
1257
  process.exit(result.exitCode);
1155
1258
  }
1156
1259
  });
1157
- const scaffoldCommand$1 = defineCommand({
1158
- name: "scaffold",
1159
- description: "Generate module doc from schema template",
1160
- args: rootArgs$1.extend({
1161
- type: arg(z.enum(MODULE_TYPES), {
1162
- positional: true,
1163
- description: `Scaffold type (${MODULE_TYPES.join(", ")})`
1164
- }),
1165
- parent: arg(z.string(), {
1260
+ const initCommand$2 = defineCommand({
1261
+ name: "init",
1262
+ description: "Bootstrap a new module with directory structure and README",
1263
+ args: z.object({
1264
+ name: arg(z.string(), {
1166
1265
  positional: true,
1167
1266
  description: "Module name"
1168
1267
  }),
1169
- name: arg(z.string().optional(), {
1268
+ dir: arg(z.string(), {
1170
1269
  positional: true,
1171
- description: "Item name (required for feature, command, model)"
1270
+ description: "Parent directory where the module will be created"
1172
1271
  })
1173
1272
  }),
1174
1273
  run: async (args) => {
1175
- const exitCode = await runScaffold(args.type, args.parent, args.name, args.root, cwd$2);
1274
+ const exitCode = await runInitModuleWithReadme(args.name, args.dir, cwd$2);
1176
1275
  process.exit(exitCode);
1177
1276
  }
1178
1277
  });
@@ -1183,65 +1282,95 @@ const moduleCommand = defineCommand({
1183
1282
  list: listCommand,
1184
1283
  check: checkCommand$1,
1185
1284
  "sync-check": syncCheckCommand$1,
1186
- scaffold: scaffoldCommand$1,
1187
- generate: generateCommand
1285
+ init: initCommand$2,
1286
+ generate: generateCommand$1
1188
1287
  }
1189
1288
  });
1190
1289
  //#endregion
1191
1290
  //#region src/commands/app/index.ts
1192
1291
  const cwd$1 = process.cwd();
1193
- const rootArgs = z.object({ root: arg(z.string(), {
1194
- alias: "r",
1292
+ const pathArgs = z.object({ path: arg(z.string(), {
1293
+ alias: "p",
1195
1294
  description: "Path to app-compose directory"
1196
1295
  }) });
1197
1296
  const checkCommand = defineCommand({
1198
1297
  name: "check",
1199
1298
  description: "Validate app docs against schemas",
1200
- args: rootArgs,
1299
+ args: pathArgs,
1201
1300
  run: async (args) => {
1202
- const exitCode = await runCheck({ appRoot: args.root }, cwd$1);
1301
+ const exitCode = await runCheck({ appRoot: args.path }, cwd$1);
1203
1302
  process.exit(exitCode);
1204
1303
  }
1205
1304
  });
1206
1305
  const syncCheckCommand = defineCommand({
1207
1306
  name: "sync-check",
1208
1307
  description: "Validate source <-> doc correspondence",
1209
- args: rootArgs,
1308
+ args: pathArgs,
1210
1309
  run: async (args) => {
1211
- const result = await runSyncCheck({ appRoot: args.root }, cwd$1);
1310
+ const result = await runSyncCheck({ appRoot: args.path }, cwd$1);
1212
1311
  console.log(formatSyncCheckReport(result));
1213
1312
  process.exit(result.exitCode);
1214
1313
  }
1215
1314
  });
1216
- const scaffoldCommand = defineCommand({
1217
- name: "scaffold",
1218
- description: "Generate app doc from schema template",
1219
- args: rootArgs.extend({
1220
- type: arg(z.enum(APP_TYPES), {
1221
- positional: true,
1222
- description: `Scaffold type (${APP_TYPES.join(", ")})`
1223
- }),
1224
- parent: arg(z.string(), {
1315
+ const initCommand$1 = defineCommand({
1316
+ name: "init",
1317
+ description: "Bootstrap a new app with directory structure and README",
1318
+ args: z.object({
1319
+ name: arg(z.string(), {
1225
1320
  positional: true,
1226
1321
  description: "App name"
1227
1322
  }),
1228
- name: arg(z.string().optional(), {
1323
+ dir: arg(z.string(), {
1229
1324
  positional: true,
1230
- description: "Item name (required for most types)"
1325
+ description: "Parent directory where the app will be created"
1231
1326
  })
1232
1327
  }),
1233
1328
  run: async (args) => {
1234
- const exitCode = await runScaffold(args.type, args.parent, args.name, args.root, cwd$1);
1329
+ const exitCode = await runInitAppWithReadme(args.name, args.dir, cwd$1);
1235
1330
  process.exit(exitCode);
1236
1331
  }
1237
1332
  });
1333
+ const generateCommand = defineCommand({
1334
+ name: "generate",
1335
+ description: "Generate docs and code for an app",
1336
+ subCommands: {
1337
+ doc: defineCommand({
1338
+ name: "doc",
1339
+ description: "Create a documentation file from a schema template",
1340
+ args: pathArgs.extend({
1341
+ type: arg(z.enum(APP_DOC_TYPES), {
1342
+ positional: true,
1343
+ description: `Doc type (${APP_DOC_TYPES.join(", ")})`
1344
+ }),
1345
+ name: arg(z.string().optional(), {
1346
+ positional: true,
1347
+ description: "Item name (required for most types)"
1348
+ })
1349
+ }),
1350
+ run: async (args) => {
1351
+ const exitCode = await runGenerateDoc(args.type, args.name, args.path, cwd$1);
1352
+ process.exit(exitCode);
1353
+ }
1354
+ }),
1355
+ code: defineCommand({
1356
+ name: "code",
1357
+ description: "Generate boilerplate source files from docs",
1358
+ args: pathArgs,
1359
+ run: (args) => {
1360
+ const exitCode = runGenerateAppCode(path.resolve(cwd$1, args.path));
1361
+ process.exit(exitCode);
1362
+ }
1363
+ })
1364
+ }
1365
+ });
1238
1366
  const appCommand = defineCommand({
1239
1367
  name: "app",
1240
1368
  description: "App-compose management",
1241
1369
  subCommands: {
1242
1370
  check: checkCommand,
1243
1371
  "sync-check": syncCheckCommand,
1244
- scaffold: scaffoldCommand
1372
+ init: initCommand$1,
1373
+ generate: generateCommand
1245
1374
  }
1246
1375
  });
1247
1376
  //#endregion