@tailor-platform/erp-kit 0.1.0 → 0.1.2

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 (102) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +77 -50
  4. package/dist/cli.js +693 -553
  5. package/package.json +4 -2
  6. package/schemas/module/command.yml +1 -0
  7. package/schemas/module/model.yml +9 -0
  8. package/schemas/module/query.yml +53 -0
  9. package/skills/1-module-docs/SKILL.md +4 -0
  10. package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
  11. package/skills/2-module-feature-breakdown/SKILL.md +6 -0
  12. package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
  13. package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
  14. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  15. package/skills/3-module-doc-review/SKILL.md +6 -0
  16. package/skills/3-module-doc-review/references/commands.md +54 -0
  17. package/skills/3-module-doc-review/references/models.md +29 -0
  18. package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
  19. package/skills/4-module-tdd-implementation/SKILL.md +24 -6
  20. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  21. package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
  22. package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
  23. package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
  24. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  25. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  26. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  27. package/skills/5-module-implementation-review/SKILL.md +8 -0
  28. package/skills/5-module-implementation-review/references/commands.md +45 -0
  29. package/skills/5-module-implementation-review/references/errors.md +7 -0
  30. package/skills/5-module-implementation-review/references/exports.md +8 -0
  31. package/skills/5-module-implementation-review/references/models.md +30 -0
  32. package/skills/5-module-implementation-review/references/testing.md +29 -0
  33. package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
  34. package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
  35. package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
  36. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
  37. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
  38. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
  39. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  40. package/skills/app-compose-3-doc-review/SKILL.md +4 -0
  41. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  42. package/skills/app-compose-4-design-mock/SKILL.md +8 -0
  43. package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
  44. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  45. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  46. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  47. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  48. package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
  49. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  50. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  51. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  52. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  53. package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
  54. package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
  55. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  56. package/src/cli.ts +8 -90
  57. package/src/commands/app/index.ts +74 -0
  58. package/src/commands/check.test.ts +2 -1
  59. package/src/commands/check.ts +1 -0
  60. package/src/commands/init.test.ts +30 -19
  61. package/src/commands/init.ts +76 -43
  62. package/src/commands/module/index.ts +85 -0
  63. package/src/commands/module/list.test.ts +62 -0
  64. package/src/commands/module/list.ts +64 -0
  65. package/src/commands/scaffold.test.ts +5 -0
  66. package/src/commands/scaffold.ts +2 -1
  67. package/src/commands/sync-check.test.ts +28 -0
  68. package/src/commands/sync-check.ts +6 -0
  69. package/src/integration.test.ts +6 -8
  70. package/src/module.ts +4 -3
  71. package/src/modules/primitives/docs/models/Currency.md +4 -0
  72. package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
  73. package/src/modules/primitives/docs/models/Unit.md +4 -1
  74. package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
  75. package/src/modules/primitives/index.ts +2 -2
  76. package/src/modules/primitives/module.ts +5 -3
  77. package/src/modules/primitives/permissions.ts +0 -2
  78. package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
  79. package/src/modules/primitives/query/convertAmount.ts +122 -0
  80. package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
  81. package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
  82. package/src/modules/shared/defineQuery.test.ts +28 -0
  83. package/src/modules/shared/defineQuery.ts +16 -0
  84. package/src/modules/shared/internal.ts +2 -1
  85. package/src/modules/shared/types.ts +8 -0
  86. package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
  87. package/src/modules/user-management/docs/models/Permission.md +2 -0
  88. package/src/modules/user-management/docs/models/Role.md +2 -0
  89. package/src/modules/user-management/docs/models/RolePermission.md +2 -0
  90. package/src/modules/user-management/docs/models/User.md +2 -0
  91. package/src/modules/user-management/docs/models/UserRole.md +2 -0
  92. package/src/schemas.ts +1 -0
  93. package/rules/app-compose/frontend/auth.md +0 -55
  94. package/rules/app-compose/frontend/page.md +0 -86
  95. package/rules/module-development/cross-module-type-injection.md +0 -28
  96. package/rules/module-development/dependency-modules.md +0 -24
  97. package/rules/module-development/executors.md +0 -67
  98. package/rules/module-development/sync-vs-async-operations.md +0 -83
  99. package/rules/sdk-best-practices/sdk-docs.md +0 -14
  100. package/src/modules/primitives/command/convertAmount.ts +0 -126
  101. /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
  102. /package/src/modules/primitives/docs/{commands → queries}/ConvertQuantity.md +0 -0
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import os from "node:os";
3
+ import path from "node:path";
4
4
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
5
  import { runInit } from "./init.js";
