@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.
- package/CHANGELOG.md +12 -0
- package/README.md +158 -62
- package/dist/cli.mjs +344 -215
- package/package.json +3 -2
- package/skills/erp-kit-app-1-requirements/SKILL.md +19 -8
- package/skills/erp-kit-app-2-requirements-review/SKILL.md +5 -4
- package/skills/erp-kit-app-2-requirements-review/references/best-practices-check.md +6 -1
- package/skills/erp-kit-app-2-requirements-review/references/boundary-consistency-check.md +6 -1
- package/skills/erp-kit-app-3-plan/SKILL.md +4 -7
- package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +1 -19
- package/skills/erp-kit-app-4-plan-review/SKILL.md +1 -10
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +1 -8
- package/skills/erp-kit-app-shared/SKILL.md +15 -0
- package/skills/erp-kit-app-shared/references/link-format-reference.md +13 -0
- package/skills/erp-kit-app-shared/references/naming-conventions.md +21 -0
- package/skills/erp-kit-app-shared/references/resolver-classification.md +23 -0
- package/skills/erp-kit-app-shared/references/schema-constraints.md +25 -0
- package/skills/erp-kit-module-1-requirements/SKILL.md +1 -1
- package/skills/erp-kit-module-1-requirements/references/feature-doc.md +1 -1
- package/skills/erp-kit-module-3-plan/SKILL.md +5 -5
- package/skills/erp-kit-module-3-plan/references/naming.md +15 -1
- package/skills/erp-kit-module-5-impl/SKILL.md +12 -10
- package/skills/erp-kit-module-5-impl/references/generated-code.md +2 -2
- package/skills/erp-kit-module-6-impl-review/SKILL.md +1 -1
- package/skills/erp-kit-module-6-impl-review/references/error-implementation-parity.md +1 -1
- package/skills/erp-kit-module-6-impl-review/references/errors.md +1 -1
- package/skills/erp-kit-module-shared/references/errors.md +1 -1
- package/skills/erp-kit-module-shared/references/queries.md +1 -1
- package/skills/erp-kit-module-shared/references/structure.md +1 -1
- package/skills/erp-kit-update/SKILL.md +2 -2
- package/src/commands/app/index.ts +57 -24
- package/src/commands/generate-doc.test.ts +63 -0
- package/src/commands/generate-doc.ts +98 -0
- package/src/commands/init-module.test.ts +43 -0
- package/src/commands/init-module.ts +74 -0
- package/src/commands/module/generate.ts +33 -13
- package/src/commands/module/index.ts +18 -28
- package/src/{commands/scaffold.test.ts → generator/generate-code-boilerplate.test.ts} +19 -89
- package/src/generator/generate-code.test.ts +24 -0
- package/src/generator/generate-code.ts +101 -4
- package/src/integration.test.ts +2 -2
- package/templates/scaffold/app/backend/package.json +4 -4
- package/templates/scaffold/app/frontend/package.json +10 -10
- package/templates/workflows/erp-kit-check.yml +2 -2
- 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 {
|
|
5
|
+
import { copyTemplateDir, scaffoldModuleBoilerplate } from "./generate-code";
|
|
6
6
|
|
|
7
|
-
describe("
|
|
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
|
-
|
|
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.
|
|
164
|
-
|
|
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.
|
|
170
|
-
|
|
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
|
}
|
package/src/integration.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ describe.skipIf(skipReason)("integration", () => {
|
|
|
15
15
|
try {
|
|
16
16
|
const result = execFileSync(
|
|
17
17
|
"node",
|
|
18
|
-
[CLI_PATH, "module", "check", "--
|
|
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", "--
|
|
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.
|
|
19
|
+
"kysely": "0.28.11"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@eslint/js": "9.39.4",
|
|
23
|
-
"@tailor-platform/function-types": "0.8.
|
|
24
|
-
"@types/node": "24.
|
|
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.
|
|
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.
|
|
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.
|
|
26
|
-
"tailwind-merge": "3.
|
|
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
|
|
34
|
-
"@types/node": "25.
|
|
35
|
-
"@types/react": "19.2.
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
+
"globals": "17.4.0",
|
|
46
46
|
"shadcn": "^3.8.4",
|
|
47
|
-
"tailwindcss": "4.1
|
|
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 --
|
|
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 --
|
|
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
|
package/src/commands/scaffold.ts
DELETED
|
@@ -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
|
-
}
|