swallowkit 1.0.0-beta.60 → 1.0.0-beta.61

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 (41) hide show
  1. package/README.ja.md +3 -0
  2. package/README.md +3 -0
  3. package/dist/cli/commands/add-connector.d.ts.map +1 -1
  4. package/dist/cli/commands/add-connector.js +2 -1
  5. package/dist/cli/commands/add-connector.js.map +1 -1
  6. package/dist/cli/commands/create-model.js +1 -1
  7. package/dist/cli/commands/create-model.js.map +1 -1
  8. package/dist/cli/commands/init.d.ts +1 -5
  9. package/dist/cli/commands/init.d.ts.map +1 -1
  10. package/dist/cli/commands/init.js +45 -154
  11. package/dist/cli/commands/init.js.map +1 -1
  12. package/dist/cli/commands/scaffold.js +1 -1
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  18. package/dist/core/project/agent-instructions.js +2 -1
  19. package/dist/core/project/agent-instructions.js.map +1 -1
  20. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  21. package/dist/core/scaffold/model-parser.js +43 -92
  22. package/dist/core/scaffold/model-parser.js.map +1 -1
  23. package/dist/utils/package-manager.d.ts +4 -10
  24. package/dist/utils/package-manager.d.ts.map +1 -1
  25. package/dist/utils/package-manager.js +23 -15
  26. package/dist/utils/package-manager.js.map +1 -1
  27. package/package.json +8 -13
  28. package/src/__tests__/agent-instructions.test.ts +1 -1
  29. package/src/__tests__/connector-mock-server.test.ts +45 -25
  30. package/src/__tests__/init.test.ts +2 -50
  31. package/src/__tests__/model-parser.test.ts +61 -0
  32. package/src/__tests__/package-manager.test.ts +22 -5
  33. package/src/cli/commands/add-connector.ts +2 -1
  34. package/src/cli/commands/create-model.ts +1 -1
  35. package/src/cli/commands/init.ts +46 -173
  36. package/src/cli/commands/scaffold.ts +1 -1
  37. package/src/cli/index.ts +2 -0
  38. package/src/core/project/agent-instructions.ts +2 -2
  39. package/src/core/project/agent-templates/AGENTS.md +2 -0
  40. package/src/core/scaffold/model-parser.ts +40 -113
  41. package/src/utils/package-manager.ts +22 -17
@@ -16,7 +16,6 @@ import {
16
16
  getGitHubFunctionsWorkflow,
17
17
  injectSwallowKitNextConfig,
18
18
  parseIgnoredBuilds,
19
- approveBuildsViaWorkspaceYaml,
20
19
  buildCosmosDbFreeTierBicepSource,
21
20
  buildFunctionsHostKeyBicepExpression,
22
21
  buildStaticWebAppConfigBicepSource,
@@ -160,7 +159,7 @@ module.exports = nextConfig;
160
159
  expect(parsed.mcpServers.swallowkit.env).toEqual({ SWALLOWKIT_MCP_VERSION: "latest" });
161
160
  });
162
161
 
163
- it("builds an npm-based MCP config when pnpm is unavailable", () => {
162
+ it("builds an npm-based MCP config resolving latest when npm is selected", () => {
164
163
  const source = buildSwallowKitMcpProjectConfigSource("npm");
165
164
  const parsed = JSON.parse(source);
166
165
 
@@ -171,6 +170,7 @@ module.exports = nextConfig;
171
170
  "swallowkit@${SWALLOWKIT_MCP_VERSION}",
172
171
  "swallowkit-mcp",
173
172
  ]);
173
+ expect(parsed.mcpServers.swallowkit.env).toEqual({ SWALLOWKIT_MCP_VERSION: "latest" });
174
174
  });
175
175
 
176
176
  it("temporarily rewrites workspace protocol dependencies for npm lockfile generation", async () => {
@@ -239,54 +239,6 @@ describe("parseIgnoredBuilds", () => {
239
239
  });
240
240
  });
241
241
 
