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.
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +66 -4
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +45 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/provision-deployment.d.ts +45 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +151 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +39 -5
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +609 -103
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/boundaries.js +3 -3
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +9 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/react-project.js +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +43 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +18 -19
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +16 -1
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +98 -4
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +20 -13
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +76 -15
- package/dist/mcp/index.js.map +1 -1
- package/dist/utils/azure-cli.d.ts +16 -4
- package/dist/utils/azure-cli.d.ts.map +1 -1
- package/dist/utils/azure-cli.js +166 -4
- package/dist/utils/azure-cli.js.map +1 -1
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
- package/src/__tests__/agent-instructions.test.ts +3 -0
- package/src/__tests__/agent-loop-phase34.test.ts +114 -8
- package/src/__tests__/azure-cli.test.ts +5 -2
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +130 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp.test.ts +71 -0
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +111 -0
- package/src/__tests__/provision-runtime.test.ts +562 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/init.ts +4 -4
- package/src/cli/commands/provision.ts +69 -5
- package/src/cli/commands/scaffold.ts +65 -1
- package/src/cli/index.ts +12 -2
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/provision-deployment.ts +167 -0
- package/src/core/operations/provision-operations.ts +597 -145
- package/src/core/project/agent-templates/AGENTS.md +46 -3
- package/src/core/project/agent-templates/CLAUDE.md +9 -0
- package/src/core/project/agent-templates/copilot-instructions.md +10 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +3 -3
- package/src/core/project/capabilities.ts +29 -6
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +43 -7
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/native-schema-generator.ts +16 -0
- package/src/core/verify/index.ts +95 -5
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +23 -16
- package/src/mcp/index.ts +68 -20
- package/src/utils/azure-cli.ts +167 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
|
698
|
+
const authImport = "from blueprints.auth import auth_bp";
|
|
693
699
|
const authRegister = "app.register_blueprint(auth_bp)";
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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('
|
|
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(
|
|
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')
|
|
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
|
|
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(
|
|
94
|
+
export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
|
|
94
95
|
const indexPath = path.join(process.cwd(), "shared", "index.ts");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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(
|
|
213
|
+
if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
|
|
176
214
|
updatedIndex = true;
|
|
177
215
|
}
|
|
178
216
|
}
|