swallowkit 2.0.0-alpha.4 → 2.0.0-alpha.6

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 (113) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/init.d.ts +2 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +3 -1
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/provision.d.ts +2 -1
  8. package/dist/cli/commands/provision.d.ts.map +1 -1
  9. package/dist/cli/commands/provision.js +66 -4
  10. package/dist/cli/commands/provision.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +45 -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 +15 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts +2 -0
  18. package/dist/core/operations/create-model.d.ts.map +1 -1
  19. package/dist/core/operations/create-model.js +60 -8
  20. package/dist/core/operations/create-model.js.map +1 -1
  21. package/dist/core/operations/provision-deployment.d.ts +45 -0
  22. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  23. package/dist/core/operations/provision-deployment.js +151 -0
  24. package/dist/core/operations/provision-deployment.js.map +1 -0
  25. package/dist/core/operations/provision-operations.d.ts +39 -5
  26. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  27. package/dist/core/operations/provision-operations.js +609 -103
  28. package/dist/core/operations/provision-operations.js.map +1 -1
  29. package/dist/core/project/boundaries.js +3 -3
  30. package/dist/core/project/boundaries.js.map +1 -1
  31. package/dist/core/project/capabilities.d.ts +9 -0
  32. package/dist/core/project/capabilities.d.ts.map +1 -1
  33. package/dist/core/project/capabilities.js +20 -6
  34. package/dist/core/project/capabilities.js.map +1 -1
  35. package/dist/core/project/react-project.js +1 -1
  36. package/dist/core/project/state.d.ts +7 -0
  37. package/dist/core/project/state.d.ts.map +1 -1
  38. package/dist/core/project/state.js +86 -0
  39. package/dist/core/project/state.js.map +1 -1
  40. package/dist/core/project/workflows.d.ts.map +1 -1
  41. package/dist/core/project/workflows.js +43 -7
  42. package/dist/core/project/workflows.js.map +1 -1
  43. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/auth-generator.js +18 -19
  45. package/dist/core/scaffold/auth-generator.js.map +1 -1
  46. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  47. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  48. package/dist/core/scaffold/native-schema-generator.js +16 -1
  49. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  50. package/dist/core/verify/index.d.ts +4 -3
  51. package/dist/core/verify/index.d.ts.map +1 -1
  52. package/dist/core/verify/index.js +98 -4
  53. package/dist/core/verify/index.js.map +1 -1
  54. package/dist/machine/contracts.d.ts +1 -0
  55. package/dist/machine/contracts.d.ts.map +1 -1
  56. package/dist/machine/errors.d.ts +3 -2
  57. package/dist/machine/errors.d.ts.map +1 -1
  58. package/dist/machine/errors.js +2 -1
  59. package/dist/machine/errors.js.map +1 -1
  60. package/dist/machine/index.d.ts.map +1 -1
  61. package/dist/machine/index.js +20 -13
  62. package/dist/machine/index.js.map +1 -1
  63. package/dist/mcp/index.d.ts +3 -2
  64. package/dist/mcp/index.d.ts.map +1 -1
  65. package/dist/mcp/index.js +76 -15
  66. package/dist/mcp/index.js.map +1 -1
  67. package/dist/utils/azure-cli.d.ts +16 -4
  68. package/dist/utils/azure-cli.d.ts.map +1 -1
  69. package/dist/utils/azure-cli.js +166 -4
  70. package/dist/utils/azure-cli.js.map +1 -1
  71. package/dist/utils/azure-diagnostics.d.ts +16 -0
  72. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  73. package/dist/utils/azure-diagnostics.js +68 -0
  74. package/dist/utils/azure-diagnostics.js.map +1 -0
  75. package/package.json +6 -3
  76. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  77. package/src/__tests__/agent-instructions.test.ts +3 -0
  78. package/src/__tests__/agent-loop-phase34.test.ts +114 -8
  79. package/src/__tests__/azure-cli.test.ts +5 -2
  80. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  81. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  82. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  83. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  84. package/src/__tests__/mcp.test.ts +71 -0
  85. package/src/__tests__/multi-auth.test.ts +115 -4
  86. package/src/__tests__/plan-apply.test.ts +111 -0
  87. package/src/__tests__/provision-runtime.test.ts +562 -0
  88. package/src/cli/commands/add-auth.ts +36 -6
  89. package/src/cli/commands/init.ts +4 -4
  90. package/src/cli/commands/provision.ts +69 -5
  91. package/src/cli/commands/scaffold.ts +65 -1
  92. package/src/cli/index.ts +12 -2
  93. package/src/core/operations/create-model.ts +51 -13
  94. package/src/core/operations/provision-deployment.ts +167 -0
  95. package/src/core/operations/provision-operations.ts +597 -145
  96. package/src/core/project/agent-templates/AGENTS.md +46 -3
  97. package/src/core/project/agent-templates/CLAUDE.md +9 -0
  98. package/src/core/project/agent-templates/copilot-instructions.md +10 -0
  99. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  100. package/src/core/project/boundaries.ts +3 -3
  101. package/src/core/project/capabilities.ts +29 -6
  102. package/src/core/project/react-project.ts +1 -1
  103. package/src/core/project/state.ts +70 -0
  104. package/src/core/project/workflows.ts +43 -7
  105. package/src/core/scaffold/auth-generator.ts +18 -19
  106. package/src/core/scaffold/native-schema-generator.ts +16 -0
  107. package/src/core/verify/index.ts +95 -5
  108. package/src/machine/contracts.ts +1 -0
  109. package/src/machine/errors.ts +4 -2
  110. package/src/machine/index.ts +23 -16
  111. package/src/mcp/index.ts +68 -20
  112. package/src/utils/azure-cli.ts +167 -6
  113. package/src/utils/azure-diagnostics.ts +75 -0
