swallowkit 1.0.0-beta.19 → 1.0.0-beta.20

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 (75) hide show
  1. package/README.ja.md +36 -4
  2. package/README.md +36 -4
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +2 -0
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/add-connector.d.ts.map +1 -1
  7. package/dist/cli/commands/add-connector.js +2 -0
  8. package/dist/cli/commands/add-connector.js.map +1 -1
  9. package/dist/cli/commands/create-model.d.ts +0 -4
  10. package/dist/cli/commands/create-model.d.ts.map +1 -1
  11. package/dist/cli/commands/create-model.js +19 -145
  12. package/dist/cli/commands/create-model.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts.map +1 -1
  14. package/dist/cli/commands/init.js +2 -0
  15. package/dist/cli/commands/init.js.map +1 -1
  16. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  17. package/dist/cli/commands/scaffold.js +22 -10
  18. package/dist/cli/commands/scaffold.js.map +1 -1
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +5 -0
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/core/operations/create-model.d.ts +15 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -0
  24. package/dist/core/operations/create-model.js +171 -0
  25. package/dist/core/operations/create-model.js.map +1 -0
  26. package/dist/core/operations/runtime.d.ts +32 -0
  27. package/dist/core/operations/runtime.d.ts.map +1 -0
  28. package/dist/core/operations/runtime.js +225 -0
  29. package/dist/core/operations/runtime.js.map +1 -0
  30. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  31. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  32. package/dist/core/operations/scaffold-machine.js +63 -0
  33. package/dist/core/operations/scaffold-machine.js.map +1 -0
  34. package/dist/core/project/manifest.d.ts +92 -0
  35. package/dist/core/project/manifest.d.ts.map +1 -0
  36. package/dist/core/project/manifest.js +321 -0
  37. package/dist/core/project/manifest.js.map +1 -0
  38. package/dist/core/project/validation.d.ts +20 -0
  39. package/dist/core/project/validation.d.ts.map +1 -0
  40. package/dist/core/project/validation.js +204 -0
  41. package/dist/core/project/validation.js.map +1 -0
  42. package/dist/machine/contracts.d.ts +16 -0
  43. package/dist/machine/contracts.d.ts.map +1 -0
  44. package/dist/machine/contracts.js +3 -0
  45. package/dist/machine/contracts.js.map +1 -0
  46. package/dist/machine/errors.d.ts +11 -0
  47. package/dist/machine/errors.d.ts.map +1 -0
  48. package/dist/machine/errors.js +34 -0
  49. package/dist/machine/errors.js.map +1 -0
  50. package/dist/machine/index.d.ts +3 -0
  51. package/dist/machine/index.d.ts.map +1 -0
  52. package/dist/machine/index.js +156 -0
  53. package/dist/machine/index.js.map +1 -0
  54. package/dist/mcp/index.d.ts +25 -0
  55. package/dist/mcp/index.d.ts.map +1 -0
  56. package/dist/mcp/index.js +184 -0
  57. package/dist/mcp/index.js.map +1 -0
  58. package/package.json +6 -4
  59. package/src/__tests__/machine.test.ts +212 -0
  60. package/src/__tests__/mcp.test.ts +56 -0
  61. package/src/cli/commands/add-auth.ts +2 -0
  62. package/src/cli/commands/add-connector.ts +2 -0
  63. package/src/cli/commands/create-model.ts +19 -168
  64. package/src/cli/commands/init.ts +3 -0
  65. package/src/cli/commands/scaffold.ts +27 -10
  66. package/src/cli/index.ts +6 -0
  67. package/src/core/operations/create-model.ts +174 -0
  68. package/src/core/operations/runtime.ts +235 -0
  69. package/src/core/operations/scaffold-machine.ts +91 -0
  70. package/src/core/project/manifest.ts +402 -0
  71. package/src/core/project/validation.ts +221 -0
  72. package/src/machine/contracts.ts +17 -0
  73. package/src/machine/errors.ts +34 -0
  74. package/src/machine/index.ts +173 -0
  75. package/src/mcp/index.ts +185 -0
@@ -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
+ }