@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +77 -50
- package/dist/cli.js +693 -553
- package/package.json +4 -2
- package/schemas/module/command.yml +1 -0
- package/schemas/module/model.yml +9 -0
- package/schemas/module/query.yml +53 -0
- package/skills/1-module-docs/SKILL.md +4 -0
- package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
- package/skills/2-module-feature-breakdown/SKILL.md +6 -0
- package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
- package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
- package/skills/2-module-feature-breakdown/references/structure.md +22 -0
- package/skills/3-module-doc-review/SKILL.md +6 -0
- package/skills/3-module-doc-review/references/commands.md +54 -0
- package/skills/3-module-doc-review/references/models.md +29 -0
- package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
- package/skills/4-module-tdd-implementation/SKILL.md +24 -6
- package/skills/4-module-tdd-implementation/references/commands.md +45 -0
- package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
- package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
- package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
- 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 +8 -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 +4 -0
- package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
- package/skills/app-compose-3-doc-review/SKILL.md +4 -0
- package/skills/app-compose-3-doc-review/references/structure.md +27 -0
- package/skills/app-compose-4-design-mock/SKILL.md +8 -0
- package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
- 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 +7 -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 +5 -0
- package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
- package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
- package/src/cli.ts +8 -90
- package/src/commands/app/index.ts +74 -0
- package/src/commands/check.test.ts +2 -1
- package/src/commands/check.ts +1 -0
- package/src/commands/init.test.ts +30 -19
- package/src/commands/init.ts +76 -43
- package/src/commands/module/index.ts +85 -0
- package/src/commands/module/list.test.ts +62 -0
- package/src/commands/module/list.ts +64 -0
- package/src/commands/scaffold.test.ts +5 -0
- package/src/commands/scaffold.ts +2 -1
- package/src/commands/sync-check.test.ts +28 -0
- package/src/commands/sync-check.ts +6 -0
- package/src/integration.test.ts +6 -8
- package/src/module.ts +4 -3
- package/src/modules/primitives/docs/models/Currency.md +4 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
- package/src/modules/primitives/docs/models/Unit.md +4 -1
- package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
- package/src/modules/primitives/index.ts +2 -2
- package/src/modules/primitives/module.ts +5 -3
- package/src/modules/primitives/permissions.ts +0 -2
- package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
- package/src/modules/primitives/query/convertAmount.ts +122 -0
- package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
- package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
- package/src/modules/shared/defineQuery.test.ts +28 -0
- package/src/modules/shared/defineQuery.ts +16 -0
- package/src/modules/shared/internal.ts +2 -1
- package/src/modules/shared/types.ts +8 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
- package/src/modules/user-management/docs/models/Permission.md +2 -0
- package/src/modules/user-management/docs/models/Role.md +2 -0
- package/src/modules/user-management/docs/models/RolePermission.md +2 -0
- package/src/modules/user-management/docs/models/User.md +2 -0
- package/src/modules/user-management/docs/models/UserRole.md +2 -0
- package/src/schemas.ts +1 -0
- package/rules/app-compose/frontend/auth.md +0 -55
- package/rules/app-compose/frontend/page.md +0 -86
- package/rules/module-development/cross-module-type-injection.md +0 -28
- package/rules/module-development/dependency-modules.md +0 -24
- package/rules/module-development/executors.md +0 -67
- package/rules/module-development/sync-vs-async-operations.md +0 -83
- package/rules/sdk-best-practices/sdk-docs.md +0 -14
- package/src/modules/primitives/command/convertAmount.ts +0 -126
- /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
- /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("
|
|
67
|
+
it("creates .claude/skills symlink to .agents/skills", () => {
|
|
52
68
|
runInit(tmpDir, false);
|
|
53
|
-
const
|
|
54
|
-
expect(fs.
|
|
55
|
-
|
|
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("
|
|
61
|
-
const
|
|
62
|
-
fs.mkdirSync(
|
|
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
|
-
|
|
66
|
-
expect(content).toBe("# Custom Rule");
|
|
78
|
+
expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(false);
|
|
67
79
|
});
|
|
68
80
|
|
|
69
|
-
it("
|
|
70
|
-
const
|
|
71
|
-
fs.mkdirSync(
|
|
72
|
-
fs.
|
|
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
|
-
|
|
75
|
-
expect(content).not.toBe("# Custom Rule");
|
|
86
|
+
expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
|
|
76
87
|
});
|
|
77
88
|
});
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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
|
|
65
|
+
const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
|
|
66
|
+
copiedCount += result.copied;
|
|
34
67
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
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}
|
|
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
|
|
76
|
+
chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`),
|
|
50
77
|
);
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
package/src/commands/scaffold.ts
CHANGED
|
@@ -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
|
|
package/src/integration.test.ts
CHANGED
|
@@ -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", "--
|
|
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", "--
|
|
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("
|
|
61
|
-
expect(result).toContain("
|
|
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/
|
|
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/
|
|
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";
|
|
@@ -15,7 +15,10 @@ AppendOnly
|
|
|
15
15
|
### Command Definitions
|
|
16
16
|
|
|
17
17
|
- [createExchangeRate](../commands/CreateExchangeRate.md)
|
|
18
|
-
|
|
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
|
-
|
|
30
|
+
|
|
31
|
+
### Query Definitions
|
|
32
|
+
|
|
33
|
+
- [convertQuantity](../queries/ConvertQuantity.md)
|
|
31
34
|
|
|
32
35
|
### Models
|
|
33
36
|
|
|
@@ -28,13 +28,13 @@ export {
|
|
|
28
28
|
} from "./lib/errors";
|
|
29
29
|
|
|
30
30
|
// input types
|
|
31
|
-
export { type ConvertQuantityInput } from "./
|
|
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 "./
|
|
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";
|