@@ -428,7 +428,7 @@ export async function addAuthCommand(options: AddAuthOptions) {
428
428
 
429
429
  // 10. Install dependencies
430
430
  console.log("\n Installing auth dependencies...");
431
- await installAuthDependencies(cwd, backendLanguage, rdbProvider, session);
431
+ installAuthDependenciesSync(cwd, backendLanguage, rdbProvider, session);
432
432
  await finalize();
433
433
 
434
434
  console.log("\n Authentication setup complete!");
@@ -458,7 +458,8 @@ function findObjectEnd(content: string, open: number): number {
458
458
  function updateConfigWithNamedScheme(cwd: string, scheme: string, provider: AuthProvider, session: FileOperationSession, allowedProviders?: string[]): void {
459
459
  const jsPath = path.join(cwd, "swallowkit.config.js");
460
460
  const jsonPath = path.join(cwd, "swallowkit.config.json");
461
- const configPath = session.fileExists(jsPath) ? jsPath : jsonPath;
461
+ // Match loadConfig precedence: swallowkit.config.json wins when both files exist.
462
+ const configPath = session.fileExists(jsonPath) ? jsonPath : jsPath;
462
463
  if (!session.fileExists(configPath)) throw new Error("swallowkit.config.js or swallowkit.config.json is required");
463
464
  const swaProviders = provider === "swa" ? normalizeSwaProviders(allowedProviders) : undefined;
464
465
  if (configPath.endsWith(".json")) {
@@ -510,6 +511,11 @@ function addNamedScheme(cwd: string, scheme: string, provider: AuthProvider, lan
510
511
  // Validate/update config before creating files, so duplicate names stop safely.
511
512
  updateConfigWithNamedScheme(cwd, scheme, provider, session, allowedProviders);
512
513
  const slug = scheme.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
514
+ if (provider === "custom-jwt") {
515
+ // The scheme's JWT helper needs the same token/password/driver packages as single-provider custom-jwt.
516
+ const connector = getConnectorDefinition(defaultCustomJwtConfig().userConnector) as RdbConnectorConfig | undefined;
517
+ installAuthDependenciesSync(cwd, language, connector?.provider ?? "mysql", session);
518
+ }
513
519
  if (language === "typescript") {
514
520
  const dir = path.join(cwd, "functions", "src", "auth", "schemes", slug);
515
521
  if (provider === "external-token") writeIfMissing(session, path.join(dir, "verifier.ts"), generateExternalTokenVerifierTS());
@@ -689,9 +695,14 @@ function generateFunctionsAuth(
689
695
  const functionAppPath = path.join(functionsDir, "function_app.py");
690
696
  if (session.fileExists(functionAppPath)) {
691
697
  const content = session.readFile(functionAppPath);
692
- const authImport = "from blueprints.auth import bp as auth_bp";
698
+ const authImport = "from blueprints.auth import auth_bp";
693
699
  const authRegister = "app.register_blueprint(auth_bp)";
694
- if (!content.includes(authImport)) {
700
+ // Earlier releases imported a non-existent `bp`; repair it in place when re-running.
701
+ const legacyImport = "from blueprints.auth import bp as auth_bp";
702
+ if (content.includes(legacyImport)) {
703
+ session.writeFile(functionAppPath, content.replace(legacyImport, authImport), extensionMeta);
704
+ console.log(` Updated: functions/function_app.py (fixed auth blueprint import)`);
705
+ } else if (!content.includes(authImport)) {
695
706
  const marker = "# SwallowKit scaffold registrations";
696
707
  if (content.includes(marker)) {
697
708
  const updated = content.replace(
@@ -742,6 +753,24 @@ function generateBFFAuth(cwd: string, projectName: string, sharedPackageName: st
742
753
  }
743
754
 
744
755
  function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: CustomJwtConfig, session: FileOperationSession, allowedProviders?: string[]): void {
756
+ const jsonPath = path.join(cwd, "swallowkit.config.json");
757
+ if (session.fileExists(jsonPath)) {
758
+ // loadConfig reads swallowkit.config.json first, so it is the effective configuration.
759
+ const value = JSON.parse(session.readFile(jsonPath));
760
+ if (value.auth) {
761
+ console.log(" 'auth' section already exists in swallowkit.config.json");
762
+ return;
763
+ }
764
+ value.auth = {
765
+ provider,
766
+ ...(provider === "custom-jwt" ? { customJwt: { ...config, jwtSecretEnv: config.jwtSecretEnv || "JWT_SECRET", tokenExpiry: config.tokenExpiry || "24h" } } : {}),
767
+ ...(provider === "swa" ? { swa: { allowedProviders: normalizeSwaProviders(allowedProviders), roleSource: "swa-roles" } } : {}),
768
+ authorization: { defaultPolicy: provider === "none" ? "anonymous" : "authenticated" },
769
+ };
770
+ session.writeFile(jsonPath, JSON.stringify(value, null, 2) + "\n", extensionMeta);
771
+ console.log(` Updated: swallowkit.config.json`);
772
+ return;
773
+ }
745
774
  const configPath = path.join(cwd, "swallowkit.config.js");
746
775
  if (!session.fileExists(configPath)) {
747
776
  console.warn(" swallowkit.config.js not found. Please add auth config manually.");
@@ -786,7 +815,8 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
786
815
  provider: '${provider}',
787
816
  ${providerSettings}
788
817
  authorization: {
789
- defaultPolicy: 'authenticated',
818
+ // Disabled auth has no scheme to authenticate against; 'authenticated' would fail validateConfig.
819
+ defaultPolicy: '${provider === "none" ? "anonymous" : "authenticated"}',
790
820
  },
791
821
  },
792
822
  `;
@@ -821,7 +851,7 @@ function updateEnvironmentFiles(cwd: string, session: FileOperationSession): voi
821
851
  }
822
852
  }
823
853
 
824
- async function installAuthDependencies(cwd: string, backendLanguage: BackendLanguage, provider: "mysql" | "postgres" | "sqlserver" = "mysql", session: FileOperationSession): Promise<void> {
854
+ function installAuthDependenciesSync(cwd: string, backendLanguage: BackendLanguage, provider: "mysql" | "postgres" | "sqlserver" = "mysql", session: FileOperationSession): void {
825
855
  if (backendLanguage === "typescript") {
826
856
  const funcPkgPath = path.join(cwd, "functions", "package.json");
827
857
  if (session.fileExists(funcPkgPath)) {
@@ -1,4 +1,4 @@
1
- import * as fs from "fs";
1
+ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { spawn, execSync } from "child_process";
4
4
  import prompts from "prompts";
@@ -1214,7 +1214,7 @@ export async function register() {
1214
1214
  createAiAgentFiles(projectDir, projectName, backendLanguage, pm, frontend);
1215
1215
  }
1216
1216
 
1217
- async function createSharedPackage(projectDir: string, projectName: string) {
1217
+ export async function createSharedPackage(projectDir: string, projectName: string) {
1218
1218
  console.log('📦 Creating shared workspace package for Zod models...\n');
1219
1219
 
1220
1220
  const sharedDir = path.join(projectDir, 'shared');
@@ -1254,7 +1254,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
1254
1254
  // shared/index.ts (empty re-export file, scaffold will add entries)
1255
1255
  fs.writeFileSync(
1256
1256
  path.join(sharedDir, 'index.ts'),
1257
- `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`
1257
+ `// Shared Zod models. SwallowKit create-model/scaffold append missing model exports.\n// You may add exports manually; existing exports and comments are preserved. No managed markers.\n`
1258
1258
  );
1259
1259
 
1260
1260
  // shared/.gitignore
@@ -1266,7 +1266,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
1266
1266
  console.log('✅ Shared package created\n');
1267
1267
  }
1268
1268
 
1269
- async function createAzureFunctionsProject(
1269
+ export async function createAzureFunctionsProject(
1270
1270
  projectDir: string,
1271
1271
  pm: PackageManager = 'pnpm',
1272
1272
  backendLanguage: BackendLanguage = 'typescript'
@@ -3,10 +3,15 @@ import prompts from "prompts";
3
3
  import { ensureSwallowKitProject } from "../../core/config";
4
4
  import {
5
5
  INTERACTIVE_PROVISION_RETRY_HINT,
6
+ ApplyProvisionResult,
6
7
  ProvisionPlanData,
8
+ applyProvisionOperation,
7
9
  executeProvisionPlan,
10
+ inspectProvisionOperation,
8
11
  planProvisionOperation,
9
12
  } from "../../core/operations/provision-operations";
13
+ import { MachineCommandError } from "../../machine/errors";
14
+ import { formatAzureDiagnostic } from "../../utils/azure-diagnostics";
10
15
 
11
16
  export function describeProvisionAction(plan: Pick<ProvisionPlanData, "mode" | "resourceGroup" | "location" | "pendingMigrations">): string {
12
17
  switch (plan.mode) {
@@ -18,6 +23,8 @@ export function describeProvisionAction(plan: Pick<ProvisionPlanData, "mode" | "
18
23
  return `The existing resource group ${plan.resourceGroup} will be recorded as SwallowKit baseline 0. Nothing will be deployed.`;
19
24
  case "reconcile":
20
25
  return `infra/main.bicep will be redeployed to ${plan.resourceGroup}. Template-managed settings may be reset.`;
26
+ case "recover":
27
+ return `The existing baseline deployment in ${plan.resourceGroup} will be observed and its migration tag/ledger completed after Azure Succeeded. Bicep will not be submitted again.`;
21
28
  default:
22
29
  return `Resource group ${plan.resourceGroup}:`;
23
30
  }
@@ -44,7 +51,7 @@ const SWA_REGIONS = [
44
51
  ];
45
52
 
46
53
  interface ProvisionCliOptions {
47
- resourceGroup: string;
54
+ resourceGroup?: string;
48
55
  subscription?: string;
49
56
  location?: string;
50
57
  swaLocation?: string;
@@ -53,6 +60,34 @@ interface ProvisionCliOptions {
53
60
  baseline?: string;
54
61
  reconcile?: boolean;
55
62
  approve?: boolean;
63
+ resume?: string;
64
+ status?: string;
65
+ resumeDeployment?: string;
66
+ waitSeconds?: string;
67
+ }
68
+
69
+ export function formatProvisionResult(result: ApplyProvisionResult): string {
70
+ const outcome = result.outcome === "succeeded" ? "✅ Provisioning completed, including migration tags and the local ledger."
71
+ : result.outcome === "in-progress" ? "⏳ Azure deployment is still in progress. The local wait ended; this is not an Azure failure."
72
+ : result.outcome === "partial" ? "⚠️ Provisioning is partially complete. Finish migration bookkeeping from the checkpoint; do not redeploy the baseline."
73
+ : result.outcome === "failed" ? "❌ Azure deployment reached a failed/cancelled terminal state."
74
+ : "⚠️ Azure deployment result is unknown. A local az interruption does not prove an Azure failure.";
75
+ const lines = [outcome, `Run: ${result.runId}; resource group: ${result.resourceGroup}`];
76
+ if (result.deployment) {
77
+ const reference = result.deployment.reference;
78
+ lines.push(`Deployment: ${reference.name}; subscription: ${reference.subscription}; state: ${result.deployment.state}${reference.id ? `; ID: ${reference.id}` : ""}`);
79
+ for (const error of result.deployment.errors) lines.push(`${error.code}: ${error.message}`);
80
+ }
81
+ for (const warning of result.warnings) lines.push(`Warning: ${warning}`);
82
+ for (const diagnostic of result.diagnostics.filter((entry) => entry.failureKind || entry.outputTruncated)) {
83
+ lines.push(formatAzureDiagnostic(diagnostic));
84
+ }
85
+ if (result.status !== "complete") {
86
+ lines.push(`Check/wait: swallowkit provision --status ${result.runId} --wait-seconds 300`);
87
+ lines.push(`Resume after approval: swallowkit provision --resume ${result.runId} --approve`);
88
+ lines.push("Azure may continue after Ctrl+C or process termination. No Azure cancellation or deletion is performed.");
89
+ }
90
+ return lines.join("\n");
56
91
  }
57
92
 
58
93
  async function resolveLocations(options: ProvisionCliOptions): Promise<{ location: string; swaLocation: string }> {
@@ -86,7 +121,7 @@ async function resolveLocations(options: ProvisionCliOptions): Promise<{ locatio
86
121
 
87
122
  export const provisionCommand = new Command("provision")
88
123
  .description("Bootstrap, adopt, migrate, or explicitly reconcile Azure resources")
89
- .requiredOption("-g, --resource-group <name>", "Resource group name")
124
+ .option("-g, --resource-group <name>", "Resource group name (required for a new preview)")
90
125
  .option("--subscription <id>", "Azure subscription ID")
91
126
  .option("--location <region>", "Primary location for Functions and Cosmos DB")
92
127
  .option("--swa-location <region>", "Static Web App location")
@@ -95,8 +130,34 @@ export const provisionCommand = new Command("provision")
95
130
  .option("--baseline <version>", "Baseline version for adoption (currently 0)")
96
131
  .option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
97
132
  .option("--approve", "Run the displayed Azure commands without the confirmation prompt", false)
133
+ .option("--status <runId>", "Read an existing execution checkpoint and Azure deployment state without writes")
134
+ .option("--resume <runId>", "Resume an approved checkpoint without resubmitting attempted deployments")
135
+ .option("--resume-deployment <name>", "Recover an older untracked baseline deployment after verifying its template and parameters")
136
+ .option("--wait-seconds <seconds>", "Bounded state wait, 0-3600 seconds (default: 300; status: 0)")
98
137
  .action(async (options: ProvisionCliOptions) => {
99
138
  ensureSwallowKitProject("provision");
139
+ const waitSeconds = options.waitSeconds === undefined ? undefined : Number(options.waitSeconds);
140
+ if ((options.resume && options.status) || ((options.resume || options.status) && (options.resourceGroup || options.resumeDeployment || options.reconcile || options.adoptExisting || options.subscription || options.location || options.swaLocation || options.whatIf || options.baseline !== undefined))) {
141
+ throw new MachineCommandError("invalid-arguments", "Use --status or --resume with the checkpoint ID alone; its original Azure target and reviewed files are preserved.", undefined, "blocked");
142
+ }
143
+ if (options.status) {
144
+ const result = await inspectProvisionOperation({ planId: options.status, waitSeconds });
145
+ console.log(formatProvisionResult(result));
146
+ if (result.status === "blocked" || result.status === "failed") process.exitCode = 1;
147
+ return;
148
+ }
149
+ if (options.resume) {
150
+ let approved = options.approve === true;
151
+ if (!approved && process.stdin.isTTY && process.stdout.isTTY) {
152
+ const answer = await prompts({ type: "confirm", name: "approved", message: "Resume the original reviewed run and finish pending steps? This requires explicit approval.", initial: false });
153
+ approved = answer.approved === true;
154
+ }
155
+ const result = await applyProvisionOperation({ planId: options.resume, approve: approved, waitSeconds });
156
+ console.log(formatProvisionResult(result));
157
+ if (result.status === "blocked" || result.status === "failed") process.exitCode = 1;
158
+ return;
159
+ }
160
+ if (!options.resourceGroup) throw new MachineCommandError("invalid-arguments", "New provision previews require --resource-group.", undefined, "blocked");
100
161
  const locations = await resolveLocations(options);
101
162
  const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
102
163
  // Preview in memory: the same Azure checks as the agent workflow, but no saved plan file.
@@ -109,6 +170,7 @@ export const provisionCommand = new Command("provision")
109
170
  adoptExisting: options.adoptExisting,
110
171
  baseline,
111
172
  reconcile: options.reconcile,
173
+ resumeDeployment: options.resumeDeployment,
112
174
  }, { persist: false });
113
175
 
114
176
  for (const warning of preview.warnings) console.warn(`Warning: ${warning}`);
@@ -126,7 +188,7 @@ export const provisionCommand = new Command("provision")
126
188
  }
127
189
 
128
190
  if (preview.mode === "adoption-required") {
129
- throw new Error('Existing resource group requires: migrations init --legacy-baseline, then provision --adopt-existing --baseline 0 --what-if.');
191
+ throw new Error('The existing resource group has no migration tag. For an interrupted baseline use --resume-deployment <original-name> to verify/observe it without redeploying. Legacy adoption applies only to a legacy manifest; do not replace an existing manifest.');
130
192
  }
131
193
 
132
194
  if (preview.mode === "noop") {
@@ -156,5 +218,7 @@ export const provisionCommand = new Command("provision")
156
218
  return;
157
219
  }
158
220
 
159
- const result = await executeProvisionPlan(preview, { approve: true, retryHint: INTERACTIVE_PROVISION_RETRY_HINT });
160
- console.log(`✅ Provisioning completed. ${result.executedCommands.length} Azure CLI command(s) ran.`); });
221
+ const result = await executeProvisionPlan(preview, { approve: true, retryHint: INTERACTIVE_PROVISION_RETRY_HINT, waitSeconds });
222
+ console.log(formatProvisionResult(result));
223
+ if (result.status === "blocked" || result.status === "failed") process.exitCode = 1;
224
+ });
@@ -54,6 +54,7 @@ import {
54
54
  } from "../../core/operations/file-session";
55
55
  import { generateModelContainerMigration } from "../../core/operations/migration-generator";
56
56
  import { recordSessionOperations } from "../../core/project/artifacts";
57
+ import { updateSharedIndex } from "../../core/operations/create-model";
57
58
 
58
59
  const SCAFFOLD_GENERATOR = "scaffold";
59
60
 
@@ -158,6 +159,11 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
158
159
  session.setSchemaAnalysis(schemaAnalysis);
159
160
  console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
160
161
 
162
+ // Validate policy prerequisites before any generated artifacts are written.
163
+ resolveAuthPolicy(modelInfo, getAuthConfig());
164
+ for (const configName of ["swallowkit.config.js", "swallowkit.config.json", ".swallowkitrc.json"]) {
165
+ session.registerInput(path.join(process.cwd(), configName));
166
+ }
161
167
  const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
162
168
  if (userScopeWarning) {
163
169
  session.addWarning(userScopeWarning);
@@ -217,6 +223,8 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
217
223
  console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
218
224
  }
219
225
 
226
+ updateSharedIndex(modelPath, modelInfo.schemaName, session);
227
+
220
228
  // 5. Generate BFF callFunction helper
221
229
  await generateCallFunctionHelper(session, apiDir);
222
230
 
@@ -332,10 +340,43 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: Fil
332
340
  export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
333
341
  // モデルに明示的な authPolicy がある場合はそれを使用
334
342
  if (modelInfo.authPolicy) {
343
+ if (!isAuthenticationConfigured(authConfig)) {
344
+ throw new MachineCommandError(
345
+ "auth-policy-without-auth",
346
+ `Model '${modelInfo.name}' declares authPolicy, but authentication is not configured. No artifacts were written. Run plan auth and apply auth before scaffolding this model.`,
347
+ { model: modelInfo.name },
348
+ "blocked",
349
+ [{
350
+ command: "swallowkit machine plan auth --provider <custom-jwt|swa|external-token>",
351
+ description: "Choose an authentication provider, review and apply its plan, then re-plan scaffold.",
352
+ }]
353
+ );
354
+ }
335
355
  const normalized = normalizeAuthConfig(authConfig);
356
+ const usesNamedPolicy = Boolean(modelInfo.authPolicy.policy ||
357
+ typeof modelInfo.authPolicy.read === "string" || typeof modelInfo.authPolicy.write === "string");
358
+ // Guards resolve each operation independently: a string selects the named router, an array (or
359
+ // the roles fallback) selects the single-provider helper. Named schemes only have the router.
360
+ const operationIsNamed = (selected: string | string[] | undefined) =>
361
+ typeof selected === "string" || (selected === undefined && Boolean(modelInfo.authPolicy!.policy));
362
+ const everyOperationNamed = operationIsNamed(modelInfo.authPolicy.read) && operationIsNamed(modelInfo.authPolicy.write) &&
363
+ !modelInfo.authPolicy.roles;
364
+ if (authConfig?.schemes && !authConfig.provider && !everyOperationNamed) {
365
+ throw new MachineCommandError(
366
+ "auth-policy-requires-named-policy",
367
+ `Model '${modelInfo.name}' declares an authPolicy whose read or write operation does not resolve to a named policy, but authentication uses named schemes. Use authPolicy = { policy: '<name>' } or name both read and write policies (role arrays are not supported with named schemes); no artifacts were written.`,
368
+ { model: modelInfo.name },
369
+ "blocked",
370
+ [{
371
+ command: "swallowkit machine inspect capabilities",
372
+ description: "Define a named authorization policy selecting the required schemes/roles, reference it from the model, then re-plan scaffold.",
373
+ }]
374
+ );
375
+ }
336
376
  for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
337
377
  if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
338
378
  }
379
+ if (usesNamedPolicy) assertNamedSchemesConfigured(modelInfo.name, "authPolicy", authConfig);
339
380
  return modelInfo.authPolicy;
340
381
  }
341
382
 
@@ -350,12 +391,35 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
350
391
  return {}; // 空のポリシー = 認証のみ、ロール制限なし
351
392
  }
352
393
 
353
- if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
394
+ if (defaultPolicy !== 'anonymous') {
395
+ assertNamedSchemesConfigured(modelInfo.name, `auth.authorization.defaultPolicy '${defaultPolicy}'`, authConfig);
396
+ return { policy: defaultPolicy };
397
+ }
354
398
 
355
399
  // defaultPolicy が 'anonymous' なら認証不要
356
400
  return undefined;
357
401
  }
358
402
 
403
+ /**
404
+ * Named authorization policies are enforced by the generated multi-scheme router and scheme-specific
405
+ * client contexts, which exist only for named auth.schemes. A single-provider configuration
406
+ * (auth.provider) has no router, so referencing a named policy would generate code that cannot build.
407
+ */
408
+ function assertNamedSchemesConfigured(modelName: string, source: string, authConfig: AuthConfig | undefined): void {
409
+ if (Object.keys(authConfig?.schemes ?? {}).some((name) => name !== "default")) return;
410
+ throw new MachineCommandError(
411
+ "auth-named-policy-requires-named-scheme",
412
+ `Model '${modelName}' uses a named authorization policy via ${source}, but authentication is configured with a single auth.provider. ` +
413
+ `Named policies require named auth.schemes; no artifacts were written. Use role lists (authPolicy = { roles: [...] } or read/write role arrays) with auth.provider, or add a named scheme.`,
414
+ { model: modelName, source },
415
+ "blocked",
416
+ [{
417
+ command: "swallowkit machine plan auth --provider <swa|external-token|custom-jwt> --scheme <name>",
418
+ description: "Add a named scheme and reference it from auth.authorization.policies, or switch the model to role lists; then re-plan scaffold.",
419
+ }]
420
+ );
421
+ }
422
+
359
423
  /**
360
424
  * モデルファイルのパスを解決
361
425
  */
package/src/cli/index.ts CHANGED
@@ -6,6 +6,7 @@ import * as path from "path";
6
6
  import { Command } from "commander";
7
7
  import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
8
8
  import { provisionCommand } from "./commands/provision";
9
+ import { AzureCliDiagnostic, formatAzureDiagnostic } from "../utils/azure-diagnostics";
9
10
  import { createMigrationCommand } from "./commands/create-migration";
10
11
  import { migrationsCommand } from "./commands/migrations";
11
12
  import { configureMcpCommand } from "./commands/configure-mcp";
@@ -223,8 +224,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
223
224
 
224
225
  program
225
226
  .command("verify")
226
- .description("Run verification checks (structure, drift, typecheck)")
227
- .option("--checks <ids>", "Comma-separated check ids to run (structure,drift,typecheck)")
227
+ .description("Run verification checks (structure, drift, typecheck, backend-build)")
228
+ .option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
228
229
  .action(async (options) => {
229
230
  await runVerifyCommand({ checks: options.checks });
230
231
  });
@@ -365,7 +366,16 @@ export function formatCliError(error: unknown): string {
365
366
  if (details && typeof details === "object") {
366
367
  if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
367
368
  if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
369
+ if (typeof details.runId === "string") lines.push(` Run: ${details.runId}`);
370
+ if (details.deployment && typeof details.deployment === "object") {
371
+ const deployment = details.deployment as { state?: string; reference?: { name?: string; id?: string; resourceGroup?: string; subscription?: string } };
372
+ lines.push(` Deployment: ${deployment.reference?.id ?? deployment.reference?.name ?? "unknown"}; state: ${deployment.state ?? "unknown"}`);
373
+ }
374
+ if (Array.isArray(details.diagnostics)) {
375
+ for (const diagnostic of details.diagnostics as AzureCliDiagnostic[]) lines.push(formatAzureDiagnostic(diagnostic));
376
+ }
368
377
  }
378
+ for (const action of error.nextActions ?? []) lines.push(` Next: ${action.command} — ${action.description}`);
369
379
  return lines.join("\n");
370
380
  }
371
381
  if (error instanceof Error) {
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import * as ts from "typescript";
2
3
  import { ensureSwallowKitProject, loadConfig } from "../config";
3
4
  import { toPascalCase } from "../scaffold/model-parser";
4
5
  import { syncProjectManifest } from "../project/manifest";
@@ -90,21 +91,58 @@ export const connectorConfig = {
90
91
  `;
91
92
  }
92
93
 
93
- function updateSharedIndex(kebabName: string, pascalName: string, session: FileOperationSession): boolean {
94
+ export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
94
95
  const indexPath = path.join(process.cwd(), "shared", "index.ts");
95
-
96
- if (!session.fileExists(indexPath)) {
97
- return false;
96
+ session.registerInput(indexPath);
97
+ const content = session.fileExists(indexPath) ? session.readFile(indexPath) : "";
98
+ const modulePath = `./${path.relative(path.dirname(indexPath), modelPath).replace(/\\/g, "/").replace(/\.ts$/, "")}`;
99
+ const source = ts.createSourceFile(indexPath, content, ts.ScriptTarget.Latest, true);
100
+ // Text edits that turn a type-only re-export of this schema into nothing (a value export of the same
101
+ // name also exports its type); keeping both would be a duplicate-identifier error.
102
+ const edits: Array<{ start: number; end: number; text: string }> = [];
103
+ let hasValueExport = false;
104
+ for (const statement of source.statements) {
105
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
106
+ const exportedPath = statement.moduleSpecifier.text.replace(/\.(ts|js)$/, "");
107
+ if (exportedPath !== modulePath) continue;
108
+ if (!statement.exportClause) {
109
+ if (!statement.isTypeOnly) hasValueExport = true;
110
+ continue;
111
+ }
112
+ if (!ts.isNamedExports(statement.exportClause)) continue;
113
+ const elements = statement.exportClause.elements;
114
+ const matches = elements.filter((element) =>
115
+ element.name.text === schemaName && (element.propertyName?.text ?? element.name.text) === schemaName);
116
+ for (const element of matches) {
117
+ if (!statement.isTypeOnly && !element.isTypeOnly) {
118
+ hasValueExport = true;
119
+ } else if (!statement.isTypeOnly) {
120
+ // export { type Todo } -> export { Todo }
121
+ edits.push({ start: element.getStart(source), end: element.getEnd(), text: schemaName });
122
+ hasValueExport = true;
123
+ } else if (elements.length === 1) {
124
+ let end = statement.getEnd();
125
+ if (content.slice(end, end + 2) === "\r\n") end += 2;
126
+ else if (content[end] === "\n") end += 1;
127
+ edits.push({ start: statement.getStart(source), end, text: "" });
128
+ } else {
129
+ const index = elements.indexOf(element);
130
+ const start = index === 0 ? element.getStart(source) : elements[index - 1].getEnd();
131
+ const end = index === 0 ? elements[index + 1].getStart(source) : element.getEnd();
132
+ edits.push({ start, end, text: "" });
133
+ }
134
+ }
98
135
  }
99
-
100
- const content = session.readFile(indexPath);
101
- const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
102
-
103
- if (content.includes(exportLine)) {
104
- return false;
136
+ let updated = content;
137
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
138
+ updated = `${updated.slice(0, edit.start)}${edit.text}${updated.slice(edit.end)}`;
105
139
  }
106
-
107
- session.writeFile(indexPath, `${content}${exportLine}\n`, {
140
+ if (hasValueExport && updated === content) return false;
141
+ if (!hasValueExport) {
142
+ const exportLine = `export { ${schemaName} } from '${modulePath}';`;
143
+ updated = `${updated}${updated && !updated.endsWith("\n") ? "\n" : ""}${exportLine}\n`;
144
+ }
145
+ session.writeFile(indexPath, updated, {
108
146
  ownership: "extension-point",
109
147
  generator: CREATE_MODEL_GENERATOR,
110
148
  });
@@ -172,7 +210,7 @@ export async function createModelOperation(options: CreateModelOperationOptions)
172
210
  });
173
211
  createdFiles.push(filePath.replace(/\\/g, "/"));
174
212
 
175
- if (updateSharedIndex(kebabName, pascalName, session)) {
213
+ if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
176
214
  updatedIndex = true;
177
215
  }
178
216
  }