swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5
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/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +19 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -4
- 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/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +2 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +29 -5
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.js +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +23 -5
- 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/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +289 -76
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-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 +122 -5
- 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 +42 -17
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +154 -15
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +124 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +64 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +170 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +28 -5
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-templates/AGENTS.md +67 -6
- package/src/core/project/agent-templates/CLAUDE.md +7 -1
- package/src/core/project/agent-templates/copilot-instructions.md +7 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +32 -5
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/workflows.ts +23 -5
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +313 -78
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +45 -20
- package/src/mcp/index.ts +173 -18
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/semver.ts +59 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ensureSwallowKitProject } from "../../core/config";
|
|
3
|
+
import {
|
|
4
|
+
MCP_CONFIG_PATH,
|
|
5
|
+
MCP_LATEST,
|
|
6
|
+
lookupPublishedLatestVersion,
|
|
7
|
+
readMcpConfigVersion,
|
|
8
|
+
resolveMcpPinVersion,
|
|
9
|
+
writeMcpConfigVersion,
|
|
10
|
+
} from "../../core/project/mcp-config";
|
|
11
|
+
import { resolveProjectSwallowKit } from "../../mcp/version-selection";
|
|
12
|
+
import { parseSemver } from "../../utils/semver";
|
|
13
|
+
import { getSwallowKitVersion } from "../../version";
|
|
14
|
+
|
|
15
|
+
export interface ConfigureMcpResult {
|
|
16
|
+
previousVersion: string | null;
|
|
17
|
+
version: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function configureMcp(
|
|
22
|
+
options: { version?: string; cwd?: string },
|
|
23
|
+
lookupLatest: (packageName: string) => string | null = lookupPublishedLatestVersion
|
|
24
|
+
): ConfigureMcpResult {
|
|
25
|
+
const cwd = options.cwd ?? process.cwd();
|
|
26
|
+
const requested = (options.version ?? "auto").trim();
|
|
27
|
+
const previousVersion = readMcpConfigVersion(cwd);
|
|
28
|
+
|
|
29
|
+
let decision: { version: string; reason: string };
|
|
30
|
+
if (requested === "auto") {
|
|
31
|
+
const projectVersion = resolveProjectSwallowKit(cwd)?.version ?? getSwallowKitVersion();
|
|
32
|
+
decision = resolveMcpPinVersion(projectVersion, lookupLatest("swallowkit"));
|
|
33
|
+
} else if (requested === MCP_LATEST) {
|
|
34
|
+
decision = { version: MCP_LATEST, reason: "Explicitly requested latest." };
|
|
35
|
+
} else if (parseSemver(requested)) {
|
|
36
|
+
decision = { version: requested, reason: "Explicitly pinned version." };
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`--version must be "auto", "latest", or a semantic version (got "${requested}").`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeMcpConfigVersion(cwd, decision.version);
|
|
42
|
+
return { previousVersion, ...decision };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const configureMcpCommand = new Command("configure-mcp")
|
|
46
|
+
.description(`Select the SwallowKit MCP version used by ${MCP_CONFIG_PATH} (auto: latest unless the project is newer)`)
|
|
47
|
+
.option("--version <version>", "auto | latest | <semver>", "auto")
|
|
48
|
+
.action((options: { version?: string }) => {
|
|
49
|
+
ensureSwallowKitProject("configure-mcp");
|
|
50
|
+
const result = configureMcp({ version: options.version });
|
|
51
|
+
console.log(`${MCP_CONFIG_PATH}: SWALLOWKIT_MCP_VERSION ${result.previousVersion ?? "(unset)"} -> ${result.version}`);
|
|
52
|
+
console.log(`Reason: ${result.reason}`);
|
|
53
|
+
console.log("Restart the MCP server (or reload your agent) to apply the change.");
|
|
54
|
+
});
|
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";
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "../../core/scaffold/native-schema-generator";
|
|
28
28
|
import { DEFAULT_AZURE_PYTHON_VERSION } from "../../utils/python-uv";
|
|
29
29
|
import { buildHomePage } from "../../core/project/home-page";
|
|
30
|
+
import { MCP_LATEST, McpPinDecision, lookupPublishedLatestVersion, resolveMcpPinVersion } from "../../core/project/mcp-config";
|
|
30
31
|
|
|
31
32
|
interface InitOptions {
|
|
32
33
|
name: string;
|
|
@@ -802,7 +803,7 @@ export function ensureNextTailwindPostcssConfig(projectDir: string): void {
|
|
|
802
803
|
`);
|
|
803
804
|
}
|
|
804
805
|
|
|
805
|
-
export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
|
|
806
|
+
export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm', mcpVersion: string = MCP_LATEST): string {
|
|
806
807
|
const { name } = getSwallowKitPackageMetadata();
|
|
807
808
|
return JSON.stringify(
|
|
808
809
|
{
|
|
@@ -814,7 +815,7 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
|
|
|
814
815
|
: ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
|
|
815
816
|
cwd: ".",
|
|
816
817
|
env: {
|
|
817
|
-
SWALLOWKIT_MCP_VERSION:
|
|
818
|
+
SWALLOWKIT_MCP_VERSION: mcpVersion,
|
|
818
819
|
},
|
|
819
820
|
},
|
|
820
821
|
},
|
|
@@ -824,6 +825,15 @@ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm
|
|
|
824
825
|
);
|
|
825
826
|
}
|
|
826
827
|
|
|
828
|
+
/** latest by default; pinned to this CLI version only when it is newer than published latest. */
|
|
829
|
+
export function selectInitialMcpVersion(
|
|
830
|
+
lookupLatest: (packageName: string) => string | null = lookupPublishedLatestVersion
|
|
831
|
+
): McpPinDecision {
|
|
832
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
833
|
+
if (version === MCP_LATEST) return { version: MCP_LATEST, reason: "SwallowKit package metadata is unavailable." };
|
|
834
|
+
return resolveMcpPinVersion(version, lookupLatest(name));
|
|
835
|
+
}
|
|
836
|
+
|
|
827
837
|
async function addSwallowKitFiles(
|
|
828
838
|
projectDir: string,
|
|
829
839
|
options: InitOptions,
|
|
@@ -1204,7 +1214,7 @@ export async function register() {
|
|
|
1204
1214
|
createAiAgentFiles(projectDir, projectName, backendLanguage, pm, frontend);
|
|
1205
1215
|
}
|
|
1206
1216
|
|
|
1207
|
-
async function createSharedPackage(projectDir: string, projectName: string) {
|
|
1217
|
+
export async function createSharedPackage(projectDir: string, projectName: string) {
|
|
1208
1218
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
1209
1219
|
|
|
1210
1220
|
const sharedDir = path.join(projectDir, 'shared');
|
|
@@ -1244,7 +1254,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
1244
1254
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
1245
1255
|
fs.writeFileSync(
|
|
1246
1256
|
path.join(sharedDir, 'index.ts'),
|
|
1247
|
-
`// 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`
|
|
1248
1258
|
);
|
|
1249
1259
|
|
|
1250
1260
|
// shared/.gitignore
|
|
@@ -1256,7 +1266,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
1256
1266
|
console.log('✅ Shared package created\n');
|
|
1257
1267
|
}
|
|
1258
1268
|
|
|
1259
|
-
async function createAzureFunctionsProject(
|
|
1269
|
+
export async function createAzureFunctionsProject(
|
|
1260
1270
|
projectDir: string,
|
|
1261
1271
|
pm: PackageManager = 'pnpm',
|
|
1262
1272
|
backendLanguage: BackendLanguage = 'typescript'
|
|
@@ -1939,11 +1949,16 @@ function createAiAgentFiles(
|
|
|
1939
1949
|
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
|
|
1940
1950
|
console.log(' ✅ .github/instructions/ (path-specific instructions)');
|
|
1941
1951
|
|
|
1952
|
+
const mcpVersion = selectInitialMcpVersion();
|
|
1942
1953
|
fs.writeFileSync(
|
|
1943
1954
|
path.join(projectDir, '.mcp.json'),
|
|
1944
|
-
buildSwallowKitMcpProjectConfigSource(pm)
|
|
1955
|
+
buildSwallowKitMcpProjectConfigSource(pm, mcpVersion.version)
|
|
1956
|
+
);
|
|
1957
|
+
console.log(
|
|
1958
|
+
mcpVersion.version === MCP_LATEST
|
|
1959
|
+
? ' ✅ .mcp.json (project-scoped MCP bootstrap, resolves latest)'
|
|
1960
|
+
: ` ✅ .mcp.json (project-scoped MCP bootstrap, pinned to ${mcpVersion.version}: ${mcpVersion.reason} Run "${runCmd} swallowkit configure-mcp" to re-select later.)`
|
|
1945
1961
|
);
|
|
1946
|
-
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1947
1962
|
|
|
1948
1963
|
writeWorkflowDocs(projectDir, runCmd);
|
|
1949
1964
|
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { ensureSwallowKitProject } from "../../core/config";
|
|
3
3
|
import { initializeLegacyMigrations } from "../../core/operations/migration-generator";
|
|
4
|
+
import { addModelContainerMigration } from "../../core/operations/model-migration";
|
|
4
5
|
|
|
5
6
|
export const migrationsCommand = new Command("migrations")
|
|
6
7
|
.description("Manage versioned Azure infrastructure migrations");
|
|
@@ -18,3 +19,24 @@ migrationsCommand
|
|
|
18
19
|
console.log(`Migration baseline 0 created for project ${manifest.projectId}.`);
|
|
19
20
|
console.log("Commit infra/migrations/manifest.json, then run provision with --adopt-existing --baseline 0.");
|
|
20
21
|
});
|
|
22
|
+
|
|
23
|
+
migrationsCommand
|
|
24
|
+
.command("add-model")
|
|
25
|
+
.description("Generate only the Cosmos DB container migration for a model (e.g. after scaffold --skip-infra)")
|
|
26
|
+
.argument("<model>", "Model file or model name")
|
|
27
|
+
.option("--dry-run", "Show the planned infra changes without writing anything", false)
|
|
28
|
+
.action(async (model: string, options: { dryRun?: boolean }) => {
|
|
29
|
+
ensureSwallowKitProject("migrations add-model");
|
|
30
|
+
const result = await addModelContainerMigration(model, { dryRun: options.dryRun });
|
|
31
|
+
for (const operation of result.operations) {
|
|
32
|
+
if (operation.action !== "skip") console.log(` ${operation.action.padEnd(7)} ${operation.path}`);
|
|
33
|
+
}
|
|
34
|
+
if (!result.migration) {
|
|
35
|
+
console.log(`No migration needed for ${result.model}: ${result.reason}`);
|
|
36
|
+
} else if (result.operations.every((operation) => operation.action === "skip")) {
|
|
37
|
+
console.log(`Migration ${result.migration.version} for ${result.model} already exists: ${result.migration.template}`);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`${result.dryRun ? "Would create" : "Created"} migration ${result.migration.version} for ${result.model}: ${result.migration.template}`);
|
|
40
|
+
if (!result.dryRun) console.log("Commit the migration, then run provision to plan and apply it.");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import prompts from "prompts";
|
|
3
3
|
import { ensureSwallowKitProject } from "../../core/config";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
INTERACTIVE_PROVISION_RETRY_HINT,
|
|
6
|
+
ProvisionPlanData,
|
|
7
|
+
executeProvisionPlan,
|
|
8
|
+
planProvisionOperation,
|
|
9
|
+
} from "../../core/operations/provision-operations";
|
|
10
|
+
|
|
11
|
+
export function describeProvisionAction(plan: Pick<ProvisionPlanData, "mode" | "resourceGroup" | "location" | "pendingMigrations">): string {
|
|
12
|
+
switch (plan.mode) {
|
|
13
|
+
case "bootstrap":
|
|
14
|
+
return `Resource group ${plan.resourceGroup} does not exist yet. It will be created in ${plan.location} and the infrastructure in infra/main.bicep will be deployed.`;
|
|
15
|
+
case "migrate":
|
|
16
|
+
return `${plan.pendingMigrations.length} infrastructure migration(s) will be deployed to ${plan.resourceGroup}: ${plan.pendingMigrations.map((migration) => `${migration.version}-${migration.slug}`).join(", ")}.`;
|
|
17
|
+
case "adopt":
|
|
18
|
+
return `The existing resource group ${plan.resourceGroup} will be recorded as SwallowKit baseline 0. Nothing will be deployed.`;
|
|
19
|
+
case "reconcile":
|
|
20
|
+
return `infra/main.bicep will be redeployed to ${plan.resourceGroup}. Template-managed settings may be reset.`;
|
|
21
|
+
default:
|
|
22
|
+
return `Resource group ${plan.resourceGroup}:`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
const PRIMARY_REGIONS = [
|
|
7
27
|
{ title: "Japan East (japaneast)", value: "japaneast" },
|
|
@@ -70,16 +90,17 @@ export const provisionCommand = new Command("provision")
|
|
|
70
90
|
.option("--subscription <id>", "Azure subscription ID")
|
|
71
91
|
.option("--location <region>", "Primary location for Functions and Cosmos DB")
|
|
72
92
|
.option("--swa-location <region>", "Static Web App location")
|
|
73
|
-
.option("--what-if", "Include Azure what-if evidence", false)
|
|
93
|
+
.option("--what-if", "Include Azure what-if evidence in the preview", false)
|
|
74
94
|
.option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
|
|
75
95
|
.option("--baseline <version>", "Baseline version for adoption (currently 0)")
|
|
76
96
|
.option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
|
|
77
|
-
.option("--approve", "
|
|
97
|
+
.option("--approve", "Run the displayed Azure commands without the confirmation prompt", false)
|
|
78
98
|
.action(async (options: ProvisionCliOptions) => {
|
|
79
99
|
ensureSwallowKitProject("provision");
|
|
80
100
|
const locations = await resolveLocations(options);
|
|
81
101
|
const baseline = options.baseline === undefined ? undefined : Number(options.baseline);
|
|
82
|
-
|
|
102
|
+
// Preview in memory: the same Azure checks as the agent workflow, but no saved plan file.
|
|
103
|
+
const preview = await planProvisionOperation({
|
|
83
104
|
resourceGroup: options.resourceGroup,
|
|
84
105
|
subscription: options.subscription,
|
|
85
106
|
location: locations.location,
|
|
@@ -88,37 +109,52 @@ export const provisionCommand = new Command("provision")
|
|
|
88
109
|
adoptExisting: options.adoptExisting,
|
|
89
110
|
baseline,
|
|
90
111
|
reconcile: options.reconcile,
|
|
91
|
-
});
|
|
112
|
+
}, { persist: false });
|
|
92
113
|
|
|
93
|
-
console.
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
console.log(
|
|
114
|
+
for (const warning of preview.warnings) console.warn(`Warning: ${warning}`);
|
|
115
|
+
|
|
116
|
+
if (preview.mode === "offline") {
|
|
117
|
+
console.log([
|
|
118
|
+
"",
|
|
119
|
+
"❌ Azure CLI is unavailable, so the current state of Azure could not be checked. Nothing was changed in Azure.",
|
|
120
|
+
" 1. Make sure `az --version` works in this terminal (install: https://aka.ms/azure-cli).",
|
|
121
|
+
" 2. Sign in with `az login` (and `az account set --subscription <id>` if needed).",
|
|
122
|
+
" 3. Run this command again.",
|
|
123
|
+
].join("\n"));
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
97
126
|
}
|
|
98
|
-
for (const command of plan.commands) console.log(` ${command}`);
|
|
99
127
|
|
|
100
|
-
if (
|
|
128
|
+
if (preview.mode === "adoption-required") {
|
|
101
129
|
throw new Error('Existing resource group requires: migrations init --legacy-baseline, then provision --adopt-existing --baseline 0 --what-if.');
|
|
102
130
|
}
|
|
103
131
|
|
|
132
|
+
if (preview.mode === "noop") {
|
|
133
|
+
console.log(`Resource group ${preview.resourceGroup} is already up to date. Nothing to do.`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`\n${describeProvisionAction(preview)}`);
|
|
138
|
+
console.log("The following Azure CLI commands will run:");
|
|
139
|
+
for (const command of preview.commands) console.log(` ${command}`);
|
|
140
|
+
|
|
104
141
|
let approved = options.approve === true;
|
|
105
142
|
if (!approved) {
|
|
106
143
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
107
|
-
throw new Error("Non-interactive provision requires --approve after reviewing the
|
|
144
|
+
throw new Error("Non-interactive provision requires --approve after reviewing the commands above.");
|
|
108
145
|
}
|
|
109
146
|
const answer = await prompts({
|
|
110
147
|
type: "confirm",
|
|
111
148
|
name: "approved",
|
|
112
|
-
message:
|
|
149
|
+
message: "Run these Azure commands? This may create billable resources.",
|
|
113
150
|
initial: false,
|
|
114
151
|
});
|
|
115
152
|
approved = answer.approved === true;
|
|
116
153
|
}
|
|
117
154
|
if (!approved) {
|
|
118
|
-
console.log("Provision cancelled.");
|
|
155
|
+
console.log("Provision cancelled. Nothing was changed in Azure.");
|
|
119
156
|
return;
|
|
120
157
|
}
|
|
121
158
|
|
|
122
|
-
const result = await
|
|
123
|
-
console.log(
|
|
124
|
-
});
|
|
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.`); });
|
|
@@ -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
|
|
|
@@ -62,9 +63,35 @@ interface ScaffoldOptions {
|
|
|
62
63
|
functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
|
|
63
64
|
apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
|
|
64
65
|
apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
|
|
66
|
+
skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
|
|
65
67
|
expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
export function buildInfraSkippedWarning(modelName: string, modelArgument: string): string {
|
|
71
|
+
return `infra-skipped: The Cosmos DB container migration for ${modelName} was not generated (--skip-infra); infra/ was left unchanged. ` +
|
|
72
|
+
`Generate it later with "swallowkit migrations add-model ${modelArgument}", or re-plan scaffold without --skip-infra.`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const USER_SCOPE_FIELD = /^(user|owner|learner|member|account|customer|student|author|creator|profile|person|employee|patient)Id$/i;
|
|
76
|
+
|
|
77
|
+
function isAuthenticationConfigured(authConfig: AuthConfig | undefined): boolean {
|
|
78
|
+
if (!authConfig || authConfig.provider === "none") return false;
|
|
79
|
+
return Boolean(authConfig.provider) || Object.keys(authConfig.schemes ?? {}).length > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Warn when a model looks per-user (partition key or owner-like id field) but no authentication is configured. */
|
|
83
|
+
export function buildUserScopedDataWarning(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): string | undefined {
|
|
84
|
+
if (isAuthenticationConfigured(authConfig)) return undefined;
|
|
85
|
+
const partitionField = modelInfo.partitionKey.replace(/^\//, "");
|
|
86
|
+
const fieldNames = modelInfo.fields.filter((field) => field.type === "string" && !field.isArray).map((field) => field.name);
|
|
87
|
+
const ownerField = [partitionField, ...fieldNames].find((name) => USER_SCOPE_FIELD.test(name) && fieldNames.includes(name));
|
|
88
|
+
if (!ownerField) return undefined;
|
|
89
|
+
const reason = ownerField === partitionField ? `is the partition key and looks like a per-user owner id` : `looks like a per-user owner id`;
|
|
90
|
+
return `user-scoped-data-without-auth: ${modelInfo.name}.${ownerField} ${reason}, but authentication is not configured. ` +
|
|
91
|
+
`Generated CRUD does not scope data to a principal, so any caller can read and write every ${modelInfo.name} document. ` +
|
|
92
|
+
`Add authentication with "plan auth", restrict or remove the generated write endpoints, and add ai-authored endpoints that derive ${ownerField} from the verified principal instead of the request body.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
69
96
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
70
97
|
}
|
|
@@ -132,6 +159,17 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
132
159
|
session.setSchemaAnalysis(schemaAnalysis);
|
|
133
160
|
console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
|
|
134
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
|
+
}
|
|
167
|
+
const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
|
|
168
|
+
if (userScopeWarning) {
|
|
169
|
+
session.addWarning(userScopeWarning);
|
|
170
|
+
console.warn(`⚠️ ${userScopeWarning}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
const backendLanguage = getBackendLanguage();
|
|
136
174
|
const frontend = getFrontendFramework();
|
|
137
175
|
const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
|
|
@@ -185,6 +223,8 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
185
223
|
console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
|
|
186
224
|
}
|
|
187
225
|
|
|
226
|
+
updateSharedIndex(modelPath, modelInfo.schemaName, session);
|
|
227
|
+
|
|
188
228
|
// 5. Generate BFF callFunction helper
|
|
189
229
|
await generateCallFunctionHelper(session, apiDir);
|
|
190
230
|
|
|
@@ -210,9 +250,15 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
210
250
|
await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
|
|
211
251
|
}
|
|
212
252
|
|
|
213
|
-
// 8. Generate Cosmos DB container Bicep file (skip for connector models)
|
|
253
|
+
// 8. Generate Cosmos DB container Bicep file (skip for connector models or when infra changes are excluded)
|
|
214
254
|
if (!isConnectorModel) {
|
|
215
|
-
|
|
255
|
+
if (options.skipInfra) {
|
|
256
|
+
const warning = buildInfraSkippedWarning(modelInfo.name, toKebabCase(modelInfo.name));
|
|
257
|
+
session.addWarning(warning);
|
|
258
|
+
console.warn(`\n⚠️ ${warning}`);
|
|
259
|
+
} else {
|
|
260
|
+
await generateCosmosContainer(modelInfo, session);
|
|
261
|
+
}
|
|
216
262
|
}
|
|
217
263
|
|
|
218
264
|
// 9. Generate UI components (unless --api-only)
|
|
@@ -294,10 +340,43 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: Fil
|
|
|
294
340
|
export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
295
341
|
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
296
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
|
+
}
|
|
297
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
|
+
}
|
|
298
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))) {
|
|
299
377
|
if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
|
|
300
378
|
}
|
|
379
|
+
if (usesNamedPolicy) assertNamedSchemesConfigured(modelInfo.name, "authPolicy", authConfig);
|
|
301
380
|
return modelInfo.authPolicy;
|
|
302
381
|
}
|
|
303
382
|
|
|
@@ -312,12 +391,35 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
|
|
|
312
391
|
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
313
392
|
}
|
|
314
393
|
|
|
315
|
-
if (defaultPolicy !== 'anonymous')
|
|
394
|
+
if (defaultPolicy !== 'anonymous') {
|
|
395
|
+
assertNamedSchemesConfigured(modelInfo.name, `auth.authorization.defaultPolicy '${defaultPolicy}'`, authConfig);
|
|
396
|
+
return { policy: defaultPolicy };
|
|
397
|
+
}
|
|
316
398
|
|
|
317
399
|
// defaultPolicy が 'anonymous' なら認証不要
|
|
318
400
|
return undefined;
|
|
319
401
|
}
|
|
320
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
|
+
|
|
321
423
|
/**
|
|
322
424
|
* モデルファイルのパスを解決
|
|
323
425
|
*/
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelC
|
|
|
8
8
|
import { provisionCommand } from "./commands/provision";
|
|
9
9
|
import { createMigrationCommand } from "./commands/create-migration";
|
|
10
10
|
import { migrationsCommand } from "./commands/migrations";
|
|
11
|
+
import { configureMcpCommand } from "./commands/configure-mcp";
|
|
11
12
|
import { addConnectorCommand } from "./commands/add-connector";
|
|
12
13
|
import { addAuthCommand } from "./commands/add-auth";
|
|
13
14
|
import { planScaffoldOperation } from "../core/operations/scaffold-plan";
|
|
@@ -15,6 +16,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
|
|
|
15
16
|
import { detectDrift } from "../core/project/drift";
|
|
16
17
|
import { runVerify } from "../core/verify";
|
|
17
18
|
import { isMachineCommand, runMachineCli } from "../machine";
|
|
19
|
+
import { MachineCommandError } from "../machine/errors";
|
|
18
20
|
|
|
19
21
|
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
20
22
|
export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
|
|
@@ -140,6 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
140
142
|
program.addCommand(provisionCommand);
|
|
141
143
|
program.addCommand(migrationsCommand);
|
|
142
144
|
program.addCommand(createMigrationCommand);
|
|
145
|
+
program.addCommand(configureMcpCommand);
|
|
143
146
|
|
|
144
147
|
program
|
|
145
148
|
.command("create-model <names...>")
|
|
@@ -160,6 +163,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
160
163
|
.option("--functions-dir <dir>", "Azure Functions directory", "functions")
|
|
161
164
|
.option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
|
|
162
165
|
.option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
|
|
166
|
+
.option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
|
|
163
167
|
.option("--dry-run", "Show planned file changes without writing anything", false)
|
|
164
168
|
.action(async (model, options) => {
|
|
165
169
|
if (options.dryRun) {
|
|
@@ -168,6 +172,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
168
172
|
functionsDir: options.functionsDir,
|
|
169
173
|
apiDir: options.apiDir,
|
|
170
174
|
apiOnly: options.apiOnly,
|
|
175
|
+
skipInfra: options.skipInfra,
|
|
171
176
|
});
|
|
172
177
|
return;
|
|
173
178
|
}
|
|
@@ -177,6 +182,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
177
182
|
functionsDir: options.functionsDir,
|
|
178
183
|
apiDir: options.apiDir,
|
|
179
184
|
apiOnly: options.apiOnly,
|
|
185
|
+
skipInfra: options.skipInfra,
|
|
180
186
|
});
|
|
181
187
|
});
|
|
182
188
|
|
|
@@ -217,8 +223,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
217
223
|
|
|
218
224
|
program
|
|
219
225
|
.command("verify")
|
|
220
|
-
.description("Run verification checks (structure, drift, typecheck)")
|
|
221
|
-
.option("--checks <ids>", "Comma-separated check ids
|
|
226
|
+
.description("Run verification checks (structure, drift, typecheck, backend-build)")
|
|
227
|
+
.option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
|
|
222
228
|
.action(async (options) => {
|
|
223
229
|
await runVerifyCommand({ checks: options.checks });
|
|
224
230
|
});
|
|
@@ -226,7 +232,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
226
232
|
return program;
|
|
227
233
|
}
|
|
228
234
|
|
|
229
|
-
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
|
|
235
|
+
async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
|
|
230
236
|
console.log("🔍 Scaffold dry-run — no files will be written.\n");
|
|
231
237
|
|
|
232
238
|
const plan = await planScaffoldOperation(options);
|
|
@@ -264,7 +270,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
|
|
|
264
270
|
console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
|
|
265
271
|
console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
|
|
266
272
|
} else {
|
|
267
|
-
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
|
|
273
|
+
console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -351,9 +357,26 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
351
357
|
await createProgram().parseAsync(normalizeDevCommandArgv(argv));
|
|
352
358
|
}
|
|
353
359
|
|
|
360
|
+
/** Human-readable error output for regular CLI commands (machine commands print JSON). */
|
|
361
|
+
export function formatCliError(error: unknown): string {
|
|
362
|
+
if (error instanceof MachineCommandError) {
|
|
363
|
+
const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
|
|
364
|
+
const details = error.details as Record<string, unknown> | undefined;
|
|
365
|
+
if (details && typeof details === "object") {
|
|
366
|
+
if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
|
|
367
|
+
if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
return lines.join("\n");
|
|
370
|
+
}
|
|
371
|
+
if (error instanceof Error) {
|
|
372
|
+
return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
|
|
373
|
+
}
|
|
374
|
+
return `❌ ${String(error)}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
354
377
|
if (require.main === module) {
|
|
355
378
|
void runCli().catch((error) => {
|
|
356
|
-
console.error(error);
|
|
379
|
+
console.error(formatCliError(error));
|
|
357
380
|
process.exitCode = 1;
|
|
358
381
|
});
|
|
359
382
|
}
|
|
@@ -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
|
}
|