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
@@ -221,7 +221,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
221
221
  console.log(
222
222
  ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
223
223
  );
224
- console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
224
+ console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).exec} swallowkit dev' to test the generated code`);
225
225
  } catch (error: any) {
226
226
  console.error("\n❌ Scaffold failed:", error.message);
227
227
  process.exit(1);
package/src/cli/index.ts CHANGED
@@ -109,6 +109,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
109
109
  .description("Initialize a new SwallowKit project")
110
110
  .option("--template <template>", "Template to use", "default")
111
111
  .option("--next-version <version>", "Next.js version to install (e.g., 16.0.7, latest)", "latest")
112
+ .option("--package-manager <manager>", "Package manager: npm | pnpm (defaults to invocation context)")
112
113
  .option("--cicd <provider>", "CI/CD provider: github | azure | skip")
113
114
  .option("--backend-language <language>", "Azure Functions backend language: typescript | csharp | python")
114
115
  .option("--cosmos-db-mode <mode>", "Cosmos DB mode: freetier | serverless")
@@ -119,6 +120,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
119
120
  name: projectName || "swallowkit-app",
120
121
  template: options.template,
121
122
  nextVersion: options.nextVersion,
123
+ packageManager: options.packageManager,
122
124
  cicd: options.cicd,
123
125
  backendLanguage: options.backendLanguage,
124
126
  cosmosDbMode: options.cosmosDbMode,
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { BackendLanguage } from "../../types";
4
- import { PackageManager } from "../../utils/package-manager";
4
+ import { PackageManager, getCommands } from "../../utils/package-manager";
5
5
 
6
6
  export interface AgentInstructionContext {
7
7
  projectName: string;
@@ -74,7 +74,7 @@ function getBackendRules(backendLanguage: BackendLanguage): string {
74
74
  }
75
75
 
76
76
  function interpolateTemplate(template: string, context: AgentInstructionContext): string {
77
- const runCmd = context.packageManager === "pnpm" ? "pnpm" : "npx";
77
+ const runCmd = getCommands(context.packageManager).exec;
78
78
  const replacements: Record<string, string> = {
79
79
  projectName: context.projectName,
80
80
  backendLanguageLabel: BACKEND_LANGUAGE_LABELS[context.backendLanguage],
@@ -500,3 +500,5 @@ Do:
500
500
  - keep specifications focused on intent and observable behavior
501
501
  - prefer vertical, independently verifiable slices
502
502
  - verify before final semantic review
503
+
504
+ Use `{{pm}}`, the package manager selected for this project. Do not introduce another package manager or lockfile. Use `{{runCmd}} swallowkit` for the project-local CLI. The MCP launcher intentionally resolves `latest` independently.
@@ -5,6 +5,7 @@
5
5
  import * as fs from "fs";
6
6
  import { execFileSync } from "child_process";
7
7
  import * as path from "path";
8
+ import * as ts from "typescript";
8
9
 
9
10
  import { ModelConnectorConfig, ModelAuthPolicy } from "../../types";
10
11
 
@@ -324,61 +325,36 @@ function mergeNestedSchemaInfo(fields: FieldInfo[], nestedRefs: NestedSchemaRef[
324
325
  }
325
326
  }
326
327
 
327
- /**
328
- * ローカル依存ファイルを再帰的にインライン化する(動的インポート用一時スクリプト生成用)。
329
- * seen によって循環依存を防ぐ。
330
- * importedNames が指定された場合はその名前の const だけを残し、他を除去する。
331
- */
332
- function inlineLocalDeps(
333
- filePath: string,
334
- importedNames: Set<string> | null,
335
- seen: Set<string>
336
- ): string {
337
- const resolvedPath = path.resolve(filePath);
338
- if (seen.has(resolvedPath)) return '';
339
- seen.add(resolvedPath);
340
-
341
- if (!fs.existsSync(resolvedPath)) return '';
342
-
343
- let content = fs.readFileSync(resolvedPath, 'utf8');
344
- const fileDir = path.dirname(resolvedPath);
345
- const transitiveParts: string[] = [];
346
-
347
- // ローカル(相対)インポートを再帰的にインライン化
348
- content = content.replace(
349
- /import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]\s*;?/g,
350
- (_match: string, _imports: string, importPath: string) => {
351
- let depPath = path.resolve(fileDir, importPath);
352
- if (!depPath.endsWith('.ts') && !depPath.endsWith('.js')) depPath += '.ts';
353
- const inlined = inlineLocalDeps(depPath, null, seen);
354
- if (inlined) transitiveParts.push(inlined);
355
- return '';
356
- }
357
- );
358
-
359
- // 外部パッケージのインポートを除去
360
- content = content.replace(/import\s+.*?\s+from\s+['"](?!\.).*?['"];?\s*/g, '');
361
- // export キーワードを除去
362
- content = content.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
363
- // 型宣言を除去(ランタイムでは不要)
364
- content = content.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
365
- content = content.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
366
- // コメントを除去
367
- content = content.replace(/\/\*[\s\S]*?\*\//g, '');
368
- content = content.replace(/\/\/.*/g, '');
369
-
370
- // 直接インポートされていない const 宣言を除去(displayName 等の重複を防ぐ)
371
- if (importedNames) {
372
- content = content.replace(/^const\s+(\w+)\s*=\s*[^;]+;/gm, (constMatch: string, varName: string) => {
373
- return importedNames.has(varName) ? constMatch : '';
374
- });
375
- }
376
-
377
- return [...transitiveParts, content.trim()].filter(Boolean).join('\n\n');
378
- }
379
-
380
- function resolveBundledTsxCliPath(): string {
381
- return require.resolve("tsx/cli");
328
+ function compileModelModule(modelPath: string, directory: string, modules = new Map<string, string>()): string {
329
+ const sourcePath = path.resolve(modelPath);
330
+ const existing = modules.get(sourcePath);
331
+ if (existing) return existing;
332
+ const outputPath = path.join(directory, `model-${modules.size}.mjs`);
333
+ modules.set(sourcePath, outputPath);
334
+ const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
335
+ fileName: sourcePath,
336
+ compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
337
+ transformers: {
338
+ after: [context => source => {
339
+ const visit: ts.Visitor = node => {
340
+ if (ts.isStringLiteral(node) &&
341
+ (ts.isImportDeclaration(node.parent) || ts.isExportDeclaration(node.parent)) &&
342
+ node.text.startsWith(".")) {
343
+ const base = path.resolve(path.dirname(sourcePath), node.text);
344
+ const candidates = [base.replace(/\.js$/, ".ts"), `${base}.ts`, path.join(base, "index.ts"), base];
345
+ const dependency = candidates.find(candidate => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
346
+ if (!dependency) throw new Error(`Cannot resolve ${node.text} from ${sourcePath}`);
347
+ const compiled = compileModelModule(dependency, directory, modules);
348
+ return context.factory.createStringLiteral(`./${path.basename(compiled)}`);
349
+ }
350
+ return ts.visitEachChild(node, visit, context);
351
+ };
352
+ return ts.visitNode(source, visit) as ts.SourceFile;
353
+ }],
354
+ },
355
+ });
356
+ fs.writeFileSync(outputPath, output.outputText);
357
+ return outputPath;
382
358
  }
383
359
 
384
360
  /**
@@ -386,53 +362,9 @@ function resolveBundledTsxCliPath(): string {
386
362
  */
387
363
  async function extractFieldsFromSchema(modelPath: string, schemaName: string): Promise<FieldInfo[]> {
388
364
  const fields: FieldInfo[] = [];
365
+ let tempDirectory: string | undefined;
389
366
 
390
367
  try {
391
- // モデルファイルの内容を読み込む
392
- let modelContent = fs.readFileSync(modelPath, 'utf8');
393
- const modelDir = path.dirname(path.resolve(modelPath));
394
-
395
- // ローカル相対インポートを再帰的にインライン化して保持
396
- // パターン: import { categorySchema } from './category'
397
- const seenPaths = new Set<string>([path.resolve(modelPath)]);
398
- const localImports: string[] = [];
399
- modelContent = modelContent.replace(
400
- /import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]\s*;?/g,
401
- (_match: string, imports: string, importPath: string) => {
402
- // インポートされた変数名を取得
403
- const importedNames = new Set(
404
- imports.split(',').map(s => s.trim().split(/\s+as\s+/).pop()!.trim()).filter(s => s.length > 0)
405
- );
406
-
407
- // 相対パスを絶対パスに解決
408
- let resolvedPath = path.resolve(modelDir, importPath);
409
- if (!resolvedPath.endsWith('.ts') && !resolvedPath.endsWith('.js')) {
410
- resolvedPath += '.ts';
411
- }
412
-
413
- // 推移的なローカル依存を含めて再帰的にインライン化
414
- const inlined = inlineLocalDeps(resolvedPath, importedNames, seenPaths);
415
- if (inlined) localImports.push(inlined);
416
- return ''; // 元のインポート文は除去
417
- }
418
- );
419
-
420
- // zod 以外のパッケージインポートを除去
421
- modelContent = modelContent.replace(/import\s+.*?\s+from\s+['"].*?['"];?\s*/g, '');
422
- modelContent = modelContent.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
423
- // type宣言を削除(ランタイムでは不要)
424
- modelContent = modelContent.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
425
- // interface宣言を削除(ランタイムでは不要、複数行対応)
426
- modelContent = modelContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
427
- // コメントを削除
428
- modelContent = modelContent.replace(/\/\*[\s\S]*?\*\//g, '');
429
- modelContent = modelContent.replace(/\/\/.*/g, '');
430
- // TypeScript の `as const` アサーションを削除(.mjs では構文エラーになる)
431
- modelContent = modelContent.replace(/\s+as\s+const\b/g, '');
432
-
433
- // インライン化したローカルインポートを先頭に追加
434
- const inlinedDeps = localImports.length > 0 ? localImports.join('\n\n') + '\n\n' : '';
435
-
436
368
  // プロジェクトルートを探す(package.jsonがある場所)
437
369
  let projectRoot = path.dirname(modelPath);
438
370
  while (projectRoot !== path.dirname(projectRoot)) {
@@ -443,16 +375,11 @@ async function extractFieldsFromSchema(modelPath: string, schemaName: string): P
443
375
  }
444
376
 
445
377
  // プロジェクトルート内に一時スクリプトファイルを作成
446
- const tempScript = path.join(projectRoot, `.swallowkit-parser-${Date.now()}.mjs`);
378
+ tempDirectory = fs.mkdtempSync(path.join(projectRoot, '.swallowkit-parser-'));
379
+ const compiledModel = compileModelModule(modelPath, tempDirectory);
380
+ const tempScript = path.join(tempDirectory, 'inspect.mjs');
447
381
  const scriptCode = `
448
- import { z } from 'zod/v4';
449
-
450
- // インライン化した依存スキーマ
451
- ${inlinedDeps}
452
- // モデルファイルの内容を評価
453
- ${modelContent}
454
-
455
- const schema = ${schemaName};
382
+ import { ${schemaName} as schema } from './${path.basename(compiledModel)}';
456
383
 
457
384
  // Zod v3とv4の両方に対応
458
385
  const isObject = (schema && schema._def &&
@@ -501,7 +428,7 @@ if (isObject) {
501
428
  // ZodArray をチェック
502
429
  if (getTypeName(fieldDef) === 'ZodArray') {
503
430
  isArray = true;
504
- fieldDef = fieldDef._def.type || fieldDef._def.element;
431
+ fieldDef = fieldDef._def.element || fieldDef._def.type;
505
432
  }
506
433
 
507
434
  // 基本型を判定
@@ -552,8 +479,7 @@ if (isObject) {
552
479
  fs.writeFileSync(tempScript, scriptCode, 'utf8');
553
480
 
554
481
  try {
555
- const tsxCliPath = resolveBundledTsxCliPath();
556
- const result = execFileSync(process.execPath, [tsxCliPath, tempScript], {
482
+ const result = execFileSync(process.execPath, [tempScript], {
557
483
  encoding: 'utf8',
558
484
  cwd: projectRoot,
559
485
  });
@@ -573,10 +499,11 @@ if (isObject) {
573
499
  }
574
500
  }
575
501
  } catch (error) {
576
- // tsxが使えない場合は正規表現フォールバック
577
502
  console.warn('Failed to use dynamic import, falling back to regex parsing');
578
503
  console.warn('Error:', error);
579
504
  return extractFieldsWithRegex(modelPath, schemaName);
505
+ } finally {
506
+ if (tempDirectory) fs.rmSync(tempDirectory, { recursive: true, force: true });
580
507
  }
581
508
 
582
509
  return fields;
@@ -30,13 +30,13 @@ export interface PackageManagerCommands {
30
30
  dlx: string;
31
31
  /** Run a script: "pnpm run" / "npm run" */
32
32
  run: string;
33
- /** Run a script with filter: "pnpm run --filter <ws>" / "npm run --workspace=<ws>" */
33
+ /** Run a script with filter: "pnpm --filter <ws> run" / "npm run --workspace=<ws>" */
34
34
  runFilter: (workspace: string) => string;
35
35
  /** Start script: "pnpm start" / "npm start" */
36
36
  start: string;
37
37
  /** Install production only (in temp dir for CI): "pnpm install --prod" / "npm install --omit=dev" */
38
38
  installProd: string;
39
- /** create-next-app flag: "--use-pnpm" / (none for npm) */
39
+ /** create-next-app flag: "--use-pnpm" / "--use-npm" */
40
40
  createNextAppFlag: string | null;
41
41
  }
42
42
 
@@ -55,9 +55,9 @@ export function getCommands(pm: PackageManager): PackageManagerCommands {
55
55
  exec: "pnpm exec",
56
56
  dlx: "pnpm dlx",
57
57
  run: "pnpm run",
58
- runFilter: (ws) => `pnpm run --filter ${ws}`,
58
+ runFilter: (ws) => `pnpm --filter ${ws} run`,
59
59
  start: "pnpm start",
60
- installProd: "pnpm install --prod",
60
+ installProd: "pnpm install --prod --no-frozen-lockfile",
61
61
  createNextAppFlag: "--use-pnpm",
62
62
  };
63
63
  }
@@ -76,22 +76,19 @@ export function getCommands(pm: PackageManager): PackageManagerCommands {
76
76
  runFilter: (ws) => `npm run --workspace=${ws}`,
77
77
  start: "npm start",
78
78
  installProd: "npm install --omit=dev",
79
- createNextAppFlag: null,
79
+ createNextAppFlag: "--use-npm",
80
80
  };
81
81
  }
82
82
 
83
83
  /**
84
84
  * Detect the preferred package manager for new project initialisation.
85
85
  *
86
- * Strategy (pnpm-preferred):
87
- * 1. If pnpm is installed on the system → always use pnpm
88
- * (even when invoked via `npx`, since `npx` is often used out of habit)
89
- * 2. Otherwise → npm
90
- *
91
- * This is intentionally independent of `npm_config_user_agent` so that
92
- * `npx swallowkit init` still creates a pnpm project when pnpm is available.
86
+ * Honor the invoking package manager before falling back to available binaries.
93
87
  */
94
88
  export function detectFromUserAgent(pnpmIsInstalled: () => boolean = isPnpmInstalled): PackageManager {
89
+ const agent = process.env.npm_config_user_agent || "";
90
+ if (agent.startsWith("pnpm/")) return "pnpm";
91
+ if (agent.startsWith("npm/")) return "npm";
95
92
  return pnpmIsInstalled() ? "pnpm" : "npm";
96
93
  }
97
94
 
@@ -111,11 +108,18 @@ function isPnpmInstalled(): boolean {
111
108
  * Detect the package manager used in an existing project directory
112
109
  * by checking for lockfiles.
113
110
  *
114
- * Priority: pnpm-lock.yaml > package-lock.json > fallback to detectFromUserAgent()
111
+ * Priority: explicit project metadata > lockfiles > workspace config > invocation/binaries
115
112
  */
116
113
  export function detectFromProject(projectDir?: string): PackageManager {
117
114
  const dir = projectDir || process.cwd();
118
115
 
116
+ const manifestPath = path.join(dir, "package.json");
117
+ if (fs.existsSync(manifestPath)) {
118
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
119
+ const manager = manifest.devEngines?.packageManager?.name || manifest.packageManager?.split("@")[0];
120
+ if (manager === "npm" || manager === "pnpm") return manager;
121
+ }
122
+
119
123
  if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) {
120
124
  return "pnpm";
121
125
  }
@@ -124,6 +128,7 @@ export function detectFromProject(projectDir?: string): PackageManager {
124
128
  }
125
129
 
126
130
  // No lockfile found — fall back to user agent detection
131
+ if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return "pnpm";
127
132
  return detectFromUserAgent();
128
133
  }
129
134
 
@@ -151,7 +156,7 @@ export function getWorkspaceConfig(pm: PackageManager, workspaces: string[]) {
151
156
  /** pnpm uses pnpm-workspace.yaml */
152
157
  type: "file" as const,
153
158
  filename: "pnpm-workspace.yaml",
154
- content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\n`,
159
+ content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\nallowBuilds:\n protobufjs: false\n unrs-resolver: false\n`,
155
160
  };
156
161
  }
157
162
 
@@ -171,7 +176,7 @@ export function getCiSetupStep(pm: PackageManager): string {
171
176
  return ` - name: Setup pnpm
172
177
  uses: pnpm/action-setup@v4
173
178
  with:
174
- version: latest`;
179
+ version: 11`;
175
180
  }
176
181
  // npm: no extra setup needed (comes with Node.js)
177
182
  return "";
@@ -199,7 +204,7 @@ export function getAzurePipelinesSetup(pm: PackageManager): string {
199
204
  if (pm === "pnpm") {
200
205
  return ` - script: |
201
206
  corepack enable
202
- corepack prepare pnpm@latest --activate
207
+ corepack prepare pnpm@11 --activate
203
208
  displayName: 'Setup pnpm'`;
204
209
  }
205
210
  // npm: no extra step needed
@@ -213,7 +218,7 @@ export function getBuildScript(pm: PackageManager): string {
213
218
  const copyStandaloneAssets = `node -e "const fs=require('fs');fs.mkdirSync('.next/standalone/.next',{recursive:true});if(fs.existsSync('.next/static'))fs.cpSync('.next/static','.next/standalone/.next/static',{recursive:true});if(fs.existsSync('public'))fs.cpSync('public','.next/standalone/public',{recursive:true});"`;
214
219
 
215
220
  if (pm === "pnpm") {
216
- return `pnpm run --filter shared build && next build --webpack && ${copyStandaloneAssets}`;
221
+ return `pnpm --filter shared run build && next build --webpack && ${copyStandaloneAssets}`;
217
222
  }
218
223
  return `npm run --workspace=shared build && next build --webpack && ${copyStandaloneAssets}`;
219
224
  }