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.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +312 -215
  3. package/README.md +369 -216
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +444 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +21 -82
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +35 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +292 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +19 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +476 -117
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +13 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2627 -1708
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +3 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +617 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +5 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +164 -42
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +8 -2
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +90 -4
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +204 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1244 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1027 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +920 -213
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +329 -135
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  93. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  94. package/dist/core/scaffold/nextjs-generator.js +314 -182
  95. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  96. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  97. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  98. package/dist/core/scaffold/openapi-generator.js +190 -0
  99. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  100. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  101. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  102. package/dist/core/scaffold/ui-generator.js +768 -663
  103. package/dist/core/scaffold/ui-generator.js.map +1 -1
  104. package/dist/database/base-model.d.ts +3 -3
  105. package/dist/database/base-model.js +3 -3
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +2 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/machine/contracts.d.ts +16 -0
  111. package/dist/machine/contracts.d.ts.map +1 -0
  112. package/dist/machine/contracts.js +3 -0
  113. package/dist/machine/contracts.js.map +1 -0
  114. package/dist/machine/errors.d.ts +11 -0
  115. package/dist/machine/errors.d.ts.map +1 -0
  116. package/dist/machine/errors.js +34 -0
  117. package/dist/machine/errors.js.map +1 -0
  118. package/dist/machine/index.d.ts +3 -0
  119. package/dist/machine/index.d.ts.map +1 -0
  120. package/dist/machine/index.js +156 -0
  121. package/dist/machine/index.js.map +1 -0
  122. package/dist/mcp/index.d.ts +25 -0
  123. package/dist/mcp/index.d.ts.map +1 -0
  124. package/dist/mcp/index.js +184 -0
  125. package/dist/mcp/index.js.map +1 -0
  126. package/dist/types/index.d.ts +65 -0
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/utils/package-manager.d.ts +109 -0
  129. package/dist/utils/package-manager.d.ts.map +1 -0
  130. package/dist/utils/package-manager.js +215 -0
  131. package/dist/utils/package-manager.js.map +1 -0
  132. package/package.json +85 -73
  133. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  134. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  135. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  136. package/src/__tests__/auth.test.ts +654 -0
  137. package/src/__tests__/config.test.ts +263 -0
  138. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  139. package/src/__tests__/connector-mock-server.test.ts +439 -0
  140. package/src/__tests__/connector-model-bff.test.ts +162 -0
  141. package/src/__tests__/dev-seeds.test.ts +112 -0
  142. package/src/__tests__/dev.test.ts +154 -0
  143. package/src/__tests__/fixtures.ts +144 -0
  144. package/src/__tests__/functions-generator.test.ts +237 -0
  145. package/src/__tests__/init.test.ts +80 -0
  146. package/src/__tests__/machine.test.ts +212 -0
  147. package/src/__tests__/mcp.test.ts +56 -0
  148. package/src/__tests__/model-parser.test.ts +72 -0
  149. package/src/__tests__/nextjs-generator.test.ts +97 -0
  150. package/src/__tests__/openapi-generator.test.ts +43 -0
  151. package/src/__tests__/package-manager.test.ts +189 -0
  152. package/src/__tests__/scaffold.test.ts +39 -0
  153. package/src/__tests__/string-utils.test.ts +75 -0
  154. package/src/__tests__/ui-generator.test.ts +144 -0
  155. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  156. package/src/cli/commands/add-auth.ts +500 -0
  157. package/src/cli/commands/add-connector.ts +158 -0
  158. package/src/cli/commands/create-model.ts +62 -0
  159. package/src/cli/commands/dev-seeds.ts +358 -0
  160. package/src/cli/commands/dev.ts +962 -0
  161. package/src/cli/commands/index.ts +9 -0
  162. package/src/cli/commands/init.ts +3371 -0
  163. package/src/cli/commands/provision.ts +193 -0
  164. package/src/cli/commands/scaffold.ts +1211 -0
  165. package/src/cli/index.ts +193 -0
  166. package/src/core/config.ts +308 -0
  167. package/src/core/mock/connector-mock-server.ts +555 -0
  168. package/src/core/mock/zod-mock-generator.ts +205 -0
  169. package/src/core/operations/create-model.ts +174 -0
  170. package/src/core/operations/runtime.ts +235 -0
  171. package/src/core/operations/scaffold-machine.ts +91 -0
  172. package/src/core/project/manifest.ts +402 -0
  173. package/src/core/project/validation.ts +221 -0
  174. package/src/core/scaffold/auth-generator.ts +1284 -0
  175. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  176. package/src/core/scaffold/functions-generator.ts +970 -0
  177. package/src/core/scaffold/model-parser.ts +841 -0
  178. package/src/core/scaffold/nextjs-generator.ts +370 -0
  179. package/src/core/scaffold/openapi-generator.ts +212 -0
  180. package/src/core/scaffold/ui-generator.ts +1061 -0
  181. package/src/database/base-model.ts +184 -0
  182. package/src/database/client.ts +140 -0
  183. package/src/database/repository.ts +104 -0
  184. package/src/database/runtime-check.ts +25 -0
  185. package/src/index.ts +27 -0
  186. package/src/machine/contracts.ts +17 -0
  187. package/src/machine/errors.ts +34 -0
  188. package/src/machine/index.ts +173 -0
  189. package/src/mcp/index.ts +185 -0
  190. package/src/types/index.ts +134 -0
  191. 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
+ }