@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.
Files changed (231) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -28
  4. package/dist/cli.js +914 -0
  5. package/package.json +67 -8
  6. package/schemas/app-compose/actors.yml +34 -0
  7. package/schemas/app-compose/business-flow.yml +50 -0
  8. package/schemas/app-compose/requirements.yml +33 -0
  9. package/schemas/app-compose/resolver.yml +47 -0
  10. package/schemas/app-compose/screen.yml +81 -0
  11. package/schemas/app-compose/story.yml +67 -0
  12. package/schemas/module/command.yml +52 -0
  13. package/schemas/module/feature.yml +58 -0
  14. package/schemas/module/model.yml +70 -0
  15. package/schemas/module/module.yml +50 -0
  16. package/skills/1-module-docs/SKILL.md +111 -0
  17. package/skills/1-module-docs/references/structure.md +22 -0
  18. package/skills/2-module-feature-breakdown/SKILL.md +72 -0
  19. package/skills/2-module-feature-breakdown/references/commands.md +48 -0
  20. package/skills/2-module-feature-breakdown/references/models.md +29 -0
  21. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  22. package/skills/3-module-doc-review/SKILL.md +236 -0
  23. package/skills/3-module-doc-review/references/commands.md +54 -0
  24. package/skills/3-module-doc-review/references/models.md +29 -0
  25. package/skills/3-module-doc-review/references/testing.md +37 -0
  26. package/skills/4-module-tdd-implementation/SKILL.md +74 -0
  27. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  28. package/skills/4-module-tdd-implementation/references/db-relations.md +69 -0
  29. package/skills/4-module-tdd-implementation/references/errors.md +7 -0
  30. package/skills/4-module-tdd-implementation/references/exports.md +8 -0
  31. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  32. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  33. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  34. package/skills/5-module-implementation-review/SKILL.md +408 -0
  35. package/skills/5-module-implementation-review/references/commands.md +45 -0
  36. package/skills/5-module-implementation-review/references/errors.md +7 -0
  37. package/skills/5-module-implementation-review/references/exports.md +8 -0
  38. package/skills/5-module-implementation-review/references/models.md +30 -0
  39. package/skills/5-module-implementation-review/references/testing.md +29 -0
  40. package/skills/app-compose-1-requirement-analysis/SKILL.md +89 -0
  41. package/skills/app-compose-1-requirement-analysis/references/structure.md +27 -0
  42. package/skills/app-compose-2-requirements-breakdown/SKILL.md +95 -0
  43. package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +106 -0
  44. package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +139 -0
  45. package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +153 -0
  46. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  47. package/skills/app-compose-3-doc-review/SKILL.md +116 -0
  48. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  49. package/skills/app-compose-4-design-mock/SKILL.md +256 -0
  50. package/skills/app-compose-4-design-mock/references/component.md +50 -0
  51. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  52. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  53. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  54. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  55. package/skills/app-compose-5-design-mock-review/SKILL.md +290 -0
  56. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  57. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  58. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  59. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  60. package/skills/app-compose-6-implementation-spec/SKILL.md +127 -0
  61. package/skills/app-compose-6-implementation-spec/references/auth.md +72 -0
  62. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  63. package/skills/mock-scenario/SKILL.md +118 -0
  64. package/src/app.ts +1 -0
  65. package/src/cli.ts +120 -0
  66. package/src/commands/check.test.ts +30 -0
  67. package/src/commands/check.ts +66 -0
  68. package/src/commands/init.test.ts +88 -0
  69. package/src/commands/init.ts +120 -0
  70. package/src/commands/mock/index.ts +53 -0
  71. package/src/commands/mock/start.ts +179 -0
  72. package/src/commands/mock/validate.test.ts +185 -0
  73. package/src/commands/mock/validate.ts +198 -0
  74. package/src/commands/scaffold.test.ts +76 -0
  75. package/src/commands/scaffold.ts +119 -0
  76. package/src/commands/sync-check.test.ts +125 -0
  77. package/src/commands/sync-check.ts +182 -0
  78. package/src/integration.test.ts +63 -0
  79. package/src/mdschema.ts +48 -0
  80. package/src/mockServer.ts +55 -0
  81. package/src/module.ts +86 -0
  82. package/src/modules/accounting/.gitkeep +0 -0
  83. package/src/modules/coa-management/.gitkeep +0 -0
  84. package/src/modules/inventory/.gitkeep +0 -0
  85. package/src/modules/manufacturing/.gitkeep +0 -0
  86. package/src/modules/primitives/README.md +39 -0
  87. package/src/modules/primitives/command/activateCategory.test.ts +75 -0
  88. package/src/modules/primitives/command/activateCategory.ts +50 -0
  89. package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
  90. package/src/modules/primitives/command/activateCurrency.ts +50 -0
  91. package/src/modules/primitives/command/activateUnit.test.ts +53 -0
  92. package/src/modules/primitives/command/activateUnit.ts +50 -0
  93. package/src/modules/primitives/command/convertAmount.test.ts +275 -0
  94. package/src/modules/primitives/command/convertAmount.ts +126 -0
  95. package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
  96. package/src/modules/primitives/command/convertQuantity.ts +73 -0
  97. package/src/modules/primitives/command/createCategory.test.ts +126 -0
  98. package/src/modules/primitives/command/createCategory.ts +89 -0
  99. package/src/modules/primitives/command/createCurrency.test.ts +191 -0
  100. package/src/modules/primitives/command/createCurrency.ts +77 -0
  101. package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
  102. package/src/modules/primitives/command/createExchangeRate.ts +91 -0
  103. package/src/modules/primitives/command/createUnit.test.ts +214 -0
  104. package/src/modules/primitives/command/createUnit.ts +88 -0
  105. package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
  106. package/src/modules/primitives/command/deactivateCategory.ts +62 -0
  107. package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
  108. package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
  109. package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
  110. package/src/modules/primitives/command/deactivateUnit.ts +62 -0
  111. package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
  112. package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
  113. package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
  114. package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
  115. package/src/modules/primitives/db/currency.ts +30 -0
  116. package/src/modules/primitives/db/exchangeRate.ts +28 -0
  117. package/src/modules/primitives/db/unit.ts +32 -0
  118. package/src/modules/primitives/db/uomCategory.ts +32 -0
  119. package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
  120. package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
  121. package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
  122. package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
  123. package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
  124. package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
  125. package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
  126. package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
  127. package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
  128. package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
  129. package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
  130. package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
  131. package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
  132. package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
  133. package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
  134. package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
  135. package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
  136. package/src/modules/primitives/docs/features/uom-categories.md +52 -0
  137. package/src/modules/primitives/docs/models/Currency.md +45 -0
  138. package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
  139. package/src/modules/primitives/docs/models/Unit.md +46 -0
  140. package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
  141. package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
  142. package/src/modules/primitives/index.ts +40 -0
  143. package/src/modules/primitives/lib/errors.ts +138 -0
  144. package/src/modules/primitives/lib/types.ts +20 -0
  145. package/src/modules/primitives/module.ts +66 -0
  146. package/src/modules/primitives/permissions.ts +18 -0
  147. package/src/modules/primitives/tailor.config.ts +11 -0
  148. package/src/modules/primitives/testing/fixtures.ts +161 -0
  149. package/src/modules/product-management/.gitkeep +0 -0
  150. package/src/modules/purchase/.gitkeep +0 -0
  151. package/src/modules/sales/.gitkeep +0 -0
  152. package/src/modules/shared/createContext.test.ts +39 -0
  153. package/src/modules/shared/createContext.ts +15 -0
  154. package/src/modules/shared/defineCommand.test.ts +42 -0
  155. package/src/modules/shared/defineCommand.ts +19 -0
  156. package/src/modules/shared/definePermissions.test.ts +146 -0
  157. package/src/modules/shared/definePermissions.ts +94 -0
  158. package/src/modules/shared/entityTypes.ts +15 -0
  159. package/src/modules/shared/errors.ts +22 -0
  160. package/src/modules/shared/index.ts +1 -0
  161. package/src/modules/shared/internal.ts +13 -0
  162. package/src/modules/shared/requirePermission.test.ts +47 -0
  163. package/src/modules/shared/requirePermission.ts +8 -0
  164. package/src/modules/shared/types.ts +4 -0
  165. package/src/modules/supplier-management/.gitkeep +0 -0
  166. package/src/modules/supplier-portal/.gitkeep +0 -0
  167. package/src/modules/testing/index.ts +120 -0
  168. package/src/modules/user-management/README.md +38 -0
  169. package/src/modules/user-management/command/activateUser.test.ts +112 -0
  170. package/src/modules/user-management/command/activateUser.ts +67 -0
  171. package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
  172. package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
  173. package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
  174. package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
  175. package/src/modules/user-management/command/createPermission.test.ts +143 -0
  176. package/src/modules/user-management/command/createPermission.ts +66 -0
  177. package/src/modules/user-management/command/createRole.test.ts +115 -0
  178. package/src/modules/user-management/command/createRole.ts +52 -0
  179. package/src/modules/user-management/command/createUser.test.ts +198 -0
  180. package/src/modules/user-management/command/createUser.ts +85 -0
  181. package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
  182. package/src/modules/user-management/command/deactivateUser.ts +67 -0
  183. package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
  184. package/src/modules/user-management/command/logAuditEvent.ts +59 -0
  185. package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
  186. package/src/modules/user-management/command/reactivateUser.ts +67 -0
  187. package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
  188. package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
  189. package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
  190. package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
  191. package/src/modules/user-management/db/auditEvent.ts +47 -0
  192. package/src/modules/user-management/db/permission.ts +31 -0
  193. package/src/modules/user-management/db/role.ts +28 -0
  194. package/src/modules/user-management/db/rolePermission.ts +44 -0
  195. package/src/modules/user-management/db/user.ts +38 -0
  196. package/src/modules/user-management/db/userRole.ts +44 -0
  197. package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
  198. package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
  199. package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
  200. package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
  201. package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
  202. package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
  203. package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
  204. package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
  205. package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
  206. package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
  207. package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
  208. package/src/modules/user-management/docs/features/audit-trail.md +80 -0
  209. package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
  210. package/src/modules/user-management/docs/features/user-account-management.md +64 -0
  211. package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
  212. package/src/modules/user-management/docs/models/Permission.md +31 -0
  213. package/src/modules/user-management/docs/models/Role.md +31 -0
  214. package/src/modules/user-management/docs/models/RolePermission.md +33 -0
  215. package/src/modules/user-management/docs/models/User.md +47 -0
  216. package/src/modules/user-management/docs/models/UserRole.md +34 -0
  217. package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
  218. package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
  219. package/src/modules/user-management/generated/enums.ts +24 -0
  220. package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
  221. package/src/modules/user-management/index.ts +32 -0
  222. package/src/modules/user-management/lib/errors.ts +81 -0
  223. package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
  224. package/src/modules/user-management/lib/types.ts +31 -0
  225. package/src/modules/user-management/module.ts +77 -0
  226. package/src/modules/user-management/permissions.ts +15 -0
  227. package/src/modules/user-management/tailor.config.ts +11 -0
  228. package/src/modules/user-management/testing/fixtures.ts +98 -0
  229. package/src/schemas.ts +25 -0
  230. package/src/testing.ts +10 -0
  231. package/src/util.ts +3 -0
