swallowkit 1.0.0-beta.34 → 1.0.0-beta.37

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 (33) hide show
  1. package/README.ja.md +84 -284
  2. package/README.md +85 -338
  3. package/dist/cli/commands/init.d.ts +7 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +136 -32
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  8. package/dist/cli/commands/scaffold.js +4 -1
  9. package/dist/cli/commands/scaffold.js.map +1 -1
  10. package/dist/cli/index.js +1 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/core/project/manifest.js +1 -1
  13. package/dist/core/project/manifest.js.map +1 -1
  14. package/dist/core/scaffold/functions-generator.js +1 -1
  15. package/dist/core/scaffold/functions-generator.js.map +1 -1
  16. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  17. package/dist/core/scaffold/native-schema-generator.js +22 -3
  18. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  19. package/dist/machine/index.js +1 -1
  20. package/dist/machine/index.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +3 -3
  23. package/src/__tests__/functions-generator.test.ts +1 -1
  24. package/src/__tests__/init.test.ts +21 -0
  25. package/src/__tests__/machine.test.ts +105 -0
  26. package/src/__tests__/scaffold.test.ts +94 -0
  27. package/src/cli/commands/init.ts +150 -39
  28. package/src/cli/commands/scaffold.ts +5 -1
  29. package/src/cli/index.ts +1 -1
  30. package/src/core/project/manifest.ts +1 -1
  31. package/src/core/scaffold/functions-generator.ts +1 -1
  32. package/src/core/scaffold/native-schema-generator.ts +27 -3
  33. package/src/machine/index.ts +1 -1
@@ -1,8 +1,19 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import { EventEmitter } from "events";
5
+ import * as childProcess from "child_process";
4
6
  import { runMachineCli } from "../machine";
5
7
 
8
+ jest.mock("child_process", () => {
9
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
10
+ return {
11
+ ...actual,
12
+ spawn: jest.fn(),
13
+ spawnSync: jest.fn(),
14
+ };
15
+ });
16
+
6
17
  const repoRoot = process.cwd();
7
18
 
