@tailor-platform/erp-kit 0.1.1 → 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 (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +77 -50
  3. package/dist/cli.js +691 -571
  4. package/package.json +1 -1
  5. package/schemas/module/command.yml +1 -0
  6. package/schemas/module/model.yml +9 -0
  7. package/schemas/module/query.yml +53 -0
  8. package/src/cli.ts +6 -88
  9. package/src/commands/app/index.ts +74 -0
  10. package/src/commands/check.test.ts +2 -1
  11. package/src/commands/check.ts +1 -0
  12. package/src/commands/module/index.ts +85 -0
  13. package/src/commands/module/list.test.ts +62 -0
  14. package/src/commands/module/list.ts +64 -0
  15. package/src/commands/scaffold.test.ts +5 -0
  16. package/src/commands/scaffold.ts +2 -1
  17. package/src/commands/sync-check.test.ts +28 -0
  18. package/src/commands/sync-check.ts +6 -0
  19. package/src/integration.test.ts +6 -8
  20. package/src/module.ts +4 -3
  21. package/src/modules/primitives/docs/models/Currency.md +4 -0
  22. package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
  23. package/src/modules/primitives/docs/models/Unit.md +4 -1
  24. package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
  25. package/src/modules/primitives/index.ts +2 -2
  26. package/src/modules/primitives/module.ts +5 -3
  27. package/src/modules/primitives/permissions.ts +0 -2
  28. package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
  29. package/src/modules/primitives/query/convertAmount.ts +122 -0
  30. package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
  31. package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
  32. package/src/modules/shared/defineQuery.test.ts +28 -0
  33. package/src/modules/shared/defineQuery.ts +16 -0
  34. package/src/modules/shared/internal.ts +2 -1
  35. package/src/modules/shared/types.ts +8 -0
  36. package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
  37. package/src/modules/user-management/docs/models/Permission.md +2 -0
  38. package/src/modules/user-management/docs/models/Role.md +2 -0
  39. package/src/modules/user-management/docs/models/RolePermission.md +2 -0
  40. package/src/modules/user-management/docs/models/User.md +2 -0
  41. package/src/modules/user-management/docs/models/UserRole.md +2 -0
  42. package/src/schemas.ts +1 -0
  43. package/src/modules/primitives/command/convertAmount.ts +0 -126
  44. /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
  45. /package/src/modules/primitives/docs/{commands → queries}/ConvertQuantity.md +0 -0
package/dist/cli.js CHANGED
@@ -1,441 +1,113 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { z as z2 } from "zod";
5
- import { defineCommand as defineCommand2, runMain, arg as arg2 } from "politty";
4
+ import { z as z4 } from "zod";
5
+ import { defineCommand as defineCommand4, runMain, arg as arg4 } from "politty";
6
6
 
7
- // src/mdschema.ts
8
- import path from "path";
7
+ // src/commands/init.ts
9
8
  import fs from "fs";
10
- import { execFile } from "child_process";
11
- import { createRequire } from "module";
12
- var require2 = createRequire(import.meta.url);
13
- function getMdschemaBin() {
14
- const pkgPath = require2.resolve("@jackchuka/mdschema/package.json");
15
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
16
- const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.mdschema;
17
- if (!bin) {
18
- throw new Error("Could not resolve mdschema binary from package.json bin field");
19
- }
20
- return path.join(path.dirname(pkgPath), bin);
21
- }
22
- function runMdschema(args, cwd2) {
23
- return new Promise((resolve4) => {
24
- execFile(
25
- getMdschemaBin(),
26
- args,
27
- { encoding: "utf-8", cwd: cwd2, timeout: 3e4 },
28
- (error, stdout, stderr) => {
29
- if (error) {
30
- const execError = error;
31
- resolve4({
32
- exitCode: execError.code === "ENOENT" ? 127 : execError.status ?? 1,
33
- stdout: stdout ?? "",
34
- stderr: stderr ?? ""
35
- });
36
- } else {
37
- resolve4({ exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
38
- }
39
- }
40
- );
41
- });
42
- }
43
-
44
- // src/schemas.ts
45
- import path3 from "path";
46
-
47
- // src/util.ts
48
9
  import path2 from "path";
49
- var PACKAGE_ROOT = path2.resolve(import.meta.dirname, "..");
10
+ import chalk from "chalk";
50
11
 
51
- // src/schemas.ts
52
- var SCHEMAS_ROOT = path3.join(PACKAGE_ROOT, "schemas");
53
- var MODULE_SCHEMAS = {
54
- module: path3.join(SCHEMAS_ROOT, "module", "module.yml"),
55
- command: path3.join(SCHEMAS_ROOT, "module", "command.yml"),
56
- model: path3.join(SCHEMAS_ROOT, "module", "model.yml"),
57
- feature: path3.join(SCHEMAS_ROOT, "module", "feature.yml")
58
- };
59
- var APP_COMPOSE_SCHEMAS = {
60
- requirements: path3.join(SCHEMAS_ROOT, "app-compose", "requirements.yml"),
61
- actors: path3.join(SCHEMAS_ROOT, "app-compose", "actors.yml"),
62
- "business-flow": path3.join(SCHEMAS_ROOT, "app-compose", "business-flow.yml"),
63
- story: path3.join(SCHEMAS_ROOT, "app-compose", "story.yml"),
64
- screen: path3.join(SCHEMAS_ROOT, "app-compose", "screen.yml"),
65
- resolver: path3.join(SCHEMAS_ROOT, "app-compose", "resolver.yml")
66
- };
67
- var ALL_SCHEMAS = {
68
- ...MODULE_SCHEMAS,
69
- ...APP_COMPOSE_SCHEMAS
70
- };
12
+ // src/util.ts
13
+ import path from "path";
14
+ var PACKAGE_ROOT = path.resolve(import.meta.dirname, "..");
71
15
 
72
- // src/commands/check.ts
73
- function buildCheckTargets(config) {
74
- const targets = [];
75
- if (config.modulesRoot) {
76
- const m = config.modulesRoot;
77
- targets.push(
78
- { glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
79
- { glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
80
- { glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
81
- { glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" }
82
- );
83
- }
84
- if (config.appRoot) {
85
- const a = config.appRoot;
86
- targets.push(
87
- { glob: `${a}/[a-zA-Z]*/README.md`, schemaKey: "requirements" },
88
- { glob: `${a}/[a-zA-Z]*/docs/actors/*.md`, schemaKey: "actors" },
89
- { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/README.md`, schemaKey: "business-flow" },
90
- { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/story/*.md`, schemaKey: "story" },
91
- { glob: `${a}/[a-zA-Z]*/docs/screen/*.md`, schemaKey: "screen" },
92
- { glob: `${a}/[a-zA-Z]*/docs/resolver/*.md`, schemaKey: "resolver" }
93
- );
94
- }
95
- return targets;
96
- }
97
- async function runCheck(config, cwd2) {
98
- const targets = buildCheckTargets(config);
99
- const results = await Promise.all(
100
- targets.map(async (target) => {
101
- const schemaPath = ALL_SCHEMAS[target.schemaKey];
102
- if (!schemaPath) {
103
- console.error(`Unknown schema key: ${target.schemaKey}`);
104
- return 2;
16
+ // src/commands/init.ts
17
+ var SKILLS_SRC = path2.join(PACKAGE_ROOT, "skills");
18
+ var FRAMEWORK_SKILLS = [
19
+ "1-module-docs",
20
+ "2-module-feature-breakdown",
21
+ "3-module-doc-review",
22
+ "4-module-tdd-implementation",
23
+ "5-module-implementation-review",
24
+ "app-compose-1-requirement-analysis",
25
+ "app-compose-2-requirements-breakdown",
26
+ "app-compose-3-doc-review",
27
+ "app-compose-4-design-mock",
28
+ "app-compose-5-design-mock-review",
29
+ "app-compose-6-implementation-spec",
30
+ "mock-scenario"
31
+ ];
32
+ function copyDirectoryRecursive(srcDir, destDir, force) {
33
+ let copied = 0;
34
+ let skipped = 0;
35
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
36
+ const srcPath = path2.join(srcDir, entry.name);
37
+ const destPath = path2.join(destDir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ const sub = copyDirectoryRecursive(srcPath, destPath, force);
40
+ copied += sub.copied;
41
+ skipped += sub.skipped;
42
+ } else {
43
+ if (!force && fs.existsSync(destPath)) {
44
+ skipped++;
45
+ continue;
105
46
  }
106
- const { exitCode, stdout, stderr } = await runMdschema(
107
- ["check", target.glob, "--schema", schemaPath],
108
- cwd2
109
- );
110
- if (stdout.trim()) console.log(stdout);
111
- if (stderr.trim()) console.error(stderr);
112
- return exitCode;
113
- })
114
- );
115
- if (results.includes(2)) return 2;
116
- return results.some((code) => code !== 0) ? 1 : 0;
117
- }
118
-
119
- // src/commands/sync-check.ts
120
- import path4 from "path";
121
- import fg from "fast-glob";
122
- import chalk from "chalk";
123
- function moduleCategories(root) {
124
- return [
125
- {
126
- name: "command",
127
- sourcePattern: `${root}/*/command/*.ts`,
128
- docPattern: `${root}/*/docs/commands/*.md`,
129
- exclusions: [/\.test\.ts$/]
130
- },
131
- {
132
- name: "model",
133
- sourcePattern: `${root}/*/db/*.ts`,
134
- docPattern: `${root}/*/docs/models/*.md`,
135
- exclusions: [/\.test\.ts$/, /^index\.ts$/]
47
+ fs.mkdirSync(destDir, { recursive: true });
48
+ fs.copyFileSync(srcPath, destPath);
49
+ copied++;
136
50
  }
137
- ];
51
+ }
52
+ return { copied, skipped };
138
53
  }
139
- function appComposeCategories(root) {
140
- return [
141
- {
142
- name: "resolver",
143
- sourcePattern: `${root}/*/backend/src/modules/**/resolvers/*.ts`,
144
- docPattern: `${root}/*/docs/resolver/*.md`,
145
- exclusions: [/\.test\.ts$/, /^index\.ts$/]
54
+ function runInit(cwd4, force) {
55
+ console.log(chalk.bold("erp-kit init\n"));
56
+ const skillsDest = path2.join(cwd4, ".agents", "skills");
57
+ let copiedCount = 0;
58
+ let skippedCount = 0;
59
+ for (const skill of FRAMEWORK_SKILLS) {
60
+ const srcSkillDir = path2.join(SKILLS_SRC, skill);
61
+ if (!fs.existsSync(srcSkillDir)) continue;
62
+ const destDir = path2.join(skillsDest, skill);
63
+ const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
64
+ copiedCount += result.copied;
65
+ if (result.skipped > 0) {
66
+ console.log(chalk.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
67
+ skippedCount += result.skipped;
146
68
  }
147
- ];
148
- }
149
- function shouldExclude(fileName, exclusions) {
150
- return exclusions.some((pattern) => pattern.test(fileName));
151
- }
152
- async function runSyncCheck(config, cwd2) {
153
- const errors = [];
154
- let totalSources = 0;
155
- let totalDocs = 0;
156
- const allCategories = [];
157
- if (config.modulesRoot) {
158
- allCategories.push(...moduleCategories(config.modulesRoot));
159
- }
160
- if (config.appRoot) {
161
- allCategories.push(...appComposeCategories(config.appRoot));
162
69
  }
163
- for (const category of allCategories) {
164
- const sources = await fg(category.sourcePattern, { cwd: cwd2 });
165
- const docs = await fg(category.docPattern, { cwd: cwd2 });
166
- const sourceBasenames = /* @__PURE__ */ new Map();
167
- const docBasenames = /* @__PURE__ */ new Map();
168
- for (const sourcePath of sources) {
169
- const fileName = path4.basename(sourcePath);
170
- if (shouldExclude(fileName, category.exclusions)) continue;
171
- const basename2 = path4.basename(sourcePath, path4.extname(sourcePath));
172
- sourceBasenames.set(basename2.toLowerCase(), sourcePath);
173
- }
174
- for (const docPath of docs) {
175
- const basename2 = path4.basename(docPath, path4.extname(docPath));
176
- docBasenames.set(basename2.toLowerCase(), docPath);
177
- }
178
- for (const [basename2, sourcePath] of sourceBasenames) {
179
- if (!docBasenames.has(basename2)) {
180
- errors.push({
181
- type: "missing-doc",
182
- category: category.name,
183
- sourcePath,
184
- expectedBasename: basename2
185
- });
186
- }
187
- }
188
- for (const [basename2, docPath] of docBasenames) {
189
- if (!sourceBasenames.has(basename2)) {
190
- errors.push({
191
- type: "orphaned-doc",
192
- category: category.name,
193
- docPath,
194
- expectedBasename: basename2
195
- });
196
- }
197
- }
198
- totalSources += sourceBasenames.size;
199
- totalDocs += docBasenames.size;
70
+ console.log(chalk.green(` Copied ${copiedCount} skill files to .agents/skills/`));
71
+ if (skippedCount > 0) {
72
+ console.log(
73
+ chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`)
74
+ );
200
75
  }
201
- return {
202
- exitCode: errors.length > 0 ? 1 : 0,
203
- errors,
204
- summary: {
205
- categoriesChecked: allCategories.length,
206
- totalSources,
207
- totalDocs
208
- }
209
- };
210
- }
211
- function formatSyncCheckReport(result) {
212
- const lines = [];
213
- lines.push(chalk.bold("docs-sync-check: Checking source-documentation correspondence...\n"));
214
- if (result.errors.length > 0) {
215
- lines.push(chalk.red.bold("Errors:\n"));
216
- const byCategory = /* @__PURE__ */ new Map();
217
- for (const error of result.errors) {
218
- const existing = byCategory.get(error.category) ?? [];
219
- existing.push(error);
220
- byCategory.set(error.category, existing);
76
+ const claudeSkills = path2.join(cwd4, ".claude", "skills");
77
+ const relTarget = path2.relative(path2.dirname(claudeSkills), skillsDest);
78
+ const claudeSkillsExists = (() => {
79
+ try {
80
+ fs.lstatSync(claudeSkills);
81
+ return true;
82
+ } catch {
83
+ return false;
221
84
  }
222
- for (const [category, categoryErrors] of byCategory) {
223
- lines.push(chalk.cyan(` Category: ${category}
224
- `));
225
- for (const error of categoryErrors) {
226
- if (error.type === "missing-doc") {
227
- lines.push(` ${chalk.red(error.sourcePath)}`);
228
- lines.push(` ${chalk.yellow("Missing documentation for:")} ${error.expectedBasename}`);
229
- } else {
230
- lines.push(` ${chalk.red(error.docPath)}`);
231
- lines.push(
232
- ` ${chalk.yellow("Orphaned documentation:")} no source file for ${error.expectedBasename}`
233
- );
234
- }
235
- lines.push("");
85
+ })();
86
+ if (claudeSkillsExists) {
87
+ const stat2 = fs.lstatSync(claudeSkills);
88
+ if (stat2.isSymbolicLink()) {
89
+ const current = fs.readlinkSync(claudeSkills);
90
+ if (current === relTarget) {
91
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (already linked)"));
92
+ } else if (force) {
93
+ fs.unlinkSync(claudeSkills);
94
+ fs.symlinkSync(relTarget, claudeSkills);
95
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (relinked)"));
96
+ } else {
97
+ console.log(
98
+ chalk.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`)
99
+ );
236
100
  }
101
+ } else {
102
+ console.log(chalk.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
237
103
  }
238
104
  } else {
239
- lines.push(chalk.green("All source files have corresponding documentation.\n"));
240
- }
241
- lines.push(chalk.bold("Summary:"));
242
- lines.push(` Categories checked: ${result.summary.categoriesChecked}`);
243
- lines.push(
244
- ` Source files: ${result.summary.totalSources}, Doc files: ${result.summary.totalDocs}`
245
- );
246
- if (result.errors.length > 0) {
247
- lines.push(chalk.red(` Errors: ${result.errors.length}`));
248
- lines.push("");
249
- lines.push(chalk.red.bold(`docs-sync-check failed with ${result.errors.length} error(s).`));
250
- } else {
251
- lines.push(chalk.green(" Errors: 0"));
252
- lines.push("");
253
- lines.push(chalk.green.bold("docs-sync-check passed."));
105
+ fs.mkdirSync(path2.dirname(claudeSkills), { recursive: true });
106
+ fs.symlinkSync(relTarget, claudeSkills);
107
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (linked)"));
254
108
  }
255
- return lines.join("\n");
256
- }
257
-
258
- // src/commands/scaffold.ts
259
- import path5 from "path";
260
- import fs2 from "fs";
261
- var MODULE_TYPES = ["module", "feature", "command", "model"];
262
- var APP_TYPES = [
263
- "requirements",
264
- "actors",
265
- "business-flow",
266
- "story",
267
- "screen",
268
- "resolver"
269
- ];
270
- var ALL_TYPES = [...MODULE_TYPES, ...APP_TYPES];
271
- var MODULE_DIR_MAP = {
272
- feature: "docs/features",
273
- command: "docs/commands",
274
- model: "docs/models"
275
- };
276
- var APP_DIR_MAP = {
277
- actors: "docs/actors",
278
- "business-flow": "docs/business-flow",
279
- screen: "docs/screen",
280
- resolver: "docs/resolver"
281
- };
282
- function isModuleType(type) {
283
- return MODULE_TYPES.includes(type);
284
- }
285
- function resolveScaffoldPath(type, parentName, name, root) {
286
- if (type === "module" || type === "requirements") {
287
- return path5.join(root, parentName, "README.md");
288
- }
289
- if (!name) {
290
- throw new Error(`Name is required for scaffold type "${type}"`);
291
- }
292
- if (type === "business-flow") {
293
- return path5.join(root, parentName, "docs/business-flow", name, "README.md");
294
- }
295
- if (type === "story") {
296
- const parts = name.split("/");
297
- if (parts.length !== 2) {
298
- throw new Error(
299
- `Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`
300
- );
301
- }
302
- return path5.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
303
- }
304
- if (MODULE_DIR_MAP[type]) {
305
- return path5.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
306
- }
307
- if (APP_DIR_MAP[type]) {
308
- return path5.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
309
- }
310
- throw new Error(`Unknown scaffold type: ${type}`);
311
- }
312
- async function runScaffold(type, parentName, name, root, cwd2) {
313
- const outputPath = resolveScaffoldPath(type, parentName, name, root);
314
- const absoluteOutput = path5.resolve(cwd2, outputPath);
315
- if (fs2.existsSync(absoluteOutput)) {
316
- console.error(`File already exists: ${outputPath}`);
317
- return 1;
318
- }
319
- const schemaPath = ALL_SCHEMAS[type];
320
- if (!schemaPath) {
321
- console.error(`No schema found for type: ${type}`);
322
- return 2;
323
- }
324
- try {
325
- fs2.mkdirSync(path5.dirname(absoluteOutput), { recursive: true });
326
- } catch (err) {
327
- console.error(
328
- `Failed to create directory: ${err instanceof Error ? err.message : String(err)}`
329
- );
330
- return 1;
331
- }
332
- const { exitCode, stdout, stderr } = await runMdschema(
333
- ["generate", "--schema", schemaPath, "--output", absoluteOutput],
334
- cwd2
335
- );
336
- if (stdout.trim()) console.log(stdout);
337
- if (stderr.trim()) console.error(stderr);
338
- return exitCode;
339
- }
340
-
341
- // src/commands/init.ts
342
- import fs3 from "fs";
343
- import path6 from "path";
344
- import chalk2 from "chalk";
345
- var SKILLS_SRC = path6.join(PACKAGE_ROOT, "skills");
346
- var FRAMEWORK_SKILLS = [
347
- "1-module-docs",
348
- "2-module-feature-breakdown",
349
- "3-module-doc-review",
350
- "4-module-tdd-implementation",
351
- "5-module-implementation-review",
352
- "app-compose-1-requirement-analysis",
353
- "app-compose-2-requirements-breakdown",
354
- "app-compose-3-doc-review",
355
- "app-compose-4-design-mock",
356
- "app-compose-5-design-mock-review",
357
- "app-compose-6-implementation-spec",
358
- "mock-scenario"
359
- ];
360
- function copyDirectoryRecursive(srcDir, destDir, force) {
361
- let copied = 0;
362
- let skipped = 0;
363
- for (const entry of fs3.readdirSync(srcDir, { withFileTypes: true })) {
364
- const srcPath = path6.join(srcDir, entry.name);
365
- const destPath = path6.join(destDir, entry.name);
366
- if (entry.isDirectory()) {
367
- const sub = copyDirectoryRecursive(srcPath, destPath, force);
368
- copied += sub.copied;
369
- skipped += sub.skipped;
370
- } else {
371
- if (!force && fs3.existsSync(destPath)) {
372
- skipped++;
373
- continue;
374
- }
375
- fs3.mkdirSync(destDir, { recursive: true });
376
- fs3.copyFileSync(srcPath, destPath);
377
- copied++;
378
- }
379
- }
380
- return { copied, skipped };
381
- }
382
- function runInit(cwd2, force) {
383
- console.log(chalk2.bold("erp-kit init\n"));
384
- const skillsDest = path6.join(cwd2, ".agents", "skills");
385
- let copiedCount = 0;
386
- let skippedCount = 0;
387
- for (const skill of FRAMEWORK_SKILLS) {
388
- const srcSkillDir = path6.join(SKILLS_SRC, skill);
389
- if (!fs3.existsSync(srcSkillDir)) continue;
390
- const destDir = path6.join(skillsDest, skill);
391
- const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
392
- copiedCount += result.copied;
393
- if (result.skipped > 0) {
394
- console.log(chalk2.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
395
- skippedCount += result.skipped;
396
- }
397
- }
398
- console.log(chalk2.green(` Copied ${copiedCount} skill files to .agents/skills/`));
399
- if (skippedCount > 0) {
400
- console.log(
401
- chalk2.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`)
402
- );
403
- }
404
- const claudeSkills = path6.join(cwd2, ".claude", "skills");
405
- const relTarget = path6.relative(path6.dirname(claudeSkills), skillsDest);
406
- const claudeSkillsExists = (() => {
407
- try {
408
- fs3.lstatSync(claudeSkills);
409
- return true;
410
- } catch {
411
- return false;
412
- }
413
- })();
414
- if (claudeSkillsExists) {
415
- const stat2 = fs3.lstatSync(claudeSkills);
416
- if (stat2.isSymbolicLink()) {
417
- const current = fs3.readlinkSync(claudeSkills);
418
- if (current === relTarget) {
419
- console.log(chalk2.green(" .claude/skills -> .agents/skills/ (already linked)"));
420
- } else if (force) {
421
- fs3.unlinkSync(claudeSkills);
422
- fs3.symlinkSync(relTarget, claudeSkills);
423
- console.log(chalk2.green(" .claude/skills -> .agents/skills/ (relinked)"));
424
- } else {
425
- console.log(
426
- chalk2.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`)
427
- );
428
- }
429
- } else {
430
- console.log(chalk2.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
431
- }
432
- } else {
433
- fs3.mkdirSync(path6.dirname(claudeSkills), { recursive: true });
434
- fs3.symlinkSync(relTarget, claudeSkills);
435
- console.log(chalk2.green(" .claude/skills -> .agents/skills/ (linked)"));
436
- }
437
- console.log(chalk2.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
438
- return 0;
109
+ console.log(chalk.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
110
+ return 0;
439
111
  }
440
112
 
441
113
  // src/commands/mock/index.ts
@@ -620,12 +292,12 @@ Reverse proxy listening on http://localhost:${port}`);
620
292
  // src/commands/mock/validate.ts
621
293
  import { readdir, readFile, stat } from "fs/promises";
622
294
  import { join as join2, resolve as resolve3, relative as relative2, dirname as dirname2, basename } from "path";
623
- import chalk3 from "chalk";
295
+ import chalk2 from "chalk";
624
296
  function pass(msg) {
625
- console.log(chalk3.green(`\u2713 ${msg}`));
297
+ console.log(chalk2.green(`\u2713 ${msg}`));
626
298
  }
627
299
  function fail(ctx, msg) {
628
- console.log(chalk3.red(`\u2717 ${msg}`));
300
+ console.log(chalk2.red(`\u2717 ${msg}`));
629
301
  ctx.failures++;
630
302
  }
631
303
  async function subdirs(dir) {
@@ -683,232 +355,680 @@ function checkResponseQuality(ctx, data, label) {
683
355
  }
684
356
  }
685
357
  }
686
- function checkDatabucketRefs(ctx, data, label) {
687
- const bucketIds = new Set((data.data ?? []).map((d) => d.id));
688
- for (const route of data.routes ?? []) {
689
- for (const resp of route.responses ?? []) {
690
- if (resp.bodyType === "DATABUCKET") {
691
- const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
692
- if (resp.databucketID && bucketIds.has(resp.databucketID)) {
693
- pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
358
+ function checkDatabucketRefs(ctx, data, label) {
359
+ const bucketIds = new Set((data.data ?? []).map((d) => d.id));
360
+ for (const route of data.routes ?? []) {
361
+ for (const resp of route.responses ?? []) {
362
+ if (resp.bodyType === "DATABUCKET") {
363
+ const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
364
+ if (resp.databucketID && bucketIds.has(resp.databucketID)) {
365
+ pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
366
+ } else {
367
+ fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ async function discoverAllScenarios(mocksDir) {
374
+ const scenarios = [];
375
+ const providers = await subdirs(mocksDir);
376
+ for (const provider of providers) {
377
+ const providerDir = join2(mocksDir, provider);
378
+ for (const scenario of await subdirs(providerDir)) {
379
+ scenarios.push(`${provider}/${scenario}`);
380
+ }
381
+ }
382
+ return scenarios;
383
+ }
384
+ function resolveScenarioDir(arg5) {
385
+ const abs = resolve3(arg5);
386
+ if (basename(abs) === "mock.json") return dirname2(abs);
387
+ return abs;
388
+ }
389
+ async function validateScenario(ctx, scenarioDir, mocksDir) {
390
+ const label = relative2(mocksDir, scenarioDir);
391
+ console.log(chalk2.bold(`
392
+ \u2500\u2500 ${label} \u2500\u2500`));
393
+ let entries;
394
+ try {
395
+ entries = await readdir(scenarioDir);
396
+ } catch {
397
+ fail(ctx, `${label}: directory not found`);
398
+ return;
399
+ }
400
+ const hasMock = checkStructure(ctx, entries, label);
401
+ if (!hasMock) return;
402
+ const mockPath = join2(scenarioDir, "mock.json");
403
+ let data;
404
+ try {
405
+ data = JSON.parse(await readFile(mockPath, "utf-8"));
406
+ } catch (err) {
407
+ const errMsg = err instanceof Error ? err.message : String(err);
408
+ fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
409
+ return;
410
+ }
411
+ await checkSchema(ctx, data, label);
412
+ checkResponseQuality(ctx, data, label);
413
+ checkDatabucketRefs(ctx, data, label);
414
+ }
415
+ async function runMockValidate(mocksRoot, paths) {
416
+ const ctx = { failures: 0 };
417
+ const mocksDir = resolve3(mocksRoot);
418
+ const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join2(mocksDir, s));
419
+ if (targets.length === 0) {
420
+ fail(ctx, "No scenarios found under mocks/");
421
+ return 1;
422
+ }
423
+ console.log(chalk2.bold("\nValidating mock configs...\n"));
424
+ for (const target of targets) {
425
+ await validateScenario(ctx, target, mocksDir);
426
+ }
427
+ console.log(chalk2.bold("\n\u2500\u2500 summary \u2500\u2500"));
428
+ if (ctx.failures === 0) {
429
+ console.log(chalk2.green("\u2713 All checks passed"));
430
+ } else {
431
+ console.log(chalk2.red(`\u2717 ${ctx.failures} check(s) failed`));
432
+ }
433
+ return ctx.failures === 0 ? 0 : 1;
434
+ }
435
+
436
+ // src/commands/mock/index.ts
437
+ var startCommand = defineCommand({
438
+ name: "start",
439
+ description: "Start mock API servers with reverse proxy",
440
+ args: z.object({
441
+ mocksRoot: arg(z.string().default("./mocks"), {
442
+ description: "Path to mocks directory"
443
+ }),
444
+ port: arg(z.coerce.number().default(3e3), {
445
+ alias: "p",
446
+ description: "Reverse proxy port"
447
+ }),
448
+ filter: arg(z.array(z.string()).default([]), {
449
+ positional: true,
450
+ description: "Filter by provider or provider/scenario"
451
+ })
452
+ }),
453
+ run: async (args) => {
454
+ const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
455
+ process.exit(exitCode);
456
+ }
457
+ });
458
+ var validateCommand = defineCommand({
459
+ name: "validate",
460
+ description: "Validate mock scenario configs",
461
+ args: z.object({
462
+ mocksRoot: arg(z.string().default("./mocks"), {
463
+ description: "Path to mocks directory"
464
+ }),
465
+ paths: arg(z.array(z.string()).default([]), {
466
+ positional: true,
467
+ description: "Specific scenario paths to validate"
468
+ })
469
+ }),
470
+ run: async (args) => {
471
+ const exitCode = await runMockValidate(args.mocksRoot, args.paths);
472
+ process.exit(exitCode);
473
+ }
474
+ });
475
+ var mockCommand = defineCommand({
476
+ name: "mock",
477
+ description: "Mock API server management",
478
+ subCommands: {
479
+ start: startCommand,
480
+ validate: validateCommand
481
+ }
482
+ });
483
+
484
+ // src/commands/module/index.ts
485
+ import { z as z2 } from "zod";
486
+ import { defineCommand as defineCommand2, arg as arg2 } from "politty";
487
+
488
+ // src/mdschema.ts
489
+ import path3 from "path";
490
+ import fs2 from "fs";
491
+ import { execFile } from "child_process";
492
+ import { createRequire } from "module";
493
+ var require2 = createRequire(import.meta.url);
494
+ function getMdschemaBin() {
495
+ const pkgPath = require2.resolve("@jackchuka/mdschema/package.json");
496
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
497
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.mdschema;
498
+ if (!bin) {
499
+ throw new Error("Could not resolve mdschema binary from package.json bin field");
500
+ }
501
+ return path3.join(path3.dirname(pkgPath), bin);
502
+ }
503
+ function runMdschema(args, cwd4) {
504
+ return new Promise((resolve4) => {
505
+ execFile(
506
+ getMdschemaBin(),
507
+ args,
508
+ { encoding: "utf-8", cwd: cwd4, timeout: 3e4 },
509
+ (error, stdout, stderr) => {
510
+ if (error) {
511
+ const execError = error;
512
+ resolve4({
513
+ exitCode: execError.code === "ENOENT" ? 127 : execError.status ?? 1,
514
+ stdout: stdout ?? "",
515
+ stderr: stderr ?? ""
516
+ });
517
+ } else {
518
+ resolve4({ exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
519
+ }
520
+ }
521
+ );
522
+ });
523
+ }
524
+
525
+ // src/schemas.ts
526
+ import path4 from "path";
527
+ var SCHEMAS_ROOT = path4.join(PACKAGE_ROOT, "schemas");
528
+ var MODULE_SCHEMAS = {
529
+ module: path4.join(SCHEMAS_ROOT, "module", "module.yml"),
530
+ command: path4.join(SCHEMAS_ROOT, "module", "command.yml"),
531
+ model: path4.join(SCHEMAS_ROOT, "module", "model.yml"),
532
+ feature: path4.join(SCHEMAS_ROOT, "module", "feature.yml"),
533
+ query: path4.join(SCHEMAS_ROOT, "module", "query.yml")
534
+ };
535
+ var APP_COMPOSE_SCHEMAS = {
536
+ requirements: path4.join(SCHEMAS_ROOT, "app-compose", "requirements.yml"),
537
+ actors: path4.join(SCHEMAS_ROOT, "app-compose", "actors.yml"),
538
+ "business-flow": path4.join(SCHEMAS_ROOT, "app-compose", "business-flow.yml"),
539
+ story: path4.join(SCHEMAS_ROOT, "app-compose", "story.yml"),
540
+ screen: path4.join(SCHEMAS_ROOT, "app-compose", "screen.yml"),
541
+ resolver: path4.join(SCHEMAS_ROOT, "app-compose", "resolver.yml")
542
+ };
543
+ var ALL_SCHEMAS = {
544
+ ...MODULE_SCHEMAS,
545
+ ...APP_COMPOSE_SCHEMAS
546
+ };
547
+
548
+ // src/commands/check.ts
549
+ function buildCheckTargets(config) {
550
+ const targets = [];
551
+ if (config.modulesRoot) {
552
+ const m = config.modulesRoot;
553
+ targets.push(
554
+ { glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
555
+ { glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
556
+ { glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
557
+ { glob: `${m}/[a-zA-Z]*/docs/queries/*.md`, schemaKey: "query" },
558
+ { glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" }
559
+ );
560
+ }
561
+ if (config.appRoot) {
562
+ const a = config.appRoot;
563
+ targets.push(
564
+ { glob: `${a}/[a-zA-Z]*/README.md`, schemaKey: "requirements" },
565
+ { glob: `${a}/[a-zA-Z]*/docs/actors/*.md`, schemaKey: "actors" },
566
+ { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/README.md`, schemaKey: "business-flow" },
567
+ { glob: `${a}/[a-zA-Z]*/docs/business-flow/*/story/*.md`, schemaKey: "story" },
568
+ { glob: `${a}/[a-zA-Z]*/docs/screen/*.md`, schemaKey: "screen" },
569
+ { glob: `${a}/[a-zA-Z]*/docs/resolver/*.md`, schemaKey: "resolver" }
570
+ );
571
+ }
572
+ return targets;
573
+ }
574
+ async function runCheck(config, cwd4) {
575
+ const targets = buildCheckTargets(config);
576
+ const results = await Promise.all(
577
+ targets.map(async (target) => {
578
+ const schemaPath = ALL_SCHEMAS[target.schemaKey];
579
+ if (!schemaPath) {
580
+ console.error(`Unknown schema key: ${target.schemaKey}`);
581
+ return 2;
582
+ }
583
+ const { exitCode, stdout, stderr } = await runMdschema(
584
+ ["check", target.glob, "--schema", schemaPath],
585
+ cwd4
586
+ );
587
+ if (stdout.trim()) console.log(stdout);
588
+ if (stderr.trim()) console.error(stderr);
589
+ return exitCode;
590
+ })
591
+ );
592
+ if (results.includes(2)) return 2;
593
+ return results.some((code) => code !== 0) ? 1 : 0;
594
+ }
595
+
596
+ // src/commands/sync-check.ts
597
+ import path5 from "path";
598
+ import fg from "fast-glob";
599
+ import chalk3 from "chalk";
600
+ function moduleCategories(root) {
601
+ return [
602
+ {
603
+ name: "command",
604
+ sourcePattern: `${root}/*/command/*.ts`,
605
+ docPattern: `${root}/*/docs/commands/*.md`,
606
+ exclusions: [/\.test\.ts$/]
607
+ },
608
+ {
609
+ name: "model",
610
+ sourcePattern: `${root}/*/db/*.ts`,
611
+ docPattern: `${root}/*/docs/models/*.md`,
612
+ exclusions: [/\.test\.ts$/, /^index\.ts$/]
613
+ },
614
+ {
615
+ name: "query",
616
+ sourcePattern: `${root}/*/query/*.ts`,
617
+ docPattern: `${root}/*/docs/queries/*.md`,
618
+ exclusions: [/\.test\.ts$/]
619
+ }
620
+ ];
621
+ }
622
+ function appComposeCategories(root) {
623
+ return [
624
+ {
625
+ name: "resolver",
626
+ sourcePattern: `${root}/*/backend/src/modules/**/resolvers/*.ts`,
627
+ docPattern: `${root}/*/docs/resolver/*.md`,
628
+ exclusions: [/\.test\.ts$/, /^index\.ts$/]
629
+ }
630
+ ];
631
+ }
632
+ function shouldExclude(fileName, exclusions) {
633
+ return exclusions.some((pattern) => pattern.test(fileName));
634
+ }
635
+ async function runSyncCheck(config, cwd4) {
636
+ const errors = [];
637
+ let totalSources = 0;
638
+ let totalDocs = 0;
639
+ const allCategories = [];
640
+ if (config.modulesRoot) {
641
+ allCategories.push(...moduleCategories(config.modulesRoot));
642
+ }
643
+ if (config.appRoot) {
644
+ allCategories.push(...appComposeCategories(config.appRoot));
645
+ }
646
+ for (const category of allCategories) {
647
+ const sources = await fg(category.sourcePattern, { cwd: cwd4 });
648
+ const docs = await fg(category.docPattern, { cwd: cwd4 });
649
+ const sourceBasenames = /* @__PURE__ */ new Map();
650
+ const docBasenames = /* @__PURE__ */ new Map();
651
+ for (const sourcePath of sources) {
652
+ const fileName = path5.basename(sourcePath);
653
+ if (shouldExclude(fileName, category.exclusions)) continue;
654
+ const basename2 = path5.basename(sourcePath, path5.extname(sourcePath));
655
+ sourceBasenames.set(basename2.toLowerCase(), sourcePath);
656
+ }
657
+ for (const docPath of docs) {
658
+ const basename2 = path5.basename(docPath, path5.extname(docPath));
659
+ docBasenames.set(basename2.toLowerCase(), docPath);
660
+ }
661
+ for (const [basename2, sourcePath] of sourceBasenames) {
662
+ if (!docBasenames.has(basename2)) {
663
+ errors.push({
664
+ type: "missing-doc",
665
+ category: category.name,
666
+ sourcePath,
667
+ expectedBasename: basename2
668
+ });
669
+ }
670
+ }
671
+ for (const [basename2, docPath] of docBasenames) {
672
+ if (!sourceBasenames.has(basename2)) {
673
+ errors.push({
674
+ type: "orphaned-doc",
675
+ category: category.name,
676
+ docPath,
677
+ expectedBasename: basename2
678
+ });
679
+ }
680
+ }
681
+ totalSources += sourceBasenames.size;
682
+ totalDocs += docBasenames.size;
683
+ }
684
+ return {
685
+ exitCode: errors.length > 0 ? 1 : 0,
686
+ errors,
687
+ summary: {
688
+ categoriesChecked: allCategories.length,
689
+ totalSources,
690
+ totalDocs
691
+ }
692
+ };
693
+ }
694
+ function formatSyncCheckReport(result) {
695
+ const lines = [];
696
+ lines.push(chalk3.bold("docs-sync-check: Checking source-documentation correspondence...\n"));
697
+ if (result.errors.length > 0) {
698
+ lines.push(chalk3.red.bold("Errors:\n"));
699
+ const byCategory = /* @__PURE__ */ new Map();
700
+ for (const error of result.errors) {
701
+ const existing = byCategory.get(error.category) ?? [];
702
+ existing.push(error);
703
+ byCategory.set(error.category, existing);
704
+ }
705
+ for (const [category, categoryErrors] of byCategory) {
706
+ lines.push(chalk3.cyan(` Category: ${category}
707
+ `));
708
+ for (const error of categoryErrors) {
709
+ if (error.type === "missing-doc") {
710
+ lines.push(` ${chalk3.red(error.sourcePath)}`);
711
+ lines.push(` ${chalk3.yellow("Missing documentation for:")} ${error.expectedBasename}`);
694
712
  } else {
695
- fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
713
+ lines.push(` ${chalk3.red(error.docPath)}`);
714
+ lines.push(
715
+ ` ${chalk3.yellow("Orphaned documentation:")} no source file for ${error.expectedBasename}`
716
+ );
696
717
  }
718
+ lines.push("");
697
719
  }
698
720
  }
721
+ } else {
722
+ lines.push(chalk3.green("All source files have corresponding documentation.\n"));
723
+ }
724
+ lines.push(chalk3.bold("Summary:"));
725
+ lines.push(` Categories checked: ${result.summary.categoriesChecked}`);
726
+ lines.push(
727
+ ` Source files: ${result.summary.totalSources}, Doc files: ${result.summary.totalDocs}`
728
+ );
729
+ if (result.errors.length > 0) {
730
+ lines.push(chalk3.red(` Errors: ${result.errors.length}`));
731
+ lines.push("");
732
+ lines.push(chalk3.red.bold(`docs-sync-check failed with ${result.errors.length} error(s).`));
733
+ } else {
734
+ lines.push(chalk3.green(" Errors: 0"));
735
+ lines.push("");
736
+ lines.push(chalk3.green.bold("docs-sync-check passed."));
699
737
  }
738
+ return lines.join("\n");
700
739
  }
701
- async function discoverAllScenarios(mocksDir) {
702
- const scenarios = [];
703
- const providers = await subdirs(mocksDir);
704
- for (const provider of providers) {
705
- const providerDir = join2(mocksDir, provider);
706
- for (const scenario of await subdirs(providerDir)) {
707
- scenarios.push(`${provider}/${scenario}`);
740
+
741
+ // src/commands/scaffold.ts
742
+ import path6 from "path";
743
+ import fs3 from "fs";
744
+ var MODULE_TYPES = ["module", "feature", "command", "model", "query"];
745
+ var APP_TYPES = [
746
+ "requirements",
747
+ "actors",
748
+ "business-flow",
749
+ "story",
750
+ "screen",
751
+ "resolver"
752
+ ];
753
+ var ALL_TYPES = [...MODULE_TYPES, ...APP_TYPES];
754
+ var MODULE_DIR_MAP = {
755
+ feature: "docs/features",
756
+ command: "docs/commands",
757
+ model: "docs/models",
758
+ query: "docs/queries"
759
+ };
760
+ var APP_DIR_MAP = {
761
+ actors: "docs/actors",
762
+ "business-flow": "docs/business-flow",
763
+ screen: "docs/screen",
764
+ resolver: "docs/resolver"
765
+ };
766
+ function resolveScaffoldPath(type, parentName, name, root) {
767
+ if (type === "module" || type === "requirements") {
768
+ return path6.join(root, parentName, "README.md");
769
+ }
770
+ if (!name) {
771
+ throw new Error(`Name is required for scaffold type "${type}"`);
772
+ }
773
+ if (type === "business-flow") {
774
+ return path6.join(root, parentName, "docs/business-flow", name, "README.md");
775
+ }
776
+ if (type === "story") {
777
+ const parts = name.split("/");
778
+ if (parts.length !== 2) {
779
+ throw new Error(
780
+ `Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`
781
+ );
708
782
  }
783
+ return path6.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
709
784
  }
710
- return scenarios;
711
- }
712
- function resolveScenarioDir(arg3) {
713
- const abs = resolve3(arg3);
714
- if (basename(abs) === "mock.json") return dirname2(abs);
715
- return abs;
716
- }
717
- async function validateScenario(ctx, scenarioDir, mocksDir) {
718
- const label = relative2(mocksDir, scenarioDir);
719
- console.log(chalk3.bold(`
720
- \u2500\u2500 ${label} \u2500\u2500`));
721
- let entries;
722
- try {
723
- entries = await readdir(scenarioDir);
724
- } catch {
725
- fail(ctx, `${label}: directory not found`);
726
- return;
785
+ if (MODULE_DIR_MAP[type]) {
786
+ return path6.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
727
787
  }
728
- const hasMock = checkStructure(ctx, entries, label);
729
- if (!hasMock) return;
730
- const mockPath = join2(scenarioDir, "mock.json");
731
- let data;
732
- try {
733
- data = JSON.parse(await readFile(mockPath, "utf-8"));
734
- } catch (err) {
735
- const errMsg = err instanceof Error ? err.message : String(err);
736
- fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
737
- return;
788
+ if (APP_DIR_MAP[type]) {
789
+ return path6.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
738
790
  }
739
- await checkSchema(ctx, data, label);
740
- checkResponseQuality(ctx, data, label);
741
- checkDatabucketRefs(ctx, data, label);
791
+ throw new Error(`Unknown scaffold type: ${type}`);
742
792
  }
743
- async function runMockValidate(mocksRoot, paths) {
744
- const ctx = { failures: 0 };
745
- const mocksDir = resolve3(mocksRoot);
746
- const targets = paths.length > 0 ? paths.map(resolveScenarioDir) : (await discoverAllScenarios(mocksDir)).map((s) => join2(mocksDir, s));
747
- if (targets.length === 0) {
748
- fail(ctx, "No scenarios found under mocks/");
793
+ async function runScaffold(type, parentName, name, root, cwd4) {
794
+ const outputPath = resolveScaffoldPath(type, parentName, name, root);
795
+ const absoluteOutput = path6.resolve(cwd4, outputPath);
796
+ if (fs3.existsSync(absoluteOutput)) {
797
+ console.error(`File already exists: ${outputPath}`);
749
798
  return 1;
750
799
  }
751
- console.log(chalk3.bold("\nValidating mock configs...\n"));
752
- for (const target of targets) {
753
- await validateScenario(ctx, target, mocksDir);
800
+ const schemaPath = ALL_SCHEMAS[type];
801
+ if (!schemaPath) {
802
+ console.error(`No schema found for type: ${type}`);
803
+ return 2;
754
804
  }
755
- console.log(chalk3.bold("\n\u2500\u2500 summary \u2500\u2500"));
756
- if (ctx.failures === 0) {
757
- console.log(chalk3.green("\u2713 All checks passed"));
758
- } else {
759
- console.log(chalk3.red(`\u2717 ${ctx.failures} check(s) failed`));
805
+ try {
806
+ fs3.mkdirSync(path6.dirname(absoluteOutput), { recursive: true });
807
+ } catch (err) {
808
+ console.error(
809
+ `Failed to create directory: ${err instanceof Error ? err.message : String(err)}`
810
+ );
811
+ return 1;
760
812
  }
761
- return ctx.failures === 0 ? 0 : 1;
813
+ const { exitCode, stdout, stderr } = await runMdschema(
814
+ ["generate", "--schema", schemaPath, "--output", absoluteOutput],
815
+ cwd4
816
+ );
817
+ if (stdout.trim()) console.log(stdout);
818
+ if (stderr.trim()) console.error(stderr);
819
+ return exitCode;
762
820
  }
763
821
 
764
- // src/commands/mock/index.ts
765
- var startCommand = defineCommand({
766
- name: "start",
767
- description: "Start mock API servers with reverse proxy",
768
- args: z.object({
769
- mocksRoot: arg(z.string().default("./mocks"), {
770
- description: "Path to mocks directory"
771
- }),
772
- port: arg(z.coerce.number().default(3e3), {
773
- alias: "p",
774
- description: "Reverse proxy port"
775
- }),
776
- filter: arg(z.array(z.string()).default([]), {
777
- positional: true,
778
- description: "Filter by provider or provider/scenario"
779
- })
780
- }),
822
+ // src/commands/module/list.ts
823
+ import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
824
+ import { join as join3 } from "path";
825
+ import chalk4 from "chalk";
826
+ var MODULES_DIR = join3(PACKAGE_ROOT, "src", "modules");
827
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["shared", "testing"]);
828
+ function countFiles(dir, pattern, exclusions) {
829
+ if (!existsSync2(dir)) return 0;
830
+ return readdirSync2(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f))).length;
831
+ }
832
+ function listModules() {
833
+ if (!existsSync2(MODULES_DIR)) return [];
834
+ return readdirSync2(MODULES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name)).map((d) => {
835
+ const modDir = join3(MODULES_DIR, d.name);
836
+ return {
837
+ name: d.name,
838
+ commands: countFiles(join3(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
839
+ models: countFiles(join3(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
840
+ features: countFiles(join3(modDir, "docs", "features"), /\.md$/, [])
841
+ };
842
+ }).sort((a, b) => a.name.localeCompare(b.name));
843
+ }
844
+ function formatModuleList(modules) {
845
+ if (modules.length === 0) return "No modules found.";
846
+ const lines = [];
847
+ lines.push(chalk4.bold("Modules:\n"));
848
+ const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
849
+ for (const mod of modules) {
850
+ const counts = [
851
+ `${mod.commands} commands`,
852
+ `${mod.models} models`,
853
+ `${mod.features} features`
854
+ ].join(", ");
855
+ lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
856
+ }
857
+ lines.push("");
858
+ lines.push(`${modules.length} modules`);
859
+ return lines.join("\n");
860
+ }
861
+ function runModuleList() {
862
+ const modules = listModules();
863
+ console.log(formatModuleList(modules));
864
+ return 0;
865
+ }
866
+
867
+ // src/commands/module/index.ts
868
+ var cwd = process.cwd();
869
+ var rootArgs = z2.object({
870
+ root: arg2(z2.string(), {
871
+ alias: "r",
872
+ description: "Path to modules directory"
873
+ })
874
+ });
875
+ var listCommand = defineCommand2({
876
+ name: "list",
877
+ description: "List available modules",
878
+ run: () => {
879
+ const exitCode = runModuleList();
880
+ process.exit(exitCode);
881
+ }
882
+ });
883
+ var checkCommand = defineCommand2({
884
+ name: "check",
885
+ description: "Validate module docs against schemas",
886
+ args: rootArgs,
781
887
  run: async (args) => {
782
- const exitCode = await runMockStart(args.mocksRoot, args.filter, args.port);
888
+ const exitCode = await runCheck({ modulesRoot: args.root }, cwd);
783
889
  process.exit(exitCode);
784
890
  }
785
891
  });
786
- var validateCommand = defineCommand({
787
- name: "validate",
788
- description: "Validate mock scenario configs",
789
- args: z.object({
790
- mocksRoot: arg(z.string().default("./mocks"), {
791
- description: "Path to mocks directory"
892
+ var syncCheckCommand = defineCommand2({
893
+ name: "sync-check",
894
+ description: "Validate source <-> doc correspondence",
895
+ args: rootArgs,
896
+ run: async (args) => {
897
+ const result = await runSyncCheck({ modulesRoot: args.root }, cwd);
898
+ console.log(formatSyncCheckReport(result));
899
+ process.exit(result.exitCode);
900
+ }
901
+ });
902
+ var scaffoldCommand = defineCommand2({
903
+ name: "scaffold",
904
+ description: "Generate module doc from schema template",
905
+ args: rootArgs.extend({
906
+ type: arg2(z2.enum(MODULE_TYPES), {
907
+ positional: true,
908
+ description: `Scaffold type (${MODULE_TYPES.join(", ")})`
792
909
  }),
793
- paths: arg(z.array(z.string()).default([]), {
910
+ parent: arg2(z2.string(), {
794
911
  positional: true,
795
- description: "Specific scenario paths to validate"
912
+ description: "Module name"
913
+ }),
914
+ name: arg2(z2.string().optional(), {
915
+ positional: true,
916
+ description: "Item name (required for feature, command, model)"
796
917
  })
797
918
  }),
798
919
  run: async (args) => {
799
- const exitCode = await runMockValidate(args.mocksRoot, args.paths);
920
+ const exitCode = await runScaffold(
921
+ args.type,
922
+ args.parent,
923
+ args.name,
924
+ args.root,
925
+ cwd
926
+ );
800
927
  process.exit(exitCode);
801
928
  }
802
929
  });
803
- var mockCommand = defineCommand({
804
- name: "mock",
805
- description: "Mock API server management",
930
+ var moduleCommand = defineCommand2({
931
+ name: "module",
932
+ description: "Module management",
806
933
  subCommands: {
807
- start: startCommand,
808
- validate: validateCommand
934
+ list: listCommand,
935
+ check: checkCommand,
936
+ "sync-check": syncCheckCommand,
937
+ scaffold: scaffoldCommand
809
938
  }
810
939
  });
811
940
 
812
- // src/cli.ts
813
- var cwd = process.cwd();
814
- var rootArgs = z2.object({
815
- modulesRoot: arg2(z2.string().optional(), {
816
- alias: "m",
817
- description: "Path to modules directory"
818
- }),
819
- appRoot: arg2(z2.string().optional(), {
820
- alias: "a",
821
- description: "Path to app-compose directory (apps/ or examples/)"
941
+ // src/commands/app/index.ts
942
+ import { z as z3 } from "zod";
943
+ import { defineCommand as defineCommand3, arg as arg3 } from "politty";
944
+ var cwd2 = process.cwd();
945
+ var rootArgs2 = z3.object({
946
+ root: arg3(z3.string(), {
947
+ alias: "r",
948
+ description: "Path to app-compose directory"
822
949
  })
823
950
  });
824
- function requireRoot(args) {
825
- const paths = { modulesRoot: args.modulesRoot, appRoot: args.appRoot };
826
- if (!paths.modulesRoot && !paths.appRoot) {
827
- console.error("At least one of --modules-root or --app-root is required.");
828
- process.exit(2);
829
- }
830
- return paths;
831
- }
832
- var checkCommand = defineCommand2({
951
+ var checkCommand2 = defineCommand3({
833
952
  name: "check",
834
- description: "Validate docs against schemas",
835
- args: rootArgs,
953
+ description: "Validate app docs against schemas",
954
+ args: rootArgs2,
836
955
  run: async (args) => {
837
- const paths = requireRoot(args);
838
- const exitCode = await runCheck(paths, cwd);
956
+ const exitCode = await runCheck({ appRoot: args.root }, cwd2);
839
957
  process.exit(exitCode);
840
958
  }
841
959
  });
842
- var syncCheckCommand = defineCommand2({
960
+ var syncCheckCommand2 = defineCommand3({
843
961
  name: "sync-check",
844
962
  description: "Validate source <-> doc correspondence",
845
- args: rootArgs,
963
+ args: rootArgs2,
846
964
  run: async (args) => {
847
- const paths = requireRoot(args);
848
- const result = await runSyncCheck(paths, cwd);
965
+ const result = await runSyncCheck({ appRoot: args.root }, cwd2);
849
966
  console.log(formatSyncCheckReport(result));
850
967
  process.exit(result.exitCode);
851
968
  }
852
969
  });
853
- var scaffoldCommand = defineCommand2({
970
+ var scaffoldCommand2 = defineCommand3({
854
971
  name: "scaffold",
855
- description: "Generate doc file from schema template",
856
- args: rootArgs.extend({
857
- type: arg2(z2.enum(ALL_TYPES), {
972
+ description: "Generate app doc from schema template",
973
+ args: rootArgs2.extend({
974
+ type: arg3(z3.enum(APP_TYPES), {
858
975
  positional: true,
859
- description: `Scaffold type (${ALL_TYPES.join(", ")})`
976
+ description: `Scaffold type (${APP_TYPES.join(", ")})`
860
977
  }),
861
- parent: arg2(z2.string(), {
978
+ parent: arg3(z3.string(), {
862
979
  positional: true,
863
- description: "Parent name (module or app name)"
980
+ description: "App name"
864
981
  }),
865
- name: arg2(z2.string().optional(), {
982
+ name: arg3(z3.string().optional(), {
866
983
  positional: true,
867
984
  description: "Item name (required for most types)"
868
985
  })
869
986
  }),
870
987
  run: async (args) => {
871
- const paths = requireRoot(args);
872
- const root = isModuleType(args.type) ? paths.modulesRoot : paths.appRoot;
873
- if (!root) {
874
- console.error(
875
- `--${isModuleType(args.type) ? "modules-root" : "app-root"} is required for scaffold type "${args.type}".`
876
- );
877
- process.exit(2);
878
- }
879
988
  const exitCode = await runScaffold(
880
989
  args.type,
881
990
  args.parent,
882
991
  args.name,
883
- root,
884
- cwd
992
+ args.root,
993
+ cwd2
885
994
  );
886
995
  process.exit(exitCode);
887
996
  }
888
997
  });
889
- var initCommand = defineCommand2({
998
+ var appCommand = defineCommand3({
999
+ name: "app",
1000
+ description: "App-compose management",
1001
+ subCommands: {
1002
+ check: checkCommand2,
1003
+ "sync-check": syncCheckCommand2,
1004
+ scaffold: scaffoldCommand2
1005
+ }
1006
+ });
1007
+
1008
+ // src/cli.ts
1009
+ var cwd3 = process.cwd();
1010
+ var initCommand = defineCommand4({
890
1011
  name: "init",
891
1012
  description: "Set up consumer repo with framework skills",
892
- args: z2.object({
893
- force: arg2(z2.boolean().default(false), {
1013
+ args: z4.object({
1014
+ force: arg4(z4.boolean().default(false), {
894
1015
  alias: "f",
895
1016
  description: "Overwrite existing skills"
896
1017
  })
897
1018
  }),
898
1019
  run: (args) => {
899
- const exitCode = runInit(cwd, args.force);
1020
+ const exitCode = runInit(cwd3, args.force);
900
1021
  process.exit(exitCode);
901
1022
  }
902
1023
  });
903
- var main = defineCommand2({
1024
+ var main = defineCommand4({
904
1025
  name: "erp-kit",
905
- description: "Documentation validation and scaffolding tool",
1026
+ description: "ERP module framework CLI",
906
1027
  subCommands: {
907
- check: checkCommand,
908
- "sync-check": syncCheckCommand,
909
- scaffold: scaffoldCommand,
910
- init: initCommand,
911
- mock: mockCommand
1028
+ module: moduleCommand,
1029
+ app: appCommand,
1030
+ mock: mockCommand,
1031
+ init: initCommand
912
1032
  }
913
1033
  });
914
1034
  void runMain(main);