swallowkit 1.0.0-beta.2 → 1.0.0-beta.21
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/LICENSE +21 -21
- package/README.ja.md +312 -215
- package/README.md +369 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +444 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +19 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +476 -117
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2627 -1708
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +617 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +164 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +63 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +204 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -213
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +329 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +156 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +184 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +85 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +263 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/dev.test.ts +154 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +80 -0
- package/src/__tests__/machine.test.ts +212 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +189 -0
- package/src/__tests__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +500 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +962 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3371 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1211 -0
- package/src/cli/index.ts +193 -0
- package/src/core/config.ts +308 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +221 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod スキーマ(ModelInfo)からモックデータを自動生成する
|
|
3
|
+
* フィールド名・型のヒューリスティクスで現実的な値を生成
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModelInfo, FieldInfo, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 単一のモックドキュメントを生成
|
|
10
|
+
*/
|
|
11
|
+
export function generateMockDocument(
|
|
12
|
+
model: ModelInfo,
|
|
13
|
+
index: number,
|
|
14
|
+
allModels: ModelInfo[] = [model],
|
|
15
|
+
seenModels: Set<string> = new Set()
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
const nextSeen = new Set(seenModels);
|
|
18
|
+
nextSeen.add(model.name);
|
|
19
|
+
|
|
20
|
+
const doc: Record<string, unknown> = {};
|
|
21
|
+
for (const field of model.fields) {
|
|
22
|
+
doc[field.name] = generateFieldValue(model, field, index, allModels, nextSeen);
|
|
23
|
+
}
|
|
24
|
+
return doc;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 複数のモックドキュメントを生成
|
|
29
|
+
*/
|
|
30
|
+
export function generateMockDocuments(
|
|
31
|
+
model: ModelInfo,
|
|
32
|
+
count: number = 5,
|
|
33
|
+
allModels: ModelInfo[] = [model]
|
|
34
|
+
): Record<string, unknown>[] {
|
|
35
|
+
const docs: Record<string, unknown>[] = [];
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
docs.push(generateMockDocument(model, i + 1, allModels));
|
|
38
|
+
}
|
|
39
|
+
return docs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Field Value Generation ─────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function generateFieldValue(
|
|
45
|
+
model: ModelInfo,
|
|
46
|
+
field: FieldInfo,
|
|
47
|
+
index: number,
|
|
48
|
+
allModels: ModelInfo[],
|
|
49
|
+
seenModels: Set<string>
|
|
50
|
+
): unknown {
|
|
51
|
+
// ネストされたスキーマ
|
|
52
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
53
|
+
const nestedModel = allModels.find((m) => m.name === field.nestedModelName);
|
|
54
|
+
if (nestedModel && !seenModels.has(nestedModel.name)) {
|
|
55
|
+
const nested = generateMockDocument(nestedModel, index, allModels, seenModels);
|
|
56
|
+
return field.isArray ? [nested] : nested;
|
|
57
|
+
}
|
|
58
|
+
const fallback = { id: `${toKebabCase(field.nestedModelName!)}-${padIndex(index)}` };
|
|
59
|
+
return field.isArray ? [fallback] : fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (field.isArray) {
|
|
63
|
+
return [generateScalarValue(model, field, index)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return generateScalarValue(model, field, index);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateScalarValue(
|
|
70
|
+
model: ModelInfo,
|
|
71
|
+
field: FieldInfo,
|
|
72
|
+
index: number
|
|
73
|
+
): unknown {
|
|
74
|
+
const name = field.name.toLowerCase();
|
|
75
|
+
const modelKebab = toKebabCase(model.name);
|
|
76
|
+
|
|
77
|
+
// id フィールド
|
|
78
|
+
if (field.name === "id") {
|
|
79
|
+
return `${modelKebab}-${padIndex(index)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// enum フィールド — ラウンドロビン
|
|
83
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
84
|
+
return field.enumValues[(index - 1) % field.enumValues.length];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// boolean フィールド
|
|
88
|
+
if (field.type === "boolean") {
|
|
89
|
+
return index % 2 === 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// number フィールド
|
|
93
|
+
if (field.type === "number") {
|
|
94
|
+
return generateNumberValue(name, index);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// date フィールド / *At パターン
|
|
98
|
+
if (field.type === "date" || name.endsWith("at")) {
|
|
99
|
+
return generateDateValue(index);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// object フィールド
|
|
103
|
+
if (field.type === "object") {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// string フィールド — 名前パターンで分岐
|
|
108
|
+
return generateStringValue(model, field, index);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateStringValue(model: ModelInfo, field: FieldInfo, index: number): string {
|
|
112
|
+
const name = field.name.toLowerCase();
|
|
113
|
+
const modelKebab = toKebabCase(model.name);
|
|
114
|
+
|
|
115
|
+
// email
|
|
116
|
+
if (name.includes("email") || name.includes("mail")) {
|
|
117
|
+
return `${modelKebab}-${padIndex(index)}@example.com`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// URL / ウェブサイト
|
|
121
|
+
if (name.includes("url") || name.includes("website") || name.includes("link") || name === "href") {
|
|
122
|
+
return `https://example.com/${modelKebab}/${padIndex(index)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 電話番号
|
|
126
|
+
if (name.includes("phone") || name.includes("tel")) {
|
|
127
|
+
return `090-0000-${String(index).padStart(4, "0")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 名前系
|
|
131
|
+
if (name === "name" || name === "displayname" || name === "fullname" || name === "username") {
|
|
132
|
+
return `${model.name} ${index}`;
|
|
133
|
+
}
|
|
134
|
+
if (name === "firstname" || name === "givenname") {
|
|
135
|
+
return `FirstName${index}`;
|
|
136
|
+
}
|
|
137
|
+
if (name === "lastname" || name === "familyname" || name === "surname") {
|
|
138
|
+
return `LastName${index}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// タイトル / 件名
|
|
142
|
+
if (name === "title" || name === "subject" || name === "summary") {
|
|
143
|
+
return `Sample ${model.displayName || model.name} ${index}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 説明 / 本文
|
|
147
|
+
if (name === "description" || name === "body" || name === "content" || name === "text") {
|
|
148
|
+
return `This is a sample ${toCamelCase(model.name)} description for item ${index}.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// コード / キー
|
|
152
|
+
if (name.includes("code") || name.includes("key") && !name.includes("api")) {
|
|
153
|
+
return `${modelKebab.toUpperCase().replace(/-/g, "_")}-${String(index).padStart(3, "0")}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ステータス
|
|
157
|
+
if (name === "status") {
|
|
158
|
+
const statuses = ["active", "inactive", "pending"];
|
|
159
|
+
return statuses[(index - 1) % statuses.length];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// タイプ / カテゴリ
|
|
163
|
+
if (name === "type" || name === "category") {
|
|
164
|
+
return `type-${((index - 1) % 3) + 1}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 住所
|
|
168
|
+
if (name.includes("address") || name.includes("city") || name.includes("country")) {
|
|
169
|
+
return `Address ${index}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 部署
|
|
173
|
+
if (name === "department" || name === "team" || name === "group") {
|
|
174
|
+
const depts = ["Engineering", "Sales", "Marketing", "Support", "HR"];
|
|
175
|
+
return depts[(index - 1) % depts.length];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// *Id パターン(外部キー)
|
|
179
|
+
if (name.endsWith("id") && name !== "id") {
|
|
180
|
+
const refName = field.name.replace(/Id$/, "");
|
|
181
|
+
return `${toKebabCase(refName)}-${padIndex(((index - 1) % 3) + 1)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// デフォルト
|
|
185
|
+
return `${modelKebab}-${toKebabCase(field.name)}-${index}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function generateNumberValue(name: string, index: number): number {
|
|
189
|
+
if (name.includes("age")) return 20 + index;
|
|
190
|
+
if (name.includes("price") || name.includes("cost") || name.includes("amount")) return index * 1000;
|
|
191
|
+
if (name.includes("count") || name.includes("quantity") || name.includes("qty")) return index * 5;
|
|
192
|
+
if (name.includes("priority") || name.includes("order") || name.includes("sort")) return index;
|
|
193
|
+
if (name.includes("rating") || name.includes("score")) return Math.min(index, 5);
|
|
194
|
+
return index;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function generateDateValue(index: number): string {
|
|
198
|
+
const baseDate = new Date("2026-01-01T00:00:00.000Z");
|
|
199
|
+
baseDate.setDate(baseDate.getDate() + index);
|
|
200
|
+
return baseDate.toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function padIndex(index: number): string {
|
|
204
|
+
return String(index).padStart(3, "0");
|
|
205
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { ensureSwallowKitProject, loadConfig } from "../config";
|
|
4
|
+
import { toPascalCase } from "../scaffold/model-parser";
|
|
5
|
+
import { syncProjectManifest } from "../project/manifest";
|
|
6
|
+
|
|
7
|
+
export interface CreateModelOperationOptions {
|
|
8
|
+
names: string[];
|
|
9
|
+
modelsDir?: string;
|
|
10
|
+
connector?: string;
|
|
11
|
+
overwriteMode?: "prompt" | "always" | "never";
|
|
12
|
+
confirmOverwrite?: (filePath: string) => Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateModelOperationResult {
|
|
16
|
+
createdFiles: string[];
|
|
17
|
+
skippedFiles: string[];
|
|
18
|
+
updatedIndex: boolean;
|
|
19
|
+
connectorType?: "rdb" | "api";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateModelTemplate(modelName: string): string {
|
|
23
|
+
const pascalName = toPascalCase(modelName);
|
|
24
|
+
|
|
25
|
+
return `import { z } from 'zod/v4';
|
|
26
|
+
|
|
27
|
+
// ${pascalName} model (Zod official pattern: same name for value and type)
|
|
28
|
+
export const ${pascalName} = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
name: z.string().min(1),
|
|
31
|
+
createdAt: z.string().optional(),
|
|
32
|
+
updatedAt: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}>;
|
|
36
|
+
|
|
37
|
+
// Display name for UI
|
|
38
|
+
export const displayName = '${pascalName}';
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function generateConnectorModelTemplate(modelName: string, connectorName: string, connectorType: "rdb" | "api"): string {
|
|
43
|
+
const pascalName = toPascalCase(modelName);
|
|
44
|
+
const kebabName = modelName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
45
|
+
const pluralName = kebabName.endsWith("s") ? kebabName : `${kebabName}s`;
|
|
46
|
+
|
|
47
|
+
const schema = `import { z } from 'zod/v4';
|
|
48
|
+
|
|
49
|
+
// ${pascalName} model (Zod official pattern: same name for value and type)
|
|
50
|
+
export const ${pascalName} = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
name: z.string().min(1),
|
|
53
|
+
createdAt: z.string().optional(),
|
|
54
|
+
updatedAt: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}>;
|
|
58
|
+
|
|
59
|
+
// Display name for UI
|
|
60
|
+
export const displayName = '${pascalName}';
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
if (connectorType === "rdb") {
|
|
64
|
+
return `${schema}
|
|
65
|
+
// SwallowKit Connector Metadata
|
|
66
|
+
export const connectorConfig = {
|
|
67
|
+
connector: '${connectorName}',
|
|
68
|
+
operations: ['getAll', 'getById'] as const,
|
|
69
|
+
table: '${pluralName}',
|
|
70
|
+
idColumn: 'id',
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `${schema}
|
|
76
|
+
// SwallowKit Connector Metadata
|
|
77
|
+
export const connectorConfig = {
|
|
78
|
+
connector: '${connectorName}',
|
|
79
|
+
operations: ['getAll', 'getById', 'create', 'update'] as const,
|
|
80
|
+
endpoints: {
|
|
81
|
+
getAll: 'GET /${pluralName}',
|
|
82
|
+
getById: 'GET /${pluralName}/{id}',
|
|
83
|
+
create: 'POST /${pluralName}',
|
|
84
|
+
update: 'PATCH /${pluralName}/{id}',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateSharedIndex(kebabName: string, pascalName: string): boolean {
|
|
91
|
+
const indexPath = path.join("shared", "index.ts");
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(indexPath)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = fs.readFileSync(indexPath, "utf-8");
|
|
98
|
+
const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
|
|
99
|
+
|
|
100
|
+
if (content.includes(exportLine)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.appendFileSync(indexPath, `${exportLine}\n`);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function createModelOperation(options: CreateModelOperationOptions): Promise<CreateModelOperationResult> {
|
|
109
|
+
ensureSwallowKitProject("create-model");
|
|
110
|
+
|
|
111
|
+
let connectorType: "rdb" | "api" | undefined;
|
|
112
|
+
if (options.connector) {
|
|
113
|
+
const config = loadConfig();
|
|
114
|
+
const connectorDefinition = config.connectors?.[options.connector];
|
|
115
|
+
if (!connectorDefinition) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Connector '${options.connector}' not found in swallowkit.config.js. ` +
|
|
118
|
+
`Run 'swallowkit add-connector ${options.connector} --type=<rdb|api>' first.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
connectorType = connectorDefinition.type;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const modelsDir = options.modelsDir || "shared/models";
|
|
126
|
+
if (!fs.existsSync(modelsDir)) {
|
|
127
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const createdFiles: string[] = [];
|
|
131
|
+
const skippedFiles: string[] = [];
|
|
132
|
+
let updatedIndex = false;
|
|
133
|
+
|
|
134
|
+
for (const name of options.names) {
|
|
135
|
+
const kebabName = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
136
|
+
const filePath = path.join(modelsDir, `${kebabName}.ts`);
|
|
137
|
+
const pascalName = toPascalCase(name);
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(filePath)) {
|
|
140
|
+
const overwriteMode = options.overwriteMode || "prompt";
|
|
141
|
+
const shouldOverwrite = overwriteMode === "always"
|
|
142
|
+
? true
|
|
143
|
+
: overwriteMode === "never"
|
|
144
|
+
? false
|
|
145
|
+
: options.confirmOverwrite
|
|
146
|
+
? await options.confirmOverwrite(filePath)
|
|
147
|
+
: false;
|
|
148
|
+
|
|
149
|
+
if (!shouldOverwrite) {
|
|
150
|
+
skippedFiles.push(filePath.replace(/\\/g, "/"));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const content = options.connector && connectorType
|
|
156
|
+
? generateConnectorModelTemplate(name, options.connector, connectorType)
|
|
157
|
+
: generateModelTemplate(name);
|
|
158
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
159
|
+
createdFiles.push(filePath.replace(/\\/g, "/"));
|
|
160
|
+
|
|
161
|
+
if (updateSharedIndex(kebabName, pascalName)) {
|
|
162
|
+
updatedIndex = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await syncProjectManifest();
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
createdFiles,
|
|
170
|
+
skippedFiles,
|
|
171
|
+
updatedIndex,
|
|
172
|
+
...(connectorType ? { connectorType } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
export interface CapturedConsoleMessages {
|
|
5
|
+
logs: string[];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
errors: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FileMutationSummary {
|
|
11
|
+
createdFiles: string[];
|
|
12
|
+
updatedFiles: string[];
|
|
13
|
+
appendedFiles: string[];
|
|
14
|
+
deletedFiles: string[];
|
|
15
|
+
createdDirectories: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ProcessExitInterceptError extends Error {
|
|
19
|
+
readonly exitCode: number;
|
|
20
|
+
|
|
21
|
+
constructor(exitCode: number) {
|
|
22
|
+
super(`Process exited with code ${exitCode}`);
|
|
23
|
+
this.name = "ProcessExitInterceptError";
|
|
24
|
+
this.exitCode = exitCode;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatConsoleArgs(args: unknown[]): string {
|
|
29
|
+
return args
|
|
30
|
+
.map((arg) => {
|
|
31
|
+
if (typeof arg === "string") {
|
|
32
|
+
return arg;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (arg instanceof Error) {
|
|
36
|
+
return arg.stack || arg.message;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(arg);
|
|
41
|
+
} catch {
|
|
42
|
+
return String(arg);
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.join(" ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizePathForSummary(targetPath: string): string {
|
|
49
|
+
const relative = path.relative(process.cwd(), targetPath);
|
|
50
|
+
return relative && !relative.startsWith("..") ? relative.replace(/\\/g, "/") : targetPath.replace(/\\/g, "/");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface FileSystemEntrySnapshot {
|
|
54
|
+
type: "file" | "directory";
|
|
55
|
+
mtimeMs: number;
|
|
56
|
+
size: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function snapshotFileSystem(rootDirectory: string): Map<string, FileSystemEntrySnapshot> {
|
|
60
|
+
const snapshot = new Map<string, FileSystemEntrySnapshot>();
|
|
61
|
+
const excludedDirectoryNames = new Set([".git", "node_modules"]);
|
|
62
|
+
|
|
63
|
+
function walk(currentDirectory: string): void {
|
|
64
|
+
for (const entry of fs.readdirSync(currentDirectory, { withFileTypes: true })) {
|
|
65
|
+
if (entry.isDirectory() && excludedDirectoryNames.has(entry.name)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const entryPath = path.join(currentDirectory, entry.name);
|
|
70
|
+
const stats = fs.statSync(entryPath);
|
|
71
|
+
snapshot.set(entryPath, {
|
|
72
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
73
|
+
mtimeMs: stats.mtimeMs,
|
|
74
|
+
size: stats.size,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
walk(entryPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(rootDirectory)) {
|
|
84
|
+
walk(rootDirectory);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return snapshot;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function withWorkingDirectory<T>(directory: string, action: () => Promise<T>): Promise<T> {
|
|
91
|
+
const originalDirectory = process.cwd();
|
|
92
|
+
process.chdir(directory);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return await action();
|
|
96
|
+
} finally {
|
|
97
|
+
process.chdir(originalDirectory);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function captureConsoleMessages<T>(
|
|
102
|
+
action: () => Promise<T>
|
|
103
|
+
): Promise<{ result: T; messages: CapturedConsoleMessages }> {
|
|
104
|
+
const messages: CapturedConsoleMessages = {
|
|
105
|
+
logs: [],
|
|
106
|
+
warnings: [],
|
|
107
|
+
errors: [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const originalLog = console.log;
|
|
111
|
+
const originalWarn = console.warn;
|
|
112
|
+
const originalError = console.error;
|
|
113
|
+
|
|
114
|
+
console.log = (...args: unknown[]) => {
|
|
115
|
+
messages.logs.push(formatConsoleArgs(args));
|
|
116
|
+
};
|
|
117
|
+
console.warn = (...args: unknown[]) => {
|
|
118
|
+
messages.warnings.push(formatConsoleArgs(args));
|
|
119
|
+
};
|
|
120
|
+
console.error = (...args: unknown[]) => {
|
|
121
|
+
messages.errors.push(formatConsoleArgs(args));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await action();
|
|
126
|
+
return { result, messages };
|
|
127
|
+
} finally {
|
|
128
|
+
console.log = originalLog;
|
|
129
|
+
console.warn = originalWarn;
|
|
130
|
+
console.error = originalError;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function captureConsoleMessagesWithError<T>(
|
|
135
|
+
action: () => Promise<T>
|
|
136
|
+
): Promise<{ result?: T; messages: CapturedConsoleMessages; error?: unknown }> {
|
|
137
|
+
const messages: CapturedConsoleMessages = {
|
|
138
|
+
logs: [],
|
|
139
|
+
warnings: [],
|
|
140
|
+
errors: [],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const originalLog = console.log;
|
|
144
|
+
const originalWarn = console.warn;
|
|
145
|
+
const originalError = console.error;
|
|
146
|
+
|
|
147
|
+
console.log = (...args: unknown[]) => {
|
|
148
|
+
messages.logs.push(formatConsoleArgs(args));
|
|
149
|
+
};
|
|
150
|
+
console.warn = (...args: unknown[]) => {
|
|
151
|
+
messages.warnings.push(formatConsoleArgs(args));
|
|
152
|
+
};
|
|
153
|
+
console.error = (...args: unknown[]) => {
|
|
154
|
+
messages.errors.push(formatConsoleArgs(args));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
return { result: await action(), messages };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { messages, error };
|
|
161
|
+
} finally {
|
|
162
|
+
console.log = originalLog;
|
|
163
|
+
console.warn = originalWarn;
|
|
164
|
+
console.error = originalError;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function interceptProcessExit<T>(action: () => Promise<T>): Promise<T> {
|
|
169
|
+
const originalExit = process.exit;
|
|
170
|
+
|
|
171
|
+
process.exit = ((code?: number | string | null) => {
|
|
172
|
+
const normalizedCode = typeof code === "number"
|
|
173
|
+
? code
|
|
174
|
+
: typeof process.exitCode === "number"
|
|
175
|
+
? process.exitCode
|
|
176
|
+
: 1;
|
|
177
|
+
throw new ProcessExitInterceptError(normalizedCode);
|
|
178
|
+
}) as typeof process.exit;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return await action();
|
|
182
|
+
} finally {
|
|
183
|
+
process.exit = originalExit;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function trackFileMutations<T>(
|
|
188
|
+
action: () => Promise<T>
|
|
189
|
+
): Promise<{ result: T; mutations: FileMutationSummary }> {
|
|
190
|
+
const rootDirectory = process.cwd();
|
|
191
|
+
const before = snapshotFileSystem(rootDirectory);
|
|
192
|
+
const result = await action();
|
|
193
|
+
const after = snapshotFileSystem(rootDirectory);
|
|
194
|
+
|
|
195
|
+
const createdFiles = new Set<string>();
|
|
196
|
+
const updatedFiles = new Set<string>();
|
|
197
|
+
const deletedFiles = new Set<string>();
|
|
198
|
+
const createdDirectories = new Set<string>();
|
|
199
|
+
|
|
200
|
+
for (const [entryPath, afterEntry] of after.entries()) {
|
|
201
|
+
const beforeEntry = before.get(entryPath);
|
|
202
|
+
if (!beforeEntry) {
|
|
203
|
+
if (afterEntry.type === "directory") {
|
|
204
|
+
createdDirectories.add(normalizePathForSummary(entryPath));
|
|
205
|
+
} else {
|
|
206
|
+
createdFiles.add(normalizePathForSummary(entryPath));
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
afterEntry.type === "file" &&
|
|
213
|
+
(beforeEntry.mtimeMs !== afterEntry.mtimeMs || beforeEntry.size !== afterEntry.size)
|
|
214
|
+
) {
|
|
215
|
+
updatedFiles.add(normalizePathForSummary(entryPath));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const [entryPath, beforeEntry] of before.entries()) {
|
|
220
|
+
if (!after.has(entryPath) && beforeEntry.type === "file") {
|
|
221
|
+
deletedFiles.add(normalizePathForSummary(entryPath));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
result,
|
|
227
|
+
mutations: {
|
|
228
|
+
createdFiles: Array.from(createdFiles).sort(),
|
|
229
|
+
updatedFiles: Array.from(updatedFiles).sort(),
|
|
230
|
+
appendedFiles: [],
|
|
231
|
+
deletedFiles: Array.from(deletedFiles).sort(),
|
|
232
|
+
createdDirectories: Array.from(createdDirectories).sort(),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { scaffoldCommand } from "../../cli/commands/scaffold";
|
|
2
|
+
import {
|
|
3
|
+
captureConsoleMessagesWithError,
|
|
4
|
+
interceptProcessExit,
|
|
5
|
+
ProcessExitInterceptError,
|
|
6
|
+
trackFileMutations,
|
|
7
|
+
} from "./runtime";
|
|
8
|
+
|
|
9
|
+
export interface MachineScaffoldOperationOptions {
|
|
10
|
+
model: string;
|
|
11
|
+
functionsDir?: string;
|
|
12
|
+
apiDir?: string;
|
|
13
|
+
apiOnly?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MachineScaffoldOperationResult {
|
|
17
|
+
createdFiles: string[];
|
|
18
|
+
updatedFiles: string[];
|
|
19
|
+
appendedFiles: string[];
|
|
20
|
+
deletedFiles: string[];
|
|
21
|
+
createdDirectories: string[];
|
|
22
|
+
diagnostics: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function deriveErrorMessage(messages: { errors: string[]; warnings: string[]; logs: string[] }): string {
|
|
26
|
+
const errorText = messages.errors[messages.errors.length - 1];
|
|
27
|
+
if (errorText) {
|
|
28
|
+
return errorText.replace(/^\s*❌\s*/, "").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const warningText = messages.warnings[messages.warnings.length - 1];
|
|
32
|
+
if (warningText) {
|
|
33
|
+
return warningText.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const logText = messages.logs[messages.logs.length - 1];
|
|
37
|
+
if (logText) {
|
|
38
|
+
return logText.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return "Scaffold failed.";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runMachineScaffoldOperation(
|
|
45
|
+
options: MachineScaffoldOperationOptions
|
|
46
|
+
): Promise<MachineScaffoldOperationResult> {
|
|
47
|
+
const originalMachineOutput = process.env.SWALLOWKIT_MACHINE_OUTPUT;
|
|
48
|
+
process.env.SWALLOWKIT_MACHINE_OUTPUT = "1";
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const tracked = await trackFileMutations(async () => {
|
|
52
|
+
const captured = await captureConsoleMessagesWithError(async () => {
|
|
53
|
+
await interceptProcessExit(async () => {
|
|
54
|
+
await scaffoldCommand(options);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (captured.error) {
|
|
59
|
+
if (captured.error instanceof ProcessExitInterceptError) {
|
|
60
|
+
throw new Error(deriveErrorMessage(captured.messages));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (captured.error instanceof Error) {
|
|
64
|
+
throw captured.error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(deriveErrorMessage(captured.messages));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return captured.messages;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
createdFiles: tracked.mutations.createdFiles,
|
|
75
|
+
updatedFiles: tracked.mutations.updatedFiles,
|
|
76
|
+
appendedFiles: tracked.mutations.appendedFiles,
|
|
77
|
+
deletedFiles: tracked.mutations.deletedFiles,
|
|
78
|
+
createdDirectories: tracked.mutations.createdDirectories,
|
|
79
|
+
diagnostics: [
|
|
80
|
+
...tracked.result.warnings.map((warning) => `warning:${warning}`),
|
|
81
|
+
...tracked.result.errors.map((error) => `error:${error}`),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
} finally {
|
|
85
|
+
if (originalMachineOutput === undefined) {
|
|
86
|
+
delete process.env.SWALLOWKIT_MACHINE_OUTPUT;
|
|
87
|
+
} else {
|
|
88
|
+
process.env.SWALLOWKIT_MACHINE_OUTPUT = originalMachineOutput;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|