8
19
  function writeFile(filePath: string, content: string): void {
@@ -68,6 +79,59 @@ function createProjectFixture(rootDir: string, options: { includeGeneratedArtifa
68
79
  }
69
80
  }
70
81
 
82
+ function createCSharpScaffoldFixture(rootDir: string): void {
83
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
84
+ writeFile(
85
+ path.join(rootDir, "swallowkit.config.js"),
86
+ `module.exports = {
87
+ backend: {
88
+ language: 'csharp',
89
+ },
90
+ api: {
91
+ endpoint: '/api/_swallowkit',
92
+ },
93
+ };
94
+ `
95
+ );
96
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
97
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
98
+ writeFile(path.join(rootDir, "shared", "models", "estimate.ts"), createModelSource("Estimate"));
99
+ writeFile(path.join(rootDir, "shared", "models", "member.ts"), createModelSource("Member"));
100
+ writeFile(path.join(rootDir, "shared", "models", "team.ts"), createModelSource("Team"));
101
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
102
+ fs.symlinkSync(
103
+ path.join(repoRoot, "node_modules", "zod"),
104
+ path.join(rootDir, "node_modules", "zod"),
105
+ "junction"
106
+ );
107
+
108
+ writeFile(
109
+ path.join(rootDir, "functions", "Crud", "EstimateFunctions.cs"),
110
+ "namespace SwallowKit.Functions;\npublic sealed class EstimateFunctions { public void Approve() {} public void Submit() {} public void Remand() {} }\n"
111
+ );
112
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "README.md"), "keep generated readme\n");
113
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"), "<Project />\n");
114
+ writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"), "solution\n");
115
+ writeFile(
116
+ path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"),
117
+ "// existing member\n"
118
+ );
119
+ writeFile(
120
+ path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"),
121
+ "// existing team\n"
122
+ );
123
+ }
124
+
125
+ function mockSuccessfulSpawn() {
126
+ const spawnMock = childProcess.spawn as unknown as any;
127
+ spawnMock.mockImplementation(() => {
128
+ const child = new EventEmitter() as childProcess.ChildProcess;
129
+ process.nextTick(() => child.emit("close", 0));
130
+ return child;
131
+ });
132
+ return spawnMock;
133
+ }
134
+
71
135
  function createFreshInitProjectFixture(rootDir: string, backendLanguage: "csharp" | "python" = "csharp"): void {
72
136
  writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
73
137
  writeFile(
@@ -248,4 +312,45 @@ describe("machine CLI", () => {
248
312
  );
249
313
  expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
250
314
  });
315
+
316
+ it("preserves C# custom Functions and shared generated assets when api-only scaffolding one model", async () => {
317
+ createCSharpScaffoldFixture(tempDir);
318
+ const spawnSyncMock = childProcess.spawnSync as unknown as any;
319
+ spawnSyncMock.mockReturnValue({ status: 0 });
320
+ const spawnMock = mockSuccessfulSpawn();
321
+
322
+ try {
323
+ const { response, exitCode } = await runMachine([
324
+ "node",
325
+ "swallowkit",
326
+ "machine",
327
+ "generate",
328
+ "scaffold",
329
+ "estimate",
330
+ "--api-only",
331
+ ]);
332
+
333
+ expect(exitCode).toBe(0);
334
+ expect(response.ok).toBe(true);
335
+ expect(response.command).toBe("generate-scaffold");
336
+ expect(response.data.createdFiles).toEqual(
337
+ expect.arrayContaining([
338
+ "functions/Crud/EstimateCrudFunctions.cs",
339
+ "app/api/estimate/route.ts",
340
+ "app/api/estimate/[id]/route.ts",
341
+ ])
342
+ );
343
+ expect(response.data.deletedFiles).toEqual([]);
344
+ expect(fs.readFileSync(path.join(tempDir, "functions", "Crud", "EstimateFunctions.cs"), "utf-8")).toContain("Approve");
345
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "README.md"))).toBe(true);
346
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"))).toBe(true);
347
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"))).toBe(true);
348
+ expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"), "utf-8")).toBe("// existing member\n");
349
+ expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"), "utf-8")).toBe("// existing team\n");
350
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Estimate.cs"))).toBe(true);
351
+ } finally {
352
+ spawnMock.mockReset();
353
+ spawnSyncMock.mockReset();
354
+ }
355
+ });
251
356
  });
@@ -1,14 +1,44 @@
1
1
  import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import { EventEmitter } from "events";
5
+ import * as childProcess from "child_process";
2
6
  import {
3
7
  NSWAG_CONSOLECORE_VERSION,
4
8
  buildCSharpCodegenToolManifestSource,
5
9
  buildPythonCodegenRequirementsSource,
10
+ generateLanguageSchemaArtifacts,
6
11
  getCSharpNativeGeneratorArgs,
7
12
  getCSharpSchemaModelPath,
8
13
  getCSharpSchemaOptionPath,
9
14
  getPythonNativeGeneratorArgs,
10
15
  getPythonSchemaModelPath,
11
16
  } from "../core/scaffold/native-schema-generator";
17
+ import { createBasicModelInfo } from "./fixtures";
18
+
19
+ jest.mock("child_process", () => {
20
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
21
+ return {
22
+ ...actual,
23
+ spawn: jest.fn(),
24
+ spawnSync: jest.fn(),
25
+ };
26
+ });
27
+
28
+ function writeFile(filePath: string, content: string): void {
29
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
30
+ fs.writeFileSync(filePath, content, "utf-8");
31
+ }
32
+
33
+ function mockSuccessfulSpawn() {
34
+ const spawnMock = childProcess.spawn as unknown as any;
35
+ spawnMock.mockImplementation(() => {
36
+ const child = new EventEmitter() as childProcess.ChildProcess;
37
+ process.nextTick(() => child.emit("close", 0));
38
+ return child;
39
+ });
40
+ return spawnMock;
41
+ }
12
42
 
