@tailor-platform/erp-kit 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +80 -12
- package/dist/cli.js +1070 -450
- package/package.json +11 -8
- package/schemas/app-compose/business-flow.yml +3 -0
- package/schemas/app-compose/story.yml +1 -1
- package/schemas/module/model.yml +5 -0
- package/skills/{app-compose-1-requirement-analysis → erp-kit-app-1-requirements}/SKILL.md +8 -14
- package/skills/{app-compose-2-requirements-breakdown → erp-kit-app-2-breakdown}/SKILL.md +6 -13
- package/skills/{app-compose-3-doc-review → erp-kit-app-3-doc-review}/SKILL.md +2 -6
- package/skills/{app-compose-6-implementation-spec → erp-kit-app-4-impl-spec}/SKILL.md +11 -22
- package/skills/erp-kit-app-5-implementation/SKILL.md +149 -0
- package/skills/erp-kit-app-5-implementation/references/backend.md +232 -0
- package/skills/erp-kit-app-5-implementation/references/frontend.md +242 -0
- package/skills/{mock-scenario → erp-kit-mock-scenario}/SKILL.md +1 -1
- package/skills/{1-module-docs → erp-kit-module-1-docs}/SKILL.md +2 -2
- package/skills/{2-module-feature-breakdown → erp-kit-module-2-feature-breakdown}/SKILL.md +13 -9
- package/skills/erp-kit-module-2-feature-breakdown/references/naming.md +59 -0
- package/skills/{3-module-doc-review → erp-kit-module-3-doc-review}/SKILL.md +83 -25
- package/skills/erp-kit-module-4-tdd/SKILL.md +94 -0
- package/skills/erp-kit-module-4-tdd/references/cross-module-dependency.md +133 -0
- package/skills/{4-module-tdd-implementation → erp-kit-module-4-tdd}/references/db-relations.md +5 -1
- package/skills/{4-module-tdd-implementation → erp-kit-module-4-tdd}/references/exports.md +1 -1
- package/skills/erp-kit-module-4-tdd/references/generated-code.md +32 -0
- package/skills/{5-module-implementation-review → erp-kit-module-5-impl-review}/SKILL.md +46 -44
- package/skills/erp-kit-module-5-impl-review/references/commands.md +62 -0
- package/skills/erp-kit-module-5-impl-review/references/errors.md +10 -0
- package/skills/{5-module-implementation-review → erp-kit-module-5-impl-review}/references/testing.md +1 -1
- package/skills/erp-kit-module-shared/SKILL.md +16 -0
- package/skills/erp-kit-module-shared/references/commands.md +203 -0
- package/skills/erp-kit-module-shared/references/errors.md +35 -0
- package/skills/erp-kit-module-shared/references/queries.md +168 -0
- package/skills/erp-kit-module-shared/references/structure.md +36 -0
- package/skills/{3-module-doc-review → erp-kit-module-shared}/references/testing.md +4 -3
- package/skills/erp-kit-update/SKILL.md +64 -0
- package/src/cli.doc.test.ts +65 -0
- package/src/cli.ts +3 -35
- package/src/commands/app/index.ts +3 -3
- package/src/commands/check.test.ts +1 -1
- package/src/commands/check.ts +2 -2
- package/src/commands/index.ts +73 -0
- package/src/commands/init.test.ts +22 -5
- package/src/commands/init.ts +25 -16
- package/src/commands/license.ts +193 -0
- package/src/commands/mock/index.ts +2 -2
- package/src/commands/mock/start.ts +1 -1
- package/src/commands/mock/validate.test.ts +1 -1
- package/src/commands/module/generate.ts +35 -0
- package/src/commands/module/index.ts +6 -4
- package/src/commands/module/list.test.ts +7 -12
- package/src/commands/module/list.ts +1 -1
- package/src/commands/scaffold-templates.ts +65 -0
- package/src/commands/scaffold.test.ts +92 -2
- package/src/commands/scaffold.ts +22 -2
- package/src/commands/sync-check.test.ts +60 -1
- package/src/commands/sync-check.ts +35 -2
- package/src/generator/generate-code.test.ts +200 -0
- package/src/generator/generate-code.ts +260 -0
- package/src/generator/parse-command-doc.test.ts +159 -0
- package/src/generator/parse-command-doc.ts +116 -0
- package/src/integration.test.ts +2 -2
- package/src/module.ts +44 -6
- package/src/modules/item-management/README.md +38 -0
- package/src/modules/item-management/command/activateItem.generated.ts +6 -0
- package/src/modules/item-management/command/activateItem.test.ts +76 -0
- package/src/modules/item-management/command/activateItem.ts +42 -0
- package/src/modules/item-management/command/assignItemToTaxonomy.generated.ts +6 -0
- package/src/modules/item-management/command/assignItemToTaxonomy.test.ts +88 -0
- package/src/modules/item-management/command/assignItemToTaxonomy.ts +63 -0
- package/src/modules/item-management/command/createItem.generated.ts +6 -0
- package/src/modules/item-management/command/createItem.test.ts +152 -0
- package/src/modules/item-management/command/createItem.ts +72 -0
- package/src/modules/item-management/command/createTaxonomyNode.generated.ts +6 -0
- package/src/modules/item-management/command/createTaxonomyNode.test.ts +126 -0
- package/src/modules/item-management/command/createTaxonomyNode.ts +70 -0
- package/src/modules/item-management/command/deactivateItem.generated.ts +6 -0
- package/src/modules/item-management/command/deactivateItem.test.ts +76 -0
- package/src/modules/item-management/command/deactivateItem.ts +42 -0
- package/src/modules/item-management/command/deleteItem.generated.ts +6 -0
- package/src/modules/item-management/command/deleteItem.test.ts +61 -0
- package/src/modules/item-management/command/deleteItem.ts +38 -0
- package/src/modules/item-management/command/deleteTaxonomyNode.generated.ts +6 -0
- package/src/modules/item-management/command/deleteTaxonomyNode.test.ts +73 -0
- package/src/modules/item-management/command/deleteTaxonomyNode.ts +50 -0
- package/src/modules/item-management/command/moveTaxonomyNode.generated.ts +6 -0
- package/src/modules/item-management/command/moveTaxonomyNode.test.ts +136 -0
- package/src/modules/item-management/command/moveTaxonomyNode.ts +85 -0
- package/src/modules/item-management/command/reactivateItem.generated.ts +6 -0
- package/src/modules/item-management/command/reactivateItem.test.ts +76 -0
- package/src/modules/item-management/command/reactivateItem.ts +42 -0
- package/src/modules/item-management/command/removeItemFromTaxonomy.generated.ts +6 -0
- package/src/modules/item-management/command/removeItemFromTaxonomy.test.ts +43 -0
- package/src/modules/item-management/command/removeItemFromTaxonomy.ts +30 -0
- package/src/modules/item-management/command/updateItem.generated.ts +6 -0
- package/src/modules/item-management/command/updateItem.test.ts +178 -0
- package/src/modules/item-management/command/updateItem.ts +103 -0
- package/src/modules/item-management/command/updateTaxonomyNode.generated.ts +6 -0
- package/src/modules/item-management/command/updateTaxonomyNode.test.ts +88 -0
- package/src/modules/item-management/command/updateTaxonomyNode.ts +62 -0
- package/src/modules/item-management/db/item.ts +47 -0
- package/src/modules/item-management/db/itemTaxonomyAssignment.ts +49 -0
- package/src/modules/item-management/db/taxonomyNode.ts +34 -0
- package/src/modules/item-management/docs/commands/ActivateItem.md +32 -0
- package/src/modules/item-management/docs/commands/AssignItemToTaxonomy.md +38 -0
- package/src/modules/item-management/docs/commands/CreateItem.md +44 -0
- package/src/modules/item-management/docs/commands/CreateTaxonomyNode.md +44 -0
- package/src/modules/item-management/docs/commands/DeactivateItem.md +34 -0
- package/src/modules/item-management/docs/commands/DeleteItem.md +35 -0
- package/src/modules/item-management/docs/commands/DeleteTaxonomyNode.md +39 -0
- package/src/modules/item-management/docs/commands/MoveTaxonomyNode.md +45 -0
- package/src/modules/item-management/docs/commands/ReactivateItem.md +34 -0
- package/src/modules/item-management/docs/commands/RemoveItemFromTaxonomy.md +30 -0
- package/src/modules/item-management/docs/commands/UpdateItem.md +55 -0
- package/src/modules/item-management/docs/commands/UpdateTaxonomyNode.md +36 -0
- package/src/modules/item-management/docs/features/item-lifecycle.md +60 -0
- package/src/modules/item-management/docs/features/item-taxonomy.md +65 -0
- package/src/modules/item-management/docs/models/ItemTaxonomyAssignment.md +36 -0
- package/src/modules/item-management/docs/models/TaxonomyNode.md +47 -0
- package/src/modules/item-management/docs/models/item.md +59 -0
- package/src/modules/item-management/docs/queries/CalculateNodeDepth.md +36 -0
- package/src/modules/item-management/docs/queries/CalculateSubtreeDepth.md +40 -0
- package/src/modules/item-management/docs/queries/DetectCircularReference.md +41 -0
- package/src/modules/item-management/docs/queries/GetItem.md +38 -0
- package/src/modules/item-management/docs/queries/GetItemTaxonomyAssignment.md +29 -0
- package/src/modules/item-management/docs/queries/GetTaxonomyNode.md +35 -0
- package/src/modules/item-management/docs/queries/GetTaxonomyNodeAssignments.md +29 -0
- package/src/modules/item-management/docs/queries/GetTaxonomyNodeChildren.md +29 -0
- package/src/modules/item-management/generated/enums.ts +9 -0
- package/src/modules/item-management/generated/kysely-tailordb.ts +62 -0
- package/src/modules/item-management/index.ts +53 -0
- package/src/modules/item-management/lib/_db_deps.ts +13 -0
- package/src/modules/item-management/lib/errors.generated.ts +117 -0
- package/src/modules/item-management/lib/permissions.generated.ts +17 -0
- package/src/modules/item-management/lib/types.ts +19 -0
- package/src/modules/item-management/module.ts +97 -0
- package/src/modules/item-management/query/calculateNodeDepth.generated.ts +5 -0
- package/src/modules/item-management/query/calculateNodeDepth.test.ts +56 -0
- package/src/modules/item-management/query/calculateNodeDepth.ts +28 -0
- package/src/modules/item-management/query/calculateSubtreeDepth.generated.ts +5 -0
- package/src/modules/item-management/query/calculateSubtreeDepth.test.ts +75 -0
- package/src/modules/item-management/query/calculateSubtreeDepth.ts +29 -0
- package/src/modules/item-management/query/detectCircularReference.generated.ts +5 -0
- package/src/modules/item-management/query/detectCircularReference.test.ts +61 -0
- package/src/modules/item-management/query/detectCircularReference.ts +32 -0
- package/src/modules/item-management/query/getItem.generated.ts +5 -0
- package/src/modules/item-management/query/getItem.test.ts +67 -0
- package/src/modules/item-management/query/getItem.ts +20 -0
- package/src/modules/item-management/query/getItemTaxonomyAssignment.generated.ts +5 -0
- package/src/modules/item-management/query/getItemTaxonomyAssignment.test.ts +25 -0
- package/src/modules/item-management/query/getItemTaxonomyAssignment.ts +18 -0
- package/src/modules/item-management/query/getTaxonomyNode.generated.ts +5 -0
- package/src/modules/item-management/query/getTaxonomyNode.test.ts +47 -0
- package/src/modules/item-management/query/getTaxonomyNode.ts +18 -0
- package/src/modules/item-management/query/getTaxonomyNodeAssignments.generated.ts +5 -0
- package/src/modules/item-management/query/getTaxonomyNodeAssignments.test.ts +25 -0
- package/src/modules/item-management/query/getTaxonomyNodeAssignments.ts +16 -0
- package/src/modules/item-management/query/getTaxonomyNodeChildren.generated.ts +5 -0
- package/src/modules/item-management/query/getTaxonomyNodeChildren.test.ts +34 -0
- package/src/modules/item-management/query/getTaxonomyNodeChildren.ts +16 -0
- package/src/modules/item-management/tailor.config.ts +11 -0
- package/src/modules/item-management/testing/fixtures.ts +81 -0
- package/src/modules/primitives/command/activateCategory.generated.ts +6 -0
- package/src/modules/primitives/command/activateCategory.test.ts +11 -29
- package/src/modules/primitives/command/activateCategory.ts +27 -34
- package/src/modules/primitives/command/activateCurrency.generated.ts +6 -0
- package/src/modules/primitives/command/activateCurrency.test.ts +11 -29
- package/src/modules/primitives/command/activateCurrency.ts +27 -34
- package/src/modules/primitives/command/activateUnit.generated.ts +6 -0
- package/src/modules/primitives/command/activateUnit.test.ts +11 -15
- package/src/modules/primitives/command/activateUnit.ts +27 -34
- package/src/modules/primitives/command/createCategory.generated.ts +6 -0
- package/src/modules/primitives/command/createCategory.test.ts +27 -39
- package/src/modules/primitives/command/createCategory.ts +53 -62
- package/src/modules/primitives/command/createCurrency.generated.ts +6 -0
- package/src/modules/primitives/command/createCurrency.test.ts +78 -71
- package/src/modules/primitives/command/createCurrency.ts +43 -48
- package/src/modules/primitives/command/createExchangeRate.generated.ts +6 -0
- package/src/modules/primitives/command/createExchangeRate.test.ts +101 -100
- package/src/modules/primitives/command/createExchangeRate.ts +50 -59
- package/src/modules/primitives/command/createUnit.generated.ts +6 -0
- package/src/modules/primitives/command/createUnit.test.ts +92 -95
- package/src/modules/primitives/command/createUnit.ts +54 -57
- package/src/modules/primitives/command/deactivateCategory.generated.ts +6 -0
- package/src/modules/primitives/command/deactivateCategory.test.ts +27 -28
- package/src/modules/primitives/command/deactivateCategory.ts +43 -50
- package/src/modules/primitives/command/deactivateCurrency.generated.ts +6 -0
- package/src/modules/primitives/command/deactivateCurrency.test.ts +23 -38
- package/src/modules/primitives/command/deactivateCurrency.ts +31 -38
- package/src/modules/primitives/command/deactivateUnit.generated.ts +6 -0
- package/src/modules/primitives/command/deactivateUnit.test.ts +27 -23
- package/src/modules/primitives/command/deactivateUnit.ts +39 -49
- package/src/modules/primitives/command/setBaseCurrency.generated.ts +6 -0
- package/src/modules/primitives/command/setBaseCurrency.test.ts +40 -33
- package/src/modules/primitives/command/setBaseCurrency.ts +43 -50
- package/src/modules/primitives/command/setReferenceUnit.generated.ts +6 -0
- package/src/modules/primitives/command/setReferenceUnit.test.ts +39 -35
- package/src/modules/primitives/command/setReferenceUnit.ts +46 -59
- package/src/modules/primitives/db/unit.ts +13 -3
- package/src/modules/primitives/docs/commands/ActivateCategory.md +1 -2
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +1 -2
- package/src/modules/primitives/docs/commands/ActivateUnit.md +1 -2
- package/src/modules/primitives/docs/commands/CreateCategory.md +1 -4
- package/src/modules/primitives/docs/commands/CreateCurrency.md +3 -4
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +4 -5
- package/src/modules/primitives/docs/commands/CreateUnit.md +5 -5
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +2 -3
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +2 -3
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +2 -3
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +2 -3
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +2 -3
- package/src/modules/primitives/docs/queries/ConvertAmount.md +3 -5
- package/src/modules/primitives/docs/queries/ConvertQuantity.md +3 -5
- package/src/modules/primitives/docs/queries/GetBaseCurrency.md +32 -0
- package/src/modules/primitives/docs/queries/GetCurrency.md +36 -0
- package/src/modules/primitives/docs/queries/GetUnit.md +36 -0
- package/src/modules/primitives/docs/queries/GetUoMCategory.md +36 -0
- package/src/modules/primitives/docs/queries/ListUnitsByCategory.md +26 -0
- package/src/modules/primitives/generated/kysely-tailordb.ts +24 -45
- package/src/modules/primitives/index.ts +15 -4
- package/src/modules/primitives/lib/errors.generated.ts +112 -0
- package/src/modules/primitives/{permissions.ts → lib/permissions.generated.ts} +9 -8
- package/src/modules/primitives/module.ts +37 -27
- package/src/modules/primitives/query/convertAmount.generated.ts +5 -0
- package/src/modules/primitives/query/convertAmount.test.ts +2 -2
- package/src/modules/primitives/query/convertAmount.ts +27 -28
- package/src/modules/primitives/query/convertQuantity.generated.ts +5 -0
- package/src/modules/primitives/query/convertQuantity.test.ts +6 -2
- package/src/modules/primitives/query/convertQuantity.ts +49 -57
- package/src/modules/primitives/query/getBaseCurrency.generated.ts +5 -0
- package/src/modules/primitives/query/getBaseCurrency.test.ts +28 -0
- package/src/modules/primitives/query/getBaseCurrency.ts +16 -0
- package/src/modules/primitives/query/getCurrency.generated.ts +5 -0
- package/src/modules/primitives/query/getCurrency.test.ts +47 -0
- package/src/modules/primitives/query/getCurrency.ts +18 -0
- package/src/modules/primitives/query/getUnit.generated.ts +5 -0
- package/src/modules/primitives/query/getUnit.test.ts +47 -0
- package/src/modules/primitives/query/getUnit.ts +18 -0
- package/src/modules/primitives/query/getUoMCategory.generated.ts +5 -0
- package/src/modules/primitives/query/getUoMCategory.test.ts +47 -0
- package/src/modules/primitives/query/getUoMCategory.ts +18 -0
- package/src/modules/primitives/query/listUnitsByCategory.generated.ts +5 -0
- package/src/modules/primitives/query/listUnitsByCategory.ts +16 -0
- package/src/modules/primitives/tailor.config.ts +3 -3
- package/src/modules/shared/defineCommand.test.ts +23 -10
- package/src/modules/shared/defineCommand.ts +23 -10
- package/src/modules/shared/internal.ts +1 -0
- package/src/modules/shared/requirePermission.test.ts +22 -21
- package/src/modules/shared/requirePermission.ts +8 -2
- package/src/modules/shared/result.ts +12 -0
- package/src/modules/testing/index.ts +36 -11
- package/src/modules/user-management/command/activateUser.generated.ts +6 -0
- package/src/modules/user-management/command/activateUser.test.ts +27 -27
- package/src/modules/user-management/command/activateUser.ts +40 -48
- package/src/modules/user-management/command/assignPermissionToRole.generated.ts +6 -0
- package/src/modules/user-management/command/assignPermissionToRole.test.ts +42 -43
- package/src/modules/user-management/command/assignPermissionToRole.ts +59 -62
- package/src/modules/user-management/command/assignRoleToUser.generated.ts +6 -0
- package/src/modules/user-management/command/assignRoleToUser.test.ts +70 -63
- package/src/modules/user-management/command/assignRoleToUser.ts +63 -66
- package/src/modules/user-management/command/createPermission.generated.ts +6 -0
- package/src/modules/user-management/command/createPermission.test.ts +45 -38
- package/src/modules/user-management/command/createPermission.ts +42 -46
- package/src/modules/user-management/command/createRole.generated.ts +6 -0
- package/src/modules/user-management/command/createRole.test.ts +30 -29
- package/src/modules/user-management/command/createRole.ts +33 -33
- package/src/modules/user-management/command/createUser.generated.ts +6 -0
- package/src/modules/user-management/command/createUser.test.ts +64 -42
- package/src/modules/user-management/command/createUser.ts +54 -56
- package/src/modules/user-management/command/deactivateUser.generated.ts +6 -0
- package/src/modules/user-management/command/deactivateUser.test.ts +27 -27
- package/src/modules/user-management/command/deactivateUser.ts +40 -48
- package/src/modules/user-management/command/logAuditEvent.generated.ts +6 -0
- package/src/modules/user-management/command/logAuditEvent.test.ts +50 -42
- package/src/modules/user-management/command/logAuditEvent.ts +25 -28
- package/src/modules/user-management/command/reactivateUser.generated.ts +6 -0
- package/src/modules/user-management/command/reactivateUser.test.ts +31 -27
- package/src/modules/user-management/command/reactivateUser.ts +40 -48
- package/src/modules/user-management/command/revokePermissionFromRole.generated.ts +6 -0
- package/src/modules/user-management/command/revokePermissionFromRole.test.ts +52 -51
- package/src/modules/user-management/command/revokePermissionFromRole.ts +60 -57
- package/src/modules/user-management/command/revokeRoleFromUser.generated.ts +6 -0
- package/src/modules/user-management/command/revokeRoleFromUser.test.ts +53 -48
- package/src/modules/user-management/command/revokeRoleFromUser.ts +58 -57
- package/src/modules/user-management/docs/commands/CreatePermission.md +2 -2
- package/src/modules/user-management/docs/commands/CreateRole.md +1 -1
- package/src/modules/user-management/generated/enums.ts +11 -11
- package/src/modules/user-management/generated/kysely-tailordb.ts +27 -56
- package/src/modules/user-management/index.ts +2 -2
- package/src/modules/user-management/lib/errors.generated.ts +67 -0
- package/src/modules/user-management/{permissions.ts → lib/permissions.generated.ts} +8 -7
- package/src/modules/user-management/module.ts +22 -22
- package/src/modules/user-management/tailor.config.ts +3 -3
- package/src/schemas.ts +1 -1
- package/skills/1-module-docs/references/structure.md +0 -22
- package/skills/2-module-feature-breakdown/references/commands.md +0 -48
- package/skills/2-module-feature-breakdown/references/structure.md +0 -22
- package/skills/3-module-doc-review/references/commands.md +0 -54
- package/skills/3-module-doc-review/references/models.md +0 -29
- package/skills/4-module-tdd-implementation/SKILL.md +0 -74
- package/skills/4-module-tdd-implementation/references/commands.md +0 -45
- package/skills/4-module-tdd-implementation/references/errors.md +0 -7
- package/skills/4-module-tdd-implementation/references/models.md +0 -30
- package/skills/4-module-tdd-implementation/references/structure.md +0 -22
- package/skills/4-module-tdd-implementation/references/testing.md +0 -37
- package/skills/5-module-implementation-review/references/commands.md +0 -45
- package/skills/5-module-implementation-review/references/errors.md +0 -7
- package/skills/5-module-implementation-review/references/exports.md +0 -8
- package/skills/5-module-implementation-review/references/models.md +0 -30
- package/skills/app-compose-1-requirement-analysis/references/structure.md +0 -27
- package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +0 -106
- package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +0 -139
- package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +0 -153
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +0 -27
- package/skills/app-compose-3-doc-review/references/structure.md +0 -27
- package/skills/app-compose-4-design-mock/SKILL.md +0 -256
- package/skills/app-compose-4-design-mock/references/component.md +0 -50
- package/skills/app-compose-4-design-mock/references/screen-detailview.md +0 -106
- package/skills/app-compose-4-design-mock/references/screen-form.md +0 -139
- package/skills/app-compose-4-design-mock/references/screen-listview.md +0 -153
- package/skills/app-compose-4-design-mock/references/structure.md +0 -27
- package/skills/app-compose-5-design-mock-review/SKILL.md +0 -290
- package/skills/app-compose-5-design-mock-review/references/component.md +0 -50
- package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +0 -106
- package/skills/app-compose-5-design-mock-review/references/screen-form.md +0 -139
- package/skills/app-compose-5-design-mock-review/references/screen-listview.md +0 -153
- package/skills/app-compose-6-implementation-spec/references/auth.md +0 -72
- package/skills/app-compose-6-implementation-spec/references/structure.md +0 -27
- package/src/modules/primitives/lib/errors.ts +0 -138
- package/src/modules/user-management/lib/errors.ts +0 -81
- /package/skills/{2-module-feature-breakdown → erp-kit-module-4-tdd}/references/models.md +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { runMain } from "politty";
|
|
5
|
+
|
|
6
|
+
// src/commands/index.ts
|
|
7
|
+
import { z as z5 } from "zod";
|
|
8
|
+
import { defineCommand as defineCommand5, arg as arg5 } from "politty";
|
|
6
9
|
|
|
7
10
|
// src/commands/init.ts
|
|
8
11
|
import fs from "fs";
|
|
@@ -15,20 +18,10 @@ var PACKAGE_ROOT = path.resolve(import.meta.dirname, "..");
|
|
|
15
18
|
|
|
16
19
|
// src/commands/init.ts
|
|
17
20
|
var SKILLS_SRC = path2.join(PACKAGE_ROOT, "skills");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"4-module-tdd-implementation",
|
|
23
|
-
"5-module-implementation-review",
|
|
24
|
-
"app-compose-1-requirement-analysis",
|
|
25
|
-
"app-compose-2-requirements-breakdown",
|
|
26
|
-
"app-compose-3-doc-review",
|
|
27
|
-
"app-compose-4-design-mock",
|
|
28
|
-
"app-compose-5-design-mock-review",
|
|
29
|
-
"app-compose-6-implementation-spec",
|
|
30
|
-
"mock-scenario"
|
|
31
|
-
];
|
|
21
|
+
function discoverFrameworkSkills() {
|
|
22
|
+
if (!fs.existsSync(SKILLS_SRC)) return [];
|
|
23
|
+
return fs.readdirSync(SKILLS_SRC, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("erp-kit-")).map((entry) => entry.name);
|
|
24
|
+
}
|
|
32
25
|
function copyDirectoryRecursive(srcDir, destDir, force) {
|
|
33
26
|
let copied = 0;
|
|
34
27
|
let skipped = 0;
|
|
@@ -51,12 +44,25 @@ function copyDirectoryRecursive(srcDir, destDir, force) {
|
|
|
51
44
|
}
|
|
52
45
|
return { copied, skipped };
|
|
53
46
|
}
|
|
54
|
-
function runInit(
|
|
47
|
+
function runInit(cwd5, force) {
|
|
55
48
|
console.log(chalk.bold("erp-kit init\n"));
|
|
56
|
-
const skillsDest = path2.join(
|
|
49
|
+
const skillsDest = path2.join(cwd5, ".agents", "skills");
|
|
50
|
+
if (force && fs.existsSync(skillsDest)) {
|
|
51
|
+
let removedCount = 0;
|
|
52
|
+
for (const entry of fs.readdirSync(skillsDest)) {
|
|
53
|
+
if (entry.startsWith("erp-kit-")) {
|
|
54
|
+
fs.rmSync(path2.join(skillsDest, entry), { recursive: true, force: true });
|
|
55
|
+
removedCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (removedCount > 0) {
|
|
59
|
+
console.log(chalk.green(` Removed ${removedCount} existing erp-kit-* skills`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
57
62
|
let copiedCount = 0;
|
|
58
63
|
let skippedCount = 0;
|
|
59
|
-
|
|
64
|
+
const frameworkSkills = discoverFrameworkSkills();
|
|
65
|
+
for (const skill of frameworkSkills) {
|
|
60
66
|
const srcSkillDir = path2.join(SKILLS_SRC, skill);
|
|
61
67
|
if (!fs.existsSync(srcSkillDir)) continue;
|
|
62
68
|
const destDir = path2.join(skillsDest, skill);
|
|
@@ -73,7 +79,7 @@ function runInit(cwd4, force) {
|
|
|
73
79
|
chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`)
|
|
74
80
|
);
|
|
75
81
|
}
|
|
76
|
-
const claudeSkills = path2.join(
|
|
82
|
+
const claudeSkills = path2.join(cwd5, ".claude", "skills");
|
|
77
83
|
const relTarget = path2.relative(path2.dirname(claudeSkills), skillsDest);
|
|
78
84
|
const claudeSkillsExists = (() => {
|
|
79
85
|
try {
|
|
@@ -110,376 +116,169 @@ function runInit(cwd4, force) {
|
|
|
110
116
|
return 0;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
// src/commands/
|
|
114
|
-
import
|
|
115
|
-
import {
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
119
|
+
// src/commands/license.ts
|
|
120
|
+
import fs2 from "fs";
|
|
121
|
+
import { execSync } from "child_process";
|
|
122
|
+
var licenseGroups = {
|
|
123
|
+
// https://github.com/google/licenseclassifier/blob/e6a9bb99b5a6f71d5a34336b8245e305f5430f99/license_type.go#L225
|
|
124
|
+
reciprocal: [
|
|
125
|
+
"APSL-1.0",
|
|
126
|
+
"APSL-1.1",
|
|
127
|
+
"APSL-1.2",
|
|
128
|
+
"APSL-2.0",
|
|
129
|
+
"CDDL-1.0",
|
|
130
|
+
"CDDL-1.1",
|
|
131
|
+
"CPL-1.0",
|
|
132
|
+
"EPL-1.0",
|
|
133
|
+
"EPL-2.0",
|
|
134
|
+
"FreeImage",
|
|
135
|
+
"IPL-1.0",
|
|
136
|
+
"MPL-1.0",
|
|
137
|
+
"MPL-1.1",
|
|
138
|
+
"MPL-2.0",
|
|
139
|
+
"Ruby"
|
|
140
|
+
],
|
|
141
|
+
// https://github.com/google/licenseclassifier/blob/e6a9bb99b5a6f71d5a34336b8245e305f5430f99/license_type.go#L249
|
|
142
|
+
notice: [
|
|
143
|
+
"AFL-1.1",
|
|
144
|
+
"AFL-1.2",
|
|
145
|
+
"AFL-2.0",
|
|
146
|
+
"AFL-2.1",
|
|
147
|
+
"AFL-3.0",
|
|
148
|
+
"Apache-1.0",
|
|
149
|
+
"Apache-1.1",
|
|
150
|
+
"Apache-2.0",
|
|
151
|
+
"Artistic-1.0-cl8",
|
|
152
|
+
"Artistic-1.0-Perl",
|
|
153
|
+
"Artistic-1.0",
|
|
154
|
+
"Artistic-2.0",
|
|
155
|
+
"BSL-1.0",
|
|
156
|
+
"BSD-2-Clause-FreeBSD",
|
|
157
|
+
"BSD-2-Clause-NetBSD",
|
|
158
|
+
"BSD-2-Clause",
|
|
159
|
+
"BSD-3-Clause-Attribution",
|
|
160
|
+
"BSD-3-Clause-Clear",
|
|
161
|
+
"BSD-3-Clause-LBNL",
|
|
162
|
+
"BSD-3-Clause",
|
|
163
|
+
"BSD-4-Clause",
|
|
164
|
+
"BSD-4-Clause-UC",
|
|
165
|
+
"BSD-Protection",
|
|
166
|
+
"CC-BY-1.0",
|
|
167
|
+
"CC-BY-2.0",
|
|
168
|
+
"CC-BY-2.5",
|
|
169
|
+
"CC-BY-3.0",
|
|
170
|
+
"CC-BY-4.0",
|
|
171
|
+
"FTL",
|
|
172
|
+
"ISC",
|
|
173
|
+
"ImageMagick",
|
|
174
|
+
"Libpng",
|
|
175
|
+
"Lil-1.0",
|
|
176
|
+
"Linux-OpenIB",
|
|
177
|
+
"LPL-1.02",
|
|
178
|
+
"LPL-1.0",
|
|
179
|
+
"MS-PL",
|
|
180
|
+
"MIT",
|
|
181
|
+
"NCSA",
|
|
182
|
+
"OpenSSL",
|
|
183
|
+
"PHP-3.01",
|
|
184
|
+
"PHP-3.0",
|
|
185
|
+
"PIL",
|
|
186
|
+
"Python-2.0",
|
|
187
|
+
"Python-2.0-complete",
|
|
188
|
+
"PostgreSQL",
|
|
189
|
+
"SGI-B-1.0",
|
|
190
|
+
"SGI-B-1.1",
|
|
191
|
+
"SGI-B-2.0",
|
|
192
|
+
"Unicode-DFS-2015",
|
|
193
|
+
"Unicode-DFS-2016",
|
|
194
|
+
"Unicode-TOU",
|
|
195
|
+
"UPL-1.0",
|
|
196
|
+
"W3C-19980720",
|
|
197
|
+
"W3C-20150513",
|
|
198
|
+
"W3C",
|
|
199
|
+
"X11",
|
|
200
|
+
"Xnet",
|
|
201
|
+
"Zend-2.0",
|
|
202
|
+
"zlib-acknowledgement",
|
|
203
|
+
"Zlib",
|
|
204
|
+
"ZPL-1.1",
|
|
205
|
+
"ZPL-2.0",
|
|
206
|
+
"ZPL-2.1"
|
|
207
|
+
],
|
|
208
|
+
// https://github.com/google/licenseclassifier/blob/e6a9bb99b5a6f71d5a34336b8245e305f5430f99/license_type.go#L324
|
|
209
|
+
unencumbered: ["CC0-1.0", "Unlicense", "0BSD"]
|
|
210
|
+
};
|
|
211
|
+
function buildAllowSet(config) {
|
|
212
|
+
const set = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const group of config.groups) {
|
|
214
|
+
for (const license of licenseGroups[group]) {
|
|
215
|
+
set.add(license);
|
|
177
216
|
}
|
|
178
217
|
}
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
function findFreePort() {
|
|
185
|
-
return new Promise((resolve4, reject) => {
|
|
186
|
-
const srv = createNetServer();
|
|
187
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
188
|
-
const addr = srv.address();
|
|
189
|
-
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
190
|
-
srv.close(() => resolve4(port));
|
|
191
|
-
});
|
|
192
|
-
srv.on("error", reject);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
async function startMock(mock) {
|
|
196
|
-
const port = await findFreePort();
|
|
197
|
-
const server = await createMockServer(mock.mockPath, port);
|
|
198
|
-
const route = `/${mock.provider}/${mock.scenario}`;
|
|
199
|
-
return { server, route, provider: mock.provider, scenario: mock.scenario };
|
|
200
|
-
}
|
|
201
|
-
function createProxy(routeTable) {
|
|
202
|
-
return createServer((req, res) => {
|
|
203
|
-
const match = req.url?.match(/^\/([^/?]+)\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
|
|
204
|
-
if (!match) {
|
|
205
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
206
|
-
res.end(JSON.stringify({ error: "Not found. Use /{provider}/{scenario}/..." }));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const [, provider, scenario] = match;
|
|
210
|
-
const key = `/${provider}/${scenario}`;
|
|
211
|
-
const target = routeTable.get(key);
|
|
212
|
-
if (!target) {
|
|
213
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
214
|
-
res.end(
|
|
215
|
-
JSON.stringify({
|
|
216
|
-
error: `Unknown route: ${key}`,
|
|
217
|
-
available: [...routeTable.keys()]
|
|
218
|
-
})
|
|
219
|
-
);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const downstream = (req.url ?? "/").slice(key.length) || "/";
|
|
223
|
-
const proxyReq = httpRequest(
|
|
224
|
-
{
|
|
225
|
-
hostname: "127.0.0.1",
|
|
226
|
-
port: target.server.port,
|
|
227
|
-
path: downstream,
|
|
228
|
-
method: req.method,
|
|
229
|
-
headers: { ...req.headers, host: `127.0.0.1:${target.server.port}` }
|
|
230
|
-
},
|
|
231
|
-
(proxyRes) => {
|
|
232
|
-
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
233
|
-
proxyRes.pipe(res);
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
proxyReq.on("error", (err) => {
|
|
237
|
-
if (!res.headersSent) {
|
|
238
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
239
|
-
}
|
|
240
|
-
res.end(JSON.stringify({ error: "Bad gateway", detail: err.message }));
|
|
241
|
-
});
|
|
242
|
-
req.pipe(proxyReq);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
async function runMockStart(mocksRoot, filters, port) {
|
|
246
|
-
const mocksDir = resolve2(mocksRoot);
|
|
247
|
-
const mocks = discoverMocks(mocksDir, filters);
|
|
248
|
-
if (mocks.length === 0) {
|
|
249
|
-
console.error("No matching mocks found.");
|
|
250
|
-
if (filters.length > 0) {
|
|
251
|
-
console.error(`Filters: ${filters.join(", ")}`);
|
|
252
|
-
console.error(`Available mocks are under: ${relative(process.cwd(), mocksDir)}/`);
|
|
218
|
+
if (config.allow) {
|
|
219
|
+
for (const license of config.allow) {
|
|
220
|
+
set.add(license);
|
|
253
221
|
}
|
|
254
|
-
return 1;
|
|
255
|
-
}
|
|
256
|
-
console.log(`Starting ${mocks.length} mock(s)...
|
|
257
|
-
`);
|
|
258
|
-
const running = [];
|
|
259
|
-
for (const mock of mocks) {
|
|
260
|
-
const info = await startMock(mock);
|
|
261
|
-
running.push(info);
|
|
262
|
-
}
|
|
263
|
-
const routeTable = /* @__PURE__ */ new Map();
|
|
264
|
-
for (const r of running) {
|
|
265
|
-
routeTable.set(r.route, r);
|
|
266
|
-
}
|
|
267
|
-
console.log("Route table:");
|
|
268
|
-
console.log("\u2500".repeat(50));
|
|
269
|
-
for (const [route, { server }] of routeTable) {
|
|
270
|
-
console.log(` ${route} \u2192 127.0.0.1:${server.port}`);
|
|
271
222
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log(`
|
|
276
|
-
Reverse proxy listening on http://localhost:${port}`);
|
|
277
|
-
console.log("Press Ctrl+C to stop all mocks.\n");
|
|
278
|
-
});
|
|
279
|
-
function shutdown() {
|
|
280
|
-
console.log("\nShutting down...");
|
|
281
|
-
proxy.close();
|
|
282
|
-
for (const { server } of running) {
|
|
283
|
-
void server.stop();
|
|
223
|
+
if (config.deny) {
|
|
224
|
+
for (const license of config.deny) {
|
|
225
|
+
set.delete(license);
|
|
284
226
|
}
|
|
285
|
-
process.exit(0);
|
|
286
|
-
}
|
|
287
|
-
process.on("SIGINT", shutdown);
|
|
288
|
-
process.on("SIGTERM", shutdown);
|
|
289
|
-
return new Promise(() => void 0);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// src/commands/mock/validate.ts
|
|
293
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
294
|
-
import { join as join2, resolve as resolve3, relative as relative2, dirname as dirname2, basename } from "path";
|
|
295
|
-
import chalk2 from "chalk";
|
|
296
|
-
function pass(msg) {
|
|
297
|
-
console.log(chalk2.green(`\u2713 ${msg}`));
|
|
298
|
-
}
|
|
299
|
-
function fail(ctx, msg) {
|
|
300
|
-
console.log(chalk2.red(`\u2717 ${msg}`));
|
|
301
|
-
ctx.failures++;
|
|
302
|
-
}
|
|
303
|
-
async function subdirs(dir) {
|
|
304
|
-
const entries = await readdir(dir);
|
|
305
|
-
const dirs = [];
|
|
306
|
-
for (const entry of entries) {
|
|
307
|
-
const full = join2(dir, entry);
|
|
308
|
-
const s = await stat(full);
|
|
309
|
-
if (s.isDirectory()) dirs.push(entry);
|
|
310
227
|
}
|
|
311
|
-
return
|
|
228
|
+
return set;
|
|
312
229
|
}
|
|
313
|
-
function
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
fail(ctx, `${label}: missing README.md`);
|
|
318
|
-
}
|
|
319
|
-
if (entries.includes("mock.json")) {
|
|
320
|
-
pass(`${label}: has mock.json`);
|
|
321
|
-
return true;
|
|
230
|
+
function isLicenseAllowed(licenseString, allowSet) {
|
|
231
|
+
if (/\s+(?:OR|AND)\s+/i.test(licenseString)) {
|
|
232
|
+
const licenses = licenseString.replace(/[()]/g, "").trim().split(/\s+(?:OR|AND)\s+/i).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
233
|
+
return licenses.every((l) => allowSet.has(l));
|
|
322
234
|
}
|
|
323
|
-
|
|
324
|
-
return false;
|
|
235
|
+
return allowSet.has(licenseString);
|
|
325
236
|
}
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
pass(`${label}: valid Mockoon schema`);
|
|
332
|
-
} else {
|
|
333
|
-
for (const detail of result.error.details) {
|
|
334
|
-
fail(ctx, `${label}: schema \u2014 ${detail.message}`);
|
|
237
|
+
function runLicenseList() {
|
|
238
|
+
for (const [group, licenses] of Object.entries(licenseGroups)) {
|
|
239
|
+
console.log(`${group} (${licenses.length} licenses):`);
|
|
240
|
+
for (const license of licenses) {
|
|
241
|
+
console.log(` ${license}`);
|
|
335
242
|
}
|
|
243
|
+
console.log();
|
|
336
244
|
}
|
|
245
|
+
return 0;
|
|
337
246
|
}
|
|
338
|
-
function
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
pass(`${respLabel}: has Content-Type`);
|
|
347
|
-
} else {
|
|
348
|
-
fail(ctx, `${respLabel}: missing Content-Type header`);
|
|
349
|
-
}
|
|
350
|
-
if (resp.label && resp.label.trim().length > 0) {
|
|
351
|
-
pass(`${respLabel}: has label "${resp.label}"`);
|
|
352
|
-
} else {
|
|
353
|
-
fail(ctx, `${respLabel}: missing or empty label`);
|
|
354
|
-
}
|
|
247
|
+
function runLicenseCheck(configPath) {
|
|
248
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
249
|
+
const config = JSON.parse(raw);
|
|
250
|
+
const validGroups = Object.keys(licenseGroups);
|
|
251
|
+
for (const group of config.groups) {
|
|
252
|
+
if (!validGroups.includes(group)) {
|
|
253
|
+
console.error(`Unknown license group: "${group}". Valid groups: ${validGroups.join(", ")}`);
|
|
254
|
+
return 2;
|
|
355
255
|
}
|
|
356
256
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
|
|
368
|
-
}
|
|
257
|
+
const allowSet = buildAllowSet(config);
|
|
258
|
+
console.log("Checking licenses...\n");
|
|
259
|
+
execSync("pnpm licenses list", { stdio: "inherit" });
|
|
260
|
+
const output = execSync("pnpm licenses list --json");
|
|
261
|
+
const licensesJson = JSON.parse(output.toString());
|
|
262
|
+
const violations = [];
|
|
263
|
+
for (const [license, packages] of Object.entries(licensesJson)) {
|
|
264
|
+
if (!isLicenseAllowed(license, allowSet)) {
|
|
265
|
+
for (const pkg of packages) {
|
|
266
|
+
violations.push({ package: pkg.name, license });
|
|
369
267
|
}
|
|
370
268
|
}
|
|
371
269
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const providers = await subdirs(mocksDir);
|
|
376
|
-
for (const provider of providers) {
|
|
377
|
-
const providerDir = join2(mocksDir, provider);
|
|
378
|
-
for (const scenario of await subdirs(providerDir)) {
|
|
379
|
-
scenarios.push(`${provider}/${scenario}`);
|
|
380
|
-
}
|
|
270
|
+
if (violations.length === 0) {
|
|
271
|
+
console.log("All licenses are allowed.");
|
|
272
|
+
return 0;
|
|
381
273
|
}
|
|
382
|
-
|
|
274
|
+
console.error("Found dependencies with disallowed licenses:\n");
|
|
275
|
+
for (const violation of violations) {
|
|
276
|
+
console.error(` - ${violation.package}`);
|
|
277
|
+
console.error(` License: ${violation.license}
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
return 1;
|
|
383
281
|
}
|
|
384
|
-
function resolveScenarioDir(arg5) {
|
|
385
|
-
const abs = resolve3(arg5);
|
|
386
|
-
if (basename(abs) === "mock.json") return dirname2(abs);
|
|
387
|
-
return abs;
|
|
388
|
-
}
|
|
389
|
-
async function validateScenario(ctx, scenarioDir, mocksDir) {
|
|
390
|
-
const label = relative2(mocksDir, scenarioDir);
|
|
391
|
-
console.log(chalk2.bold(`
|
|
392
|
-
\u2500\u2500 ${label} \u2500\u2500`));
|
|
393
|
-
let entries;
|
|
394
|
-
try {
|
|
395
|
-
entries = await readdir(scenarioDir);
|
|
396
|
-
} catch {
|
|
397
|
-
fail(ctx, `${label}: directory not found`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const hasMock = checkStructure(ctx, entries, label);
|
|
401
|
-
if (!hasMock) return;
|
|
402
|
-
const mockPath = join2(scenarioDir, "mock.json");
|
|
403
|
-
let data;
|
|
404
|
-
try {
|
|
405
|
-
data = JSON.parse(await readFile(mockPath, "utf-8"));
|
|
406
|
-
} catch (err) {
|
|
407
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
408
|
-
fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
await checkSchema(ctx, data, label);
|
|
412
|
-
checkResponseQuality(ctx, data, label);
|
|
413
|
-
checkDatabucketRefs(ctx, data, label);
|
|
414
|
-
}
|
|
415
|
-
async function runMockValidate(mocksRoot, paths) {
|
|
416
|
-
const ctx = { failures: 0 };
|
|
417
|
-
const mocksDir = resolve3(mocksRoot);
|
|
418
|
-
const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join2(mocksDir, s));
|
|
419
|
-
if (targets.length === 0) {
|
|
420
|
-
fail(ctx, "No scenarios found under mocks/");
|
|
421
|
-
return 1;
|
|
422
|
-
}
|
|
423
|
-
console.log(chalk2.bold("\nValidating mock configs...\n"));
|
|
424
|
-
for (const target of targets) {
|
|
425
|
-
await validateScenario(ctx, target, mocksDir);
|
|
426
|
-
}
|
|
427
|
-
console.log(chalk2.bold("\n\u2500\u2500 summary \u2500\u2500"));
|
|
428
|
-
if (ctx.failures === 0) {
|
|
429
|
-
console.log(chalk2.green("\u2713 All checks passed"));
|
|
430
|
-
} else {
|
|
431
|
-
console.log(chalk2.red(`\u2717 ${ctx.failures} check(s) failed`));
|
|
432
|
-
}
|
|
433
|
-
return ctx.failures === 0 ? 0 : 1;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// src/commands/mock/index.ts
|
|
437
|
-
var startCommand = defineCommand({
|
|
438
|
-
name: "start",
|
|
439
|
-
description: "Start mock API servers with reverse proxy",
|
|
440
|
-
args: z.object({
|
|
441
|
-
mocksRoot: arg(z.string().default("./mocks"), {
|
|
442
|
-
description: "Path to mocks directory"
|
|
443
|
-
}),
|
|
444
|
-
port: arg(z.coerce.number().default(3e3), {
|
|
445
|
-
alias: "p",
|
|
446
|
-
description: "Reverse proxy port"
|
|
447
|
-
}),
|
|
448
|
-
filter: arg(z.array(z.string()).default([]), {
|
|
449
|
-
positional: true,
|
|
450
|
-
description: "Filter by provider or provider/scenario"
|
|
451
|
-
})
|
|
452
|
-
}),
|
|
453
|
-
run: async (args) => {
|
|
454
|
-
const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
|
|
455
|
-
process.exit(exitCode);
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
var validateCommand = defineCommand({
|
|
459
|
-
name: "validate",
|
|
460
|
-
description: "Validate mock scenario configs",
|
|
461
|
-
args: z.object({
|
|
462
|
-
mocksRoot: arg(z.string().default("./mocks"), {
|
|
463
|
-
description: "Path to mocks directory"
|
|
464
|
-
}),
|
|
465
|
-
paths: arg(z.array(z.string()).default([]), {
|
|
466
|
-
positional: true,
|
|
467
|
-
description: "Specific scenario paths to validate"
|
|
468
|
-
})
|
|
469
|
-
}),
|
|
470
|
-
run: async (args) => {
|
|
471
|
-
const exitCode = await runMockValidate(args.mocksRoot, args.paths);
|
|
472
|
-
process.exit(exitCode);
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
var mockCommand = defineCommand({
|
|
476
|
-
name: "mock",
|
|
477
|
-
description: "Mock API server management",
|
|
478
|
-
subCommands: {
|
|
479
|
-
start: startCommand,
|
|
480
|
-
validate: validateCommand
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
282
|
|
|
484
283
|
// src/commands/module/index.ts
|
|
485
284
|
import { z as z2 } from "zod";
|
|
@@ -487,25 +286,25 @@ import { defineCommand as defineCommand2, arg as arg2 } from "politty";
|
|
|
487
286
|
|
|
488
287
|
// src/mdschema.ts
|
|
489
288
|
import path3 from "path";
|
|
490
|
-
import
|
|
289
|
+
import fs3 from "fs";
|
|
491
290
|
import { execFile } from "child_process";
|
|
492
291
|
import { createRequire } from "module";
|
|
493
292
|
var require2 = createRequire(import.meta.url);
|
|
494
293
|
function getMdschemaBin() {
|
|
495
294
|
const pkgPath = require2.resolve("@jackchuka/mdschema/package.json");
|
|
496
|
-
const pkg = JSON.parse(
|
|
295
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
497
296
|
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.mdschema;
|
|
498
297
|
if (!bin) {
|
|
499
298
|
throw new Error("Could not resolve mdschema binary from package.json bin field");
|
|
500
299
|
}
|
|
501
300
|
return path3.join(path3.dirname(pkgPath), bin);
|
|
502
301
|
}
|
|
503
|
-
function runMdschema(args,
|
|
302
|
+
function runMdschema(args, cwd5) {
|
|
504
303
|
return new Promise((resolve4) => {
|
|
505
304
|
execFile(
|
|
506
305
|
getMdschemaBin(),
|
|
507
306
|
args,
|
|
508
|
-
{ encoding: "utf-8", cwd:
|
|
307
|
+
{ encoding: "utf-8", cwd: cwd5, timeout: 3e4 },
|
|
509
308
|
(error, stdout, stderr) => {
|
|
510
309
|
if (error) {
|
|
511
310
|
const execError = error;
|
|
@@ -571,7 +370,7 @@ function buildCheckTargets(config) {
|
|
|
571
370
|
}
|
|
572
371
|
return targets;
|
|
573
372
|
}
|
|
574
|
-
async function runCheck(config,
|
|
373
|
+
async function runCheck(config, cwd5) {
|
|
575
374
|
const targets = buildCheckTargets(config);
|
|
576
375
|
const results = await Promise.all(
|
|
577
376
|
targets.map(async (target) => {
|
|
@@ -582,7 +381,7 @@ async function runCheck(config, cwd4) {
|
|
|
582
381
|
}
|
|
583
382
|
const { exitCode, stdout, stderr } = await runMdschema(
|
|
584
383
|
["check", target.glob, "--schema", schemaPath],
|
|
585
|
-
|
|
384
|
+
cwd5
|
|
586
385
|
);
|
|
587
386
|
if (stdout.trim()) console.log(stdout);
|
|
588
387
|
if (stderr.trim()) console.error(stderr);
|
|
@@ -596,14 +395,14 @@ async function runCheck(config, cwd4) {
|
|
|
596
395
|
// src/commands/sync-check.ts
|
|
597
396
|
import path5 from "path";
|
|
598
397
|
import fg from "fast-glob";
|
|
599
|
-
import
|
|
398
|
+
import chalk2 from "chalk";
|
|
600
399
|
function moduleCategories(root) {
|
|
601
400
|
return [
|
|
602
401
|
{
|
|
603
402
|
name: "command",
|
|
604
403
|
sourcePattern: `${root}/*/command/*.ts`,
|
|
605
404
|
docPattern: `${root}/*/docs/commands/*.md`,
|
|
606
|
-
exclusions: [/\.test\.ts$/]
|
|
405
|
+
exclusions: [/\.test\.ts$/, /\.generated\.ts$/]
|
|
607
406
|
},
|
|
608
407
|
{
|
|
609
408
|
name: "model",
|
|
@@ -632,7 +431,7 @@ function appComposeCategories(root) {
|
|
|
632
431
|
function shouldExclude(fileName, exclusions) {
|
|
633
432
|
return exclusions.some((pattern) => pattern.test(fileName));
|
|
634
433
|
}
|
|
635
|
-
async function runSyncCheck(config,
|
|
434
|
+
async function runSyncCheck(config, cwd5) {
|
|
636
435
|
const errors = [];
|
|
637
436
|
let totalSources = 0;
|
|
638
437
|
let totalDocs = 0;
|
|
@@ -644,14 +443,17 @@ async function runSyncCheck(config, cwd4) {
|
|
|
644
443
|
allCategories.push(...appComposeCategories(config.appRoot));
|
|
645
444
|
}
|
|
646
445
|
for (const category of allCategories) {
|
|
647
|
-
const sources = await fg(category.sourcePattern, { cwd:
|
|
648
|
-
const docs = await fg(category.docPattern, { cwd:
|
|
446
|
+
const sources = await fg(category.sourcePattern, { cwd: cwd5 });
|
|
447
|
+
const docs = await fg(category.docPattern, { cwd: cwd5 });
|
|
649
448
|
const sourceBasenames = /* @__PURE__ */ new Map();
|
|
650
449
|
const docBasenames = /* @__PURE__ */ new Map();
|
|
651
450
|
for (const sourcePath of sources) {
|
|
652
451
|
const fileName = path5.basename(sourcePath);
|
|
653
452
|
if (shouldExclude(fileName, category.exclusions)) continue;
|
|
654
|
-
|
|
453
|
+
let basename2 = path5.basename(sourcePath, path5.extname(sourcePath));
|
|
454
|
+
if (basename2.endsWith(".generated")) {
|
|
455
|
+
basename2 = basename2.slice(0, -".generated".length);
|
|
456
|
+
}
|
|
655
457
|
sourceBasenames.set(basename2.toLowerCase(), sourcePath);
|
|
656
458
|
}
|
|
657
459
|
for (const docPath of docs) {
|
|
@@ -693,9 +495,9 @@ async function runSyncCheck(config, cwd4) {
|
|
|
693
495
|
}
|
|
694
496
|
function formatSyncCheckReport(result) {
|
|
695
497
|
const lines = [];
|
|
696
|
-
lines.push(
|
|
498
|
+
lines.push(chalk2.bold("docs-sync-check: Checking source-documentation correspondence...\n"));
|
|
697
499
|
if (result.errors.length > 0) {
|
|
698
|
-
lines.push(
|
|
500
|
+
lines.push(chalk2.red.bold("Errors:\n"));
|
|
699
501
|
const byCategory = /* @__PURE__ */ new Map();
|
|
700
502
|
for (const error of result.errors) {
|
|
701
503
|
const existing = byCategory.get(error.category) ?? [];
|
|
@@ -703,44 +505,104 @@ function formatSyncCheckReport(result) {
|
|
|
703
505
|
byCategory.set(error.category, existing);
|
|
704
506
|
}
|
|
705
507
|
for (const [category, categoryErrors] of byCategory) {
|
|
706
|
-
lines.push(
|
|
508
|
+
lines.push(chalk2.cyan(` Category: ${category}
|
|
707
509
|
`));
|
|
708
510
|
for (const error of categoryErrors) {
|
|
709
511
|
if (error.type === "missing-doc") {
|
|
710
|
-
lines.push(` ${
|
|
711
|
-
lines.push(` ${
|
|
512
|
+
lines.push(` ${chalk2.red(error.sourcePath)}`);
|
|
513
|
+
lines.push(` ${chalk2.yellow("Missing documentation for:")} ${error.expectedBasename}`);
|
|
712
514
|
} else {
|
|
713
|
-
lines.push(` ${
|
|
515
|
+
lines.push(` ${chalk2.red(error.docPath)}`);
|
|
714
516
|
lines.push(
|
|
715
|
-
` ${
|
|
517
|
+
` ${chalk2.yellow("Orphaned documentation:")} no source file for ${error.expectedBasename}`
|
|
716
518
|
);
|
|
717
519
|
}
|
|
718
520
|
lines.push("");
|
|
719
521
|
}
|
|
720
522
|
}
|
|
721
523
|
} else {
|
|
722
|
-
lines.push(
|
|
524
|
+
lines.push(chalk2.green("All source files have corresponding documentation.\n"));
|
|
723
525
|
}
|
|
724
|
-
lines.push(
|
|
526
|
+
lines.push(chalk2.bold("Summary:"));
|
|
725
527
|
lines.push(` Categories checked: ${result.summary.categoriesChecked}`);
|
|
726
528
|
lines.push(
|
|
727
529
|
` Source files: ${result.summary.totalSources}, Doc files: ${result.summary.totalDocs}`
|
|
728
530
|
);
|
|
729
531
|
if (result.errors.length > 0) {
|
|
730
|
-
lines.push(
|
|
532
|
+
lines.push(chalk2.red(` Errors: ${result.errors.length}`));
|
|
731
533
|
lines.push("");
|
|
732
|
-
lines.push(
|
|
534
|
+
lines.push(chalk2.red.bold(`docs-sync-check failed with ${result.errors.length} error(s).`));
|
|
733
535
|
} else {
|
|
734
|
-
lines.push(
|
|
536
|
+
lines.push(chalk2.green(" Errors: 0"));
|
|
735
537
|
lines.push("");
|
|
736
|
-
lines.push(
|
|
538
|
+
lines.push(chalk2.green.bold("docs-sync-check passed."));
|
|
737
539
|
}
|
|
738
540
|
return lines.join("\n");
|
|
739
541
|
}
|
|
740
542
|
|
|
741
543
|
// src/commands/scaffold.ts
|
|
742
544
|
import path6 from "path";
|
|
743
|
-
import
|
|
545
|
+
import fs4 from "fs";
|
|
546
|
+
|
|
547
|
+
// src/commands/scaffold-templates.ts
|
|
548
|
+
function getModuleSrcTemplates() {
|
|
549
|
+
return [
|
|
550
|
+
{ type: "dir", path: "db" },
|
|
551
|
+
{ type: "dir", path: "command" },
|
|
552
|
+
{ type: "dir", path: "executor" },
|
|
553
|
+
{ type: "dir", path: "generated" },
|
|
554
|
+
{ type: "dir", path: "query" },
|
|
555
|
+
{ type: "file", path: "module.ts", content: () => MODULE_TEMPLATE },
|
|
556
|
+
{ type: "file", path: "index.ts", content: () => INDEX_TEMPLATE },
|
|
557
|
+
{ type: "file", path: "permissions.ts", content: (n) => permissionsTemplate(n) },
|
|
558
|
+
{ type: "file", path: "tailor.config.ts", content: (n) => tailorConfigTemplate(n) },
|
|
559
|
+
{ type: "file", path: "lib/errors.ts", content: () => ERRORS_TEMPLATE },
|
|
560
|
+
{ type: "file", path: "lib/types.ts", content: () => TYPES_TEMPLATE },
|
|
561
|
+
{ type: "file", path: "testing/fixtures.ts", content: () => FIXTURES_TEMPLATE }
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
var MODULE_TEMPLATE = `export const defineModule = () => {
|
|
565
|
+
return {
|
|
566
|
+
db: {},
|
|
567
|
+
commands: {},
|
|
568
|
+
queries: {},
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
`;
|
|
572
|
+
var INDEX_TEMPLATE = `export { defineModule } from "./module";
|
|
573
|
+
export { permissions, own, all } from "./permissions";
|
|
574
|
+
`;
|
|
575
|
+
var ERRORS_TEMPLATE = `import { createDomainError } from "../../shared/internal";
|
|
576
|
+
`;
|
|
577
|
+
var TYPES_TEMPLATE = `import type { InferSchema, Selectable, Insertable, Updateable } from "../../shared/internal";
|
|
578
|
+
import type { DB } from "../generated/kysely-tailordb";
|
|
579
|
+
|
|
580
|
+
export type Schema = InferSchema<DB>;
|
|
581
|
+
`;
|
|
582
|
+
var FIXTURES_TEMPLATE = `// Add test fixtures here
|
|
583
|
+
`;
|
|
584
|
+
function permissionsTemplate(moduleName) {
|
|
585
|
+
return `import { definePermissions } from "../shared/internal";
|
|
586
|
+
|
|
587
|
+
export const { permissions, own, all } = definePermissions("${moduleName}", [] as const);
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
function tailorConfigTemplate(moduleName) {
|
|
591
|
+
return `import { defineConfig, defineGenerators } from "@tailor-platform/sdk";
|
|
592
|
+
|
|
593
|
+
export default defineConfig({
|
|
594
|
+
name: "${moduleName}",
|
|
595
|
+
db: { "main-db": { files: [\`./db/*.ts\`] } },
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
export const generators = defineGenerators(
|
|
599
|
+
["@tailor-platform/kysely-type", { distPath: \`./generated/kysely-tailordb.ts\` }],
|
|
600
|
+
["@tailor-platform/enum-constants", { distPath: "./generated/enums.ts" }],
|
|
601
|
+
);
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/commands/scaffold.ts
|
|
744
606
|
var MODULE_TYPES = ["module", "feature", "command", "model", "query"];
|
|
745
607
|
var APP_TYPES = [
|
|
746
608
|
"requirements",
|
|
@@ -790,10 +652,10 @@ function resolveScaffoldPath(type, parentName, name, root) {
|
|
|
790
652
|
}
|
|
791
653
|
throw new Error(`Unknown scaffold type: ${type}`);
|
|
792
654
|
}
|
|
793
|
-
async function runScaffold(type, parentName, name, root,
|
|
655
|
+
async function runScaffold(type, parentName, name, root, cwd5) {
|
|
794
656
|
const outputPath = resolveScaffoldPath(type, parentName, name, root);
|
|
795
|
-
const absoluteOutput = path6.resolve(
|
|
796
|
-
if (
|
|
657
|
+
const absoluteOutput = path6.resolve(cwd5, outputPath);
|
|
658
|
+
if (fs4.existsSync(absoluteOutput)) {
|
|
797
659
|
console.error(`File already exists: ${outputPath}`);
|
|
798
660
|
return 1;
|
|
799
661
|
}
|
|
@@ -803,7 +665,7 @@ async function runScaffold(type, parentName, name, root, cwd4) {
|
|
|
803
665
|
return 2;
|
|
804
666
|
}
|
|
805
667
|
try {
|
|
806
|
-
|
|
668
|
+
fs4.mkdirSync(path6.dirname(absoluteOutput), { recursive: true });
|
|
807
669
|
} catch (err) {
|
|
808
670
|
console.error(
|
|
809
671
|
`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -812,39 +674,55 @@ async function runScaffold(type, parentName, name, root, cwd4) {
|
|
|
812
674
|
}
|
|
813
675
|
const { exitCode, stdout, stderr } = await runMdschema(
|
|
814
676
|
["generate", "--schema", schemaPath, "--output", absoluteOutput],
|
|
815
|
-
|
|
677
|
+
cwd5
|
|
816
678
|
);
|
|
817
679
|
if (stdout.trim()) console.log(stdout);
|
|
818
680
|
if (stderr.trim()) console.error(stderr);
|
|
681
|
+
if (exitCode !== 0) return exitCode;
|
|
682
|
+
if (type === "module") {
|
|
683
|
+
scaffoldModuleSrc(path6.dirname(absoluteOutput), parentName);
|
|
684
|
+
}
|
|
819
685
|
return exitCode;
|
|
820
686
|
}
|
|
687
|
+
function scaffoldModuleSrc(moduleDir, moduleName) {
|
|
688
|
+
for (const entry of getModuleSrcTemplates()) {
|
|
689
|
+
const fullPath = path6.join(moduleDir, entry.path);
|
|
690
|
+
if (entry.type === "dir") {
|
|
691
|
+
fs4.mkdirSync(fullPath, { recursive: true });
|
|
692
|
+
fs4.writeFileSync(path6.join(fullPath, ".gitkeep"), "");
|
|
693
|
+
} else {
|
|
694
|
+
fs4.mkdirSync(path6.dirname(fullPath), { recursive: true });
|
|
695
|
+
fs4.writeFileSync(fullPath, entry.content(moduleName));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
821
699
|
|
|
822
700
|
// src/commands/module/list.ts
|
|
823
|
-
import { readdirSync
|
|
824
|
-
import { join
|
|
825
|
-
import
|
|
826
|
-
var MODULES_DIR =
|
|
701
|
+
import { readdirSync, existsSync } from "fs";
|
|
702
|
+
import { join } from "path";
|
|
703
|
+
import chalk3 from "chalk";
|
|
704
|
+
var MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
|
|
827
705
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["shared", "testing"]);
|
|
828
706
|
function countFiles(dir, pattern, exclusions) {
|
|
829
|
-
if (!
|
|
830
|
-
return
|
|
707
|
+
if (!existsSync(dir)) return 0;
|
|
708
|
+
return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f))).length;
|
|
831
709
|
}
|
|
832
710
|
function listModules() {
|
|
833
|
-
if (!
|
|
834
|
-
return
|
|
835
|
-
const modDir =
|
|
711
|
+
if (!existsSync(MODULES_DIR)) return [];
|
|
712
|
+
return readdirSync(MODULES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name)).map((d) => {
|
|
713
|
+
const modDir = join(MODULES_DIR, d.name);
|
|
836
714
|
return {
|
|
837
715
|
name: d.name,
|
|
838
|
-
commands: countFiles(
|
|
839
|
-
models: countFiles(
|
|
840
|
-
features: countFiles(
|
|
716
|
+
commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
|
|
717
|
+
models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
|
|
718
|
+
features: countFiles(join(modDir, "docs", "features"), /\.md$/, [])
|
|
841
719
|
};
|
|
842
720
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
843
721
|
}
|
|
844
722
|
function formatModuleList(modules) {
|
|
845
723
|
if (modules.length === 0) return "No modules found.";
|
|
846
724
|
const lines = [];
|
|
847
|
-
lines.push(
|
|
725
|
+
lines.push(chalk3.bold("Modules:\n"));
|
|
848
726
|
const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
|
|
849
727
|
for (const mod of modules) {
|
|
850
728
|
const counts = [
|
|
@@ -864,8 +742,345 @@ function runModuleList() {
|
|
|
864
742
|
return 0;
|
|
865
743
|
}
|
|
866
744
|
|
|
867
|
-
// src/commands/module/
|
|
745
|
+
// src/commands/module/generate.ts
|
|
746
|
+
import path8 from "path";
|
|
747
|
+
import { defineCommand, arg } from "politty";
|
|
748
|
+
import { z } from "zod";
|
|
749
|
+
|
|
750
|
+
// src/generator/generate-code.ts
|
|
751
|
+
import fs5 from "fs";
|
|
752
|
+
import path7 from "path";
|
|
753
|
+
|
|
754
|
+
// src/generator/parse-command-doc.ts
|
|
755
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
756
|
+
import { toString } from "mdast-util-to-string";
|
|
757
|
+
function parseCommandDoc(fileName, markdown) {
|
|
758
|
+
const commandName = fileName.charAt(0).toLowerCase() + fileName.slice(1);
|
|
759
|
+
const tree = fromMarkdown(markdown);
|
|
760
|
+
return {
|
|
761
|
+
commandName,
|
|
762
|
+
errors: parseErrorScenarios(tree),
|
|
763
|
+
externalDependencies: parseExternalDependencies(tree)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function errorCodeToClassName(code) {
|
|
767
|
+
const pascal = code.toLowerCase().split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
768
|
+
return pascal + "Error";
|
|
769
|
+
}
|
|
770
|
+
function isHeading(node) {
|
|
771
|
+
return node.type === "heading";
|
|
772
|
+
}
|
|
773
|
+
function isList(node) {
|
|
774
|
+
return node.type === "list";
|
|
775
|
+
}
|
|
776
|
+
function getNodesUnderHeading(tree, headingText) {
|
|
777
|
+
const nodes = [];
|
|
778
|
+
let collecting = false;
|
|
779
|
+
for (const node of tree.children) {
|
|
780
|
+
if (isHeading(node)) {
|
|
781
|
+
if (collecting) break;
|
|
782
|
+
if (node.depth === 2 && toString(node) === headingText) {
|
|
783
|
+
collecting = true;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (collecting) {
|
|
788
|
+
nodes.push(node);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return nodes;
|
|
792
|
+
}
|
|
793
|
+
var ERROR_PATTERN = /^([A-Z_]+):\s*(.+)$/;
|
|
794
|
+
function parseErrorScenarios(tree) {
|
|
795
|
+
const nodes = getNodesUnderHeading(tree, "Error Scenarios");
|
|
796
|
+
const errors = [];
|
|
797
|
+
for (const node of nodes) {
|
|
798
|
+
if (!isList(node)) continue;
|
|
799
|
+
for (const item of node.children) {
|
|
800
|
+
const text = toString(item);
|
|
801
|
+
const match = ERROR_PATTERN.exec(text);
|
|
802
|
+
if (match) {
|
|
803
|
+
errors.push({ code: match[1], description: match[2].trim() });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return errors;
|
|
808
|
+
}
|
|
809
|
+
var DEPENDENCY_PATTERN = /^([^:]+)::(.+)$/;
|
|
810
|
+
function parseExternalDependencies(tree) {
|
|
811
|
+
const nodes = getNodesUnderHeading(tree, "External Dependencies");
|
|
812
|
+
const deps = [];
|
|
813
|
+
for (const node of nodes) {
|
|
814
|
+
if (!isList(node)) continue;
|
|
815
|
+
for (const item of node.children) {
|
|
816
|
+
const firstChild = item.children[0];
|
|
817
|
+
if (firstChild?.type !== "paragraph") continue;
|
|
818
|
+
for (const inline of firstChild.children) {
|
|
819
|
+
if (inline.type === "link" || inline.type === "linkReference") {
|
|
820
|
+
const linkText = toString(inline);
|
|
821
|
+
const match = DEPENDENCY_PATTERN.exec(linkText);
|
|
822
|
+
if (match) {
|
|
823
|
+
deps.push({ module: match[1], entity: match[2] });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return deps;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/generator/generate-code.ts
|
|
833
|
+
function generateErrors(docs) {
|
|
834
|
+
const seen = /* @__PURE__ */ new Set();
|
|
835
|
+
const lines = [];
|
|
836
|
+
for (const doc of docs) {
|
|
837
|
+
for (const error of doc.errors) {
|
|
838
|
+
if (seen.has(error.code)) continue;
|
|
839
|
+
seen.add(error.code);
|
|
840
|
+
const className = errorCodeToClassName(error.code);
|
|
841
|
+
lines.push(`export const ${className} = createDomainError(`);
|
|
842
|
+
lines.push(` "${className}", "${error.code}",`);
|
|
843
|
+
const escapedDesc = error.description.replace(/`/g, "'");
|
|
844
|
+
lines.push(` (identifier: string) => \`${escapedDesc}: \${identifier}\`,`);
|
|
845
|
+
lines.push(`);`);
|
|
846
|
+
lines.push(``);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (lines.length === 0) return "";
|
|
850
|
+
return `// @generated \u2014 do not edit
|
|
851
|
+
import { createDomainError } from "../../shared/internal";
|
|
852
|
+
|
|
853
|
+
${lines.join("\n")}`;
|
|
854
|
+
}
|
|
855
|
+
function generateCommandStub(doc) {
|
|
856
|
+
const pascal = doc.commandName.charAt(0).toUpperCase() + doc.commandName.slice(1);
|
|
857
|
+
const inputType = `${pascal}Input`;
|
|
858
|
+
return `import { ok, type CommandContext } from "../../shared/internal";
|
|
859
|
+
import type { DB } from "../generated/kysely-tailordb";
|
|
860
|
+
|
|
861
|
+
export interface ${inputType} {
|
|
862
|
+
// TODO: define input fields
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function run(db: DB, input: ${inputType}, ctx: CommandContext) {
|
|
866
|
+
// TODO: implement
|
|
867
|
+
return ok({});
|
|
868
|
+
}
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
function generateTestStub(doc) {
|
|
872
|
+
const pascal = doc.commandName.charAt(0).toUpperCase() + doc.commandName.slice(1);
|
|
873
|
+
return `import { describe, expect, it } from "vitest";
|
|
874
|
+
import { createMockDb } from "../../testing/index";
|
|
875
|
+
import type { CommandContext } from "../../shared/internal";
|
|
876
|
+
import type { DB } from "../generated/kysely-tailordb";
|
|
877
|
+
import { run, ${pascal}Input } from "./${doc.commandName}";
|
|
878
|
+
|
|
879
|
+
describe("${doc.commandName} - test scenario", () => {
|
|
880
|
+
const ctx: CommandContext = {
|
|
881
|
+
actorId: "test-actor",
|
|
882
|
+
permissions: ["TODO:${doc.commandName}"],
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
it("should be implemented", async () => {
|
|
886
|
+
const { db } = createMockDb<DB>();
|
|
887
|
+
const result = await run(db, {} as ${pascal}Input, ctx);
|
|
888
|
+
expect(result.ok).toBe(true);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
`;
|
|
892
|
+
}
|
|
893
|
+
function generateCommandShell(doc) {
|
|
894
|
+
const lines = [
|
|
895
|
+
`// @generated \u2014 do not edit`,
|
|
896
|
+
`import { defineCommand } from "../../shared/internal";`,
|
|
897
|
+
`import { permissions } from "../lib/permissions.generated";`,
|
|
898
|
+
`import { run } from "./${doc.commandName}";`,
|
|
899
|
+
``,
|
|
900
|
+
`export const ${doc.commandName} = defineCommand(permissions.${doc.commandName}, run);`,
|
|
901
|
+
``
|
|
902
|
+
];
|
|
903
|
+
return lines.join("\n");
|
|
904
|
+
}
|
|
905
|
+
function generateQueryShell(doc) {
|
|
906
|
+
const lines = [
|
|
907
|
+
`// @generated \u2014 do not edit`,
|
|
908
|
+
`import { defineQuery } from "../../shared/internal";`,
|
|
909
|
+
`import { run } from "./${doc.commandName}";`,
|
|
910
|
+
``,
|
|
911
|
+
`export const ${doc.commandName} = defineQuery(run);`,
|
|
912
|
+
``
|
|
913
|
+
];
|
|
914
|
+
return lines.join("\n");
|
|
915
|
+
}
|
|
916
|
+
function generateQueryStub(doc) {
|
|
917
|
+
const pascal = doc.commandName.charAt(0).toUpperCase() + doc.commandName.slice(1);
|
|
918
|
+
const inputType = `${pascal}Input`;
|
|
919
|
+
return `import type { ReadonlyDB } from "../../shared/internal";
|
|
920
|
+
import type { DB } from "../generated/kysely-tailordb";
|
|
921
|
+
|
|
922
|
+
export interface ${inputType} {
|
|
923
|
+
// TODO: define input fields
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export async function run(db: ReadonlyDB<DB>, input: ${inputType}) {
|
|
927
|
+
// TODO: implement
|
|
928
|
+
return {};
|
|
929
|
+
}
|
|
930
|
+
`;
|
|
931
|
+
}
|
|
932
|
+
function generateQueryTestStub(doc) {
|
|
933
|
+
const pascal = doc.commandName.charAt(0).toUpperCase() + doc.commandName.slice(1);
|
|
934
|
+
const inputType = `${pascal}Input`;
|
|
935
|
+
return `import { describe, expect, it } from "vitest";
|
|
936
|
+
import { createMockDb } from "../../testing/index";
|
|
937
|
+
import type { DB } from "../generated/kysely-tailordb";
|
|
938
|
+
import { run, type ${inputType} } from "./${doc.commandName}";
|
|
939
|
+
|
|
940
|
+
describe("${doc.commandName}", () => {
|
|
941
|
+
it("should be implemented", async () => {
|
|
942
|
+
const { db } = createMockDb<DB>();
|
|
943
|
+
const result = await run(db, {} as ${inputType});
|
|
944
|
+
expect(result).toBeDefined();
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
`;
|
|
948
|
+
}
|
|
949
|
+
function generatePermissions(moduleName, commandNames) {
|
|
950
|
+
const sorted = [...commandNames].sort();
|
|
951
|
+
const entries = sorted.map((name) => ` "${name}",`).join("\n");
|
|
952
|
+
return `// @generated \u2014 do not edit
|
|
953
|
+
import { definePermissions } from "../../shared/internal";
|
|
954
|
+
|
|
955
|
+
export const { permissions, own, all } = definePermissions("${moduleName}", [
|
|
956
|
+
${entries}
|
|
957
|
+
] as const);
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
function runGenerateCode(modulePath, moduleName) {
|
|
961
|
+
const docsDir = path7.join(modulePath, "docs", "commands");
|
|
962
|
+
const libDir = path7.join(modulePath, "lib");
|
|
963
|
+
const commandDir = path7.join(modulePath, "command");
|
|
964
|
+
if (!fs5.existsSync(docsDir)) {
|
|
965
|
+
console.error(`No docs/commands/ directory found at ${docsDir}`);
|
|
966
|
+
return 1;
|
|
967
|
+
}
|
|
968
|
+
const mdFiles = fs5.readdirSync(docsDir).filter((f) => f.endsWith(".md"));
|
|
969
|
+
if (mdFiles.length === 0) {
|
|
970
|
+
console.error(`No command docs found in ${docsDir}`);
|
|
971
|
+
return 1;
|
|
972
|
+
}
|
|
973
|
+
const parsedDocs = [];
|
|
974
|
+
for (const file of mdFiles) {
|
|
975
|
+
const content = fs5.readFileSync(path7.join(docsDir, file), "utf-8");
|
|
976
|
+
const name = path7.basename(file, ".md");
|
|
977
|
+
parsedDocs.push(parseCommandDoc(name, content));
|
|
978
|
+
}
|
|
979
|
+
const queryDocsDir = path7.join(modulePath, "docs", "queries");
|
|
980
|
+
const allDocsForErrors = [...parsedDocs];
|
|
981
|
+
if (fs5.existsSync(queryDocsDir)) {
|
|
982
|
+
const queryFiles = fs5.readdirSync(queryDocsDir).filter((f) => f.endsWith(".md"));
|
|
983
|
+
for (const file of queryFiles) {
|
|
984
|
+
const content = fs5.readFileSync(path7.join(queryDocsDir, file), "utf-8");
|
|
985
|
+
const name = path7.basename(file, ".md");
|
|
986
|
+
allDocsForErrors.push(parseCommandDoc(name, content));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
let generated = 0;
|
|
990
|
+
const errorsContent = generateErrors(allDocsForErrors);
|
|
991
|
+
if (errorsContent) {
|
|
992
|
+
fs5.mkdirSync(libDir, { recursive: true });
|
|
993
|
+
const errorsFile = path7.join(libDir, "errors.generated.ts");
|
|
994
|
+
fs5.writeFileSync(errorsFile, errorsContent);
|
|
995
|
+
console.log(` wrote ${path7.relative(modulePath, errorsFile)}`);
|
|
996
|
+
generated++;
|
|
997
|
+
}
|
|
998
|
+
const commandNames = parsedDocs.map((d) => d.commandName);
|
|
999
|
+
const permissionsContent = generatePermissions(moduleName, commandNames);
|
|
1000
|
+
const permissionsFile = path7.join(libDir, "permissions.generated.ts");
|
|
1001
|
+
fs5.writeFileSync(permissionsFile, permissionsContent);
|
|
1002
|
+
console.log(` wrote ${path7.relative(modulePath, permissionsFile)}`);
|
|
1003
|
+
generated++;
|
|
1004
|
+
fs5.mkdirSync(commandDir, { recursive: true });
|
|
1005
|
+
for (const doc of parsedDocs) {
|
|
1006
|
+
const implFile = path7.join(commandDir, `${doc.commandName}.ts`);
|
|
1007
|
+
const shellContent = generateCommandShell(doc);
|
|
1008
|
+
const shellFile = path7.join(commandDir, `${doc.commandName}.generated.ts`);
|
|
1009
|
+
fs5.writeFileSync(shellFile, shellContent);
|
|
1010
|
+
console.log(` wrote ${path7.relative(modulePath, shellFile)}`);
|
|
1011
|
+
generated++;
|
|
1012
|
+
if (!fs5.existsSync(implFile)) {
|
|
1013
|
+
fs5.writeFileSync(implFile, generateCommandStub(doc));
|
|
1014
|
+
console.log(` scaffolded ${path7.relative(modulePath, implFile)}`);
|
|
1015
|
+
}
|
|
1016
|
+
const testFile = path7.join(commandDir, `${doc.commandName}.test.ts`);
|
|
1017
|
+
if (!fs5.existsSync(testFile)) {
|
|
1018
|
+
fs5.writeFileSync(testFile, generateTestStub(doc));
|
|
1019
|
+
console.log(` scaffolded ${path7.relative(modulePath, testFile)}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (fs5.existsSync(queryDocsDir)) {
|
|
1023
|
+
const queryFiles = fs5.readdirSync(queryDocsDir).filter((f) => f.endsWith(".md"));
|
|
1024
|
+
if (queryFiles.length > 0) {
|
|
1025
|
+
const queryDir = path7.join(modulePath, "query");
|
|
1026
|
+
fs5.mkdirSync(queryDir, { recursive: true });
|
|
1027
|
+
for (const file of queryFiles) {
|
|
1028
|
+
const content = fs5.readFileSync(path7.join(queryDocsDir, file), "utf-8");
|
|
1029
|
+
const name = path7.basename(file, ".md");
|
|
1030
|
+
const doc = parseCommandDoc(name, content);
|
|
1031
|
+
const shellFile = path7.join(queryDir, `${doc.commandName}.generated.ts`);
|
|
1032
|
+
fs5.writeFileSync(shellFile, generateQueryShell(doc));
|
|
1033
|
+
console.log(` wrote ${path7.relative(modulePath, shellFile)}`);
|
|
1034
|
+
generated++;
|
|
1035
|
+
const implFile = path7.join(queryDir, `${doc.commandName}.ts`);
|
|
1036
|
+
if (!fs5.existsSync(implFile)) {
|
|
1037
|
+
fs5.writeFileSync(implFile, generateQueryStub(doc));
|
|
1038
|
+
console.log(` scaffolded ${path7.relative(modulePath, implFile)}`);
|
|
1039
|
+
}
|
|
1040
|
+
const testFile = path7.join(queryDir, `${doc.commandName}.test.ts`);
|
|
1041
|
+
if (!fs5.existsSync(testFile)) {
|
|
1042
|
+
fs5.writeFileSync(testFile, generateQueryTestStub(doc));
|
|
1043
|
+
console.log(` scaffolded ${path7.relative(modulePath, testFile)}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
console.log(`Generated ${generated} file(s) for ${moduleName}`);
|
|
1049
|
+
return 0;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/commands/module/generate.ts
|
|
868
1053
|
var cwd = process.cwd();
|
|
1054
|
+
var codeCommand = defineCommand({
|
|
1055
|
+
name: "code",
|
|
1056
|
+
description: "Generate errors, permissions, command shells, and query shells from docs",
|
|
1057
|
+
args: z.object({
|
|
1058
|
+
root: arg(z.string(), {
|
|
1059
|
+
alias: "r",
|
|
1060
|
+
description: "Path to modules directory"
|
|
1061
|
+
}),
|
|
1062
|
+
module: arg(z.string(), {
|
|
1063
|
+
positional: true,
|
|
1064
|
+
description: "Module name (e.g., primitives, item-management)"
|
|
1065
|
+
})
|
|
1066
|
+
}),
|
|
1067
|
+
run: (args) => {
|
|
1068
|
+
const modulePath = path8.resolve(cwd, args.root, args.module);
|
|
1069
|
+
console.log(`Generating code for ${args.module}...`);
|
|
1070
|
+
const exitCode = runGenerateCode(modulePath, args.module);
|
|
1071
|
+
process.exit(exitCode);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
var generateCommand = defineCommand({
|
|
1075
|
+
name: "generate",
|
|
1076
|
+
description: "Generate code from model definitions and docs",
|
|
1077
|
+
subCommands: {
|
|
1078
|
+
code: codeCommand
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// src/commands/module/index.ts
|
|
1083
|
+
var cwd2 = process.cwd();
|
|
869
1084
|
var rootArgs = z2.object({
|
|
870
1085
|
root: arg2(z2.string(), {
|
|
871
1086
|
alias: "r",
|
|
@@ -885,7 +1100,7 @@ var checkCommand = defineCommand2({
|
|
|
885
1100
|
description: "Validate module docs against schemas",
|
|
886
1101
|
args: rootArgs,
|
|
887
1102
|
run: async (args) => {
|
|
888
|
-
const exitCode = await runCheck({ modulesRoot: args.root },
|
|
1103
|
+
const exitCode = await runCheck({ modulesRoot: args.root }, cwd2);
|
|
889
1104
|
process.exit(exitCode);
|
|
890
1105
|
}
|
|
891
1106
|
});
|
|
@@ -894,7 +1109,7 @@ var syncCheckCommand = defineCommand2({
|
|
|
894
1109
|
description: "Validate source <-> doc correspondence",
|
|
895
1110
|
args: rootArgs,
|
|
896
1111
|
run: async (args) => {
|
|
897
|
-
const result = await runSyncCheck({ modulesRoot: args.root },
|
|
1112
|
+
const result = await runSyncCheck({ modulesRoot: args.root }, cwd2);
|
|
898
1113
|
console.log(formatSyncCheckReport(result));
|
|
899
1114
|
process.exit(result.exitCode);
|
|
900
1115
|
}
|
|
@@ -922,7 +1137,7 @@ var scaffoldCommand = defineCommand2({
|
|
|
922
1137
|
args.parent,
|
|
923
1138
|
args.name,
|
|
924
1139
|
args.root,
|
|
925
|
-
|
|
1140
|
+
cwd2
|
|
926
1141
|
);
|
|
927
1142
|
process.exit(exitCode);
|
|
928
1143
|
}
|
|
@@ -934,14 +1149,15 @@ var moduleCommand = defineCommand2({
|
|
|
934
1149
|
list: listCommand,
|
|
935
1150
|
check: checkCommand,
|
|
936
1151
|
"sync-check": syncCheckCommand,
|
|
937
|
-
scaffold: scaffoldCommand
|
|
1152
|
+
scaffold: scaffoldCommand,
|
|
1153
|
+
generate: generateCommand
|
|
938
1154
|
}
|
|
939
1155
|
});
|
|
940
1156
|
|
|
941
1157
|
// src/commands/app/index.ts
|
|
942
1158
|
import { z as z3 } from "zod";
|
|
943
1159
|
import { defineCommand as defineCommand3, arg as arg3 } from "politty";
|
|
944
|
-
var
|
|
1160
|
+
var cwd3 = process.cwd();
|
|
945
1161
|
var rootArgs2 = z3.object({
|
|
946
1162
|
root: arg3(z3.string(), {
|
|
947
1163
|
alias: "r",
|
|
@@ -953,7 +1169,7 @@ var checkCommand2 = defineCommand3({
|
|
|
953
1169
|
description: "Validate app docs against schemas",
|
|
954
1170
|
args: rootArgs2,
|
|
955
1171
|
run: async (args) => {
|
|
956
|
-
const exitCode = await runCheck({ appRoot: args.root },
|
|
1172
|
+
const exitCode = await runCheck({ appRoot: args.root }, cwd3);
|
|
957
1173
|
process.exit(exitCode);
|
|
958
1174
|
}
|
|
959
1175
|
});
|
|
@@ -962,7 +1178,7 @@ var syncCheckCommand2 = defineCommand3({
|
|
|
962
1178
|
description: "Validate source <-> doc correspondence",
|
|
963
1179
|
args: rootArgs2,
|
|
964
1180
|
run: async (args) => {
|
|
965
|
-
const result = await runSyncCheck({ appRoot: args.root },
|
|
1181
|
+
const result = await runSyncCheck({ appRoot: args.root }, cwd3);
|
|
966
1182
|
console.log(formatSyncCheckReport(result));
|
|
967
1183
|
process.exit(result.exitCode);
|
|
968
1184
|
}
|
|
@@ -990,7 +1206,7 @@ var scaffoldCommand2 = defineCommand3({
|
|
|
990
1206
|
args.parent,
|
|
991
1207
|
args.name,
|
|
992
1208
|
args.root,
|
|
993
|
-
|
|
1209
|
+
cwd3
|
|
994
1210
|
);
|
|
995
1211
|
process.exit(exitCode);
|
|
996
1212
|
}
|
|
@@ -1005,30 +1221,434 @@ var appCommand = defineCommand3({
|
|
|
1005
1221
|
}
|
|
1006
1222
|
});
|
|
1007
1223
|
|
|
1008
|
-
// src/
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
subCommands: {
|
|
1028
|
-
module: moduleCommand,
|
|
1029
|
-
app: appCommand,
|
|
1030
|
-
mock: mockCommand,
|
|
1031
|
-
init: initCommand
|
|
1224
|
+
// src/commands/mock/index.ts
|
|
1225
|
+
import { z as z4 } from "zod";
|
|
1226
|
+
import { defineCommand as defineCommand4, arg as arg4 } from "politty";
|
|
1227
|
+
|
|
1228
|
+
// src/commands/mock/start.ts
|
|
1229
|
+
import { createServer, request as httpRequest } from "http";
|
|
1230
|
+
import { createServer as createNetServer } from "net";
|
|
1231
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
1232
|
+
import { resolve as resolve2, relative, join as join2 } from "path";
|
|
1233
|
+
|
|
1234
|
+
// src/mockServer.ts
|
|
1235
|
+
import { readFileSync } from "fs";
|
|
1236
|
+
import { dirname, resolve } from "path";
|
|
1237
|
+
async function createMockServer(mockJsonPath, port) {
|
|
1238
|
+
const { MockoonServer, createLoggerInstance, listenServerEvents } = await import("@mockoon/commons-server");
|
|
1239
|
+
const resolvedPath = resolve(mockJsonPath);
|
|
1240
|
+
const environment = JSON.parse(readFileSync(resolvedPath, "utf-8"));
|
|
1241
|
+
if (port !== void 0) {
|
|
1242
|
+
environment.port = port;
|
|
1032
1243
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1244
|
+
const actualPort = environment.port;
|
|
1245
|
+
const logger = createLoggerInstance(null);
|
|
1246
|
+
const server = new MockoonServer(environment, {
|
|
1247
|
+
environmentDirectory: dirname(resolvedPath),
|
|
1248
|
+
enableAdminApi: false,
|
|
1249
|
+
disableTls: true,
|
|
1250
|
+
enableRandomLatency: false,
|
|
1251
|
+
maxTransactionLogs: 100,
|
|
1252
|
+
envVarsPrefix: "MOCKOON_"
|
|
1253
|
+
});
|
|
1254
|
+
listenServerEvents(server, environment, logger, false);
|
|
1255
|
+
await new Promise((resolve4, reject) => {
|
|
1256
|
+
server.on("started", resolve4);
|
|
1257
|
+
server.on("error", (errorCode, originalError) => {
|
|
1258
|
+
reject(originalError ?? new Error(`Mockoon error: ${errorCode}`));
|
|
1259
|
+
});
|
|
1260
|
+
server.start();
|
|
1261
|
+
});
|
|
1262
|
+
return {
|
|
1263
|
+
url: `http://127.0.0.1:${actualPort}`,
|
|
1264
|
+
port: actualPort,
|
|
1265
|
+
stop: () => new Promise((resolve4) => {
|
|
1266
|
+
server.on("stopped", resolve4);
|
|
1267
|
+
server.stop();
|
|
1268
|
+
})
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/commands/mock/start.ts
|
|
1273
|
+
function readdirSafe(dir) {
|
|
1274
|
+
try {
|
|
1275
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return [];
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function discoverMocks(mocksDir, filters) {
|
|
1281
|
+
const mocks = [];
|
|
1282
|
+
for (const provider of readdirSafe(mocksDir)) {
|
|
1283
|
+
const providerDir = join2(mocksDir, provider);
|
|
1284
|
+
for (const scenario of readdirSafe(providerDir)) {
|
|
1285
|
+
const mockPath = join2(providerDir, scenario, "mock.json");
|
|
1286
|
+
if (!existsSync2(mockPath)) continue;
|
|
1287
|
+
mocks.push({ provider, scenario, mockPath });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (filters.length === 0) return mocks;
|
|
1291
|
+
return mocks.filter(
|
|
1292
|
+
({ provider, scenario }) => filters.some((f) => f === provider || f === `${provider}/${scenario}`)
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
function findFreePort() {
|
|
1296
|
+
return new Promise((resolve4, reject) => {
|
|
1297
|
+
const srv = createNetServer();
|
|
1298
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
1299
|
+
const addr = srv.address();
|
|
1300
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1301
|
+
srv.close(() => resolve4(port));
|
|
1302
|
+
});
|
|
1303
|
+
srv.on("error", reject);
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
async function startMock(mock) {
|
|
1307
|
+
const port = await findFreePort();
|
|
1308
|
+
const server = await createMockServer(mock.mockPath, port);
|
|
1309
|
+
const route = `/${mock.provider}/${mock.scenario}`;
|
|
1310
|
+
return { server, route, provider: mock.provider, scenario: mock.scenario };
|
|
1311
|
+
}
|
|
1312
|
+
function createProxy(routeTable) {
|
|
1313
|
+
return createServer((req, res) => {
|
|
1314
|
+
const match = req.url?.match(/^\/([^/?]+)\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
|
|
1315
|
+
if (!match) {
|
|
1316
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1317
|
+
res.end(JSON.stringify({ error: "Not found. Use /{provider}/{scenario}/..." }));
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const [, provider, scenario] = match;
|
|
1321
|
+
const key = `/${provider}/${scenario}`;
|
|
1322
|
+
const target = routeTable.get(key);
|
|
1323
|
+
if (!target) {
|
|
1324
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1325
|
+
res.end(
|
|
1326
|
+
JSON.stringify({
|
|
1327
|
+
error: `Unknown route: ${key}`,
|
|
1328
|
+
available: [...routeTable.keys()]
|
|
1329
|
+
})
|
|
1330
|
+
);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const downstream = (req.url ?? "/").slice(key.length) || "/";
|
|
1334
|
+
const proxyReq = httpRequest(
|
|
1335
|
+
{
|
|
1336
|
+
hostname: "127.0.0.1",
|
|
1337
|
+
port: target.server.port,
|
|
1338
|
+
path: downstream,
|
|
1339
|
+
method: req.method,
|
|
1340
|
+
headers: { ...req.headers, host: `127.0.0.1:${target.server.port}` }
|
|
1341
|
+
},
|
|
1342
|
+
(proxyRes) => {
|
|
1343
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
1344
|
+
proxyRes.pipe(res);
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
proxyReq.on("error", (err) => {
|
|
1348
|
+
if (!res.headersSent) {
|
|
1349
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
1350
|
+
}
|
|
1351
|
+
res.end(JSON.stringify({ error: "Bad gateway", detail: err.message }));
|
|
1352
|
+
});
|
|
1353
|
+
req.pipe(proxyReq);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
async function runMockStart(mocksRoot, filters, port) {
|
|
1357
|
+
const mocksDir = resolve2(mocksRoot);
|
|
1358
|
+
const mocks = discoverMocks(mocksDir, filters);
|
|
1359
|
+
if (mocks.length === 0) {
|
|
1360
|
+
console.error("No matching mocks found.");
|
|
1361
|
+
if (filters.length > 0) {
|
|
1362
|
+
console.error(`Filters: ${filters.join(", ")}`);
|
|
1363
|
+
console.error(`Available mocks are under: ${relative(process.cwd(), mocksDir)}/`);
|
|
1364
|
+
}
|
|
1365
|
+
return 1;
|
|
1366
|
+
}
|
|
1367
|
+
console.log(`Starting ${mocks.length} mock(s)...
|
|
1368
|
+
`);
|
|
1369
|
+
const running = [];
|
|
1370
|
+
for (const mock of mocks) {
|
|
1371
|
+
const info = await startMock(mock);
|
|
1372
|
+
running.push(info);
|
|
1373
|
+
}
|
|
1374
|
+
const routeTable = /* @__PURE__ */ new Map();
|
|
1375
|
+
for (const r of running) {
|
|
1376
|
+
routeTable.set(r.route, r);
|
|
1377
|
+
}
|
|
1378
|
+
console.log("Route table:");
|
|
1379
|
+
console.log("\u2500".repeat(50));
|
|
1380
|
+
for (const [route, { server }] of routeTable) {
|
|
1381
|
+
console.log(` ${route} \u2192 127.0.0.1:${server.port}`);
|
|
1382
|
+
}
|
|
1383
|
+
console.log("\u2500".repeat(50));
|
|
1384
|
+
const proxy = createProxy(routeTable);
|
|
1385
|
+
proxy.listen(port, () => {
|
|
1386
|
+
console.log(`
|
|
1387
|
+
Reverse proxy listening on http://localhost:${port}`);
|
|
1388
|
+
console.log("Press Ctrl+C to stop all mocks.\n");
|
|
1389
|
+
});
|
|
1390
|
+
function shutdown() {
|
|
1391
|
+
console.log("\nShutting down...");
|
|
1392
|
+
proxy.close();
|
|
1393
|
+
for (const { server } of running) {
|
|
1394
|
+
void server.stop();
|
|
1395
|
+
}
|
|
1396
|
+
process.exit(0);
|
|
1397
|
+
}
|
|
1398
|
+
process.on("SIGINT", shutdown);
|
|
1399
|
+
process.on("SIGTERM", shutdown);
|
|
1400
|
+
return new Promise(() => void 0);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/commands/mock/validate.ts
|
|
1404
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
1405
|
+
import { join as join3, resolve as resolve3, relative as relative2, dirname as dirname2, basename } from "path";
|
|
1406
|
+
import chalk4 from "chalk";
|
|
1407
|
+
function pass(msg) {
|
|
1408
|
+
console.log(chalk4.green(`\u2713 ${msg}`));
|
|
1409
|
+
}
|
|
1410
|
+
function fail(ctx, msg) {
|
|
1411
|
+
console.log(chalk4.red(`\u2717 ${msg}`));
|
|
1412
|
+
ctx.failures++;
|
|
1413
|
+
}
|
|
1414
|
+
async function subdirs(dir) {
|
|
1415
|
+
const entries = await readdir(dir);
|
|
1416
|
+
const dirs = [];
|
|
1417
|
+
for (const entry of entries) {
|
|
1418
|
+
const full = join3(dir, entry);
|
|
1419
|
+
const s = await stat(full);
|
|
1420
|
+
if (s.isDirectory()) dirs.push(entry);
|
|
1421
|
+
}
|
|
1422
|
+
return dirs.sort();
|
|
1423
|
+
}
|
|
1424
|
+
function checkStructure(ctx, entries, label) {
|
|
1425
|
+
if (entries.includes("README.md")) {
|
|
1426
|
+
pass(`${label}: has README.md`);
|
|
1427
|
+
} else {
|
|
1428
|
+
fail(ctx, `${label}: missing README.md`);
|
|
1429
|
+
}
|
|
1430
|
+
if (entries.includes("mock.json")) {
|
|
1431
|
+
pass(`${label}: has mock.json`);
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
fail(ctx, `${label}: missing mock.json`);
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
async function checkSchema(ctx, data, label) {
|
|
1438
|
+
const commons = await import("@mockoon/commons");
|
|
1439
|
+
const schema = commons.EnvironmentSchemaNoFix;
|
|
1440
|
+
const result = schema.validate(data, { abortEarly: false });
|
|
1441
|
+
if (!result.error) {
|
|
1442
|
+
pass(`${label}: valid Mockoon schema`);
|
|
1443
|
+
} else {
|
|
1444
|
+
for (const detail of result.error.details) {
|
|
1445
|
+
fail(ctx, `${label}: schema \u2014 ${detail.message}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function checkResponseQuality(ctx, data, label) {
|
|
1450
|
+
const routes = data.routes ?? [];
|
|
1451
|
+
for (const route of routes) {
|
|
1452
|
+
for (const resp of route.responses ?? []) {
|
|
1453
|
+
const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
|
|
1454
|
+
const headers = resp.headers ?? [];
|
|
1455
|
+
const hasContentType = headers.some((h) => h.key.toLowerCase() === "content-type");
|
|
1456
|
+
if (hasContentType) {
|
|
1457
|
+
pass(`${respLabel}: has Content-Type`);
|
|
1458
|
+
} else {
|
|
1459
|
+
fail(ctx, `${respLabel}: missing Content-Type header`);
|
|
1460
|
+
}
|
|
1461
|
+
if (resp.label && resp.label.trim().length > 0) {
|
|
1462
|
+
pass(`${respLabel}: has label "${resp.label}"`);
|
|
1463
|
+
} else {
|
|
1464
|
+
fail(ctx, `${respLabel}: missing or empty label`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function checkDatabucketRefs(ctx, data, label) {
|
|
1470
|
+
const bucketIds = new Set((data.data ?? []).map((d) => d.id));
|
|
1471
|
+
for (const route of data.routes ?? []) {
|
|
1472
|
+
for (const resp of route.responses ?? []) {
|
|
1473
|
+
if (resp.bodyType === "DATABUCKET") {
|
|
1474
|
+
const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
|
|
1475
|
+
if (resp.databucketID && bucketIds.has(resp.databucketID)) {
|
|
1476
|
+
pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
|
|
1477
|
+
} else {
|
|
1478
|
+
fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function discoverAllScenarios(mocksDir) {
|
|
1485
|
+
const scenarios = [];
|
|
1486
|
+
const providers = await subdirs(mocksDir);
|
|
1487
|
+
for (const provider of providers) {
|
|
1488
|
+
const providerDir = join3(mocksDir, provider);
|
|
1489
|
+
for (const scenario of await subdirs(providerDir)) {
|
|
1490
|
+
scenarios.push(`${provider}/${scenario}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return scenarios;
|
|
1494
|
+
}
|
|
1495
|
+
function resolveScenarioDir(arg6) {
|
|
1496
|
+
const abs = resolve3(arg6);
|
|
1497
|
+
if (basename(abs) === "mock.json") return dirname2(abs);
|
|
1498
|
+
return abs;
|
|
1499
|
+
}
|
|
1500
|
+
async function validateScenario(ctx, scenarioDir, mocksDir) {
|
|
1501
|
+
const label = relative2(mocksDir, scenarioDir);
|
|
1502
|
+
console.log(chalk4.bold(`
|
|
1503
|
+
\u2500\u2500 ${label} \u2500\u2500`));
|
|
1504
|
+
let entries;
|
|
1505
|
+
try {
|
|
1506
|
+
entries = await readdir(scenarioDir);
|
|
1507
|
+
} catch {
|
|
1508
|
+
fail(ctx, `${label}: directory not found`);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const hasMock = checkStructure(ctx, entries, label);
|
|
1512
|
+
if (!hasMock) return;
|
|
1513
|
+
const mockPath = join3(scenarioDir, "mock.json");
|
|
1514
|
+
let data;
|
|
1515
|
+
try {
|
|
1516
|
+
data = JSON.parse(await readFile(mockPath, "utf-8"));
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1519
|
+
fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
await checkSchema(ctx, data, label);
|
|
1523
|
+
checkResponseQuality(ctx, data, label);
|
|
1524
|
+
checkDatabucketRefs(ctx, data, label);
|
|
1525
|
+
}
|
|
1526
|
+
async function runMockValidate(mocksRoot, paths) {
|
|
1527
|
+
const ctx = { failures: 0 };
|
|
1528
|
+
const mocksDir = resolve3(mocksRoot);
|
|
1529
|
+
const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join3(mocksDir, s));
|
|
1530
|
+
if (targets.length === 0) {
|
|
1531
|
+
fail(ctx, "No scenarios found under mocks/");
|
|
1532
|
+
return 1;
|
|
1533
|
+
}
|
|
1534
|
+
console.log(chalk4.bold("\nValidating mock configs...\n"));
|
|
1535
|
+
for (const target of targets) {
|
|
1536
|
+
await validateScenario(ctx, target, mocksDir);
|
|
1537
|
+
}
|
|
1538
|
+
console.log(chalk4.bold("\n\u2500\u2500 summary \u2500\u2500"));
|
|
1539
|
+
if (ctx.failures === 0) {
|
|
1540
|
+
console.log(chalk4.green("\u2713 All checks passed"));
|
|
1541
|
+
} else {
|
|
1542
|
+
console.log(chalk4.red(`\u2717 ${ctx.failures} check(s) failed`));
|
|
1543
|
+
}
|
|
1544
|
+
return ctx.failures === 0 ? 0 : 1;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/commands/mock/index.ts
|
|
1548
|
+
var startCommand = defineCommand4({
|
|
1549
|
+
name: "start",
|
|
1550
|
+
description: "Start mock API servers with reverse proxy",
|
|
1551
|
+
args: z4.object({
|
|
1552
|
+
mocksRoot: arg4(z4.string().default("./mocks"), {
|
|
1553
|
+
description: "Path to mocks directory"
|
|
1554
|
+
}),
|
|
1555
|
+
port: arg4(z4.coerce.number().default(3e3), {
|
|
1556
|
+
alias: "p",
|
|
1557
|
+
description: "Reverse proxy port"
|
|
1558
|
+
}),
|
|
1559
|
+
filter: arg4(z4.array(z4.string()).default([]), {
|
|
1560
|
+
positional: true,
|
|
1561
|
+
description: "Filter by provider or provider/scenario"
|
|
1562
|
+
})
|
|
1563
|
+
}),
|
|
1564
|
+
run: async (args) => {
|
|
1565
|
+
const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
|
|
1566
|
+
process.exit(exitCode);
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
var validateCommand = defineCommand4({
|
|
1570
|
+
name: "validate",
|
|
1571
|
+
description: "Validate mock scenario configs",
|
|
1572
|
+
args: z4.object({
|
|
1573
|
+
mocksRoot: arg4(z4.string().default("./mocks"), {
|
|
1574
|
+
description: "Path to mocks directory"
|
|
1575
|
+
}),
|
|
1576
|
+
paths: arg4(z4.array(z4.string()).default([]), {
|
|
1577
|
+
positional: true,
|
|
1578
|
+
description: "Specific scenario paths to validate"
|
|
1579
|
+
})
|
|
1580
|
+
}),
|
|
1581
|
+
run: async (args) => {
|
|
1582
|
+
const exitCode = await runMockValidate(args.mocksRoot, args.paths);
|
|
1583
|
+
process.exit(exitCode);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
var mockCommand = defineCommand4({
|
|
1587
|
+
name: "mock",
|
|
1588
|
+
description: "Mock API server management",
|
|
1589
|
+
subCommands: {
|
|
1590
|
+
start: startCommand,
|
|
1591
|
+
validate: validateCommand
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
// src/commands/index.ts
|
|
1596
|
+
var cwd4 = process.cwd();
|
|
1597
|
+
var initCommand = defineCommand5({
|
|
1598
|
+
name: "init",
|
|
1599
|
+
description: "Set up consumer repo with framework skills",
|
|
1600
|
+
args: z5.object({
|
|
1601
|
+
force: arg5(z5.boolean().default(false), {
|
|
1602
|
+
alias: "f",
|
|
1603
|
+
description: "Overwrite existing skills"
|
|
1604
|
+
})
|
|
1605
|
+
}),
|
|
1606
|
+
run: (args) => {
|
|
1607
|
+
const exitCode = runInit(cwd4, args.force);
|
|
1608
|
+
process.exit(exitCode);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
var licenseCheckCommand = defineCommand5({
|
|
1612
|
+
name: "check",
|
|
1613
|
+
description: "Check dependency licenses against allowlist",
|
|
1614
|
+
args: z5.object({
|
|
1615
|
+
config: arg5(z5.string(), {
|
|
1616
|
+
alias: "c",
|
|
1617
|
+
description: "Path to license config JSON file"
|
|
1618
|
+
})
|
|
1619
|
+
}),
|
|
1620
|
+
run: (args) => {
|
|
1621
|
+
const exitCode = runLicenseCheck(args.config);
|
|
1622
|
+
process.exit(exitCode);
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
var licenseListCommand = defineCommand5({
|
|
1626
|
+
name: "list",
|
|
1627
|
+
description: "List available license groups",
|
|
1628
|
+
run: () => {
|
|
1629
|
+
const exitCode = runLicenseList();
|
|
1630
|
+
process.exit(exitCode);
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
var licenseCommand = defineCommand5({
|
|
1634
|
+
name: "license",
|
|
1635
|
+
description: "License management",
|
|
1636
|
+
subCommands: {
|
|
1637
|
+
check: licenseCheckCommand,
|
|
1638
|
+
list: licenseListCommand
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
var mainCommand = defineCommand5({
|
|
1642
|
+
name: "erp-kit",
|
|
1643
|
+
description: "ERP module framework CLI",
|
|
1644
|
+
subCommands: {
|
|
1645
|
+
module: moduleCommand,
|
|
1646
|
+
app: appCommand,
|
|
1647
|
+
mock: mockCommand,
|
|
1648
|
+
init: initCommand,
|
|
1649
|
+
license: licenseCommand
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// src/cli.ts
|
|
1654
|
+
void runMain(mainCommand);
|