@@ -0,0 +1,66 @@
1
+ import { runMdschema } from "../mdschema.js";
2
+ import { ALL_SCHEMAS } from "../schemas.js";
3
+ export interface CheckTarget {
4
+ glob: string;
5
+ schemaKey: string;
6
+ }
7
+
8
+ export function buildCheckTargets(config: {
9
+ modulesRoot?: string;
10
+ appRoot?: string;
11
+ }): CheckTarget[] {
12
+ const targets: CheckTarget[] = [];
13
+
14
+ if (config.modulesRoot) {
15
+ const m = config.modulesRoot;
16
+ targets.push(
17
+ { glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
18
+ { glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
19
+ { glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
20
+ { glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" },
21
+ );
22
+ }
23
+
24
+ if (config.appRoot) {
25
+ const a = config.appRoot;
26
+ targets.push(
27
+ { glob: `${a}/[a-zA-Z]*/README.md`, schemaKey: "requirements" },
28
+ { glob: `${a}/[a-zA-Z]*/docs/actors/*.md`, schemaKey: "actors" },
29
+ { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/README.md`, schemaKey: "business-flow" },
30
+ { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/story/*.md`, schemaKey: "story" },
31
+ { glob: `${a}/[a-zA-Z]*/docs/screen/*.md`, schemaKey: "screen" },
32
+ { glob: `${a}/[a-zA-Z]*/docs/resolver/*.md`, schemaKey: "resolver" },
33
+ );
34
+ }
35
+
36
+ return targets;
37
+ }
38
+
39
+ export async function runCheck(
40
+ config: { modulesRoot?: string; appRoot?: string },
41
+ cwd: string,
42
+ ): Promise<number> {
43
+ const targets = buildCheckTargets(config);
44
+
45
+ const results = await Promise.all(
46
+ targets.map(async (target) => {
47
+ const schemaPath = ALL_SCHEMAS[target.schemaKey];
48
+ if (!schemaPath) {
49
+ console.error(`Unknown schema key: ${target.schemaKey}`);
50
+ return 2;
51
+ }
52
+ const { exitCode, stdout, stderr } = await runMdschema(
53
+ ["check", target.glob, "--schema", schemaPath],
54
+ cwd,
55
+ );
56
+
57
+ if (stdout.trim()) console.log(stdout);
58
+ if (stderr.trim()) console.error(stderr);
59
+
60
+ return exitCode;
61
+ }),
62
+ );
63
+
64
+ if (results.includes(2)) return 2;
65
+ return results.some((code) => code !== 0) ? 1 : 0;
66
+ }
@@ -0,0 +1,88 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import { runInit } from "./init.js";
6
+
7
+ describe("runInit", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "init-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("copies framework skills to .agents/skills/", () => {
19
+ runInit(tmpDir, false);
20
+ const skillPath = path.join(tmpDir, ".agents", "skills", "1-module-docs", "SKILL.md");
21
+ expect(fs.existsSync(skillPath)).toBe(true);
22
+ });
23
+
24
+ it("copies skill references/ directories", () => {
25
+ runInit(tmpDir, false);
26
+ const refPath = path.join(
27
+ tmpDir,
28
+ ".agents",
29
+ "skills",
30
+ "4-module-tdd-implementation",
31
+ "references",
32
+ "models.md",
33
+ );
34
+ expect(fs.existsSync(refPath)).toBe(true);
35
+
36
+ const content = fs.readFileSync(refPath, "utf-8");
37
+ expect(content).toContain("# Database Models");
38
+ });
39
+
40
+ it("does not overwrite project-specific skills", () => {
41
+ const customSkillDir = path.join(tmpDir, ".agents", "skills", "my-custom-skill");
42
+ fs.mkdirSync(customSkillDir, { recursive: true });
43
+ fs.writeFileSync(path.join(customSkillDir, "SKILL.md"), "# Custom");
44
+ runInit(tmpDir, false);
45
+ const content = fs.readFileSync(path.join(customSkillDir, "SKILL.md"), "utf-8");
46
+ expect(content).toBe("# Custom");
47
+ });
48
+
49
+ it("does not overwrite existing framework skills", () => {
50
+ const skillDir = path.join(tmpDir, ".agents", "skills", "1-module-docs");
51
+ fs.mkdirSync(skillDir, { recursive: true });
52
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), "# Customized");
53
+ runInit(tmpDir, false);
54
+ const content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf-8");
55
+ expect(content).toBe("# Customized");
56
+ });
57
+
58
+ it("overwrites existing framework skills with --force", () => {
59
+ const skillDir = path.join(tmpDir, ".agents", "skills", "1-module-docs");
60
+ fs.mkdirSync(skillDir, { recursive: true });
61
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), "# Customized");
62
+ runInit(tmpDir, true);
63
+ const content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf-8");
64
+ expect(content).not.toBe("# Customized");
65
+ });
66
+
67
+ it("creates .claude/skills symlink to .agents/skills", () => {
68
+ runInit(tmpDir, false);
69
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
70
+ expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(true);
71
+ expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
72
+ });
73
+
74
+ it("skips symlink when .claude/skills is a real directory", () => {
75
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
76
+ fs.mkdirSync(claudeSkills, { recursive: true });
77
+ runInit(tmpDir, false);
78
+ expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(false);
79
+ });
80
+
81
+ it("relinks symlink with --force when target differs", () => {
82
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
83
+ fs.mkdirSync(path.dirname(claudeSkills), { recursive: true });
84
+ fs.symlinkSync("../old-target", claudeSkills);
85
+ runInit(tmpDir, true);
86
+ expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
87
+ });
88
+ });
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { PACKAGE_ROOT } from "../util.js";
5
+
6
+ const SKILLS_SRC = path.join(PACKAGE_ROOT, "skills");
7
+
8
+ const FRAMEWORK_SKILLS = [
9
+ "1-module-docs",
10
+ "2-module-feature-breakdown",
11
+ "3-module-doc-review",
12
+ "4-module-tdd-implementation",
13
+ "5-module-implementation-review",
14
+ "app-compose-1-requirement-analysis",
15
+ "app-compose-2-requirements-breakdown",
16
+ "app-compose-3-doc-review",
17
+ "app-compose-4-design-mock",
18
+ "app-compose-5-design-mock-review",
19
+ "app-compose-6-implementation-spec",
20
+ "mock-scenario",
21
+ ];
22
+
23
+ function copyDirectoryRecursive(
24
+ srcDir: string,
25
+ destDir: string,
26
+ force: boolean,
27
+ ): { copied: number; skipped: number } {
28
+ let copied = 0;
29
+ let skipped = 0;
30
+
31
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
32
+ const srcPath = path.join(srcDir, entry.name);
33
+ const destPath = path.join(destDir, entry.name);
34
+
35
+ if (entry.isDirectory()) {
36
+ const sub = copyDirectoryRecursive(srcPath, destPath, force);
37
+ copied += sub.copied;
38
+ skipped += sub.skipped;
39
+ } else {
40
+ if (!force && fs.existsSync(destPath)) {
41
+ skipped++;
42
+ continue;
43
+ }
44
+ fs.mkdirSync(destDir, { recursive: true });
45
+ fs.copyFileSync(srcPath, destPath);
46
+ copied++;
47
+ }
48
+ }
49
+
50
+ return { copied, skipped };
51
+ }
52
+
53
+ export function runInit(cwd: string, force: boolean): number {
54
+ console.log(chalk.bold("erp-kit init\n"));
55
+
56
+ // --- Skills ---
57
+ const skillsDest = path.join(cwd, ".agents", "skills");
58
+ let copiedCount = 0;
59
+ let skippedCount = 0;
60
+ for (const skill of FRAMEWORK_SKILLS) {
61
+ const srcSkillDir = path.join(SKILLS_SRC, skill);
62
+ if (!fs.existsSync(srcSkillDir)) continue;
63
+
64
+ const destDir = path.join(skillsDest, skill);
65
+ const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
66
+ copiedCount += result.copied;
67
+
68
+ if (result.skipped > 0) {
69
+ console.log(chalk.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
70
+ skippedCount += result.skipped;
71
+ }
72
+ }
73
+ console.log(chalk.green(` Copied ${copiedCount} skill files to .agents/skills/`));
74
+ if (skippedCount > 0) {
75
+ console.log(
76
+ chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`),
77
+ );
78
+ }
79
+
80
+ // --- Claude Code symlink ---
81
+ const claudeSkills = path.join(cwd, ".claude", "skills");
82
+ const relTarget = path.relative(path.dirname(claudeSkills), skillsDest);
83
+
84
+ // lstatSync doesn't follow symlinks, so dangling symlinks are detected
85
+ const claudeSkillsExists = (() => {
86
+ try {
87
+ fs.lstatSync(claudeSkills);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ })();
93
+
94
+ if (claudeSkillsExists) {
95
+ const stat = fs.lstatSync(claudeSkills);
96
+ if (stat.isSymbolicLink()) {
97
+ const current = fs.readlinkSync(claudeSkills);
98
+ if (current === relTarget) {
99
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (already linked)"));
100
+ } else if (force) {
101
+ fs.unlinkSync(claudeSkills);
102
+ fs.symlinkSync(relTarget, claudeSkills);
103
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (relinked)"));
104
+ } else {
105
+ console.log(
106
+ chalk.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`),
107
+ );
108
+ }
109
+ } else {
110
+ console.log(chalk.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
111
+ }
112
+ } else {
113
+ fs.mkdirSync(path.dirname(claudeSkills), { recursive: true });
114
+ fs.symlinkSync(relTarget, claudeSkills);
115
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (linked)"));
116
+ }
117
+
118
+ console.log(chalk.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
119
+ return 0;
120
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { defineCommand, arg } from "politty";
3
+ import { runMockStart } from "./start.js";
4
+ import { runMockValidate } from "./validate.js";
5
+
6
+ const startCommand = defineCommand({
7
+ name: "start",
8
+ description: "Start mock API servers with reverse proxy",
9
+ args: z.object({
10
+ mocksRoot: arg(z.string().default("./mocks"), {
11
+ description: "Path to mocks directory",
12
+ }),
13
+ port: arg(z.coerce.number().default(3000), {
14
+ alias: "p",
15
+ description: "Reverse proxy port",
16
+ }),
17
+ filter: arg(z.array(z.string()).default([]), {
18
+ positional: true,
19
+ description: "Filter by provider or provider/scenario",
20
+ }),
21
+ }),
22
+ run: async (args) => {
23
+ const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
24
+ process.exit(exitCode);
25
+ },
26
+ });
27
+
28
+ const validateCommand = defineCommand({
29
+ name: "validate",
30
+ description: "Validate mock scenario configs",
31
+ args: z.object({
32
+ mocksRoot: arg(z.string().default("./mocks"), {
33
+ description: "Path to mocks directory",
34
+ }),
35
+ paths: arg(z.array(z.string()).default([]), {
36
+ positional: true,
37
+ description: "Specific scenario paths to validate",
38
+ }),
39
+ }),
40
+ run: async (args) => {
41
+ const exitCode = await runMockValidate(args.mocksRoot, args.paths);
42
+ process.exit(exitCode);
43
+ },
44
+ });
45
+
46
+ export const mockCommand = defineCommand({
47
+ name: "mock",
48
+ description: "Mock API server management",
49
+ subCommands: {
50
+ start: startCommand,
51
+ validate: validateCommand,
52
+ },
53
+ });
@@ -0,0 +1,179 @@
1
+ import { createServer, request as httpRequest } from "node:http";
2
+ import { createServer as createNetServer } from "node:net";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { resolve, relative, join } from "node:path";
5
+ import { createMockServer, type MockServer } from "../../mockServer.js";
6
+
7
+ interface MockEntry {
8
+ provider: string;
9
+ scenario: string;
10
+ mockPath: string;
11
+ }
12
+
13
+ interface RunningMock {
14
+ server: MockServer;
15
+ route: string;
16
+ provider: string;
17
+ scenario: string;
18
+ }
19
+
20
+ function readdirSafe(dir: string): string[] {
21
+ try {
22
+ return readdirSync(dir, { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name);
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function discoverMocks(mocksDir: string, filters: string[]): MockEntry[] {
31
+ const mocks: MockEntry[] = [];
32
+
33
+ for (const provider of readdirSafe(mocksDir)) {
34
+ const providerDir = join(mocksDir, provider);
35
+ for (const scenario of readdirSafe(providerDir)) {
36
+ const mockPath = join(providerDir, scenario, "mock.json");
37
+ if (!existsSync(mockPath)) continue;
38
+ mocks.push({ provider, scenario, mockPath });
39
+ }
40
+ }
41
+
42
+ if (filters.length === 0) return mocks;
43
+
44
+ return mocks.filter(({ provider, scenario }) =>
45
+ filters.some((f) => f === provider || f === `${provider}/${scenario}`),
46
+ );
47
+ }
48
+
49
+ // Mockoon doesn't expose the actual listening port, so we can't use port 0 with readback.
50
+ // This has a minor TOCTOU window between closing this server and Mockoon binding.
51
+ function findFreePort(): Promise<number> {
52
+ return new Promise((resolve, reject) => {
53
+ const srv = createNetServer();
54
+ srv.listen(0, "127.0.0.1", () => {
55
+ const addr = srv.address();
56
+ const port = typeof addr === "object" && addr ? addr.port : 0;
57
+ srv.close(() => resolve(port));
58
+ });
59
+ srv.on("error", reject);
60
+ });
61
+ }
62
+
63
+ async function startMock(mock: MockEntry): Promise<RunningMock> {
64
+ const port = await findFreePort();
65
+ const server = await createMockServer(mock.mockPath, port);
66
+ const route = `/${mock.provider}/${mock.scenario}`;
67
+ return { server, route, provider: mock.provider, scenario: mock.scenario };
68
+ }
69
+
70
+ function createProxy(routeTable: Map<string, RunningMock>) {
71
+ return createServer((req, res) => {
72
+ const match = req.url?.match(/^\/([^/?]+)\/([^/?]+)(\/[^?]*)?(\?.*)?$/);
73
+ if (!match) {
74
+ res.writeHead(404, { "Content-Type": "application/json" });
75
+ res.end(JSON.stringify({ error: "Not found. Use /{provider}/{scenario}/..." }));
76
+ return;
77
+ }
78
+
79
+ const [, provider, scenario] = match;
80
+ const key = `/${provider}/${scenario}`;
81
+ const target = routeTable.get(key);
82
+
83
+ if (!target) {
84
+ res.writeHead(404, { "Content-Type": "application/json" });
85
+ res.end(
86
+ JSON.stringify({
87
+ error: `Unknown route: ${key}`,
88
+ available: [...routeTable.keys()],
89
+ }),
90
+ );
91
+ return;
92
+ }
93
+
94
+ // Strip the /{provider}/{scenario} prefix, keep the rest including query string
95
+ const downstream = (req.url ?? "/").slice(key.length) || "/";
96
+
97
+ const proxyReq = httpRequest(
98
+ {
99
+ hostname: "127.0.0.1",
100
+ port: target.server.port,
101
+ path: downstream,
102
+ method: req.method,
103
+ headers: { ...req.headers, host: `127.0.0.1:${target.server.port}` },
104
+ },
105
+ (proxyRes) => {
106
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
107
+ proxyRes.pipe(res);
108
+ },
109
+ );
110
+
111
+ proxyReq.on("error", (err) => {
112
+ if (!res.headersSent) {
113
+ res.writeHead(502, { "Content-Type": "application/json" });
114
+ }
115
+ res.end(JSON.stringify({ error: "Bad gateway", detail: err.message }));
116
+ });
117
+
118
+ req.pipe(proxyReq);
119
+ });
120
+ }
121
+
122
+ export async function runMockStart(
123
+ mocksRoot: string,
124
+ filters: string[],
125
+ port: number,
126
+ ): Promise<number> {
127
+ const mocksDir = resolve(mocksRoot);
128
+ const mocks = discoverMocks(mocksDir, filters);
129
+
130
+ if (mocks.length === 0) {
131
+ console.error("No matching mocks found.");
132
+ if (filters.length > 0) {
133
+ console.error(`Filters: ${filters.join(", ")}`);
134
+ console.error(`Available mocks are under: ${relative(process.cwd(), mocksDir)}/`);
135
+ }
136
+ return 1;
137
+ }
138
+
139
+ console.log(`Starting ${mocks.length} mock(s)...\n`);
140
+
141
+ const running: RunningMock[] = [];
142
+ for (const mock of mocks) {
143
+ const info = await startMock(mock);
144
+ running.push(info);
145
+ }
146
+
147
+ const routeTable = new Map<string, RunningMock>();
148
+ for (const r of running) {
149
+ routeTable.set(r.route, r);
150
+ }
151
+
152
+ console.log("Route table:");
153
+ console.log("\u2500".repeat(50));
154
+ for (const [route, { server }] of routeTable) {
155
+ console.log(` ${route} \u2192 127.0.0.1:${server.port}`);
156
+ }
157
+ console.log("\u2500".repeat(50));
158
+
159
+ const proxy = createProxy(routeTable);
160
+ proxy.listen(port, () => {
161
+ console.log(`\nReverse proxy listening on http://localhost:${port}`);
162
+ console.log("Press Ctrl+C to stop all mocks.\n");
163
+ });
164
+
165
+ function shutdown() {
166
+ console.log("\nShutting down...");
167
+ proxy.close();
168
+ for (const { server } of running) {
169
+ void server.stop();
170
+ }
171
+ process.exit(0);
172
+ }
173
+
174
+ process.on("SIGINT", shutdown);
175
+ process.on("SIGTERM", shutdown);
176
+
177
+ // Block until signal handler calls process.exit
178
+ return new Promise<number>(() => void 0);
179
+ }
@@ -0,0 +1,185 @@
1
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import { runMockValidate } from "./validate.js";
6
+
7
+ let tempDir: string;
8
+
9
+ beforeEach(async () => {
10
+ tempDir = await mkdtemp(join(tmpdir(), "mock-validate-"));
11
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
12
+ vi.spyOn(console, "log").mockImplementation(() => {});
13
+ });
14
+
15
+ afterEach(async () => {
16
+ vi.restoreAllMocks();
17
+ await rm(tempDir, { recursive: true, force: true });
18
+ });
19
+
20
+ function minimalMockJson() {
21
+ return {
22
+ uuid: "f7a1b2c3-d4e5-4f6a-8b7c-9d0e1f2a3b4c",
23
+ lastMigration: 32,
24
+ name: "Test API",
25
+ endpointPrefix: "",
26
+ port: 3000,
27
+ hostname: "0.0.0.0",
28
+ latency: 0,
29
+ folders: [],
30
+ rootChildren: [{ type: "route", uuid: "c5d8e2f1-3a4b-4c6d-9e7f-1a2b3c4d5e6f" }],
31
+ proxyMode: false,
32
+ proxyHost: "",
33
+ proxyRemovePrefix: false,
34
+ tlsOptions: {
35
+ enabled: false,
36
+ type: "CERT",
37
+ pfxPath: "",
38
+ certPath: "",
39
+ keyPath: "",
40
+ caPath: "",
41
+ passphrase: "",
42
+ },
43
+ cors: true,
44
+ headers: [{ key: "Content-Type", value: "application/json" }],
45
+ proxyReqHeaders: [],
46
+ proxyResHeaders: [],
47
+ callbacks: [],
48
+ data: [
49
+ {
50
+ uuid: "a3e72f41-8b56-4c9d-a1e7-2f4b8c56d9e3",
51
+ id: "items",
52
+ name: "Items",
53
+ documentation: "",
54
+ value: "[]",
55
+ },
56
+ ],
57
+ routes: [
58
+ {
59
+ uuid: "c5d8e2f1-3a4b-4c6d-9e7f-1a2b3c4d5e6f",
60
+ type: "crud",
61
+ documentation: "CRUD items",
62
+ method: "",
63
+ endpoint: "items",
64
+ responseMode: null,
65
+ streamingMode: null,
66
+ streamingInterval: 0,
67
+ responses: [
68
+ {
69
+ uuid: "d6e9f3a2-4b5c-4d7e-8f1a-2b3c4d5e6f7a",
70
+ label: "CRUD Items",
71
+ statusCode: 200,
72
+ latency: 0,
73
+ headers: [{ key: "Content-Type", value: "application/json" }],
74
+ body: "",
75
+ bodyType: "DATABUCKET",
76
+ databucketID: "items",
77
+ filePath: "",
78
+ sendFileAsBody: false,
79
+ rules: [],
80
+ rulesOperator: "OR",
81
+ disableTemplating: false,
82
+ fallbackTo404: false,
83
+ default: true,
84
+ crudKey: "id",
85
+ callbacks: [],
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ };
91
+ }
92
+
93
+ async function createScenario(
94
+ provider: string,
95
+ scenario: string,
96
+ mockData?: object,
97
+ includeReadme = true,
98
+ ) {
99
+ const scenarioDir = join(tempDir, provider, scenario);
100
+ await mkdir(scenarioDir, { recursive: true });
101
+ if (mockData) {
102
+ await writeFile(join(scenarioDir, "mock.json"), JSON.stringify(mockData));
103
+ }
104
+ if (includeReadme) {
105
+ await writeFile(join(scenarioDir, "README.md"), "# Test");
106
+ }
107
+ }
108
+
109
+ describe("runMockValidate", () => {
110
+ it("returns 0 for a valid scenario", async () => {
111
+ await createScenario("test-provider", "test-scenario", minimalMockJson());
112
+ const exitCode = await runMockValidate(tempDir, []);
113
+ expect(exitCode).toBe(0);
114
+ });
115
+
116
+ it("returns 1 when no scenarios found", async () => {
117
+ const exitCode = await runMockValidate(tempDir, []);
118
+ expect(exitCode).toBe(1);
119
+ });
120
+
121
+ it("returns 1 when mock.json is missing", async () => {
122
+ await createScenario("test-provider", "test-scenario", undefined, true);
123
+ const exitCode = await runMockValidate(tempDir, []);
124
+ expect(exitCode).toBe(1);
125
+ });
126
+
127
+ it("returns 1 when README.md is missing", async () => {
128
+ await createScenario("test-provider", "test-scenario", minimalMockJson(), false);
129
+ const exitCode = await runMockValidate(tempDir, []);
130
+ expect(exitCode).toBe(1);
131
+ });
132
+
133
+ it("returns 1 for invalid JSON", async () => {
134
+ const scenarioDir = join(tempDir, "test-provider", "test-scenario");
135
+ await mkdir(scenarioDir, { recursive: true });
136
+ await writeFile(join(scenarioDir, "mock.json"), "not json");
137
+ await writeFile(join(scenarioDir, "README.md"), "# Test");
138
+ const exitCode = await runMockValidate(tempDir, []);
139
+ expect(exitCode).toBe(1);
140
+ });
141
+
142
+ it("detects missing Content-Type header", async () => {
143
+ const data = minimalMockJson();
144
+ data.routes[0].responses[0].headers = [];
145
+ await createScenario("test-provider", "test-scenario", data);
146
+ const exitCode = await runMockValidate(tempDir, []);
147
+ expect(exitCode).toBe(1);
148
+ });
149
+
150
+ it("detects empty response label", async () => {
151
+ const data = minimalMockJson();
152
+ data.routes[0].responses[0].label = "";
153
+ await createScenario("test-provider", "test-scenario", data);
154
+ const exitCode = await runMockValidate(tempDir, []);
155
+ expect(exitCode).toBe(1);
156
+ });
157
+
158
+ it("detects broken databucket reference", async () => {
159
+ const data = minimalMockJson();
160
+ data.routes[0].responses[0].databucketID = "nonexistent";
161
+ await createScenario("test-provider", "test-scenario", data);
162
+ const exitCode = await runMockValidate(tempDir, []);
163
+ expect(exitCode).toBe(1);
164
+ });
165
+
166
+ it("validates specific paths when provided", async () => {
167
+ await createScenario("provider-a", "scenario-a", minimalMockJson());
168
+ await createScenario("provider-b", "scenario-b", minimalMockJson());
169
+
170
+ const exitCode = await runMockValidate(tempDir, [join(tempDir, "provider-a", "scenario-a")]);
171
+ expect(exitCode).toBe(0);
172
+ });
173
+
174
+ it("can be called multiple times without state leaking", async () => {
175
+ // First call: failing scenario
176
+ await createScenario("bad", "scenario", undefined, true);
177
+ const first = await runMockValidate(tempDir, [join(tempDir, "bad", "scenario")]);
178
+ expect(first).toBe(1);
179
+
180
+ // Second call: valid scenario — failures counter must be reset
181
+ await createScenario("good", "scenario", minimalMockJson());
182
+ const second = await runMockValidate(tempDir, [join(tempDir, "good", "scenario")]);
183
+ expect(second).toBe(0);
184
+ });
185
+ });