13
43
  describe("native schema generators", () => {
14
44
  it("writes a dotnet tool manifest for NSwag", () => {
@@ -64,4 +94,68 @@ describe("native schema generators", () => {
64
94
  path.join("C:\\temp\\generated\\python-models", "backend_models", "models", "product.py")
65
95
  );
66
96
  });
97
+
98
+ describe("non-destructive native schema generation", () => {
99
+ const originalCwd = process.cwd();
100
+ let tempDir: string;
101
+ let spawnSyncMock: any;
102
+ let spawnMock: any;
103
+
104
+ beforeEach(() => {
105
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-native-schema-"));
106
+ process.chdir(tempDir);
107
+ fs.mkdirSync(path.join(tempDir, "functions"), { recursive: true });
108
+ spawnSyncMock = childProcess.spawnSync as unknown as any;
109
+ spawnSyncMock.mockReturnValue({ status: 0 });
110
+ spawnMock = mockSuccessfulSpawn();
111
+ });
112
+
113
+ afterEach(() => {
114
+ spawnMock.mockReset();
115
+ spawnSyncMock.mockReset();
116
+ process.chdir(originalCwd);
117
+ fs.rmSync(tempDir, { recursive: true, force: true });
118
+ });
119
+
120
+ it("preserves existing C# generated directory contents while writing only requested model files", async () => {
121
+ const outputDir = path.join(tempDir, "functions", "generated", "csharp-models");
122
+ writeFile(path.join(outputDir, "README.md"), "keep me\n");
123
+ writeFile(path.join(outputDir, "SwallowKitBackendModels.csproj"), "<Project />\n");
124
+ writeFile(path.join(outputDir, "src", "SwallowKitBackendModels", "Model", "Member.cs"), "// existing member\n");
125
+
126
+ const estimate = createBasicModelInfo({ name: "Estimate", filePath: path.join(tempDir, "shared", "models", "estimate.ts") });
127
+
128
+ await generateLanguageSchemaArtifacts([estimate], estimate, "functions", "csharp");
129
+
130
+ expect(fs.readFileSync(path.join(outputDir, "README.md"), "utf-8")).toBe("keep me\n");
131
+ expect(fs.existsSync(path.join(outputDir, "SwallowKitBackendModels.csproj"))).toBe(true);
132
+ expect(fs.readFileSync(getCSharpSchemaModelPath(outputDir, "Member"), "utf-8")).toBe("// existing member\n");
133
+ expect(fs.existsSync(getCSharpSchemaModelPath(outputDir, "Todo"))).toBe(false);
134
+ expect(fs.existsSync(getCSharpSchemaModelPath(outputDir, "Estimate"))).toBe(true);
135
+ expect(fs.existsSync(getCSharpSchemaOptionPath(outputDir))).toBe(true);
136
+ });
137
+
138
+ it("preserves existing Python generated directory contents while appending requested model exports", async () => {
139
+ const outputDir = path.join(tempDir, "functions", "generated", "python-models");
140
+ writeFile(path.join(outputDir, "README.md"), "keep python\n");
141
+ writeFile(path.join(outputDir, "backend_models", "models", "member.py"), "# existing member\n");
142
+ writeFile(path.join(outputDir, "backend_models", "__init__.py"), "from .models.member import Member\n");
143
+ writeFile(path.join(outputDir, "backend_models", "models", "__init__.py"), "from .member import Member\n");
144
+
145
+ const estimate = createBasicModelInfo({ name: "Estimate", filePath: path.join(tempDir, "shared", "models", "estimate.ts") });
146
+
147
+ await generateLanguageSchemaArtifacts([estimate], estimate, "functions", "python");
148
+
149
+ expect(fs.readFileSync(path.join(outputDir, "README.md"), "utf-8")).toBe("keep python\n");
150
+ expect(fs.readFileSync(path.join(outputDir, "backend_models", "models", "member.py"), "utf-8")).toBe("# existing member\n");
151
+ expect(fs.existsSync(getPythonSchemaModelPath(outputDir, "Todo"))).toBe(false);
152
+ expect(fs.existsSync(getPythonSchemaModelPath(outputDir, "Estimate"))).toBe(true);
153
+ const packageInit = fs.readFileSync(path.join(outputDir, "backend_models", "__init__.py"), "utf-8");
154
+ const modelsInit = fs.readFileSync(path.join(outputDir, "backend_models", "models", "__init__.py"), "utf-8");
155
+ expect(packageInit).toContain("from .models.member import Member");
156
+ expect(packageInit).toContain("from .models.estimate import Estimate");
157
+ expect(modelsInit).toContain("from .member import Member");
158
+ expect(modelsInit).toContain("from .estimate import Estimate");
159
+ });
160
+ });
67
161
  });
@@ -375,32 +375,136 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
375
375
  }
