@tailor-platform/erp-kit 0.2.2 → 0.3.0

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 +12 -0
  2. package/README.md +158 -62
  3. package/dist/cli.mjs +344 -215
  4. package/package.json +3 -2
  5. package/skills/erp-kit-app-1-requirements/SKILL.md +19 -8
  6. package/skills/erp-kit-app-2-requirements-review/SKILL.md +5 -4
  7. package/skills/erp-kit-app-2-requirements-review/references/best-practices-check.md +6 -1
  8. package/skills/erp-kit-app-2-requirements-review/references/boundary-consistency-check.md +6 -1
  9. package/skills/erp-kit-app-3-plan/SKILL.md +4 -7
  10. package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +1 -19
  11. package/skills/erp-kit-app-4-plan-review/SKILL.md +1 -10
  12. package/skills/erp-kit-app-5-impl-backend/SKILL.md +1 -8
  13. package/skills/erp-kit-app-shared/SKILL.md +15 -0
  14. package/skills/erp-kit-app-shared/references/link-format-reference.md +13 -0
  15. package/skills/erp-kit-app-shared/references/naming-conventions.md +21 -0
  16. package/skills/erp-kit-app-shared/references/resolver-classification.md +23 -0
  17. package/skills/erp-kit-app-shared/references/schema-constraints.md +25 -0
  18. package/skills/erp-kit-module-1-requirements/SKILL.md +1 -1
  19. package/skills/erp-kit-module-1-requirements/references/feature-doc.md +1 -1
  20. package/skills/erp-kit-module-3-plan/SKILL.md +5 -5
  21. package/skills/erp-kit-module-3-plan/references/naming.md +15 -1
  22. package/skills/erp-kit-module-5-impl/SKILL.md +12 -10
  23. package/skills/erp-kit-module-5-impl/references/generated-code.md +2 -2
  24. package/skills/erp-kit-module-6-impl-review/SKILL.md +1 -1
  25. package/skills/erp-kit-module-6-impl-review/references/error-implementation-parity.md +1 -1
  26. package/skills/erp-kit-module-6-impl-review/references/errors.md +1 -1
  27. package/skills/erp-kit-module-shared/references/errors.md +1 -1
  28. package/skills/erp-kit-module-shared/references/queries.md +1 -1
  29. package/skills/erp-kit-module-shared/references/structure.md +1 -1
  30. package/skills/erp-kit-update/SKILL.md +2 -2
  31. package/src/commands/app/index.ts +57 -24
  32. package/src/commands/generate-doc.test.ts +63 -0
  33. package/src/commands/generate-doc.ts +98 -0
  34. package/src/commands/init-module.test.ts +43 -0
  35. package/src/commands/init-module.ts +74 -0
  36. package/src/commands/module/generate.ts +33 -13
  37. package/src/commands/module/index.ts +18 -28
  38. package/src/{commands/scaffold.test.ts → generator/generate-code-boilerplate.test.ts} +19 -89
  39. package/src/generator/generate-code.test.ts +24 -0
  40. package/src/generator/generate-code.ts +101 -4
  41. package/src/integration.test.ts +2 -2
  42. package/templates/scaffold/app/backend/package.json +4 -4
  43. package/templates/scaffold/app/frontend/package.json +10 -10
  44. package/templates/workflows/erp-kit-check.yml +2 -2
  45. package/src/commands/scaffold.ts +0 -176
@@ -2,88 +2,9 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { describe, it, expect, afterEach } from "vitest";
5
- import { resolveScaffoldPath, scaffoldModuleSrc, copyTemplateDir } from "./scaffold";
5
+ import { copyTemplateDir, scaffoldModuleBoilerplate } from "./generate-code";
6
6
 
