@tailor-platform/erp-kit 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +196 -28
- package/dist/cli.js +914 -0
- package/package.json +67 -8
- package/schemas/app-compose/actors.yml +34 -0
- package/schemas/app-compose/business-flow.yml +50 -0
- package/schemas/app-compose/requirements.yml +33 -0
- package/schemas/app-compose/resolver.yml +47 -0
- package/schemas/app-compose/screen.yml +81 -0
- package/schemas/app-compose/story.yml +67 -0
- package/schemas/module/command.yml +52 -0
- package/schemas/module/feature.yml +58 -0
- package/schemas/module/model.yml +70 -0
- package/schemas/module/module.yml +50 -0
- package/skills/1-module-docs/SKILL.md +111 -0
- package/skills/1-module-docs/references/structure.md +22 -0
- package/skills/2-module-feature-breakdown/SKILL.md +72 -0
- package/skills/2-module-feature-breakdown/references/commands.md +48 -0
- package/skills/2-module-feature-breakdown/references/models.md +29 -0
- package/skills/2-module-feature-breakdown/references/structure.md +22 -0
- package/skills/3-module-doc-review/SKILL.md +236 -0
- package/skills/3-module-doc-review/references/commands.md +54 -0
- package/skills/3-module-doc-review/references/models.md +29 -0
- package/skills/3-module-doc-review/references/testing.md +37 -0
- package/skills/4-module-tdd-implementation/SKILL.md +74 -0
- package/skills/4-module-tdd-implementation/references/commands.md +45 -0
- package/skills/4-module-tdd-implementation/references/db-relations.md +69 -0
- package/skills/4-module-tdd-implementation/references/errors.md +7 -0
- package/skills/4-module-tdd-implementation/references/exports.md +8 -0
- package/skills/4-module-tdd-implementation/references/models.md +30 -0
- package/skills/4-module-tdd-implementation/references/structure.md +22 -0
- package/skills/4-module-tdd-implementation/references/testing.md +37 -0
- package/skills/5-module-implementation-review/SKILL.md +408 -0
- package/skills/5-module-implementation-review/references/commands.md +45 -0
- package/skills/5-module-implementation-review/references/errors.md +7 -0
- package/skills/5-module-implementation-review/references/exports.md +8 -0
- package/skills/5-module-implementation-review/references/models.md +30 -0
- package/skills/5-module-implementation-review/references/testing.md +29 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +89 -0
- package/skills/app-compose-1-requirement-analysis/references/structure.md +27 -0
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +95 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +106 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +139 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +153 -0
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
- package/skills/app-compose-3-doc-review/SKILL.md +116 -0
- package/skills/app-compose-3-doc-review/references/structure.md +27 -0
- package/skills/app-compose-4-design-mock/SKILL.md +256 -0
- package/skills/app-compose-4-design-mock/references/component.md +50 -0
- package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
- package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
- package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
- package/skills/app-compose-4-design-mock/references/structure.md +27 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +290 -0
- package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
- package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
- package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
- package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +127 -0
- package/skills/app-compose-6-implementation-spec/references/auth.md +72 -0
- package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
- package/skills/mock-scenario/SKILL.md +118 -0
- package/src/app.ts +1 -0
- package/src/cli.ts +120 -0
- package/src/commands/check.test.ts +30 -0
- package/src/commands/check.ts +66 -0
- package/src/commands/init.test.ts +88 -0
- package/src/commands/init.ts +120 -0
- package/src/commands/mock/index.ts +53 -0
- package/src/commands/mock/start.ts +179 -0
- package/src/commands/mock/validate.test.ts +185 -0
- package/src/commands/mock/validate.ts +198 -0
- package/src/commands/scaffold.test.ts +76 -0
- package/src/commands/scaffold.ts +119 -0
- package/src/commands/sync-check.test.ts +125 -0
- package/src/commands/sync-check.ts +182 -0
- package/src/integration.test.ts +63 -0
- package/src/mdschema.ts +48 -0
- package/src/mockServer.ts +55 -0
- package/src/module.ts +86 -0
- package/src/modules/accounting/.gitkeep +0 -0
- package/src/modules/coa-management/.gitkeep +0 -0
- package/src/modules/inventory/.gitkeep +0 -0
- package/src/modules/manufacturing/.gitkeep +0 -0
- package/src/modules/primitives/README.md +39 -0
- package/src/modules/primitives/command/activateCategory.test.ts +75 -0
- package/src/modules/primitives/command/activateCategory.ts +50 -0
- package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
- package/src/modules/primitives/command/activateCurrency.ts +50 -0
- package/src/modules/primitives/command/activateUnit.test.ts +53 -0
- package/src/modules/primitives/command/activateUnit.ts +50 -0
- package/src/modules/primitives/command/convertAmount.test.ts +275 -0
- package/src/modules/primitives/command/convertAmount.ts +126 -0
- package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
- package/src/modules/primitives/command/convertQuantity.ts +73 -0
- package/src/modules/primitives/command/createCategory.test.ts +126 -0
- package/src/modules/primitives/command/createCategory.ts +89 -0
- package/src/modules/primitives/command/createCurrency.test.ts +191 -0
- package/src/modules/primitives/command/createCurrency.ts +77 -0
- package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
- package/src/modules/primitives/command/createExchangeRate.ts +91 -0
- package/src/modules/primitives/command/createUnit.test.ts +214 -0
- package/src/modules/primitives/command/createUnit.ts +88 -0
- package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
- package/src/modules/primitives/command/deactivateCategory.ts +62 -0
- package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
- package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
- package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
- package/src/modules/primitives/command/deactivateUnit.ts +62 -0
- package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
- package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
- package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
- package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
- package/src/modules/primitives/db/currency.ts +30 -0
- package/src/modules/primitives/db/exchangeRate.ts +28 -0
- package/src/modules/primitives/db/unit.ts +32 -0
- package/src/modules/primitives/db/uomCategory.ts +32 -0
- package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
- package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
- package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
- package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
- package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
- package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
- package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
- package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
- package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
- package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
- package/src/modules/primitives/docs/features/uom-categories.md +52 -0
- package/src/modules/primitives/docs/models/Currency.md +45 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
- package/src/modules/primitives/docs/models/Unit.md +46 -0
- package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
- package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
- package/src/modules/primitives/index.ts +40 -0
- package/src/modules/primitives/lib/errors.ts +138 -0
- package/src/modules/primitives/lib/types.ts +20 -0
- package/src/modules/primitives/module.ts +66 -0
- package/src/modules/primitives/permissions.ts +18 -0
- package/src/modules/primitives/tailor.config.ts +11 -0
- package/src/modules/primitives/testing/fixtures.ts +161 -0
- package/src/modules/product-management/.gitkeep +0 -0
- package/src/modules/purchase/.gitkeep +0 -0
- package/src/modules/sales/.gitkeep +0 -0
- package/src/modules/shared/createContext.test.ts +39 -0
- package/src/modules/shared/createContext.ts +15 -0
- package/src/modules/shared/defineCommand.test.ts +42 -0
- package/src/modules/shared/defineCommand.ts +19 -0
- package/src/modules/shared/definePermissions.test.ts +146 -0
- package/src/modules/shared/definePermissions.ts +94 -0
- package/src/modules/shared/entityTypes.ts +15 -0
- package/src/modules/shared/errors.ts +22 -0
- package/src/modules/shared/index.ts +1 -0
- package/src/modules/shared/internal.ts +13 -0
- package/src/modules/shared/requirePermission.test.ts +47 -0
- package/src/modules/shared/requirePermission.ts +8 -0
- package/src/modules/shared/types.ts +4 -0
- package/src/modules/supplier-management/.gitkeep +0 -0
- package/src/modules/supplier-portal/.gitkeep +0 -0
- package/src/modules/testing/index.ts +120 -0
- package/src/modules/user-management/README.md +38 -0
- package/src/modules/user-management/command/activateUser.test.ts +112 -0
- package/src/modules/user-management/command/activateUser.ts +67 -0
- package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
- package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
- package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
- package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
- package/src/modules/user-management/command/createPermission.test.ts +143 -0
- package/src/modules/user-management/command/createPermission.ts +66 -0
- package/src/modules/user-management/command/createRole.test.ts +115 -0
- package/src/modules/user-management/command/createRole.ts +52 -0
- package/src/modules/user-management/command/createUser.test.ts +198 -0
- package/src/modules/user-management/command/createUser.ts +85 -0
- package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
- package/src/modules/user-management/command/deactivateUser.ts +67 -0
- package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
- package/src/modules/user-management/command/logAuditEvent.ts +59 -0
- package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
- package/src/modules/user-management/command/reactivateUser.ts +67 -0
- package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
- package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
- package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
- package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
- package/src/modules/user-management/db/auditEvent.ts +47 -0
- package/src/modules/user-management/db/permission.ts +31 -0
- package/src/modules/user-management/db/role.ts +28 -0
- package/src/modules/user-management/db/rolePermission.ts +44 -0
- package/src/modules/user-management/db/user.ts +38 -0
- package/src/modules/user-management/db/userRole.ts +44 -0
- package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
- package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
- package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
- package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
- package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
- package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
- package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
- package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
- package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
- package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
- package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
- package/src/modules/user-management/docs/features/audit-trail.md +80 -0
- package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
- package/src/modules/user-management/docs/features/user-account-management.md +64 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
- package/src/modules/user-management/docs/models/Permission.md +31 -0
- package/src/modules/user-management/docs/models/Role.md +31 -0
- package/src/modules/user-management/docs/models/RolePermission.md +33 -0
- package/src/modules/user-management/docs/models/User.md +47 -0
- package/src/modules/user-management/docs/models/UserRole.md +34 -0
- package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
- package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
- package/src/modules/user-management/generated/enums.ts +24 -0
- package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
- package/src/modules/user-management/index.ts +32 -0
- package/src/modules/user-management/lib/errors.ts +81 -0
- package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
- package/src/modules/user-management/lib/types.ts +31 -0
- package/src/modules/user-management/module.ts +77 -0
- package/src/modules/user-management/permissions.ts +15 -0
- package/src/modules/user-management/tailor.config.ts +11 -0
- package/src/modules/user-management/testing/fixtures.ts +98 -0
- package/src/schemas.ts +25 -0
- package/src/testing.ts +10 -0
- package/src/util.ts +3 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { z as z2 } from "zod";
|
|
5
|
+
import { defineCommand as defineCommand2, runMain, arg as arg2 } from "politty";
|
|
6
|
+
|
|
7
|
+
// src/mdschema.ts
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import { execFile } from "child_process";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
var require2 = createRequire(import.meta.url);
|
|
13
|
+
function getMdschemaBin() {
|
|
14
|
+
const pkgPath = require2.resolve("@jackchuka/mdschema/package.json");
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.mdschema;
|
|
17
|
+
if (!bin) {
|
|
18
|
+
throw new Error("Could not resolve mdschema binary from package.json bin field");
|
|
19
|
+
}
|
|
20
|
+
return path.join(path.dirname(pkgPath), bin);
|
|
21
|
+
}
|
|
22
|
+
function runMdschema(args, cwd2) {
|
|
23
|
+
return new Promise((resolve4) => {
|
|
24
|
+
execFile(
|
|
25
|
+
getMdschemaBin(),
|
|
26
|
+
args,
|
|
27
|
+
{ encoding: "utf-8", cwd: cwd2, timeout: 3e4 },
|
|
28
|
+
(error, stdout, stderr) => {
|
|
29
|
+
if (error) {
|
|
30
|
+
const execError = error;
|
|
31
|
+
resolve4({
|
|
32
|
+
exitCode: execError.code === "ENOENT" ? 127 : execError.status ?? 1,
|
|
33
|
+
stdout: stdout ?? "",
|
|
34
|
+
stderr: stderr ?? ""
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
resolve4({ exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/schemas.ts
|
|
45
|
+
import path3 from "path";
|
|
46
|
+
|
|
47
|
+
// src/util.ts
|
|
48
|
+
import path2 from "path";
|
|
49
|
+
var PACKAGE_ROOT = path2.resolve(import.meta.dirname, "..");
|
|
50
|
+
|
|
51
|
+
// src/schemas.ts
|
|
52
|
+
var SCHEMAS_ROOT = path3.join(PACKAGE_ROOT, "schemas");
|
|
53
|
+
var MODULE_SCHEMAS = {
|
|
54
|
+
module: path3.join(SCHEMAS_ROOT, "module", "module.yml"),
|
|
55
|
+
command: path3.join(SCHEMAS_ROOT, "module", "command.yml"),
|
|
56
|
+
model: path3.join(SCHEMAS_ROOT, "module", "model.yml"),
|
|
57
|
+
feature: path3.join(SCHEMAS_ROOT, "module", "feature.yml")
|
|
58
|
+
};
|
|
59
|
+
var APP_COMPOSE_SCHEMAS = {
|
|
60
|
+
requirements: path3.join(SCHEMAS_ROOT, "app-compose", "requirements.yml"),
|
|
61
|
+
actors: path3.join(SCHEMAS_ROOT, "app-compose", "actors.yml"),
|
|
62
|
+
"business-flow": path3.join(SCHEMAS_ROOT, "app-compose", "business-flow.yml"),
|
|
63
|
+
story: path3.join(SCHEMAS_ROOT, "app-compose", "story.yml"),
|
|
64
|
+
screen: path3.join(SCHEMAS_ROOT, "app-compose", "screen.yml"),
|
|
65
|
+
resolver: path3.join(SCHEMAS_ROOT, "app-compose", "resolver.yml")
|
|
66
|
+
};
|
|
67
|
+
var ALL_SCHEMAS = {
|
|
68
|
+
...MODULE_SCHEMAS,
|
|
69
|
+
...APP_COMPOSE_SCHEMAS
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/commands/check.ts
|
|
73
|
+
function buildCheckTargets(config) {
|
|
74
|
+
const targets = [];
|
|
75
|
+
if (config.modulesRoot) {
|
|
76
|
+
const m = config.modulesRoot;
|
|
77
|
+
targets.push(
|
|
78
|
+
{ glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
|
|
79
|
+
{ glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
|
|
80
|
+
{ glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
|
|
81
|
+
{ glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (config.appRoot) {
|
|
85
|
+
const a = config.appRoot;
|
|
86
|
+
targets.push(
|
|
87
|
+
{ glob: `${a}/[a-zA-Z]*/README.md`, schemaKey: "requirements" },
|
|
88
|
+
{ glob: `${a}/[a-zA-Z]*/docs/actors/*.md`, schemaKey: "actors" },
|
|
89
|
+
{ glob: `${a}/[a-zA-Z]*/docs/business-flow/*/README.md`, schemaKey: "business-flow" },
|
|
90
|
+
{ glob: `${a}/[a-zA-Z]*/docs/business-flow/*/story/*.md`, schemaKey: "story" },
|
|
91
|
+
{ glob: `${a}/[a-zA-Z]*/docs/screen/*.md`, schemaKey: "screen" },
|
|
92
|
+
{ glob: `${a}/[a-zA-Z]*/docs/resolver/*.md`, schemaKey: "resolver" }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return targets;
|
|
96
|
+
}
|
|
97
|
+
async function runCheck(config, cwd2) {
|
|
98
|
+
const targets = buildCheckTargets(config);
|
|
99
|
+
const results = await Promise.all(
|
|
100
|
+
targets.map(async (target) => {
|
|
101
|
+
const schemaPath = ALL_SCHEMAS[target.schemaKey];
|
|
102
|
+
if (!schemaPath) {
|
|
103
|
+
console.error(`Unknown schema key: ${target.schemaKey}`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
const { exitCode, stdout, stderr } = await runMdschema(
|
|
107
|
+
["check", target.glob, "--schema", schemaPath],
|
|
108
|
+
cwd2
|
|
109
|
+
);
|
|
110
|
+
if (stdout.trim()) console.log(stdout);
|
|
111
|
+
if (stderr.trim()) console.error(stderr);
|
|
112
|
+
return exitCode;
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
if (results.includes(2)) return 2;
|
|
116
|
+
return results.some((code) => code !== 0) ? 1 : 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/commands/sync-check.ts
|
|
120
|
+
import path4 from "path";
|
|
121
|
+
import fg from "fast-glob";
|
|
122
|
+
import chalk from "chalk";
|
|
123
|
+
function moduleCategories(root) {
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
name: "command",
|
|
127
|
+
sourcePattern: `${root}/*/command/*.ts`,
|
|
128
|
+
docPattern: `${root}/*/docs/commands/*.md`,
|
|
129
|
+
exclusions: [/\.test\.ts$/]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "model",
|
|
133
|
+
sourcePattern: `${root}/*/db/*.ts`,
|
|
134
|
+
docPattern: `${root}/*/docs/models/*.md`,
|
|
135
|
+
exclusions: [/\.test\.ts$/, /^index\.ts$/]
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
function appComposeCategories(root) {
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
name: "resolver",
|
|
143
|
+
sourcePattern: `${root}/*/backend/src/modules/**/resolvers/*.ts`,
|
|
144
|
+
docPattern: `${root}/*/docs/resolver/*.md`,
|
|
145
|
+
exclusions: [/\.test\.ts$/, /^index\.ts$/]
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
function shouldExclude(fileName, exclusions) {
|
|
150
|
+
return exclusions.some((pattern) => pattern.test(fileName));
|
|
151
|
+
}
|
|
152
|
+
async function runSyncCheck(config, cwd2) {
|
|
153
|
+
const errors = [];
|
|
154
|
+
let totalSources = 0;
|
|
155
|
+
let totalDocs = 0;
|
|
156
|
+
const allCategories = [];
|
|
157
|
+
if (config.modulesRoot) {
|
|
158
|
+
allCategories.push(...moduleCategories(config.modulesRoot));
|
|
159
|
+
}
|
|
160
|
+
if (config.appRoot) {
|
|
161
|
+
allCategories.push(...appComposeCategories(config.appRoot));
|
|
162
|
+
}
|
|
163
|
+
for (const category of allCategories) {
|
|
164
|
+
const sources = await fg(category.sourcePattern, { cwd: cwd2 });
|
|
165
|
+
const docs = await fg(category.docPattern, { cwd: cwd2 });
|
|
166
|
+
const sourceBasenames = /* @__PURE__ */ new Map();
|
|
167
|
+
const docBasenames = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const sourcePath of sources) {
|
|
169
|
+
const fileName = path4.basename(sourcePath);
|
|
170
|
+
if (shouldExclude(fileName, category.exclusions)) continue;
|
|
171
|
+
const basename2 = path4.basename(sourcePath, path4.extname(sourcePath));
|
|
172
|
+
sourceBasenames.set(basename2.toLowerCase(), sourcePath);
|
|
173
|
+
}
|
|
174
|
+
for (const docPath of docs) {
|
|
175
|
+
const basename2 = path4.basename(docPath, path4.extname(docPath));
|
|
176
|
+
docBasenames.set(basename2.toLowerCase(), docPath);
|
|
177
|
+
}
|
|
178
|
+
for (const [basename2, sourcePath] of sourceBasenames) {
|
|
179
|
+
if (!docBasenames.has(basename2)) {
|
|
180
|
+
errors.push({
|
|
181
|
+
type: "missing-doc",
|
|
182
|
+
category: category.name,
|
|
183
|
+
sourcePath,
|
|
184
|
+
expectedBasename: basename2
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const [basename2, docPath] of docBasenames) {
|
|
189
|
+
if (!sourceBasenames.has(basename2)) {
|
|
190
|
+
errors.push({
|
|
191
|
+
type: "orphaned-doc",
|
|
192
|
+
category: category.name,
|
|
193
|
+
docPath,
|
|
194
|
+
expectedBasename: basename2
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
totalSources += sourceBasenames.size;
|
|
199
|
+
totalDocs += docBasenames.size;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
exitCode: errors.length > 0 ? 1 : 0,
|
|
203
|
+
errors,
|
|
204
|
+
summary: {
|
|
205
|
+
categoriesChecked: allCategories.length,
|
|
206
|
+
totalSources,
|
|
207
|
+
totalDocs
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function formatSyncCheckReport(result) {
|
|
212
|
+
const lines = [];
|
|
213
|
+
lines.push(chalk.bold("docs-sync-check: Checking source-documentation correspondence...\n"));
|
|
214
|
+
if (result.errors.length > 0) {
|
|
215
|
+
lines.push(chalk.red.bold("Errors:\n"));
|
|
216
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const error of result.errors) {
|
|
218
|
+
const existing = byCategory.get(error.category) ?? [];
|
|
219
|
+
existing.push(error);
|
|
220
|
+
byCategory.set(error.category, existing);
|
|
221
|
+
}
|
|
222
|
+
for (const [category, categoryErrors] of byCategory) {
|
|
223
|
+
lines.push(chalk.cyan(` Category: ${category}
|
|
224
|
+
`));
|
|
225
|
+
for (const error of categoryErrors) {
|
|
226
|
+
if (error.type === "missing-doc") {
|
|
227
|
+
lines.push(` ${chalk.red(error.sourcePath)}`);
|
|
228
|
+
lines.push(` ${chalk.yellow("Missing documentation for:")} ${error.expectedBasename}`);
|
|
229
|
+
} else {
|
|
230
|
+
lines.push(` ${chalk.red(error.docPath)}`);
|
|
231
|
+
lines.push(
|
|
232
|
+
` ${chalk.yellow("Orphaned documentation:")} no source file for ${error.expectedBasename}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
lines.push(chalk.green("All source files have corresponding documentation.\n"));
|
|
240
|
+
}
|
|
241
|
+
lines.push(chalk.bold("Summary:"));
|
|
242
|
+
lines.push(` Categories checked: ${result.summary.categoriesChecked}`);
|
|
243
|
+
lines.push(
|
|
244
|
+
` Source files: ${result.summary.totalSources}, Doc files: ${result.summary.totalDocs}`
|
|
245
|
+
);
|
|
246
|
+
if (result.errors.length > 0) {
|
|
247
|
+
lines.push(chalk.red(` Errors: ${result.errors.length}`));
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push(chalk.red.bold(`docs-sync-check failed with ${result.errors.length} error(s).`));
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(chalk.green(" Errors: 0"));
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push(chalk.green.bold("docs-sync-check passed."));
|
|
254
|
+
}
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/commands/scaffold.ts
|
|
259
|
+
import path5 from "path";
|
|
260
|
+
import fs2 from "fs";
|
|
261
|
+
var MODULE_TYPES = ["module", "feature", "command", "model"];
|
|
262
|
+
var APP_TYPES = [
|
|
263
|
+
"requirements",
|
|
264
|
+
"actors",
|
|
265
|
+
"business-flow",
|
|
266
|
+
"story",
|
|
267
|
+
"screen",
|
|
268
|
+
"resolver"
|
|
269
|
+
];
|
|
270
|
+
var ALL_TYPES = [...MODULE_TYPES, ...APP_TYPES];
|
|
271
|
+
var MODULE_DIR_MAP = {
|
|
272
|
+
feature: "docs/features",
|
|
273
|
+
command: "docs/commands",
|
|
274
|
+
model: "docs/models"
|
|
275
|
+
};
|
|
276
|
+
var APP_DIR_MAP = {
|
|
277
|
+
actors: "docs/actors",
|
|
278
|
+
"business-flow": "docs/business-flow",
|
|
279
|
+
screen: "docs/screen",
|
|
280
|
+
resolver: "docs/resolver"
|
|
281
|
+
};
|
|
282
|
+
function isModuleType(type) {
|
|
283
|
+
return MODULE_TYPES.includes(type);
|
|
284
|
+
}
|
|
285
|
+
function resolveScaffoldPath(type, parentName, name, root) {
|
|
286
|
+
if (type === "module" || type === "requirements") {
|
|
287
|
+
return path5.join(root, parentName, "README.md");
|
|
288
|
+
}
|
|
289
|
+
if (!name) {
|
|
290
|
+
throw new Error(`Name is required for scaffold type "${type}"`);
|
|
291
|
+
}
|
|
292
|
+
if (type === "business-flow") {
|
|
293
|
+
return path5.join(root, parentName, "docs/business-flow", name, "README.md");
|
|
294
|
+
}
|
|
295
|
+
if (type === "story") {
|
|
296
|
+
const parts = name.split("/");
|
|
297
|
+
if (parts.length !== 2) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return path5.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
|
|
303
|
+
}
|
|
304
|
+
if (MODULE_DIR_MAP[type]) {
|
|
305
|
+
return path5.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
|
|
306
|
+
}
|
|
307
|
+
if (APP_DIR_MAP[type]) {
|
|
308
|
+
return path5.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Unknown scaffold type: ${type}`);
|
|
311
|
+
}
|
|
312
|
+
async function runScaffold(type, parentName, name, root, cwd2) {
|
|
313
|
+
const outputPath = resolveScaffoldPath(type, parentName, name, root);
|
|
314
|
+
const absoluteOutput = path5.resolve(cwd2, outputPath);
|
|
315
|
+
if (fs2.existsSync(absoluteOutput)) {
|
|
316
|
+
console.error(`File already exists: ${outputPath}`);
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const schemaPath = ALL_SCHEMAS[type];
|
|
320
|
+
if (!schemaPath) {
|
|
321
|
+
console.error(`No schema found for type: ${type}`);
|
|
322
|
+
return 2;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
fs2.mkdirSync(path5.dirname(absoluteOutput), { recursive: true });
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error(
|
|
328
|
+
`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`
|
|
329
|
+
);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
const { exitCode, stdout, stderr } = await runMdschema(
|
|
333
|
+
["generate", "--schema", schemaPath, "--output", absoluteOutput],
|
|
334
|
+
cwd2
|
|
335
|
+
);
|
|
336
|
+
if (stdout.trim()) console.log(stdout);
|
|
337
|
+
if (stderr.trim()) console.error(stderr);
|
|
338
|
+
return exitCode;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/commands/init.ts
|
|
342
|
+
import fs3 from "fs";
|
|
343
|
+
import path6 from "path";
|
|
344
|
+
import chalk2 from "chalk";
|
|
345
|
+
var SKILLS_SRC = path6.join(PACKAGE_ROOT, "skills");
|
|
346
|
+
var FRAMEWORK_SKILLS = [
|
|
347
|
+
"1-module-docs",
|
|
348
|
+
"2-module-feature-breakdown",
|
|
349
|
+
"3-module-doc-review",
|
|
350
|
+
"4-module-tdd-implementation",
|
|
351
|
+
"5-module-implementation-review",
|
|
352
|
+
"app-compose-1-requirement-analysis",
|
|
353
|
+
"app-compose-2-requirements-breakdown",
|
|
354
|
+
"app-compose-3-doc-review",
|
|
355
|
+
"app-compose-4-design-mock",
|
|
356
|
+
"app-compose-5-design-mock-review",
|
|
357
|
+
"app-compose-6-implementation-spec",
|
|
358
|
+
"mock-scenario"
|
|
359
|
+
];
|
|
360
|
+
function copyDirectoryRecursive(srcDir, destDir, force) {
|
|
361
|
+
let copied = 0;
|
|
362
|
+
let skipped = 0;
|
|
363
|
+
for (const entry of fs3.readdirSync(srcDir, { withFileTypes: true })) {
|
|
364
|
+
const srcPath = path6.join(srcDir, entry.name);
|
|
365
|
+
const destPath = path6.join(destDir, entry.name);
|
|
366
|
+
if (entry.isDirectory()) {
|
|
367
|
+
const sub = copyDirectoryRecursive(srcPath, destPath, force);
|
|
368
|
+
copied += sub.copied;
|
|
369
|
+
skipped += sub.skipped;
|
|
370
|
+
} else {
|
|
371
|
+
if (!force && fs3.existsSync(destPath)) {
|
|
372
|
+
skipped++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
fs3.mkdirSync(destDir, { recursive: true });
|
|
376
|
+
fs3.copyFileSync(srcPath, destPath);
|
|
377
|
+
copied++;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { copied, skipped };
|
|
381
|
+
}
|
|
382
|
+
function runInit(cwd2, force) {
|
|
383
|
+
console.log(chalk2.bold("erp-kit init\n"));
|
|
384
|
+
const skillsDest = path6.join(cwd2, ".agents", "skills");
|
|
385
|
+
let copiedCount = 0;
|
|
386
|
+
let skippedCount = 0;
|
|
387
|
+
for (const skill of FRAMEWORK_SKILLS) {
|
|
388
|
+
const srcSkillDir = path6.join(SKILLS_SRC, skill);
|
|
389
|
+
if (!fs3.existsSync(srcSkillDir)) continue;
|
|
390
|
+
const destDir = path6.join(skillsDest, skill);
|
|
391
|
+
const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
|
|
392
|
+
copiedCount += result.copied;
|
|
393
|
+
if (result.skipped > 0) {
|
|
394
|
+
console.log(chalk2.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
|
|
395
|
+
skippedCount += result.skipped;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk2.green(` Copied ${copiedCount} skill files to .agents/skills/`));
|
|
399
|
+
if (skippedCount > 0) {
|
|
400
|
+
console.log(
|
|
401
|
+
chalk2.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const claudeSkills = path6.join(cwd2, ".claude", "skills");
|
|
405
|
+
const relTarget = path6.relative(path6.dirname(claudeSkills), skillsDest);
|
|
406
|
+
const claudeSkillsExists = (() => {
|
|
407
|
+
try {
|
|
408
|
+
fs3.lstatSync(claudeSkills);
|
|
409
|
+
return true;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
if (claudeSkillsExists) {
|
|
415
|
+
const stat2 = fs3.lstatSync(claudeSkills);
|
|
416
|
+
if (stat2.isSymbolicLink()) {
|
|
417
|
+
const current = fs3.readlinkSync(claudeSkills);
|
|
418
|
+
if (current === relTarget) {
|
|
419
|
+
console.log(chalk2.green(" .claude/skills -> .agents/skills/ (already linked)"));
|
|
420
|
+
} else if (force) {
|
|
421
|
+
fs3.unlinkSync(claudeSkills);
|
|
422
|
+
fs3.symlinkSync(relTarget, claudeSkills);
|
|
423
|
+
console.log(chalk2.green(" .claude/skills -> .agents/skills/ (relinked)"));
|
|
424
|
+
} else {
|
|
425
|
+
console.log(
|
|
426
|
+
chalk2.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
console.log(chalk2.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
fs3.mkdirSync(path6.dirname(claudeSkills), { recursive: true });
|
|
434
|
+
fs3.symlinkSync(relTarget, claudeSkills);
|
|
435
|
+
console.log(chalk2.green(" .claude/skills -> .agents/skills/ (linked)"));
|
|
436
|
+
}
|
|
437
|
+
console.log(chalk2.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/commands/mock/index.ts
|
|
442
|
+
import { z } from "zod";
|
|
443
|
+
import { defineCommand, arg } from "politty";
|
|
444
|
+
|
|
445
|
+
// src/commands/mock/start.ts
|
|
446
|
+
import { createServer, request as httpRequest } from "http";
|
|
447
|
+
import { createServer as createNetServer } from "net";
|
|
448
|
+
import { existsSync, readdirSync } from "fs";
|
|
449
|
+
import { resolve as resolve2, relative, join } from "path";
|
|
450
|
+
|
|
451
|
+
// src/mockServer.ts
|
|
452
|
+
import { readFileSync } from "fs";
|
|
453
|
+
import { dirname, resolve } from "path";
|
|
454
|
+
async function createMockServer(mockJsonPath, port) {
|
|
455
|
+
const { MockoonServer, createLoggerInstance, listenServerEvents } = await import("@mockoon/commons-server");
|
|
456
|
+
const resolvedPath = resolve(mockJsonPath);
|
|
457
|
+
const environment = JSON.parse(readFileSync(resolvedPath, "utf-8"));
|
|
458
|
+
if (port !== void 0) {
|
|
459
|
+
environment.port = port;
|
|
460
|
+
}
|
|
461
|
+
const actualPort = environment.port;
|
|
462
|
+
const logger = createLoggerInstance(null);
|
|
463
|
+
const server = new MockoonServer(environment, {
|
|
464
|
+
environmentDirectory: dirname(resolvedPath),
|
|
465
|
+
enableAdminApi: false,
|
|
466
|
+
disableTls: true,
|
|
467
|
+
enableRandomLatency: false,
|
|
468
|
+
maxTransactionLogs: 100,
|
|
469
|
+
envVarsPrefix: "MOCKOON_"
|
|
470
|
+
});
|
|
471
|
+
listenServerEvents(server, environment, logger, false);
|
|
472
|
+
await new Promise((resolve4, reject) => {
|
|
473
|
+
server.on("started", resolve4);
|
|
474
|
+
server.on("error", (errorCode, originalError) => {
|
|
475
|
+
reject(originalError ?? new Error(`Mockoon error: ${errorCode}`));
|
|
476
|
+
});
|
|
477
|
+
server.start();
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
url: `http://127.0.0.1:${actualPort}`,
|
|
481
|
+
port: actualPort,
|
|
482
|
+
stop: () => new Promise((resolve4) => {
|
|
483
|
+
server.on("stopped", resolve4);
|
|
484
|
+
server.stop();
|
|
485
|
+
})
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/commands/mock/start.ts
|
|
490
|
+
function readdirSafe(dir) {
|
|
491
|
+
try {
|
|
492
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function discoverMocks(mocksDir, filters) {
|
|
498
|
+
const mocks = [];
|
|
499
|
+
for (const provider of readdirSafe(mocksDir)) {
|
|
500
|
+
const providerDir = join(mocksDir, provider);
|
|
501
|
+
for (const scenario of readdirSafe(providerDir)) {
|
|
502
|
+
const mockPath = join(providerDir, scenario, "mock.json");
|
|
503
|
+
if (!existsSync(mockPath)) continue;
|
|
504
|
+
mocks.push({ provider, scenario, mockPath });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (filters.length === 0) return mocks;
|
|
508
|
+
return mocks.filter(
|
|
509
|
+
({ provider, scenario }) => filters.some((f) => f === provider || f === `${provider}/${scenario}`)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
function findFreePort() {
|
|
513
|
+
return new Promise((resolve4, reject) => {
|
|
514
|
+
const srv = createNetServer();
|
|
515
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
516
|
+
const addr = srv.address();
|
|
517
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
518
|
+
srv.close(() => resolve4(port));
|
|
519
|
+
});
|
|
520
|
+
srv.on("error", reject);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function startMock(mock) {
|
|
524
|
+
const port = await findFreePort();
|
|
525
|
+
const server = await createMockServer(mock.mockPath, port);
|
|
526
|
+
const route = `/${mock.provider}/${mock.scenario}`;
|
|
527
|
+
return { server, route, provider: mock.provider, scenario: mock.scenario };
|
|
528
|
+
}
|
|
529
|
+
function createProxy(routeTable) {
|
|
530
|
+
return createServer((req, res) => {
|
|
531
|
+
const match = req.url?.match(/^\/([^/?]+)\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
|
|
532
|
+
if (!match) {
|
|
533
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
534
|
+
res.end(JSON.stringify({ error: "Not found. Use /{provider}/{scenario}/..." }));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const [, provider, scenario] = match;
|
|
538
|
+
const key = `/${provider}/${scenario}`;
|
|
539
|
+
const target = routeTable.get(key);
|
|
540
|
+
if (!target) {
|
|
541
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
542
|
+
res.end(
|
|
543
|
+
JSON.stringify({
|
|
544
|
+
error: `Unknown route: ${key}`,
|
|
545
|
+
available: [...routeTable.keys()]
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const downstream = (req.url ?? "/").slice(key.length) || "/";
|
|
551
|
+
const proxyReq = httpRequest(
|
|
552
|
+
{
|
|
553
|
+
hostname: "127.0.0.1",
|
|
554
|
+
port: target.server.port,
|
|
555
|
+
path: downstream,
|
|
556
|
+
method: req.method,
|
|
557
|
+
headers: { ...req.headers, host: `127.0.0.1:${target.server.port}` }
|
|
558
|
+
},
|
|
559
|
+
(proxyRes) => {
|
|
560
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
561
|
+
proxyRes.pipe(res);
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
proxyReq.on("error", (err) => {
|
|
565
|
+
if (!res.headersSent) {
|
|
566
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
567
|
+
}
|
|
568
|
+
res.end(JSON.stringify({ error: "Bad gateway", detail: err.message }));
|
|
569
|
+
});
|
|
570
|
+
req.pipe(proxyReq);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
async function runMockStart(mocksRoot, filters, port) {
|
|
574
|
+
const mocksDir = resolve2(mocksRoot);
|
|
575
|
+
const mocks = discoverMocks(mocksDir, filters);
|
|
576
|
+
if (mocks.length === 0) {
|
|
577
|
+
console.error("No matching mocks found.");
|
|
578
|
+
if (filters.length > 0) {
|
|
579
|
+
console.error(`Filters: ${filters.join(", ")}`);
|
|
580
|
+
console.error(`Available mocks are under: ${relative(process.cwd(), mocksDir)}/`);
|
|
581
|
+
}
|
|
582
|
+
return 1;
|
|
583
|
+
}
|
|
584
|
+
console.log(`Starting ${mocks.length} mock(s)...
|
|
585
|
+
`);
|
|
586
|
+
const running = [];
|
|
587
|
+
for (const mock of mocks) {
|
|
588
|
+
const info = await startMock(mock);
|
|
589
|
+
running.push(info);
|
|
590
|
+
}
|
|
591
|
+
const routeTable = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const r of running) {
|
|
593
|
+
routeTable.set(r.route, r);
|
|
594
|
+
}
|
|
595
|
+
console.log("Route table:");
|
|
596
|
+
console.log("\u2500".repeat(50));
|
|
597
|
+
for (const [route, { server }] of routeTable) {
|
|
598
|
+
console.log(` ${route} \u2192 127.0.0.1:${server.port}`);
|
|
599
|
+
}
|
|
600
|
+
console.log("\u2500".repeat(50));
|
|
601
|
+
const proxy = createProxy(routeTable);
|
|
602
|
+
proxy.listen(port, () => {
|
|
603
|
+
console.log(`
|
|
604
|
+
Reverse proxy listening on http://localhost:${port}`);
|
|
605
|
+
console.log("Press Ctrl+C to stop all mocks.\n");
|
|
606
|
+
});
|
|
607
|
+
function shutdown() {
|
|
608
|
+
console.log("\nShutting down...");
|
|
609
|
+
proxy.close();
|
|
610
|
+
for (const { server } of running) {
|
|
611
|
+
void server.stop();
|
|
612
|
+
}
|
|
613
|
+
process.exit(0);
|
|
614
|
+
}
|
|
615
|
+
process.on("SIGINT", shutdown);
|
|
616
|
+
process.on("SIGTERM", shutdown);
|
|
617
|
+
return new Promise(() => void 0);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/commands/mock/validate.ts
|
|
621
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
622
|
+
import { join as join2, resolve as resolve3, relative as relative2, dirname as dirname2, basename } from "path";
|
|
623
|
+
import chalk3 from "chalk";
|
|
624
|
+
function pass(msg) {
|
|
625
|
+
console.log(chalk3.green(`\u2713 ${msg}`));
|
|
626
|
+
}
|
|
627
|
+
function fail(ctx, msg) {
|
|
628
|
+
console.log(chalk3.red(`\u2717 ${msg}`));
|
|
629
|
+
ctx.failures++;
|
|
630
|
+
}
|
|
631
|
+
async function subdirs(dir) {
|
|
632
|
+
const entries = await readdir(dir);
|
|
633
|
+
const dirs = [];
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
const full = join2(dir, entry);
|
|
636
|
+
const s = await stat(full);
|
|
637
|
+
if (s.isDirectory()) dirs.push(entry);
|
|
638
|
+
}
|
|
639
|
+
return dirs.sort();
|
|
640
|
+
}
|
|
641
|
+
function checkStructure(ctx, entries, label) {
|
|
642
|
+
if (entries.includes("README.md")) {
|
|
643
|
+
pass(`${label}: has README.md`);
|
|
644
|
+
} else {
|
|
645
|
+
fail(ctx, `${label}: missing README.md`);
|
|
646
|
+
}
|
|
647
|
+
if (entries.includes("mock.json")) {
|
|
648
|
+
pass(`${label}: has mock.json`);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
fail(ctx, `${label}: missing mock.json`);
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
async function checkSchema(ctx, data, label) {
|
|
655
|
+
const commons = await import("@mockoon/commons");
|
|
656
|
+
const schema = commons.EnvironmentSchemaNoFix;
|
|
657
|
+
const result = schema.validate(data, { abortEarly: false });
|
|
658
|
+
if (!result.error) {
|
|
659
|
+
pass(`${label}: valid Mockoon schema`);
|
|
660
|
+
} else {
|
|
661
|
+
for (const detail of result.error.details) {
|
|
662
|
+
fail(ctx, `${label}: schema \u2014 ${detail.message}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function checkResponseQuality(ctx, data, label) {
|
|
667
|
+
const routes = data.routes ?? [];
|
|
668
|
+
for (const route of routes) {
|
|
669
|
+
for (const resp of route.responses ?? []) {
|
|
670
|
+
const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
|
|
671
|
+
const headers = resp.headers ?? [];
|
|
672
|
+
const hasContentType = headers.some((h) => h.key.toLowerCase() === "content-type");
|
|
673
|
+
if (hasContentType) {
|
|
674
|
+
pass(`${respLabel}: has Content-Type`);
|
|
675
|
+
} else {
|
|
676
|
+
fail(ctx, `${respLabel}: missing Content-Type header`);
|
|
677
|
+
}
|
|
678
|
+
if (resp.label && resp.label.trim().length > 0) {
|
|
679
|
+
pass(`${respLabel}: has label "${resp.label}"`);
|
|
680
|
+
} else {
|
|
681
|
+
fail(ctx, `${respLabel}: missing or empty label`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function checkDatabucketRefs(ctx, data, label) {
|
|
687
|
+
const bucketIds = new Set((data.data ?? []).map((d) => d.id));
|
|
688
|
+
for (const route of data.routes ?? []) {
|
|
689
|
+
for (const resp of route.responses ?? []) {
|
|
690
|
+
if (resp.bodyType === "DATABUCKET") {
|
|
691
|
+
const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
|
|
692
|
+
if (resp.databucketID && bucketIds.has(resp.databucketID)) {
|
|
693
|
+
pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
|
|
694
|
+
} else {
|
|
695
|
+
fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function discoverAllScenarios(mocksDir) {
|
|
702
|
+
const scenarios = [];
|
|
703
|
+
const providers = await subdirs(mocksDir);
|
|
704
|
+
for (const provider of providers) {
|
|
705
|
+
const providerDir = join2(mocksDir, provider);
|
|
706
|
+
for (const scenario of await subdirs(providerDir)) {
|
|
707
|
+
scenarios.push(`${provider}/${scenario}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return scenarios;
|
|
711
|
+
}
|
|
712
|
+
function resolveScenarioDir(arg3) {
|
|
713
|
+
const abs = resolve3(arg3);
|
|
714
|
+
if (basename(abs) === "mock.json") return dirname2(abs);
|
|
715
|
+
return abs;
|
|
716
|
+
}
|
|
717
|
+
async function validateScenario(ctx, scenarioDir, mocksDir) {
|
|
718
|
+
const label = relative2(mocksDir, scenarioDir);
|
|
719
|
+
console.log(chalk3.bold(`
|
|
720
|
+
\u2500\u2500 ${label} \u2500\u2500`));
|
|
721
|
+
let entries;
|
|
722
|
+
try {
|
|
723
|
+
entries = await readdir(scenarioDir);
|
|
724
|
+
} catch {
|
|
725
|
+
fail(ctx, `${label}: directory not found`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const hasMock = checkStructure(ctx, entries, label);
|
|
729
|
+
if (!hasMock) return;
|
|
730
|
+
const mockPath = join2(scenarioDir, "mock.json");
|
|
731
|
+
let data;
|
|
732
|
+
try {
|
|
733
|
+
data = JSON.parse(await readFile(mockPath, "utf-8"));
|
|
734
|
+
} catch (err) {
|
|
735
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
736
|
+
fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
await checkSchema(ctx, data, label);
|
|
740
|
+
checkResponseQuality(ctx, data, label);
|
|
741
|
+
checkDatabucketRefs(ctx, data, label);
|
|
742
|
+
}
|
|
743
|
+
async function runMockValidate(mocksRoot, paths) {
|
|
744
|
+
const ctx = { failures: 0 };
|
|
745
|
+
const mocksDir = resolve3(mocksRoot);
|
|
746
|
+
const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join2(mocksDir, s));
|
|
747
|
+
if (targets.length === 0) {
|
|
748
|
+
fail(ctx, "No scenarios found under mocks/");
|
|
749
|
+
return 1;
|
|
750
|
+
}
|
|
751
|
+
console.log(chalk3.bold("\nValidating mock configs...\n"));
|
|
752
|
+
for (const target of targets) {
|
|
753
|
+
await validateScenario(ctx, target, mocksDir);
|
|
754
|
+
}
|
|
755
|
+
console.log(chalk3.bold("\n\u2500\u2500 summary \u2500\u2500"));
|
|
756
|
+
if (ctx.failures === 0) {
|
|
757
|
+
console.log(chalk3.green("\u2713 All checks passed"));
|
|
758
|
+
} else {
|
|
759
|
+
console.log(chalk3.red(`\u2717 ${ctx.failures} check(s) failed`));
|
|
760
|
+
}
|
|
761
|
+
return ctx.failures === 0 ? 0 : 1;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/commands/mock/index.ts
|
|
765
|
+
var startCommand = defineCommand({
|
|
766
|
+
name: "start",
|
|
767
|
+
description: "Start mock API servers with reverse proxy",
|
|
768
|
+
args: z.object({
|
|
769
|
+
mocksRoot: arg(z.string().default("./mocks"), {
|
|
770
|
+
description: "Path to mocks directory"
|
|
771
|
+
}),
|
|
772
|
+
port: arg(z.coerce.number().default(3e3), {
|
|
773
|
+
alias: "p",
|
|
774
|
+
description: "Reverse proxy port"
|
|
775
|
+
}),
|
|
776
|
+
filter: arg(z.array(z.string()).default([]), {
|
|
777
|
+
positional: true,
|
|
778
|
+
description: "Filter by provider or provider/scenario"
|
|
779
|
+
})
|
|
780
|
+
}),
|
|
781
|
+
run: async (args) => {
|
|
782
|
+
const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
|
|
783
|
+
process.exit(exitCode);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
var validateCommand = defineCommand({
|
|
787
|
+
name: "validate",
|
|
788
|
+
description: "Validate mock scenario configs",
|
|
789
|
+
args: z.object({
|
|
790
|
+
mocksRoot: arg(z.string().default("./mocks"), {
|
|
791
|
+
description: "Path to mocks directory"
|
|
792
|
+
}),
|
|
793
|
+
paths: arg(z.array(z.string()).default([]), {
|
|
794
|
+
positional: true,
|
|
795
|
+
description: "Specific scenario paths to validate"
|
|
796
|
+
})
|
|
797
|
+
}),
|
|
798
|
+
run: async (args) => {
|
|
799
|
+
const exitCode = await runMockValidate(args.mocksRoot, args.paths);
|
|
800
|
+
process.exit(exitCode);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
var mockCommand = defineCommand({
|
|
804
|
+
name: "mock",
|
|
805
|
+
description: "Mock API server management",
|
|
806
|
+
subCommands: {
|
|
807
|
+
start: startCommand,
|
|
808
|
+
validate: validateCommand
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// src/cli.ts
|
|
813
|
+
var cwd = process.cwd();
|
|
814
|
+
var rootArgs = z2.object({
|
|
815
|
+
modulesRoot: arg2(z2.string().optional(), {
|
|
816
|
+
alias: "m",
|
|
817
|
+
description: "Path to modules directory"
|
|
818
|
+
}),
|
|
819
|
+
appRoot: arg2(z2.string().optional(), {
|
|
820
|
+
alias: "a",
|
|
821
|
+
description: "Path to app-compose directory (apps/ or examples/)"
|
|
822
|
+
})
|
|
823
|
+
});
|
|
824
|
+
function requireRoot(args) {
|
|
825
|
+
const paths = { modulesRoot: args.modulesRoot, appRoot: args.appRoot };
|
|
826
|
+
if (!paths.modulesRoot && !paths.appRoot) {
|
|
827
|
+
console.error("At least one of --modules-root or --app-root is required.");
|
|
828
|
+
process.exit(2);
|
|
829
|
+
}
|
|
830
|
+
return paths;
|
|
831
|
+
}
|
|
832
|
+
var checkCommand = defineCommand2({
|
|
833
|
+
name: "check",
|
|
834
|
+
description: "Validate docs against schemas",
|
|
835
|
+
args: rootArgs,
|
|
836
|
+
run: async (args) => {
|
|
837
|
+
const paths = requireRoot(args);
|
|
838
|
+
const exitCode = await runCheck(paths, cwd);
|
|
839
|
+
process.exit(exitCode);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
var syncCheckCommand = defineCommand2({
|
|
843
|
+
name: "sync-check",
|
|
844
|
+
description: "Validate source <-> doc correspondence",
|
|
845
|
+
args: rootArgs,
|
|
846
|
+
run: async (args) => {
|
|
847
|
+
const paths = requireRoot(args);
|
|
848
|
+
const result = await runSyncCheck(paths, cwd);
|
|
849
|
+
console.log(formatSyncCheckReport(result));
|
|
850
|
+
process.exit(result.exitCode);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
var scaffoldCommand = defineCommand2({
|
|
854
|
+
name: "scaffold",
|
|
855
|
+
description: "Generate doc file from schema template",
|
|
856
|
+
args: rootArgs.extend({
|
|
857
|
+
type: arg2(z2.enum(ALL_TYPES), {
|
|
858
|
+
positional: true,
|
|
859
|
+
description: `Scaffold type (${ALL_TYPES.join(", ")})`
|
|
860
|
+
}),
|
|
861
|
+
parent: arg2(z2.string(), {
|
|
862
|
+
positional: true,
|
|
863
|
+
description: "Parent name (module or app name)"
|
|
864
|
+
}),
|
|
865
|
+
name: arg2(z2.string().optional(), {
|
|
866
|
+
positional: true,
|
|
867
|
+
description: "Item name (required for most types)"
|
|
868
|
+
})
|
|
869
|
+
}),
|
|
870
|
+
run: async (args) => {
|
|
871
|
+
const paths = requireRoot(args);
|
|
872
|
+
const root = isModuleType(args.type) ? paths.modulesRoot : paths.appRoot;
|
|
873
|
+
if (!root) {
|
|
874
|
+
console.error(
|
|
875
|
+
`--${isModuleType(args.type) ? "modules-root" : "app-root"} is required for scaffold type "${args.type}".`
|
|
876
|
+
);
|
|
877
|
+
process.exit(2);
|
|
878
|
+
}
|
|
879
|
+
const exitCode = await runScaffold(
|
|
880
|
+
args.type,
|
|
881
|
+
args.parent,
|
|
882
|
+
args.name,
|
|
883
|
+
root,
|
|
884
|
+
cwd
|
|
885
|
+
);
|
|
886
|
+
process.exit(exitCode);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
var initCommand = defineCommand2({
|
|
890
|
+
name: "init",
|
|
891
|
+
description: "Set up consumer repo with framework skills",
|
|
892
|
+
args: z2.object({
|
|
893
|
+
force: arg2(z2.boolean().default(false), {
|
|
894
|
+
alias: "f",
|
|
895
|
+
description: "Overwrite existing skills"
|
|
896
|
+
})
|
|
897
|
+
}),
|
|
898
|
+
run: (args) => {
|
|
899
|
+
const exitCode = runInit(cwd, args.force);
|
|
900
|
+
process.exit(exitCode);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
var main = defineCommand2({
|
|
904
|
+
name: "erp-kit",
|
|
905
|
+
description: "Documentation validation and scaffolding tool",
|
|
906
|
+
subCommands: {
|
|
907
|
+
check: checkCommand,
|
|
908
|
+
"sync-check": syncCheckCommand,
|
|
909
|
+
scaffold: scaffoldCommand,
|
|
910
|
+
init: initCommand,
|
|
911
|
+
mock: mockCommand
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
void runMain(main);
|