@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.
- package/CHANGELOG.md +12 -0
- package/README.md +158 -62
- package/dist/cli.mjs +344 -215
- package/package.json +3 -2
- package/skills/erp-kit-app-1-requirements/SKILL.md +19 -8
- package/skills/erp-kit-app-2-requirements-review/SKILL.md +5 -4
- package/skills/erp-kit-app-2-requirements-review/references/best-practices-check.md +6 -1
- package/skills/erp-kit-app-2-requirements-review/references/boundary-consistency-check.md +6 -1
- package/skills/erp-kit-app-3-plan/SKILL.md +4 -7
- package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +1 -19
- package/skills/erp-kit-app-4-plan-review/SKILL.md +1 -10
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +1 -8
- package/skills/erp-kit-app-shared/SKILL.md +15 -0
- package/skills/erp-kit-app-shared/references/link-format-reference.md +13 -0
- package/skills/erp-kit-app-shared/references/naming-conventions.md +21 -0
- package/skills/erp-kit-app-shared/references/resolver-classification.md +23 -0
- package/skills/erp-kit-app-shared/references/schema-constraints.md +25 -0
- package/skills/erp-kit-module-1-requirements/SKILL.md +1 -1
- package/skills/erp-kit-module-1-requirements/references/feature-doc.md +1 -1
- package/skills/erp-kit-module-3-plan/SKILL.md +5 -5
- package/skills/erp-kit-module-3-plan/references/naming.md +15 -1
- package/skills/erp-kit-module-5-impl/SKILL.md +12 -10
- package/skills/erp-kit-module-5-impl/references/generated-code.md +2 -2
- package/skills/erp-kit-module-6-impl-review/SKILL.md +1 -1
- package/skills/erp-kit-module-6-impl-review/references/error-implementation-parity.md +1 -1
- package/skills/erp-kit-module-6-impl-review/references/errors.md +1 -1
- package/skills/erp-kit-module-shared/references/errors.md +1 -1
- package/skills/erp-kit-module-shared/references/queries.md +1 -1
- package/skills/erp-kit-module-shared/references/structure.md +1 -1
- package/skills/erp-kit-update/SKILL.md +2 -2
- package/src/commands/app/index.ts +57 -24
- package/src/commands/generate-doc.test.ts +63 -0
- package/src/commands/generate-doc.ts +98 -0
- package/src/commands/init-module.test.ts +43 -0
- package/src/commands/init-module.ts +74 -0
- package/src/commands/module/generate.ts +33 -13
- package/src/commands/module/index.ts +18 -28
- package/src/{commands/scaffold.test.ts → generator/generate-code-boilerplate.test.ts} +19 -89
- package/src/generator/generate-code.test.ts +24 -0
- package/src/generator/generate-code.ts +101 -4
- package/src/integration.test.ts +2 -2
- package/templates/scaffold/app/backend/package.json +4 -4
- package/templates/scaffold/app/frontend/package.json +10 -10
- package/templates/workflows/erp-kit-check.yml +2 -2
- 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.
|
|
1013
|
-
|
|
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.
|
|
1018
|
-
|
|
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
|
|
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
|
|
1102
|
-
subCommands: {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
|
1127
|
-
alias: "
|
|
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:
|
|
1244
|
+
args: pathArgs$1,
|
|
1142
1245
|
run: async (args) => {
|
|
1143
|
-
const exitCode = await runCheck({ modulesRoot: args.
|
|
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:
|
|
1253
|
+
args: pathArgs$1,
|
|
1151
1254
|
run: async (args) => {
|
|
1152
|
-
const result = await runSyncCheck({ modulesRoot: args.
|
|
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
|
|
1158
|
-
name: "
|
|
1159
|
-
description: "
|
|
1160
|
-
args:
|
|
1161
|
-
|
|
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
|
-
|
|
1268
|
+
dir: arg(z.string(), {
|
|
1170
1269
|
positional: true,
|
|
1171
|
-
description: "
|
|
1270
|
+
description: "Parent directory where the module will be created"
|
|
1172
1271
|
})
|
|
1173
1272
|
}),
|
|
1174
1273
|
run: async (args) => {
|
|
1175
|
-
const exitCode = await
|
|
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
|
-
|
|
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
|
|
1194
|
-
alias: "
|
|
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:
|
|
1299
|
+
args: pathArgs,
|
|
1201
1300
|
run: async (args) => {
|
|
1202
|
-
const exitCode = await runCheck({ appRoot: args.
|
|
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:
|
|
1308
|
+
args: pathArgs,
|
|
1210
1309
|
run: async (args) => {
|
|
1211
|
-
const result = await runSyncCheck({ appRoot: args.
|
|
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
|
|
1217
|
-
name: "
|
|
1218
|
-
description: "
|
|
1219
|
-
args:
|
|
1220
|
-
|
|
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
|
-
|
|
1323
|
+
dir: arg(z.string(), {
|
|
1229
1324
|
positional: true,
|
|
1230
|
-
description: "
|
|
1325
|
+
description: "Parent directory where the app will be created"
|
|
1231
1326
|
})
|
|
1232
1327
|
}),
|
|
1233
1328
|
run: async (args) => {
|
|
1234
|
-
const exitCode = await
|
|
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
|
-
|
|
1372
|
+
init: initCommand$1,
|
|
1373
|
+
generate: generateCommand
|
|
1245
1374
|
}
|
|
1246
1375
|
});
|
|
1247
1376
|
//#endregion
|