242
- describe("approveBuildsViaWorkspaceYaml", () => {
243
- let tempDir: string;
244
-
245
- beforeEach(() => {
246
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sk-allowbuilds-"));
247
- });
248
-
249
- afterEach(() => {
250
- fs.rmSync(tempDir, { recursive: true, force: true });
251
- });
252
-
253
- const yamlPath = () => path.join(tempDir, "pnpm-workspace.yaml");
254
-
255
- it("appends an allowBuilds map to an existing workspace file", () => {
256
- fs.writeFileSync(yamlPath(), "packages:\n - shared\n - functions\n", "utf-8");
257
- approveBuildsViaWorkspaceYaml(tempDir, ["esbuild", "protobufjs"]);
258
- const content = fs.readFileSync(yamlPath(), "utf-8");
259
- expect(content).toContain("packages:");
260
- expect(content).toContain("allowBuilds:\n esbuild: true\n protobufjs: true");
261
- });
262
-
263
- it("replaces pnpm's placeholder entries with true", () => {
264
- fs.writeFileSync(
265
- yamlPath(),
266
- "packages:\n - shared\nallowBuilds:\n esbuild: set this to true or false\n",
267
- "utf-8"
268
- );
269
- approveBuildsViaWorkspaceYaml(tempDir, ["esbuild", "protobufjs"]);
270
- const content = fs.readFileSync(yamlPath(), "utf-8");
271
- expect(content).toContain("esbuild: true");
272
- expect(content).toContain("protobufjs: true");
273
- expect(content).not.toContain("set this to true or false");
274
- });
275
-
276
- it("quotes scoped package names and creates the file when missing", () => {
277
- approveBuildsViaWorkspaceYaml(tempDir, ["@scope/pkg"]);
278
- const content = fs.readFileSync(yamlPath(), "utf-8");
279
- expect(content).toContain("allowBuilds:\n '@scope/pkg': true");
280
- });
281
-
282
- it("keeps existing entries untouched", () => {
283
- fs.writeFileSync(yamlPath(), "allowBuilds:\n core-js: false\n", "utf-8");
284
- approveBuildsViaWorkspaceYaml(tempDir, ["esbuild"]);
285
- const content = fs.readFileSync(yamlPath(), "utf-8");
286
- expect(content).toContain("core-js: false");
287
- expect(content).toContain("esbuild: true");
288
- });
289
- });
290
242
 