6
6
 
@@ -21,6 +21,22 @@ describe("runInit", () => {
21
21
  expect(fs.existsSync(skillPath)).toBe(true);
22
22
  });
23
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
+
24
40
  it("does not overwrite project-specific skills", () => {
25
41
  const customSkillDir = path.join(tmpDir, ".agents", "skills", "my-custom-skill");
26
42
  fs.mkdirSync(customSkillDir, { recursive: true });
@@ -48,30 +64,25 @@ describe("runInit", () => {
48
64
  expect(content).not.toBe("# Customized");
49
65
  });
50
66
 
51
- it("copies framework rules to .agents/rules/", () => {
67
+ it("creates .claude/skills symlink to .agents/skills", () => {
52
68
  runInit(tmpDir, false);
53
- const rulePath = path.join(tmpDir, ".agents", "rules", "module-development", "structure.md");
54
- expect(fs.existsSync(rulePath)).toBe(true);
55
- // Also check nested subdirectory
56
- const nestedRule = path.join(tmpDir, ".agents", "rules", "app-compose", "frontend", "auth.md");
57
- expect(fs.existsSync(nestedRule)).toBe(true);
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"));
58
72
  });
59
73
 
60
- it("does not overwrite existing rules", () => {
61
- const ruleDir = path.join(tmpDir, ".agents", "rules", "module-development");
62
- fs.mkdirSync(ruleDir, { recursive: true });
63
- fs.writeFileSync(path.join(ruleDir, "structure.md"), "# Custom Rule");
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 });
64
77
  runInit(tmpDir, false);
65
- const content = fs.readFileSync(path.join(ruleDir, "structure.md"), "utf-8");
66
- expect(content).toBe("# Custom Rule");
78
+ expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(false);
67
79
  });
68
80
 
69
- it("overwrites existing rules with --force", () => {
70
- const ruleDir = path.join(tmpDir, ".agents", "rules", "module-development");
71
- fs.mkdirSync(ruleDir, { recursive: true });
72
- fs.writeFileSync(path.join(ruleDir, "structure.md"), "# Custom Rule");
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);
73
85
  runInit(tmpDir, true);
74
- const content = fs.readFileSync(path.join(ruleDir, "structure.md"), "utf-8");
75
- expect(content).not.toBe("# Custom Rule");
86
+ expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
76
87
  });
77
88
  });
@@ -4,7 +4,6 @@ import chalk from "chalk";
4
4
  import { PACKAGE_ROOT } from "../util.js";
5
5
 
6
6
  const SKILLS_SRC = path.join(PACKAGE_ROOT, "skills");
7
- const RULES_SRC = path.join(PACKAGE_ROOT, "rules");
8
7
 