7
- describe("resolveScaffoldPath", () => {
8
- // Module types
9
- it("resolves module scaffold path", () => {
10
- const result = resolveScaffoldPath("module", "inventory", undefined, "modules");
11
- expect(result).toBe("modules/inventory/README.md");
12
- });
13
-
14
- it("resolves command scaffold path", () => {
15
- const result = resolveScaffoldPath("command", "inventory", "CreateOrder", "modules");
16
- expect(result).toBe("modules/inventory/docs/commands/CreateOrder.md");
17
- });
18
-
19
- it("resolves feature scaffold path", () => {
20
- const result = resolveScaffoldPath("feature", "inventory", "stock-tracking", "modules");
21
- expect(result).toBe("modules/inventory/docs/features/stock-tracking.md");
22
- });
23
-
24
- it("resolves model scaffold path", () => {
25
- const result = resolveScaffoldPath("model", "inventory", "StockItem", "modules");
26
- expect(result).toBe("modules/inventory/docs/models/StockItem.md");
27
- });
28
-
29
- // App-compose types
30
- it("resolves app scaffold path", () => {
31
- const result = resolveScaffoldPath("app", "my-app", undefined, "examples");
32
- expect(result).toBe("examples/my-app/README.md");
33
- });
34
-
35
- it("resolves actors scaffold path", () => {
36
- const result = resolveScaffoldPath("actors", "my-app", "admin", "examples");
37
- expect(result).toBe("examples/my-app/docs/actors/admin.md");
38
- });
39
-
40
- it("resolves business-flow scaffold path", () => {
41
- const result = resolveScaffoldPath("business-flow", "my-app", "onboarding", "examples");
42
- expect(result).toBe("examples/my-app/docs/business-flow/onboarding/README.md");
43
- });
44
-
45
- it("resolves story scaffold path with flow/name format", () => {
46
- const result = resolveScaffoldPath(
47
- "story",
48
- "my-app",
49
- "onboarding/admin--create-user",
50
- "examples",
51
- );
52
- expect(result).toBe(
53
- "examples/my-app/docs/business-flow/onboarding/story/admin--create-user.md",
54
- );
55
- });
56
-
57
- it("resolves screen scaffold path", () => {
58
- const result = resolveScaffoldPath("screen", "my-app", "supplier-list", "examples");
59
- expect(result).toBe("examples/my-app/docs/screen/supplier-list.md");
60
- });
61
-
62
- it("resolves resolver scaffold path", () => {
63
- const result = resolveScaffoldPath("resolver", "my-app", "create-supplier", "examples");
64
- expect(result).toBe("examples/my-app/docs/resolver/create-supplier.md");
65
- });
66
-
67
- it("resolves query scaffold path", () => {
68
- const result = resolveScaffoldPath("query", "inventory", "ConvertQuantity", "modules");
69
- expect(result).toBe("modules/inventory/docs/queries/ConvertQuantity.md");
70
- });
71
-
72
- // Validation
73
- it("throws when name is required but missing", () => {
74
- expect(() => resolveScaffoldPath("command", "inventory", undefined, "modules")).toThrow(
75
- "Name is required",
76
- );
77
- });
78
-
79
- it("throws when story name is not flow/name format", () => {
80
- expect(() => resolveScaffoldPath("story", "my-app", "bad-name", "examples")).toThrow(
81
- "Story name must be",
82
- );
83
- });
84
- });
85
-
86
- describe("scaffoldModuleSrc", () => {
7
+ describe("scaffoldModuleBoilerplate", () => {
87
8
  let tmpDir: string;
88
9
 
89
10
  afterEach(() => {
@@ -97,19 +18,16 @@ describe("scaffoldModuleSrc", () => {
97
18
  const moduleDir = path.join(tmpDir, "test-module");
98
19
  fs.mkdirSync(moduleDir, { recursive: true });
99
20
 
100
- scaffoldModuleSrc(moduleDir, "test-module");
21
+ scaffoldModuleBoilerplate(moduleDir, "test-module");
101
22
 
102
- // Verify directories exist directly under module root (flat structure)
103
23
  for (const dir of ["db", "command", "executor", "generated", "lib", "query", "testing"]) {
104
24
  expect(fs.existsSync(path.join(moduleDir, dir))).toBe(true);
105
25
  }
106
26
 
107
- // Verify .gitkeep in empty directories
108
27
  for (const dir of ["db", "command", "executor", "generated", "query"]) {
109
28
  expect(fs.existsSync(path.join(moduleDir, dir, ".gitkeep"))).toBe(true);
110
29
  }
111
30
 
112
- // Verify starter files exist
113
31
  const expectedFiles = [
114
32
  "module.ts",
115
33
  "index.ts",
@@ -123,7 +41,6 @@ describe("scaffoldModuleSrc", () => {
123
41
  expect(fs.existsSync(path.join(moduleDir, file))).toBe(true);
124
42
  }
125
43
 
126
- // Verify module name is used in content
127
44
  const permissions = fs.readFileSync(path.join(moduleDir, "permissions.ts"), "utf-8");
128
45
  expect(permissions).toContain('definePermissions("test-module"');
129
46
 
@@ -151,7 +68,6 @@ describe("copyTemplateDir", () => {
151
68
  const srcDir = path.join(tmpDir, "src");
152
69
  const destDir = path.join(tmpDir, "dest");
153
70
 
154
- // Build a minimal fixture
155
71
  fs.mkdirSync(path.join(srcDir, "backend"), { recursive: true });
156
72
  fs.mkdirSync(path.join(srcDir, "frontend/src"), { recursive: true });
157
73
  fs.writeFileSync(
@@ -174,13 +90,11 @@ describe("copyTemplateDir", () => {
174
90
  const placeholderFiles = new Set(["package.json", "index.html"]);
175
91
  copyTemplateDir(srcDir, destDir, replacements, placeholderFiles);
176
92
 
177
- // Files are copied
178
93
  expect(fs.existsSync(path.join(destDir, "backend/package.json"))).toBe(true);
179
94
  expect(fs.existsSync(path.join(destDir, "frontend/package.json"))).toBe(true);
180
95
  expect(fs.existsSync(path.join(destDir, "frontend/index.html"))).toBe(true);
181
96
  expect(fs.existsSync(path.join(destDir, "frontend/src/main.tsx"))).toBe(true);
182
97
 
183
- // Placeholder replacement
184
98
  const backendPkg = fs.readFileSync(path.join(destDir, "backend/package.json"), "utf-8");
185
99
  expect(backendPkg).toContain('"name": "my-app"');
186
100
  expect(backendPkg).not.toContain("template-app-backend");
@@ -209,4 +123,20 @@ describe("copyTemplateDir", () => {
209
123
  expect(fs.existsSync(path.join(destDir, ".gitignore"))).toBe(true);
210
124
  expect(fs.existsSync(path.join(destDir, "__dot__gitignore"))).toBe(false);
211
125
  });
126
+
127
+ it("does not overwrite existing files", () => {
128
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "copy-template-test-"));
129
+ const srcDir = path.join(tmpDir, "src");
130
+ const destDir = path.join(tmpDir, "dest");
131
+
132
+ fs.mkdirSync(srcDir, { recursive: true });
133
+ fs.mkdirSync(destDir, { recursive: true });
134
+ fs.writeFileSync(path.join(srcDir, "file.txt"), "template content");
135
+ fs.writeFileSync(path.join(destDir, "file.txt"), "existing content");
136
+
137
+ copyTemplateDir(srcDir, destDir, {}, new Set());
138
+
139
+ const content = fs.readFileSync(path.join(destDir, "file.txt"), "utf-8");
140
+ expect(content).toBe("existing content");
141
+ });
212
142
  });
@@ -6,6 +6,7 @@ import {
6
6
  generateQueryShell,
7
7
  generateQueryStub,
8
8
  generateQueryTestStub,
9
+ generateDbStub,
9
10
  moduleNameToPrefix,
10
11
  } from "./generate-code";
11
12
  import type { ParsedCommandDoc } from "./parse-command-doc";
@@ -189,6 +190,29 @@ describe("generateQueryStub", () => {
189
190
  });
190
191
  });
191
192
 
193
+ describe("generateDbStub", () => {
194
+ it("generates an empty db type definition with TODO", () => {
195
+ const result = generateDbStub("item");
196
+
197
+ expect(result).toContain(
198
+ "import { db, unsafeAllowAllGqlPermission, unsafeAllowAllTypePermission }",
199
+ );
200
+ expect(result).toContain("export const item = db");
201
+ expect(result).toContain('.type("Item"');
202
+ expect(result).toContain("// TODO: define fields");
203
+ expect(result).toContain("...db.fields.timestamps()");
204
+ expect(result).toContain(".permission(unsafeAllowAllTypePermission)");
205
+ expect(result).toContain(".gqlPermission(unsafeAllowAllGqlPermission)");
206
+ });
207
+
208
+ it("handles camelCase model names", () => {
209
+ const result = generateDbStub("taxonomyNode");
210
+
211
+ expect(result).toContain("export const taxonomyNode = db");
212
+ expect(result).toContain('.type("TaxonomyNode"');
213
+ });
214
+ });
215
+
192
216
  describe("generateQueryTestStub", () => {
193
217
  it("generates a test stub with createMockDb and run import", () => {
194
218
  const doc: ParsedCommandDoc = {
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { PACKAGE_ROOT, readErpKitVersion } from "../util";
3
4
  import { parseCommandDoc, errorCodeToClassName, type ParsedCommandDoc } from "./parse-command-doc";
4
5
 
5
6
  export function moduleNameToPrefix(moduleName: string): string {
@@ -141,6 +142,19 @@ describe("${doc.commandName}", () => {
141
142
  `;
142
143
  }
143
144
 
145
+ export function generateDbStub(modelName: string): string {
146
+ return `import { db, unsafeAllowAllGqlPermission, unsafeAllowAllTypePermission } from "@tailor-platform/sdk";
147
+
148
+ export const ${modelName} = db
149
+ .type("${modelName.charAt(0).toUpperCase() + modelName.slice(1)}", {
150
+ // TODO: define fields
151
+ ...db.fields.timestamps(),
152
+ })
153
+ .permission(unsafeAllowAllTypePermission)
154
+ .gqlPermission(unsafeAllowAllGqlPermission);
155
+ `;
156
+ }
157
+
144
158
  export function generatePermissions(moduleName: string, commandNames: string[]): string {
145
159
  const sorted = [...commandNames].sort();
146
160
  const entries = sorted.map((name) => ` "${name}",`).join("\n");
@@ -154,20 +168,83 @@ ${entries}
154
168
  `;
155
169
  }
156
170
 
171
+ // npm renames .gitignore during pack (npm/cli#5756).
172
+ // prepack copies .gitignore to __dot__gitignore to preserve it.
173
+ export function copyTemplateDir(
174
+ srcDir: string,
175
+ destDir: string,
176
+ replacements: Record<string, string>,
177
+ placeholderFiles: Set<string>,
178
+ ): void {
179
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
180
+ const srcPath = path.join(srcDir, entry.name);
181
+ const destName = entry.name === "__dot__gitignore" ? ".gitignore" : entry.name;
182
+ const destPath = path.join(destDir, destName);
183
+
184
+ if (entry.isDirectory()) {
185
+ fs.mkdirSync(destPath, { recursive: true });
186
+ copyTemplateDir(srcPath, destPath, replacements, placeholderFiles);
187
+ } else {
188
+ if (fs.existsSync(destPath)) continue;
189
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
190
+ if (placeholderFiles.has(entry.name)) {
191
+ let content = fs.readFileSync(srcPath, "utf-8");
192
+ for (const [from, to] of Object.entries(replacements)) {
193
+ content = content.replaceAll(from, to);
194
+ }
195
+ fs.writeFileSync(destPath, content);
196
+ } else {
197
+ fs.copyFileSync(srcPath, destPath);
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ export function scaffoldModuleBoilerplate(moduleDir: string, moduleName: string): void {
204
+ const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "module");
205
+ const replacements = { "template-module": moduleName };
206
+ const placeholderFiles = new Set(["permissions.ts", "tailor.config.ts"]);
207
+ copyTemplateDir(templateDir, moduleDir, replacements, placeholderFiles);
208
+ }
209
+
210
+ export function scaffoldAppBoilerplate(appDir: string, appName: string): void {
211
+ const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "app");
212
+ const erpKitVersion = readErpKitVersion();
213
+ const replacements = {
214
+ "template-app-frontend": `${appName}-frontend`,
215
+ "template-app-backend": appName,
216
+ "template-app": appName,
217
+ '"workspace:*"': `"${erpKitVersion}"`,
218
+ };
219
+ const placeholderFiles = new Set(["package.json", "tailor.config.ts", "index.html"]);
220
+ copyTemplateDir(templateDir, appDir, replacements, placeholderFiles);
221
+ }
222
+
223
+ export function runGenerateAppCode(appPath: string): number {
224
+ const appName = path.basename(appPath);
225
+ scaffoldAppBoilerplate(appPath, appName);
226
+ console.log(`Generated boilerplate for ${appName}`);
227
+ return 0;
228
+ }
229
+
157
230
  export function runGenerateCode(modulePath: string, moduleName: string): number {
231
+ scaffoldModuleBoilerplate(modulePath, moduleName);
232
+
158
233
  const docsDir = path.join(modulePath, "docs", "commands");
159
234
  const libDir = path.join(modulePath, "lib");
160
235
  const commandDir = path.join(modulePath, "command");
161
236
 
162
237
  if (!fs.existsSync(docsDir)) {
163
- console.error(`No docs/commands/ directory found at ${docsDir}`);
164
- return 1;
238
+ console.log(`No docs/commands/ directory found skipping code generation`);
239
+ console.log(`Generated boilerplate for ${moduleName}`);
240
+ return 0;
165
241
  }
166
242
 
167
243
  const mdFiles = fs.readdirSync(docsDir).filter((f) => f.endsWith(".md"));
168
244
  if (mdFiles.length === 0) {
169
- console.error(`No command docs found in ${docsDir}`);
170
- return 1;
245
+ console.log(`No command docs found skipping code generation`);
246
+ console.log(`Generated boilerplate for ${moduleName}`);
247
+ return 0;
171
248
  }
172
249
 
173
250
  const parsedDocs: ParsedCommandDoc[] = [];
@@ -261,6 +338,26 @@ export function runGenerateCode(modulePath: string, moduleName: string): number
261
338
  }
262
339
  }
263
340
 
341
+ // Generate db stubs from docs/models/*.md
342
+ const modelDocsDir = path.join(modulePath, "docs", "models");
343
+ if (fs.existsSync(modelDocsDir)) {
344
+ const modelFiles = fs.readdirSync(modelDocsDir).filter((f) => f.endsWith(".md"));
345
+ if (modelFiles.length > 0) {
346
+ const dbDir = path.join(modulePath, "db");
347
+ fs.mkdirSync(dbDir, { recursive: true });
348
+
349
+ for (const file of modelFiles) {
350
+ const modelName = path.basename(file, ".md");
351
+ const camelName = modelName.charAt(0).toLowerCase() + modelName.slice(1);
352
+ const dbFile = path.join(dbDir, `${camelName}.ts`);
353
+ if (!fs.existsSync(dbFile)) {
354
+ fs.writeFileSync(dbFile, generateDbStub(camelName));
355
+ console.log(` scaffolded ${path.relative(modulePath, dbFile)}`);
356
+ }
357
+ }
358
+ }
359
+ }
360
+
264
361
  console.log(`Generated ${generated} file(s) for ${moduleName}`);
265
362
  return 0;
266
363
  }
@@ -15,7 +15,7 @@ describe.skipIf(skipReason)("integration", () => {
15
15
  try {
16
16
  const result = execFileSync(
17
17
  "node",
18
- [CLI_PATH, "module", "check", "--root", "packages/erp-kit/src/modules"],
18
+ [CLI_PATH, "module", "check", "--path", "packages/erp-kit/src/modules"],
19
19
  {
20
20
  cwd: REPO_ROOT,
21
21
  encoding: "utf-8",
@@ -35,7 +35,7 @@ describe.skipIf(skipReason)("integration", () => {
35
35
  try {
36
36
  const result = execFileSync(
37
37
  "node",
38
- [CLI_PATH, "module", "sync-check", "--root", "packages/erp-kit/src/modules"],
38
+ [CLI_PATH, "module", "sync-check", "--path", "packages/erp-kit/src/modules"],
39
39
  {
40
40
  cwd: REPO_ROOT,
41
41
  encoding: "utf-8",
@@ -16,15 +16,15 @@
16
16
  "@tailor-platform/erp-kit": "workspace:*",
17
17
  "@tailor-platform/function-kysely-tailordb": "0.1.3",
18
18
  "@tailor-platform/sdk": "1.25.1",
19
- "kysely": "0.28.10"
19
+ "kysely": "0.28.11"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@eslint/js": "9.39.4",
23
- "@tailor-platform/function-types": "0.8.1",
24
- "@types/node": "24.10.9",
23
+ "@tailor-platform/function-types": "0.8.2",
24
+ "@types/node": "24.12.0",
25
25
  "eslint": "9.39.4",
26
26
  "eslint-import-resolver-typescript": "4.4.4",
27
- "eslint-plugin-import-x": "4.16.1",
27
+ "eslint-plugin-import-x": "4.16.2",
28
28
  "typescript": "5.9.3",
29
29
  "typescript-eslint": "8.57.0"
30
30
  }
@@ -19,32 +19,32 @@
19
19
  "class-variance-authority": "0.7.1",
20
20
  "clsx": "2.1.1",
21
21
  "gql.tada": "1.9.0",
22
- "lucide-react": "0.563.0",
22
+ "lucide-react": "0.577.0",
23
23
  "react": "19.2.4",
24
24
  "react-dom": "19.2.4",
25
- "react-hook-form": "7.71.1",
26
- "tailwind-merge": "3.4.0",
25
+ "react-hook-form": "7.71.2",
26
+ "tailwind-merge": "3.5.0",
27
27
  "urql": "5.0.1",
28
28
  "zod": "4.3.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@eslint/js": "9.39.4",
32
32
  "@tailor-platform/app-shell-vite-plugin": "0.1.0",
33
- "@tailwindcss/vite": "4.1.18",
34
- "@types/node": "25.1.0",
35
- "@types/react": "19.2.10",
33
+ "@tailwindcss/vite": "4.2.1",
34
+ "@types/node": "25.5.0",
35
+ "@types/react": "19.2.14",
36
36
  "@types/react-dom": "19.2.3",
37
- "@vitejs/plugin-react": "5.1.2",
37
+ "@vitejs/plugin-react": "5.2.0",
38
38
  "babel-plugin-react-compiler": "1.0.0",
39
39
  "eslint": "9.39.4",
40
40
  "eslint-import-resolver-typescript": "4.4.4",
41
- "eslint-plugin-import-x": "4.16.1",
41
+ "eslint-plugin-import-x": "4.16.2",
42
42
  "eslint-plugin-react-dom": "2.13.0",
43
43
  "eslint-plugin-react-hooks": "7.0.1",
44
44
  "eslint-plugin-react-x": "2.13.0",
45
- "globals": "17.2.0",
45
+ "globals": "17.4.0",
46
46
  "shadcn": "^3.8.4",
47
- "tailwindcss": "4.1.18",
47
+ "tailwindcss": "4.2.1",
48
48
  "tw-animate-css": "1.4.0",
49
49
  "typescript": "5.9.3",
50
50
  "typescript-eslint": "8.57.0",
@@ -28,10 +28,10 @@ jobs:
28
28
  - run: pnpm install --frozen-lockfile
29
29
 
30
30
  - name: Validate app documentation
31
- run: npx @tailor-platform/erp-kit app check --root .
31
+ run: npx @tailor-platform/erp-kit app check --path .
32
32
 
33
33
  - name: Validate source-doc correspondence
34
- run: npx @tailor-platform/erp-kit app sync-check --root .
34
+ run: npx @tailor-platform/erp-kit app sync-check --path .
35
35
 
36
36
  - name: Check licenses
37
37
  run: npx @tailor-platform/erp-kit license check --config license.config.json
@@ -1,176 +0,0 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import { runMdschema } from "../mdschema";
4
- import { ALL_SCHEMAS } from "../schemas";
5
- import { PACKAGE_ROOT, readErpKitVersion } from "../util";
6
-
7
- export const MODULE_TYPES = ["module", "feature", "command", "model", "query"] as const;
8
- export const APP_TYPES = ["app", "actors", "business-flow", "story", "screen", "resolver"] as const;
9
- export const ALL_TYPES = [...MODULE_TYPES, ...APP_TYPES] as const;
10
-
11
- export type ScaffoldType = (typeof ALL_TYPES)[number];
12
-
13
- const MODULE_DIR_MAP: Record<string, string> = {
14
- feature: "docs/features",
15
- command: "docs/commands",
16
- model: "docs/models",
17
- query: "docs/queries",
18
- };
19
-
20
- const APP_DIR_MAP: Record<string, string> = {
21
- actors: "docs/actors",
22
- "business-flow": "docs/business-flow",
23
- screen: "docs/screen",
24
- resolver: "docs/resolver",
25
- };
26
-
27
- export function isModuleType(type: string): boolean {
28
- return (MODULE_TYPES as readonly string[]).includes(type);
29
- }
30
-
31
- export function resolveScaffoldPath(
32
- type: ScaffoldType,
33
- parentName: string,
34
- name: string | undefined,
35
- root: string,
36
- ): string {
37
- // Types that map to README.md (no name needed)
38
- if (type === "module" || type === "app") {
39
- return path.join(root, parentName, "README.md");
40
- }
41
-
42
- if (!name) {
43
- throw new Error(`Name is required for scaffold type "${type}"`);
44
- }
45
-
46
- // business-flow produces a README inside a named directory
47
- if (type === "business-flow") {
48
- return path.join(root, parentName, "docs/business-flow", name, "README.md");
49
- }
50
-
51
- // story accepts "flow/story-name" to place under the correct flow directory
52
- if (type === "story") {
53
- const parts = name.split("/");
54
- if (parts.length !== 2) {
55
- throw new Error(
56
- `Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`,
57
- );
58
- }
59
- return path.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
60
- }
61
-
62
- // Module sub-types
63
- if (MODULE_DIR_MAP[type]) {
64
- return path.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
65
- }
66
-
67
- // App sub-types
68
- if (APP_DIR_MAP[type]) {
69
- return path.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
70
- }
71
-
72
- throw new Error(`Unknown scaffold type: ${type}`);
73
- }
74
-
75
- export async function runScaffold(
76
- type: ScaffoldType,
77
- parentName: string,
78
- name: string | undefined,
79
- root: string,
80
- cwd: string,
81
- ): Promise<number> {
82
- const outputPath = resolveScaffoldPath(type, parentName, name, root);
83
- const absoluteOutput = path.resolve(cwd, outputPath);
84
-
85
- if (fs.existsSync(absoluteOutput)) {
86
- console.error(`File already exists: ${outputPath}`);
87
- return 1;
88
- }
89
-
90
- const schemaPath = ALL_SCHEMAS[type];
91
- if (!schemaPath) {
92
- console.error(`No schema found for type: ${type}`);
93
- return 2;
94
- }
95
-
96
- try {
97
- fs.mkdirSync(path.dirname(absoluteOutput), { recursive: true });
98
- } catch (err) {
99
- console.error(
100
- `Failed to create directory: ${err instanceof Error ? err.message : String(err)}`,
101
- );
102
- return 1;
103
- }
104
-
105
- const { exitCode, stdout, stderr } = await runMdschema(
106
- ["generate", "--schema", schemaPath, "--output", absoluteOutput],
107
- cwd,
108
- );
109
-
110
- if (stdout.trim()) console.log(stdout);
111
- if (stderr.trim()) console.error(stderr);
112
-
113
- if (exitCode !== 0) return exitCode;
114
-
115
- if (type === "module") {
116
- scaffoldModuleSrc(path.dirname(absoluteOutput), parentName);
117
- }
118
-
119
- if (type === "app") {
120
- scaffoldAppSrc(path.dirname(absoluteOutput), parentName);
121
- }
122
-
123
- return exitCode;
124
- }
125
-
126
- // npm renames .gitignore → .npmignore during pack (npm/cli#5756).
127
- // prepack copies .gitignore → __dot__gitignore to preserve it.
128
- export function copyTemplateDir(
129
- srcDir: string,
130
- destDir: string,
131
- replacements: Record<string, string>,
132
- placeholderFiles: Set<string>,
133
- ): void {
134
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
135
- const srcPath = path.join(srcDir, entry.name);
136
- const destName = entry.name === "__dot__gitignore" ? ".gitignore" : entry.name;
137
- const destPath = path.join(destDir, destName);
138
-
139
- if (entry.isDirectory()) {
140
- fs.mkdirSync(destPath, { recursive: true });
141
- copyTemplateDir(srcPath, destPath, replacements, placeholderFiles);
142
- } else {
143
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
144
- if (placeholderFiles.has(entry.name)) {
145
- // Replace placeholders (e.g. "template-module" → actual name)
146
- let content = fs.readFileSync(srcPath, "utf-8");
147
- for (const [from, to] of Object.entries(replacements)) {
148
- content = content.replaceAll(from, to);
149
- }
150
- fs.writeFileSync(destPath, content);
151
- } else {
152
- fs.copyFileSync(srcPath, destPath);
153
- }
154
- }
155
- }
156
- }
157
-
158
- export function scaffoldModuleSrc(moduleDir: string, moduleName: string): void {
159
- const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "module");
160
- const replacements = { "template-module": moduleName };
161
- const placeholderFiles = new Set(["permissions.ts", "tailor.config.ts"]);
162
- copyTemplateDir(templateDir, moduleDir, replacements, placeholderFiles);
163
- }
164
-
165
- export function scaffoldAppSrc(appDir: string, appName: string): void {
166
- const templateDir = path.join(PACKAGE_ROOT, "templates", "scaffold", "app");
167
- const erpKitVersion = readErpKitVersion();
168
- const replacements = {
169
- "template-app-frontend": `${appName}-frontend`,
170
- "template-app-backend": appName,
171
- "template-app": appName,
172
- '"workspace:*"': `"${erpKitVersion}"`,
173
- };
174
- const placeholderFiles = new Set(["package.json", "tailor.config.ts", "index.html"]);
175
- copyTemplateDir(templateDir, appDir, replacements, placeholderFiles);
176
- }