291
243
  describe("buildCosmosDbFreeTierBicepSource", () => {
292
244
  it("keeps database-level shared throughput at 1000 RU/s for Free Tier", () => {
@@ -3,6 +3,67 @@ import * as path from "path";
3
3
  import { parseModelFile } from "../core/scaffold/model-parser";
4
4
 
5
5
  describe("parseModelFile", () => {
6
+ afterEach(() => { jest.restoreAllMocks(); });
7
+
8
+ it.each(["zod", "zod/v4"])("executes TypeScript modules and nested imports with %s without fallback", async (zodImport) => {
9
+ const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
10
+ const warning = jest.spyOn(console, "warn").mockImplementation(() => {});
11
+ try {
12
+ fs.writeFileSync(path.join(tempDir, "leaf.ts"), `
13
+ import { z } from '${zodImport}';
14
+ export const displayName = 'Leaf';
15
+ export const leafSchema = z.object({ name: z.string() });
16
+ `);
17
+ fs.writeFileSync(path.join(tempDir, "nested.ts"), `
18
+ import { z } from '${zodImport}';
19
+ import { leafSchema } from './leaf';
20
+ export const displayName = 'Nested';
21
+ export const nestedSchema = z.object({ leaf: leafSchema });
22
+ `);
23
+ const modelPath = path.join(tempDir, "record.ts");
24
+ fs.writeFileSync(modelPath, `
25
+ import { z } from '${zodImport}';
26
+ import { nestedSchema } from './nested';
27
+ import { leafSchema as otherSchema } from './leaf';
28
+ interface Settings { inner: { count: number }; }
29
+ type Label = string;
30
+ const label: Label = 'https://example.com';
31
+ const statuses = ['draft', 'active'] as const;
32
+ export const recordSchema = z.object({
33
+ id: z.string(), title: z.string().default(label), count: z.number(), enabled: z.boolean(),
34
+ optional: z.string().optional(), nullable: z.number().nullable(), status: z.enum(statuses),
35
+ numbers: z.array(z.number()), nested: nestedSchema, others: z.array(otherSchema),
36
+ inline: z.object({ name: z.string() }), tenantId: z.string(),
37
+ });
38
+ export type Record = z.infer<typeof recordSchema>;
39
+ export const displayName = 'Record';
40
+ export const partitionKey = '/tenantId';
41
+ export const connectorConfig = { connector: 'catalog', table: 'records', operations: ['read'] } as const;
42
+ export const authPolicy = { read: ['reader'], write: ['editor'] } as const;
43
+ `);
44
+ const model = await parseModelFile(modelPath);
45
+ expect(warning).not.toHaveBeenCalled();
46
+ expect(model.fields).toHaveLength(12);
47
+ expect(model.fields).toEqual(expect.arrayContaining([
48
+ expect.objectContaining({ name: "count", type: "number" }),
49
+ expect.objectContaining({ name: "enabled", type: "boolean" }),
50
+ expect.objectContaining({ name: "title", isOptional: true }),
51
+ expect.objectContaining({ name: "optional", isOptional: true }),
52
+ expect.objectContaining({ name: "nullable", isNullable: true }),
53
+ expect.objectContaining({ name: "status", enumValues: ["draft", "active"] }),
54
+ expect.objectContaining({ name: "numbers", type: "number", isArray: true }),
55
+ expect.objectContaining({ name: "nested", type: "object" }),
56
+ expect.objectContaining({ name: "others", type: "object", isArray: true }),
57
+ ]));
58
+ expect(model.connectorConfig).toEqual(expect.objectContaining({ connector: "catalog", table: "records", operations: ["read"] }));
59
+ expect(model.partitionKey).toBe("/tenantId");
60
+ expect(model.authPolicy).toEqual({ read: ["reader"], write: ["editor"] });
61
+ expect(fs.readdirSync(tempDir).some(name => name.startsWith(".swallowkit-parser-"))).toBe(false);
62
+ } finally {
63
+ fs.rmSync(tempDir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
6
67
  it("preserves array and enum metadata for schemas with defaults", async () => {
7
68
  const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
8
69
  const modelPath = path.join(tempDir, "product.ts");
@@ -13,6 +13,20 @@ import {
13
13
  } from "../utils/package-manager";
14
14
 
15
15
  describe("detectFromUserAgent", () => {
16
+ const originalAgent = process.env.npm_config_user_agent;
17
+ beforeEach(() => { delete process.env.npm_config_user_agent; });
18
+ afterEach(() => {
19
+ if (originalAgent === undefined) delete process.env.npm_config_user_agent;
20
+ else process.env.npm_config_user_agent = originalAgent;
21
+ });
22
+ it("honors npm invocation even with pnpm installed", () => {
23
+ process.env.npm_config_user_agent = "npm/11.0.0 node/v22.14.0";
24
+ expect(detectFromUserAgent(() => true)).toBe("npm");
25
+ });
26
+ it("honors pnpm invocation", () => {
27
+ process.env.npm_config_user_agent = "pnpm/11.26.0 npm/? node/v22.14.0";
28
+ expect(detectFromUserAgent(() => false)).toBe("pnpm");
29
+ });
16
30
  it("falls back to npm when pnpm is not installed", () => {
17
31
  expect(detectFromUserAgent(() => false)).toBe("npm");
18
32
  });
@@ -55,7 +69,7 @@ describe("getCommands", () => {
55
69
  });
56
70
 
57
71
  it("returns correct runFilter for workspace", () => {
58
- expect(cmds.runFilter("shared")).toBe("pnpm run --filter shared");
72
+ expect(cmds.runFilter("shared")).toBe("pnpm --filter shared run");
59
73
  });
60
74
 
61
75
  it("returns --use-pnpm flag for create-next-app", () => {
@@ -98,8 +112,8 @@ describe("getCommands", () => {
98
112
  expect(cmds.runFilter("shared")).toBe("npm run --workspace=shared");
99
113
  });
100
114
 
101
- it("returns null for create-next-app flag", () => {
102
- expect(cmds.createNextAppFlag).toBeNull();
115
+ it("explicitly selects npm for create-next-app", () => {
116
+ expect(cmds.createNextAppFlag).toBe("--use-npm");
103
117
  });
104
118
  });
105
119
  });
@@ -113,6 +127,9 @@ describe("getWorkspaceConfig", () => {
113
127
  expect(config.content).toContain("packages:");
114
128
  expect(config.content).toContain(" - packages/*");
115
129
  expect(config.content).toContain(" - apps/*");
130
+ expect(config.content).toContain("protobufjs: false");
131
+ expect(config.content).toContain("unrs-resolver: false");
132
+ expect(config.content).not.toContain(": true");
116
133
  }
117
134
  });
118
135
 
@@ -130,7 +147,7 @@ describe("getCiSetupStep", () => {
130
147
  it("returns pnpm setup step for pnpm", () => {
131
148
  const step = getCiSetupStep("pnpm");
132
149
  expect(step).toContain("pnpm/action-setup");
133
- expect(step).toContain("version: latest");
150
+ expect(step).toContain("version: 11");
134
151
  });
135
152
 
136
153
  it("returns empty string for npm", () => {
@@ -176,7 +193,7 @@ describe("getAzurePipelinesSetup", () => {
176
193
 
177
194
  describe("getBuildScript", () => {
178
195
  it("uses pnpm run --filter for pnpm", () => {
179
- expect(getBuildScript("pnpm")).toContain("pnpm run --filter shared build");
196
+ expect(getBuildScript("pnpm")).toContain("pnpm --filter shared run build");
180
197
  expect(getBuildScript("pnpm")).toContain("fs.cpSync");
181
198
  });
182
199
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as fs from "fs";
7
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
7
8
  import * as path from "path";
8
9
  import { ensureSwallowKitProject } from "../../core/config";
9
10
  import { ApiConnectorConfig, ConnectorDefinition } from "../../types";
@@ -40,7 +41,7 @@ export async function addConnectorCommand(options: AddConnectorOptions) {
40
41
  console.log("\n📝 Next steps:");
41
42
  console.log(` 1. Review the connector settings in ${configPath}`);
42
43
  console.log(` 2. Set the required environment variables in functions/local.settings.json`);
43
- console.log(` 3. Create models with: npx swallowkit create-model <name> --connector=${options.name}`);
44
+ console.log(` 3. Create models with: ${getCommands(detectFromProject()).exec} swallowkit create-model <name> --connector=${options.name}`);
44
45
  }
45
46
 
46
47
  function findConfigFile(): string | null {
@@ -57,6 +57,6 @@ export async function createModelCommand(options: CreateModelOptions) {
57
57
  if (result.createdFiles.length > 0) {
58
58
  console.log("\n📝 Next steps:");
59
59
  console.log(" 1. Customize the generated model fields in shared/models/");
60
- console.log(` 2. Run '${getCommands(detectFromProject()).dlx} swallowkit scaffold <model>' to generate CRUD code`);
60
+ console.log(` 2. Run '${getCommands(detectFromProject()).exec} swallowkit scaffold <model>' to generate CRUD code`);
61
61
  }
62
62
  }
@@ -34,6 +34,7 @@ interface InitOptions {
34
34
  cosmosDbMode?: CosmosDbMode;
35
35
  vnet?: VNetOption;
36
36
  swaPlan?: StaticWebAppPlan;
37
+ packageManager?: PackageManager;
37
38
  }
38
39
 
39
40
  type CiCdProvider = 'github' | 'azure' | 'skip';
@@ -255,6 +256,9 @@ const VALID_VNET: VNetOption[] = ['none', 'outbound'];
255
256
  const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
256
257
 
257
258
  function validateInitFlags(options: InitOptions): void {
259
+ if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
260
+ throw new Error('Invalid --package-manager: expected npm or pnpm');
261
+ }
258
262
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
259
263
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
260
264
  process.exit(1);
@@ -285,7 +289,7 @@ export async function initCommand(options: InitOptions) {
285
289
  console.log(`📋 Template: ${options.template}`);
286
290
 
287
291
  // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
288
- const pm: PackageManager = detectFromUserAgent();
292
+ const pm: PackageManager = options.packageManager || detectFromUserAgent();
289
293
  const pmCmd = getCommands(pm);
290
294
  console.log(`📦 Package manager: ${pm}`);
291
295
 
@@ -388,11 +392,11 @@ export async function initCommand(options: InitOptions) {
388
392
  console.log(`\n✅ Project "${options.name}" created successfully!`);
389
393
  console.log("\n📝 Next steps:");
390
394
  console.log(` cd ${options.name}`);
391
- console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
392
- console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
393
- console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
395
+ console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
396
+ console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
397
+ console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
394
398
  console.log("\n🚀 Deploy to Azure:");
395
- console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
399
+ console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
396
400
  if (cicdProvider !== 'skip') {
397
401
  console.log(" Configure CI/CD secrets and push to repository");
398
402
  }
@@ -416,7 +420,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
416
420
  // for npm use "npx create-next-app@latest ..."
417
421
  const baseArgs = pm === 'pnpm'
418
422
  ? ['dlx', 'create-next-app@latest']
419
- : ['create-next-app@latest'];
423
+ : ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
420
424
 
421
425
  const args = [
422
426
  ...baseArgs,
@@ -426,6 +430,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
426
430
  '--app',
427
431
  '--no-src',
428
432
  '--disable-git',
433
+ '--skip-install',
429
434
  '--import-alias',
430
435
  '@/*',
431
436
  ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
@@ -462,8 +467,8 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
462
467
 
463
468
  // pnpm: pnpm add next@... ; npm: npm install next@...
464
469
  const args = pm === 'pnpm'
465
- ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
466
- : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
470
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
471
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
467
472
 
468
473
  await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
469
474
 
@@ -472,19 +477,7 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
472
477
 
473
478
  async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
474
479
  console.log('\n📦 Installing dependencies...\n');
475
- await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
476
-
477
- if (pm === 'pnpm') {
478
- console.log('\n📦 Generating npm lockfile for CI/CD...\n');
479
- await withNpmLockfileSafeManifests(projectDir, async () => {
480
- await runPackageManagerCommand(
481
- 'npm',
482
- ['install', '--package-lock-only', '--ignore-scripts'],
483
- projectDir,
484
- 'npm install --package-lock-only'
485
- );
486
- });
487
- }
480
+ await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
488
481
 
489
482
  console.log('\n✅ Dependencies installed\n');
490
483
  }
@@ -579,44 +572,22 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
579
572
  }
580
573
  }
581
574
 
582
- /**
583
- * Run a package-manager command with stdio passed through to the user.
584
- *
585
- * For pnpm, additionally tee stdout/stderr to a buffer so that if the command
586
- * fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
587
- * 1. detect which packages were ignored,
588
- * 2. approve them (interactively or automatically),
589
- * 3. clean-reinstall and retry — repeating if pnpm reports further ignored
590
- * packages (pnpm does not always list every unapproved package at once).
591
- */
575
+ /** Run installs without changing the project security policy. */
592
576
  async function runPackageManagerCommand(
593
577
  pm: PackageManager,
594
578
  args: string[],
595
579
  projectDir: string,
596
580
  label: string,
597
581
  ): Promise<void> {
598
- let { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
582
+ const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
599
583
 
600
584
  if (code === 0) return;
601
585
 
602
586
  if (pm === 'pnpm') {
603
- for (let attempt = 0; attempt < 3; attempt++) {
604
- const ignoredBuilds = parseIgnoredBuilds(output);
605
- if (ignoredBuilds.length === 0) break;
606
- const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
607
- if (!approved) break;
608
- // 中断された install はパッケージ抽出が不完全なまま "Already up to date"
609
- // と判定されることがあるため、node_modules を消して決定論的に入れ直す
610
- removeWorkspaceNodeModules(projectDir);
611
- const retry = await spawnAndCapture(pm, args, projectDir, true);
612
- code = retry.code;
613
- output = retry.output;
614
- if (code === 0) return;
615
- }
616
587
  if (parseIgnoredBuilds(output).length > 0) {
617
588
  throw new Error(
618
589
  `${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
619
- 'Run "pnpm approve-builds" inside the project, then re-run the command.'
590
+ 'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.'
620
591
  );
621
592
  }
622
593
  }
@@ -624,19 +595,6 @@ async function runPackageManagerCommand(
624
595
  throw new Error(`${label} exited with code ${code}`);
625
596
  }
626
597
 
627
- /** workspace root と直下パッケージの node_modules を削除する(クリーン再インストール用)。 */
628
- function removeWorkspaceNodeModules(projectDir: string): void {
629
- const targets = [projectDir, ...fs.readdirSync(projectDir, { withFileTypes: true })
630
- .filter((entry) => entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.'))
631
- .map((entry) => path.join(projectDir, entry.name))];
632
- for (const dir of targets) {
633
- const nodeModules = path.join(dir, 'node_modules');
634
- if (fs.existsSync(nodeModules)) {
635
- fs.rmSync(nodeModules, { recursive: true, force: true });
636
- }
637
- }
638
- }
639
-
640
598
  /**
641
599
  * Spawn a child process inheriting stdin (so interactive prompts still work)
642
600
  * while either inheriting or capturing+teeing stdout/stderr.
@@ -691,102 +649,6 @@ export function parseIgnoredBuilds(output: string): string[] {
691
649
  return Array.from(new Set(list));
692
650
  }
693
651
 
694
- /**
695
- * Prompt the user and, if approved, run `pnpm approve-builds` followed by
696
- * `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
697
- * which packages they actually selected inside approve-builds).
698
- *
699
- * In non-interactive environments (CI, coding agents, piped output) prompting
700
- * would hang forever, so build scripts are approved automatically via
701
- * package.json `pnpm.onlyBuiltDependencies` instead.
702
- */
703
- async function maybeApproveBuilds(projectDir: string, ignoredBuilds: string[]): Promise<boolean> {
704
- console.log(
705
- `\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
706
- ' These packages (e.g. sharp) need their build scripts to run correctly.\n',
707
- );
708
-
709
- const interactive =
710
- process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
711
- if (!interactive) {
712
- console.log(
713
- 'ℹ️ Non-interactive environment detected — approving build scripts automatically\n' +
714
- ' via "allowBuilds" in pnpm-workspace.yaml.\n',
715
- );
716
- // rebuild は不要: 呼び出し側がクリーン再インストールで build script を実行する
717
- approveBuildsViaWorkspaceYaml(projectDir, ignoredBuilds);
718
- return true;
719
- }
720
-
721
- const response = await prompts({
722
- type: 'confirm',
723
- name: 'approve',
724
- message: 'Run `pnpm approve-builds` now to approve these build scripts?',
725
- initial: true,
726
- });
727
-
728
- if (!response.approve) {
729
- console.log(
730
- '\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n',
731
- );
732
- return false;
733
- }
734
-
735
- await runSimple('pnpm', ['approve-builds'], projectDir);
736
- await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
737
-
738
- console.log('\n✅ Build scripts approved and packages rebuilt\n');
739
- return true;
740
- }
741
-
742
- /**
743
- * pnpm-workspace.yaml の allowBuilds にパッケージを true で登録して build script を許可する。
744
- * (pnpm 11+ の正準設定。失敗時に pnpm が追記する placeholder 行も true に置き換える)
745
- */
746
- export function approveBuildsViaWorkspaceYaml(projectDir: string, packages: string[]): void {
747
- const workspaceYamlPath = path.join(projectDir, 'pnpm-workspace.yaml');
748
- const existing = fs.existsSync(workspaceYamlPath)
749
- ? fs.readFileSync(workspaceYamlPath, 'utf-8')
750
- : '';
751
- const yamlKey = (pkg: string) => (/^[A-Za-z0-9_.-]+$/.test(pkg) ? pkg : `'${pkg}'`);
752
-
753
- const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
754
- const keyIndex = lines.findIndex((line) => /^allowBuilds\s*:/.test(line));
755
-
756
- if (keyIndex === -1) {
757
- const block = `allowBuilds:\n${packages.map((pkg) => ` ${yamlKey(pkg)}: true`).join('\n')}\n`;
758
- const base = existing.length > 0 && !existing.endsWith('\n') ? `${existing}\n` : existing;
759
- fs.writeFileSync(workspaceYamlPath, `${base}${block}`, 'utf-8');
760
- return;
761
- }
762
-
763
- // 既存マップ内の対象パッケージ(placeholder 含む)を true に更新し、無いものは追記する
764
- let endIndex = keyIndex + 1;
765
- const updated = new Set<string>();
766
- while (endIndex < lines.length && /^\s+\S/.test(lines[endIndex])) {
767
- const entry = lines[endIndex].match(/^\s+['"]?([A-Za-z0-9@/_.-]+)['"]?\s*:/);
768
- if (entry && packages.includes(entry[1])) {
769
- lines[endIndex] = ` ${yamlKey(entry[1])}: true`;
770
- updated.add(entry[1]);
771
- }
772
- endIndex++;
773
- }
774
- const additions = packages.filter((pkg) => !updated.has(pkg)).map((pkg) => ` ${yamlKey(pkg)}: true`);
775
- lines.splice(endIndex, 0, ...additions);
776
- fs.writeFileSync(workspaceYamlPath, lines.join('\n'), 'utf-8');
777
- }
778
-
779
- function runSimple(command: string, args: string[], cwd: string): Promise<void> {
780
- return new Promise((resolve, reject) => {
781
- const child = spawn(command, args, { cwd, stdio: 'inherit', shell: true });
782
- child.on('close', (code) => {
783
- if (code !== 0) reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
784
- else resolve();
785
- });
786
- child.on('error', reject);
787
- });
788
- }
789
-
790
652
  export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
791
653
  return nextConfigContent.replace(
792
654
  /(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
@@ -937,7 +799,7 @@ async function addSwallowKitFiles(
937
799
  const packageJsonPath = path.join(projectDir, 'package.json');
938
800
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
939
801
 
940
- // zod is in the shared workspace package, not here
802
+ // Root BFF/auth code imports Zod directly; declare it as well as the shared package.
941
803
  packageJson.dependencies = {
942
804
  ...packageJson.dependencies,
943
805
  ...buildGeneratedProjectDependencies(projectName, pm),
@@ -955,8 +817,13 @@ async function addSwallowKitFiles(
955
817
  };
956
818
 
957
819
  packageJson.engines = {
958
- node: '20.x',
820
+ node: '>=22.14.0',
821
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
822
+ };
823
+ packageJson.devEngines = {
824
+ packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
959
825
  };
826
+ delete packageJson.packageManager;
960
827
 
961
828
  // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
962
829
  const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
@@ -1823,7 +1690,7 @@ export default function Home() {
1823
1690
  Create your first model with Zod and generate CRUD operations automatically.
1824
1691
  </p>
1825
1692
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1826
- ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1693
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1827
1694
  </code>
1828
1695
  </div>
1829
1696
  </section>
@@ -1924,7 +1791,7 @@ This project was initialized with the following settings:
1924
1791
 
1925
1792
  Before you begin, ensure you have the following installed:
1926
1793
 
1927
- 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1794
+ 1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1928
1795
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1929
1796
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1930
1797
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1932,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1932
1799
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1933
1800
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1934
1801
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1935
- ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1802
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
1936
1803
 
1937
1804
  ## 📁 Project Structure
1938
1805
 
@@ -1958,7 +1825,7 @@ ${functionsTree}
1958
1825
  Define your data model with Zod schema:
1959
1826
 
1960
1827
  \`\`\`bash
1961
- ${pmCmd.dlx} swallowkit create-model <model-name>
1828
+ ${pmCmd.exec} swallowkit create-model <model-name>
1962
1829
  \`\`\`
1963
1830
 
1964
1831
  This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
@@ -1968,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
1968
1835
  Generate complete CRUD operations (Functions, API routes, UI):
1969
1836
 
1970
1837
  \`\`\`bash
1971
- ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1838
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1972
1839
  \`\`\`
1973
1840
 
1974
1841
  This generates:
@@ -1980,7 +1847,7 @@ ${backendScaffoldNote}
1980
1847
  ### 3. Start Development Servers
1981
1848
 
1982
1849
  \`\`\`bash
1983
- ${pmCmd.dlx} swallowkit dev
1850
+ ${pmCmd.exec} swallowkit dev
1984
1851
  \`\`\`
1985
1852
 
1986
1853
  This starts:
@@ -1998,7 +1865,7 @@ ${pythonLocalDevNote}
1998
1865
  Create all required Azure resources using Bicep:
1999
1866
 
2000
1867
  \`\`\`bash
2001
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1868
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
2002
1869
  \`\`\`
2003
1870
 
2004
1871
  This creates:
@@ -2054,10 +1921,10 @@ func azure functionapp publish func-${projectName}
2054
1921
 
2055
1922
  ## 🔧 Available Commands
2056
1923
 
2057
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
2058
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
2059
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
2060
- - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1924
+ - \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
1925
+ - \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
1926
+ - \`${pmCmd.exec} swallowkit dev\` - Start development servers
1927
+ - \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
2061
1928
  ${azureConfig.vnetOption !== 'none' ? `
2062
1929
  ## 🔒 Network Security (VNet Configuration)
2063
1930
 
@@ -2113,7 +1980,7 @@ function createAiAgentFiles(
2113
1980
  pm: PackageManager
2114
1981
  ) {
2115
1982
  console.log('🤖 Creating AI agent instruction files...\n');
2116
- const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
1983
+ const runCmd = getCommands(pm).exec;
2117
1984
 
2118
1985
  writeAgentInstructionFiles(projectDir, {
2119
1986
  projectName,
@@ -2902,11 +2769,13 @@ export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage:
2902
2769
 
2903
2770
  const commonSetup = ` - uses: actions/checkout@v4
2904
2771
 
2772
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2905
2773
  - name: Setup Node.js
2906
2774
  uses: actions/setup-node@v4
2907
2775
  with:
2908
2776
  node-version: '22'
2909
- ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2777
+ cache: ${pm}
2778
+ cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
2910
2779
  ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
2911
2780
  - name: Install dependencies
2912
2781
  run: |
@@ -3441,7 +3310,7 @@ async function createGitHubActionsWorkflows(
3441
3310
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
3442
3311
  fs.mkdirSync(workflowsDir, { recursive: true });
3443
3312
 
3444
- const actionsPm: PackageManager = 'npm';
3313
+ const actionsPm: PackageManager = pm;
3445
3314
 
3446
3315
  const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3447
3316
  fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
@@ -3469,6 +3338,7 @@ on:
3469
3338
  - 'shared/**'
3470
3339
  - 'public/**'
3471
3340
  - 'package.json'
3341
+ - '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3472
3342
  - 'next.config.js'
3473
3343
  - 'next.config.ts'
3474
3344
  workflow_dispatch:
@@ -3482,6 +3352,7 @@ on:
3482
3352
  - 'shared/**'
3483
3353
  - 'public/**'
3484
3354
  - 'package.json'
3355
+ - '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3485
3356
  - 'next.config.js'
3486
3357
  - 'next.config.ts'
3487
3358
 
@@ -3495,11 +3366,13 @@ jobs:
3495
3366
  with:
3496
3367
  submodules: true
3497
3368
 
3369
+ ${getCiSetupStep(pm) ? `${getCiSetupStep(pm)}\n` : ''}
3498
3370
  - name: Setup Node.js
3499
3371
  uses: actions/setup-node@v4
3500
3372
  with:
3501
3373
  node-version: '22'
3502
- ${getCiSetupStep(pm) ? `\n${getCiSetupStep(pm)}\n` : ''}
3374
+ cache: ${pm}
3375
+ cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
3503
3376
  ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3504
3377
  - name: Install and build app
3505
3378
  run: |
@@ -3524,7 +3397,7 @@ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3524
3397
  async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
3525
3398
  console.log('📦 Creating Azure Pipelines...\n');
3526
3399
 
3527
- const pipelinesPm: PackageManager = 'npm';
3400
+ const pipelinesPm: PackageManager = pm;
3528
3401
  const pipelinesDir = path.join(projectDir, 'pipelines');
3529
3402
  fs.mkdirSync(pipelinesDir, { recursive: true });
3530
3403