376
376
 
377
377
  async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
378
+ console.log('\n📦 Installing dependencies...\n');
379
+
380
+ const { ignoredBuilds } = await runInstallAndDetectIgnoredBuilds(projectDir, pm);
381
+
382
+ console.log('\n✅ Dependencies installed\n');
383
+
384
+ if (pm === 'pnpm' && ignoredBuilds.length > 0) {
385
+ await maybeApproveBuilds(projectDir, ignoredBuilds);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Run `<pm> install` while passing through stdio so the user sees progress.
391
+ * For pnpm, also tee stdout/stderr to detect the ERR_PNPM_IGNORED_BUILDS warning
392
+ * and extract the affected package names.
393
+ */
394
+ async function runInstallAndDetectIgnoredBuilds(
395
+ projectDir: string,
396
+ pm: PackageManager,
397
+ ): Promise<{ ignoredBuilds: string[] }> {
378
398
  return new Promise((resolve, reject) => {
379
- console.log('\n📦 Installing dependencies...\n');
380
-
381
- const child = spawn(
382
- pm,
383
- ['install'],
384
- {
399
+ // For npm, we don't need to detect anything — keep simple inherit behavior.
400
+ if (pm !== 'pnpm') {
401
+ const child = spawn(pm, ['install'], {
385
402
  cwd: projectDir,
386
403
  stdio: 'inherit',
387
404
  shell: true,
388
- }
389
- );
405
+ });
406
+ child.on('close', (code) => {
407
+ if (code !== 0) reject(new Error(`${pm} install exited with code ${code}`));
408
+ else resolve({ ignoredBuilds: [] });
409
+ });
410
+ child.on('error', reject);
411
+ return;
412
+ }
390
413
 
391
- child.on('close', (code: number | null) => {
414
+ // pnpm: capture output while teeing to the user's terminal.
415
+ const child = spawn(pm, ['install'], {
416
+ cwd: projectDir,
417
+ stdio: ['inherit', 'pipe', 'pipe'],
418
+ shell: true,
419
+ });
420
+
421
+ let combined = '';
422
+ child.stdout?.on('data', (chunk: Buffer) => {
423
+ process.stdout.write(chunk);
424
+ combined += chunk.toString('utf8');
425
+ });
426
+ child.stderr?.on('data', (chunk: Buffer) => {
427
+ process.stderr.write(chunk);
428
+ combined += chunk.toString('utf8');
429
+ });
430
+
431
+ child.on('close', (code) => {
392
432
  if (code !== 0) {
393
433
  reject(new Error(`${pm} install exited with code ${code}`));
394
- } else {
395
- console.log('\n✅ Dependencies installed\n');
396
- resolve();
434
+ return;
397
435
  }
436
+ resolve({ ignoredBuilds: parseIgnoredBuilds(combined) });
398
437
  });
438
+ child.on('error', reject);
439
+ });
440
+ }
399
441
 
400
- child.on('error', (error: Error) => {
401
- reject(error);
442
+ /**
443
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning and return
444
+ * the bare package names (without versions), de-duplicated.
445
+ *
446
+ * Matches both `ERR_PNPM_IGNORED_BUILDS` and the plain `Ignored build scripts:` line.
447
+ */
448
+ export function parseIgnoredBuilds(output: string): string[] {
449
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
450
+ if (!match) return [];
451
+ const list = match[1]
452
+ .split(',')
453
+ .map((entry) => entry.trim())
454
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
455
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
456
+ .filter((name) => name.length > 0);
457
+ return Array.from(new Set(list));
458
+ }
459
+
460
+ async function maybeApproveBuilds(projectDir: string, ignoredBuilds: string[]): Promise<void> {
461
+ console.log(
462
+ `\n⚠️ pnpm skipped build scripts for: ${ignoredBuilds.join(', ')}\n` +
463
+ ' These packages (e.g. sharp) need their build scripts to run correctly.\n',
464
+ );
465
+
466
+ const response = await prompts({
467
+ type: 'confirm',
468
+ name: 'approve',
469
+ message: 'Run `pnpm approve-builds` now to approve these build scripts?',
470
+ initial: true,
471
+ });
472
+
473
+ if (!response.approve) {
474
+ console.log(
475
+ '\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n',
476
+ );
477
+ return;
478
+ }
479
+
480
+ await new Promise<void>((resolve, reject) => {
481
+ const child = spawn('pnpm', ['approve-builds'], {
482
+ cwd: projectDir,
483
+ stdio: 'inherit',
484
+ shell: true,
485
+ });
486
+ child.on('close', (code) => {
487
+ if (code !== 0) reject(new Error(`pnpm approve-builds exited with code ${code}`));
488
+ else resolve();
402
489
  });
490
+ child.on('error', reject);
403
491
  });
492
+
493
+ // After approval, rebuild the approved packages so their native binaries are present.
494
+ await new Promise<void>((resolve, reject) => {
495
+ const child = spawn('pnpm', ['rebuild', ...ignoredBuilds], {
496
+ cwd: projectDir,
497
+ stdio: 'inherit',
498
+ shell: true,
499
+ });
500
+ child.on('close', (code) => {
501
+ if (code !== 0) reject(new Error(`pnpm rebuild exited with code ${code}`));
502
+ else resolve();
503
+ });
504
+ child.on('error', reject);
505
+ });
506
+
507
+ console.log('\n✅ Build scripts approved and packages rebuilt\n');
404
508
  }
405
509
 
406
510
  export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
@@ -876,7 +980,7 @@ export async function register() {
876
980
  createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
877
981
 
878
982
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
879
- createAiAgentFiles(projectDir, projectName, backendLanguage);
983
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
880
984
  }
881
985
 
882
986
  async function createSharedPackage(projectDir: string, projectName: string) {
@@ -1700,9 +1804,10 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1700
1804
  console.log('✅ README.md created\n');
1701
1805
  }
1702
1806
 
1703
- function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage) {
1807
+ function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage, pm: PackageManager) {
1704
1808
  console.log('🤖 Creating AI agent instruction files...\n');
1705
1809
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1810
+ const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
1706
1811
  const projectMcpConfigSource = buildSwallowKitMcpProjectConfigSource();
1707
1812
  const functionsStructureLine = backendLanguage === 'typescript'
1708
1813
  ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
@@ -1764,11 +1869,12 @@ ${functionsStructureLine}
1764
1869
  - This repository includes a project-scoped \`.mcp.json\` file that starts the locally installed SwallowKit MCP server on runtimes that auto-load project MCP configurations.
1765
1870
  - Prefer the \`swallowkit_*\` MCP tools for framework-owned inspection, validation, and generation when they are available.
1766
1871
  - If MCP is unavailable in your runtime, fall back to the machine CLI:
1767
- - \`npx swallowkit machine inspect project\`
1768
- - \`npx swallowkit machine validate project\`
1769
- - \`npx swallowkit machine generate scaffold <name> --api-only\`
1872
+ - \`${runCmd} swallowkit machine inspect project\`
1873
+ - \`${runCmd} swallowkit machine validate project\`
1874
+ - \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`
1770
1875
  - Do not hand-edit framework-owned artifacts when the MCP or machine interface can generate or validate them for you.
1771
1876
  - The local MCP bootstrap depends on project dependencies already being installed.
1877
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
1772
1878
 
1773
1879
  ## Critical Design Principles
1774
1880
 
@@ -1869,7 +1975,7 @@ app.http('{model}-get-all', {
1869
1975
  |------|-----------|---------|
1870
1976
  | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1871
1977
  | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1872
- | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1978
+ | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoCrudFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1873
1979
  | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1874
1980
  | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1875
1981
  | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
@@ -1887,9 +1993,9 @@ Use the SwallowKit CLI — do **not** manually create model files or CRUD boiler
1887
1993
  ### Skill: Create a new data model
1888
1994
 
1889
1995
  \`\`\`bash
1890
- npx swallowkit create-model <name>
1996
+ ${runCmd} swallowkit create-model <name>
1891
1997
  # Multiple models at once:
1892
- npx swallowkit create-model user post comment
1998
+ ${runCmd} swallowkit create-model user post comment
1893
1999
  \`\`\`
1894
2000
 
1895
2001
  Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
@@ -1898,7 +2004,7 @@ Edit the generated file to add your domain-specific fields, then run scaffold.
1898
2004
  ### Skill: Generate full CRUD from a model
1899
2005
 
1900
2006
  \`\`\`bash
1901
- npx swallowkit scaffold shared/models/<name>.ts
2007
+ ${runCmd} swallowkit scaffold shared/models/<name>.ts
1902
2008
  \`\`\`
1903
2009
 
1904
2010
  Generates:
@@ -1910,7 +2016,7 @@ Generates:
1910
2016
  ### Skill: Start development servers
1911
2017
 
1912
2018
  \`\`\`bash
1913
- npx swallowkit dev
2019
+ ${runCmd} swallowkit dev
1914
2020
  \`\`\`
1915
2021
 
1916
2022
  Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
@@ -1919,17 +2025,18 @@ Checks for Cosmos DB Emulator availability.
1919
2025
  ### Skill: Provision Azure resources
1920
2026
 
1921
2027
  \`\`\`bash
1922
- npx swallowkit provision --resource-group <name> --location <region>
2028
+ ${runCmd} swallowkit provision --resource-group <name> --location <region>
1923
2029
  \`\`\`
1924
2030
 
1925
2031
  Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1926
2032
 
1927
2033
  ### Typical workflow for "add a new feature/model"
1928
2034
 
1929
- 1. \`npx swallowkit create-model <name>\`
2035
+ 1. \`${runCmd} swallowkit create-model <name>\`
1930
2036
  2. Edit \`shared/models/<name>.ts\` — add fields
1931
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1932
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
2037
+ 3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
2038
+ 4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
2039
+ 5. If \`dev-seeds/\` already exists, update the seed JSON files to include realistic data for the new model and adjust existing seeds if relationships changed.
1933
2040
 
1934
2041
  ## Do NOT
1935
2042
 
@@ -1974,23 +2081,25 @@ This file is for Claude Code. Read AGENTS.md in the project root for the full ar
1974
2081
 
1975
2082
  - This repository includes a project-scoped \`.mcp.json\` that registers the locally installed SwallowKit MCP server for runtimes that support project MCP files.
1976
2083
  - When the \`swallowkit_*\` tools are available, prefer them for inspect / validate / generate tasks.
1977
- - If MCP is unavailable, use \`npx swallowkit machine ...\` instead.
2084
+ - If MCP is unavailable, use \`${runCmd} swallowkit machine ...\` instead.
2085
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
1978
2086
 
1979
2087
  ## SwallowKit CLI Commands
1980
2088
 
1981
2089
  | Task | Command |
1982
2090
  |------|---------|
1983
- | Create model | \`npx swallowkit create-model <name>\` |
1984
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1985
- | Dev servers | \`npx swallowkit dev\` |
1986
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
2091
+ | Create model | \`${runCmd} swallowkit create-model <name>\` |
2092
+ | Generate CRUD | \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` |
2093
+ | Dev servers | \`${runCmd} swallowkit dev\` |
2094
+ | Provision Azure | \`${runCmd} swallowkit provision --resource-group <rg> --location <region>\` |
1987
2095
 
1988
2096
  ## Workflow: Add a new model
1989
2097
 
1990
- 1. \`npx swallowkit create-model <name>\`
2098
+ 1. \`${runCmd} swallowkit create-model <name>\`
1991
2099
  2. Edit \`shared/models/<name>.ts\` — add your fields
1992
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1993
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
2100
+ 3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
2101
+ 4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
2102
+ 5. If \`dev-seeds/\` exists, update the seed JSON files to include data for the new model and adjust existing seeds if relationships changed.
1994
2103
  `;
1995
2104
 
1996
2105
  fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
@@ -2019,13 +2128,15 @@ Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Fu
2019
2128
  1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
2020
2129
  2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
2021
2130
  3. **Backend owns data** — All CRUD and business logic stay in \`functions/\`, and generated contract assets under \`functions/generated/\` must stay aligned with \`shared/models/\`.
2022
- 4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
2131
+ 4. **Use the CLI** — Run \`${runCmd} swallowkit create-model <name>\` then \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
2132
+ 5. **Maintain seed data** — When adding models or changing schemas, update the JSON files under \`dev-seeds/\` to keep seed data consistent with the current schema.
2023
2133
 
2024
2134
  ## SwallowKit Framework Operations
2025
2135
 
2026
2136
  - Prefer the SwallowKit MCP or machine interface for framework-owned inspection, validation, and generation instead of hand-editing generated files.
2027
2137
  - If your runtime loads project-scoped MCP config from \`.mcp.json\`, use the \`swallowkit_*\` tools.
2028
- - Otherwise use \`npx swallowkit machine inspect project\`, \`npx swallowkit machine validate project\`, and \`npx swallowkit machine generate scaffold <name> --api-only\`.
2138
+ - Otherwise use \`${runCmd} swallowkit machine inspect project\`, \`${runCmd} swallowkit machine validate project\`, and \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`.
2139
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
2029
2140
 
2030
2141
  ## Naming
2031
2142
 
@@ -2067,7 +2178,7 @@ Files in this directory are the **single source of truth** for data models acros
2067
2178
  - Export a \`displayName\` string constant for UI display.
2068
2179
  - Re-export every model from \`shared/index.ts\`.
2069
2180
  - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
2070
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
2181
+ - After editing a model, run \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
2071
2182
  `;
2072
2183
 
2073
2184
  fs.writeFileSync(
@@ -120,6 +120,10 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
120
120
  ? [modelInfo]
121
121
  : await collectModelGraph(modelPath);
122
122
 
123
+ if (options.apiOnly) {
124
+ console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
125
+ }
126
+
123
127
  // 5. Generate BFF callFunction helper
124
128
  await generateCallFunctionHelper();
125
129
 
@@ -351,7 +355,7 @@ async function generateFunctionsCode(
351
355
 
352
356
  if (backendLanguage === "csharp") {
353
357
  const crudDir = path.join(process.cwd(), functionsDir, "Crud");
354
- const functionFilePath = path.join(crudDir, `${modelInfo.name}Functions.cs`);
358
+ const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
355
359
  fs.mkdirSync(crudDir, { recursive: true });
356
360
 
357
361
  // Remove init-generated template (singular) to avoid route conflicts
package/src/cli/index.ts CHANGED
@@ -142,7 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
142
142
  .description("Generate CRUD code for Azure Functions and Next.js BFF from Zod models")
143
143
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
144
144
  .option("--api-dir <dir>", "Next.js API routes directory", "app/api")
145
- .option("--api-only", "Generate API only, skip UI components", false)
145
+ .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
146
146
  .action((model, options) => {
147
147
  scaffoldCommand({
148
148
  model,
@@ -198,7 +198,7 @@ function buildEntityRoutes(
198
198
  : backendLanguage === "csharp"
199
199
  ? isConnectorEntity
200
200
  ? `functions/Connectors/${entity.name}ConnectorFunctions.cs`
201
- : `functions/Crud/${entity.name}Functions.cs`
201
+ : `functions/Crud/${entity.name}CrudFunctions.cs`
202
202
  : `functions/blueprints/${modelKebab.replace(/-/g, "_")}.py`;
203
203
 
204
204
  const operations = entity.connectorConfig?.operations ?? ["getAll", "getById", "create", "update", "delete"];
@@ -418,7 +418,7 @@ ${authCatchBlock} context.error(\`Error deleting item from \${containerName
418
418
  export function generateCSharpAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): string {
419
419
  const modelName = model.name;
420
420
  const modelCamel = toCamelCase(modelName);
421
- const className = `${modelName}Functions`;
421
+ const className = `${modelName}CrudFunctions`;
422
422
  const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
423
423
 
424
424
  const partitionKeyPath = model.partitionKey;
@@ -601,6 +601,31 @@ function buildGeneratedPythonModelsInitSource(models: ModelInfo[]): string {
601
601
  return models.map((model) => `from .${toSnakeCase(model.name)} import ${model.name}`).join("\n") + "\n";
602
602
  }
603
603
 
604
+ function mergePythonInitSource(existingSource: string | undefined, generatedSource: string): string {
605
+ if (!existingSource) {
606
+ return generatedSource;
607
+ }
608
+
609
+ const existingLines = existingSource.split(/\r?\n/);
610
+ const lineSet = new Set(existingLines);
611
+ const missingLines = generatedSource
612
+ .trim()
613
+ .split(/\r?\n/)
614
+ .filter((line) => line.length > 0 && !lineSet.has(line));
615
+
616
+ if (missingLines.length === 0) {
617
+ return existingSource.endsWith("\n") ? existingSource : `${existingSource}\n`;
618
+ }
619
+
620
+ const normalizedExisting = existingSource.endsWith("\n") ? existingSource : `${existingSource}\n`;
621
+ return `${normalizedExisting}${missingLines.join("\n")}\n`;
622
+ }
623
+
624
+ function writeMergedPythonInitFile(filePath: string, generatedSource: string): void {
625
+ const existingSource = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined;
626
+ fs.writeFileSync(filePath, mergePythonInitSource(existingSource, generatedSource), "utf-8");
627
+ }
628
+
604
629
  function ensureCSharpCodegenProjectFiles(functionsRoot: string): void {
605
630
  const toolManifestPath = path.join(functionsRoot, ".config", "dotnet-tools.json");
606
631
  fs.mkdirSync(path.dirname(toolManifestPath), { recursive: true });
@@ -752,8 +777,8 @@ async function generatePythonSchemaArtifacts(
752
777
  const packageRoot = path.join(outputDir, "backend_models");
753
778
  const modelsRoot = path.join(packageRoot, "models");
754
779
  fs.mkdirSync(modelsRoot, { recursive: true });
755
- fs.writeFileSync(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models), "utf-8");
756
- fs.writeFileSync(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models), "utf-8");
780
+ writeMergedPythonInitFile(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models));
781
+ writeMergedPythonInitFile(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models));
757
782
 
758
783
  for (const model of models) {
759
784
  const modelPath = getPythonSchemaModelPath(outputDir, model.name);
@@ -785,7 +810,6 @@ export async function generateLanguageSchemaArtifacts(
785
810
  backendLanguage === "csharp" ? "csharp-models" : "python-models"
786
811
  );
787
812
 
788
- fs.rmSync(outputDir, { recursive: true, force: true });
789
813
  fs.mkdirSync(outputDir, { recursive: true });
790
814
 
791
815
  if (backendLanguage === "csharp") {
@@ -134,7 +134,7 @@ function createMachineProgram(): Command {
134
134
  .argument("<model>", "Model file or model name")
135
135
  .option("--functions-dir <dir>", "Functions directory", "functions")
136
136
  .option("--api-dir <dir>", "API routes directory", "app/api")
137
- .option("--api-only", "Generate only API artifacts", false)
137
+ .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
138
138
  .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
139
139
  await handleMachineAction("generate-scaffold", async () => runMachineScaffoldOperation({
140
140
  model,