9
8
  const FRAMEWORK_SKILLS = [
10
9
  "1-module-docs",
@@ -21,65 +20,99 @@ const FRAMEWORK_SKILLS = [
21
20
  "mock-scenario",
22
21
  ];
23
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
+
24
53
  export function runInit(cwd: string, force: boolean): number {
25
54
  console.log(chalk.bold("erp-kit init\n"));
26
55
 
56
+ // --- Skills ---
27
57
  const skillsDest = path.join(cwd, ".agents", "skills");
28
58
  let copiedCount = 0;
29
59
  let skippedCount = 0;
30
60
  for (const skill of FRAMEWORK_SKILLS) {
31
- const srcSkill = path.join(SKILLS_SRC, skill, "SKILL.md");
61
+ const srcSkillDir = path.join(SKILLS_SRC, skill);
62
+ if (!fs.existsSync(srcSkillDir)) continue;
63
+
32
64
  const destDir = path.join(skillsDest, skill);
33
- const destSkill = path.join(destDir, "SKILL.md");
65
+ const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
66
+ copiedCount += result.copied;
34
67
 
35
- if (!fs.existsSync(srcSkill)) continue;
36
- if (!force && fs.existsSync(destSkill)) {
37
- console.log(chalk.yellow(` Skipped ${skill}/SKILL.md (already exists)`));
38
- skippedCount++;
39
- continue;
68
+ if (result.skipped > 0) {
69
+ console.log(chalk.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
70
+ skippedCount += result.skipped;
40
71
  }
41
-
42
- fs.mkdirSync(destDir, { recursive: true });
43
- fs.copyFileSync(srcSkill, destSkill);
44
- copiedCount++;
45
72
  }
46
- console.log(chalk.green(` Copied ${copiedCount} framework skills to .agents/skills/`));
73
+ console.log(chalk.green(` Copied ${copiedCount} skill files to .agents/skills/`));
47
74
  if (skippedCount > 0) {
48
75
  console.log(
49
- chalk.yellow(` Skipped ${skippedCount} existing skills (use --force to overwrite)`),
76
+ chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`),
50
77
  );
51
78
  }
52
79
 
53
- const rulesDest = path.join(cwd, ".agents", "rules");
54
- let rulesCount = 0;
55
- let rulesSkipped = 0;
56
- if (fs.existsSync(RULES_SRC)) {
57
- const copyRulesRecursive = (srcDir: string, destDir: string) => {
58
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
59
- const srcPath = path.join(srcDir, entry.name);
60
- const destPath = path.join(destDir, entry.name);
61
- if (entry.isDirectory()) {
62
- copyRulesRecursive(srcPath, destPath);
63
- } else {
64
- if (!force && fs.existsSync(destPath)) {
65
- const rel = path.relative(rulesDest, destPath);
66
- console.log(chalk.yellow(` Skipped rule ${rel} (already exists)`));
67
- rulesSkipped++;
68
- continue;
69
- }
70
- fs.mkdirSync(destDir, { recursive: true });
71
- fs.copyFileSync(srcPath, destPath);
72
- rulesCount++;
73
- }
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
+ );
74
108
  }
75
- };
76
- copyRulesRecursive(RULES_SRC, rulesDest);
77
- }
78
- console.log(chalk.green(` Copied ${rulesCount} framework rules to .agents/rules/`));
79
- if (rulesSkipped > 0) {
80
- console.log(
81
- chalk.yellow(` Skipped ${rulesSkipped} existing rules (use --force to overwrite)`),
82
- );
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)"));
83
116
  }
84
117
 
85
118
  console.log(chalk.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ import { defineCommand, arg } from "politty";
3
+ import { runCheck } from "../check.js";
4
+ import { runSyncCheck, formatSyncCheckReport } from "../sync-check.js";
5
+ import { runScaffold, MODULE_TYPES, type ScaffoldType } from "../scaffold.js";
6
+ import { runModuleList } from "./list.js";
7
+
8
+ const cwd = process.cwd();
9
+
10
+ const rootArgs = z.object({
11
+ root: arg(z.string(), {
12
+ alias: "r",
13
+ description: "Path to modules directory",
14
+ }),
15
+ });
16
+
17
+ const listCommand = defineCommand({
18
+ name: "list",
19
+ description: "List available modules",
20
+ run: () => {
21
+ const exitCode = runModuleList();
22
+ process.exit(exitCode);
23
+ },
24
+ });
25
+
26
+ const checkCommand = defineCommand({
27
+ name: "check",
28
+ description: "Validate module docs against schemas",
29
+ args: rootArgs,
30
+ run: async (args) => {
31
+ const exitCode = await runCheck({ modulesRoot: args.root }, cwd);
32
+ process.exit(exitCode);
33
+ },
34
+ });
35
+
36
+ const syncCheckCommand = defineCommand({
37
+ name: "sync-check",
38
+ description: "Validate source <-> doc correspondence",
39
+ args: rootArgs,
40
+ run: async (args) => {
41
+ const result = await runSyncCheck({ modulesRoot: args.root }, cwd);
42
+ console.log(formatSyncCheckReport(result));
43
+ process.exit(result.exitCode);
44
+ },
45
+ });
46
+
47
+ const scaffoldCommand = defineCommand({
48
+ name: "scaffold",
49
+ description: "Generate module doc from schema template",
50
+ args: rootArgs.extend({
51
+ type: arg(z.enum(MODULE_TYPES as unknown as [string, ...string[]]), {
52
+ positional: true,
53
+ description: `Scaffold type (${MODULE_TYPES.join(", ")})`,
54
+ }),
55
+ parent: arg(z.string(), {
56
+ positional: true,
57
+ description: "Module name",
58
+ }),
59
+ name: arg(z.string().optional(), {
60
+ positional: true,
61
+ description: "Item name (required for feature, command, model)",
62
+ }),
63
+ }),
64
+ run: async (args) => {
65
+ const exitCode = await runScaffold(
66
+ args.type as ScaffoldType,
67
+ args.parent,
68
+ args.name,
69
+ args.root,
70
+ cwd,
71
+ );
72
+ process.exit(exitCode);
73
+ },
74
+ });
75
+
76
+ export const moduleCommand = defineCommand({
77
+ name: "module",
78
+ description: "Module management",
79
+ subCommands: {
80
+ list: listCommand,
81
+ check: checkCommand,
82
+ "sync-check": syncCheckCommand,
83
+ scaffold: scaffoldCommand,
84
+ },
85
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("listModules", () => {
4
+ it("returns the built-in modules with counts", async () => {
5
+ const { listModules } = await import("./list.js");
6
+ const modules = listModules();
7
+ expect(modules.length).toBeGreaterThan(0);
8
+
9
+ const primitives = modules.find((m) => m.name === "primitives");
10
+ expect(primitives).toBeDefined();
11
+ expect(primitives!.commands).toBeGreaterThan(0);
12
+ expect(primitives!.models).toBeGreaterThan(0);
13
+ expect(primitives!.features).toBeGreaterThan(0);
14
+ });
15
+
16
+ it("returns zero counts for stub modules", async () => {
17
+ const { listModules } = await import("./list.js");
18
+ const modules = listModules();
19
+ const inventory = modules.find((m) => m.name === "inventory");
20
+ expect(inventory).toBeDefined();
21
+ expect(inventory!.commands).toBe(0);
22
+ expect(inventory!.models).toBe(0);
23
+ expect(inventory!.features).toBe(0);
24
+ });
25
+
26
+ it("excludes shared and testing directories", async () => {
27
+ const { listModules } = await import("./list.js");
28
+ const modules = listModules();
29
+ const names = modules.map((m) => m.name);
30
+ expect(names).not.toContain("shared");
31
+ expect(names).not.toContain("testing");
32
+ });
33
+
34
+ it("returns modules in alphabetical order", async () => {
35
+ const { listModules } = await import("./list.js");
36
+ const modules = listModules();
37
+ const names = modules.map((m) => m.name);
38
+ const sorted = [...names].sort();
39
+ expect(names).toEqual(sorted);
40
+ });
41
+ });
42
+
43
+ describe("formatModuleList", () => {
44
+ it("formats modules with counts", async () => {
45
+ const { formatModuleList } = await import("./list.js");
46
+ const output = formatModuleList([
47
+ { name: "primitives", commands: 14, models: 4, features: 4 },
48
+ { name: "inventory", commands: 0, models: 0, features: 0 },
49
+ ]);
50
+ expect(output).toContain("primitives");
51
+ expect(output).toContain("14 commands");
52
+ expect(output).toContain("4 models");
53
+ expect(output).toContain("4 features");
54
+ expect(output).toContain("inventory");
55
+ expect(output).toContain("2 modules");
56
+ });
57
+
58
+ it("handles empty list", async () => {
59
+ const { formatModuleList } = await import("./list.js");
60
+ expect(formatModuleList([])).toBe("No modules found.");
61
+ });
62
+ });
@@ -0,0 +1,64 @@
1
+ import { readdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import { PACKAGE_ROOT } from "../../util.js";
5
+
6
+ const MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
7
+ const EXCLUDED_DIRS = new Set(["shared", "testing"]);
8
+
9
+ export interface ModuleInfo {
10
+ name: string;
11
+ commands: number;
12
+ models: number;
13
+ features: number;
14
+ }
15
+
16
+ function countFiles(dir: string, pattern: RegExp, exclusions: RegExp[]): number {
17
+ if (!existsSync(dir)) return 0;
18
+ return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f)))
19
+ .length;
20
+ }
21
+
22
+ export function listModules(): ModuleInfo[] {
23
+ if (!existsSync(MODULES_DIR)) return [];
24
+ return readdirSync(MODULES_DIR, { withFileTypes: true })
25
+ .filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name))
26
+ .map((d) => {
27
+ const modDir = join(MODULES_DIR, d.name);
28
+ return {
29
+ name: d.name,
30
+ commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
31
+ models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
32
+ features: countFiles(join(modDir, "docs", "features"), /\.md$/, []),
33
+ };
34
+ })
35
+ .sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+
38
+ export function formatModuleList(modules: ModuleInfo[]): string {
39
+ if (modules.length === 0) return "No modules found.";
40
+
41
+ const lines: string[] = [];
42
+ lines.push(chalk.bold("Modules:\n"));
43
+
44
+ const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
45
+
46
+ for (const mod of modules) {
47
+ const counts = [
48
+ `${mod.commands} commands`,
49
+ `${mod.models} models`,
50
+ `${mod.features} features`,
51
+ ].join(", ");
52
+ lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
53
+ }
54
+
55
+ lines.push("");
56
+ lines.push(`${modules.length} modules`);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ export function runModuleList(): number {
61
+ const modules = listModules();
62
+ console.log(formatModuleList(modules));
63
+ return 0;
64
+ }
@@ -61,6 +61,11 @@ describe("resolveScaffoldPath", () => {
61
61
  expect(result).toBe("examples/my-app/docs/resolver/create-supplier.md");
62
62
  });
63
63
 
64
+ it("resolves query scaffold path", () => {
65
+ const result = resolveScaffoldPath("query", "inventory", "ConvertQuantity", "modules");
66
+ expect(result).toBe("modules/inventory/docs/queries/ConvertQuantity.md");
67
+ });
68
+
64
69
  // Validation
65
70
  it("throws when name is required but missing", () => {
66
71
  expect(() => resolveScaffoldPath("command", "inventory", undefined, "modules")).toThrow(
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import { runMdschema } from "../mdschema.js";
4
4
  import { ALL_SCHEMAS } from "../schemas.js";
5
5
 
6
- export const MODULE_TYPES = ["module", "feature", "command", "model"] as const;
6
+ export const MODULE_TYPES = ["module", "feature", "command", "model", "query"] as const;
7
7
  export const APP_TYPES = [
8
8
  "requirements",
9
9
  "actors",
@@ -20,6 +20,7 @@ const MODULE_DIR_MAP: Record<string, string> = {
20
20
  feature: "docs/features",
21
21
  command: "docs/commands",
22
22
  model: "docs/models",
23
+ query: "docs/queries",
23
24
  };
24
25
 
25
26
  const APP_DIR_MAP: Record<string, string> = {
@@ -103,6 +103,34 @@ describe("runSyncCheck", () => {
103
103
  expect(result.summary.totalSources).toBe(1);
104
104
  });
105
105
 
106
+ it("reports missing doc for query source without doc", async () => {
107
+ const srcDir = path.join(tmpDir, "modules", "foo", "query");
108
+ fs.mkdirSync(srcDir, { recursive: true });
109
+ fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
110
+
111
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
112
+ expect(result.exitCode).toBe(1);
113
+ expect(result.errors).toContainEqual(
114
+ expect.objectContaining({
115
+ type: "missing-doc",
116
+ category: "query",
117
+ expectedBasename: "convertamount",
118
+ }),
119
+ );
120
+ });
121
+
122
+ it("returns 0 when query source and doc match", async () => {
123
+ const srcDir = path.join(tmpDir, "modules", "foo", "query");
124
+ const docDir = path.join(tmpDir, "modules", "foo", "docs", "queries");
125
+ fs.mkdirSync(srcDir, { recursive: true });
126
+ fs.mkdirSync(docDir, { recursive: true });
127
+ fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
128
+ fs.writeFileSync(path.join(docDir, "convertAmount.md"), "# ConvertAmount");
129
+
130
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
131
+ expect(result.exitCode).toBe(0);
132
+ });
133
+
106
134
  it("checks only module patterns when appRoot is not set", async () => {
107
135
  const resolverSrcDir = path.join(
108
136
  tmpDir,
@@ -41,6 +41,12 @@ function moduleCategories(root: string): CategoryConfig[] {
41
41
  docPattern: `${root}/*/docs/models/*.md`,
42
42
  exclusions: [/\.test\.ts$/, /^index\.ts$/],
43
43
  },
44
+ {
45
+ name: "query",
46
+ sourcePattern: `${root}/*/query/*.ts`,
47
+ docPattern: `${root}/*/docs/queries/*.md`,
48
+ exclusions: [/\.test\.ts$/],
49
+ },
44
50
  ];
45
51
  }
46
52
 
@@ -11,13 +11,11 @@ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
11
11
  const skipReason = fs.existsSync(CLI_PATH) ? undefined : "Run `pnpm build` first";
12
12
 
13
13
  describe.skipIf(skipReason)("integration", () => {
14
- it("check command runs against sdk-plugins modules", () => {
15
- // mdschema may report validation errors in real docs, so we allow non-zero exit codes
16
- // but verify the CLI itself doesn't crash with an unexpected error
14
+ it("module check command runs against sdk-plugins modules", () => {
17
15
  try {
18
16
  const result = execFileSync(
19
17
  "node",
20
- [CLI_PATH, "check", "--modules-root", "packages/erp-kit/src/modules"],
18
+ [CLI_PATH, "module", "check", "--root", "packages/erp-kit/src/modules"],
21
19
  {
22
20
  cwd: REPO_ROOT,
23
21
  encoding: "utf-8",
@@ -33,11 +31,11 @@ describe.skipIf(skipReason)("integration", () => {
33
31
  }
34
32
  });
35
33
 
36
- it("sync-check command runs against sdk-plugins modules", () => {
34
+ it("module sync-check command runs against sdk-plugins modules", () => {
37
35
  try {
38
36
  const result = execFileSync(
39
37
  "node",
40
- [CLI_PATH, "sync-check", "--modules-root", "packages/erp-kit/src/modules"],
38
+ [CLI_PATH, "module", "sync-check", "--root", "packages/erp-kit/src/modules"],
41
39
  {
42
40
  cwd: REPO_ROOT,
43
41
  encoding: "utf-8",
@@ -57,7 +55,7 @@ describe.skipIf(skipReason)("integration", () => {
57
55
  encoding: "utf-8",
58
56
  });
59
57
  expect(result).toContain("erp-kit");
60
- expect(result).toContain("check");
61
- expect(result).toContain("scaffold");
58
+ expect(result).toContain("module");
59
+ expect(result).toContain("app");
62
60
  });
63
61
  });
package/src/module.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  // shared/internal
2
2
  export { defineCommand, type Command } from "./modules/shared/defineCommand";
3
+ export { defineQuery, type Query } from "./modules/shared/defineQuery";
3
4
  export { definePermissions } from "./modules/shared/definePermissions";
4
5
  export { requirePermission } from "./modules/shared/requirePermission";
5
6
  export { createDomainError, InsufficientPermissionError } from "./modules/shared/errors";
6
- export type { CommandContext } from "./modules/shared/types";
7
+ export type { CommandContext, QueryContext, ReadonlyDB } from "./modules/shared/types";
7
8
  export type {
8
9
  InferSchema,
9
10
  Selectable,
@@ -43,13 +44,13 @@ export {
43
44
  SameCurrencyPairError,
44
45
  InvalidExchangeRateError,
45
46
  } from "./modules/primitives/lib/errors";
46
- export { type ConvertQuantityInput } from "./modules/primitives/command/convertQuantity";
47
+ export { type ConvertQuantityInput } from "./modules/primitives/query/convertQuantity";
47
48
  export { type ActivateCategoryInput } from "./modules/primitives/command/activateCategory";
48
49
  export { type DeactivateCategoryInput } from "./modules/primitives/command/deactivateCategory";
49
50
  export { type SetReferenceUnitInput } from "./modules/primitives/command/setReferenceUnit";
50
51
  export { type ActivateUnitInput } from "./modules/primitives/command/activateUnit";
51
52
  export { type DeactivateUnitInput } from "./modules/primitives/command/deactivateUnit";
52
- export { type ConvertAmountInput } from "./modules/primitives/command/convertAmount";
53
+ export { type ConvertAmountInput } from "./modules/primitives/query/convertAmount";
53
54
  export { type ActivateCurrencyInput } from "./modules/primitives/command/activateCurrency";
54
55
  export { type DeactivateCurrencyInput } from "./modules/primitives/command/deactivateCurrency";
55
56
  export { type SetBaseCurrencyInput } from "./modules/primitives/command/setBaseCurrency";
@@ -29,6 +29,10 @@ stateDiagram-v2
29
29
  - [deactivateCurrency](../commands/DeactivateCurrency.md)
30
30
  - [setBaseCurrency](../commands/SetBaseCurrency.md)
31
31
 
32
+ ### Query Definitions
33
+
34
+ - [convertAmount](../queries/ConvertAmount.md)
35
+
32
36
  ### Models
33
37
 
34
38
  - Currency
@@ -15,7 +15,10 @@ AppendOnly
15
15
  ### Command Definitions
16
16
 
17
17
  - [createExchangeRate](../commands/CreateExchangeRate.md)
18
- - [convertAmount](../commands/ConvertAmount.md)
18
+
19
+ ### Query Definitions
20
+
21
+ - [convertAmount](../queries/ConvertAmount.md)
19
22
 
20
23
  ### Models
21
24
 
@@ -27,7 +27,10 @@ stateDiagram-v2
27
27
  - [createUnit](../commands/CreateUnit.md)
28
28
  - [activateUnit](../commands/ActivateUnit.md)
29
29
  - [deactivateUnit](../commands/DeactivateUnit.md)
30
- - [convertQuantity](../commands/ConvertQuantity.md)
30
+
31
+ ### Query Definitions
32
+
33
+ - [convertQuantity](../queries/ConvertQuantity.md)
31
34
 
32
35
  ### Models
33
36
 
@@ -28,6 +28,8 @@ stateDiagram-v2
28
28
  - [deactivateCategory](../commands/DeactivateCategory.md)
29
29
  - [setReferenceUnit](../commands/SetReferenceUnit.md)
30
30
 
31
+ ### Query Definitions
32
+
31
33
  ### Models
32
34
 
33
35
  - UoMCategory
@@ -28,13 +28,13 @@ export {
28
28
  } from "./lib/errors";
29
29
 
30
30
  // input types
31
- export { type ConvertQuantityInput } from "./command/convertQuantity";
31
+ export { type ConvertQuantityInput } from "./query/convertQuantity";
32
32
  export { type ActivateCategoryInput } from "./command/activateCategory";
33
33
  export { type DeactivateCategoryInput } from "./command/deactivateCategory";
34
34
  export { type SetReferenceUnitInput } from "./command/setReferenceUnit";
35
35
  export { type ActivateUnitInput } from "./command/activateUnit";
36
36
  export { type DeactivateUnitInput } from "./command/deactivateUnit";
37
- export { type ConvertAmountInput } from "./command/convertAmount";
37
+ export { type ConvertAmountInput } from "./query/convertAmount";
38
38
  export { type ActivateCurrencyInput } from "./command/activateCurrency";
39
39
  export { type DeactivateCurrencyInput } from "./command/deactivateCurrency";
40
40
  export { type SetBaseCurrencyInput } from "./command/setBaseCurrency";