swallowkit 1.0.0-beta.9 → 1.0.0
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/README.ja.md +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +366 -2
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +851 -786
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { ensureSwallowKitProject, loadConfig } from "../config";
|
|
3
|
+
import { toPascalCase } from "../scaffold/model-parser";
|
|
4
|
+
import { syncProjectManifest } from "../project/manifest";
|
|
5
|
+
import { recordSessionOperations } from "../project/artifacts";
|
|
6
|
+
import { FileOperationSession, getActiveFileSession } from "./file-session";
|
|
7
|
+
|
|
8
|
+
const CREATE_MODEL_GENERATOR = "create-model";
|
|
9
|
+
|
|
10
|
+
export interface CreateModelOperationOptions {
|
|
11
|
+
names: string[];
|
|
12
|
+
modelsDir?: string;
|
|
13
|
+
connector?: string;
|
|
14
|
+
overwriteMode?: "prompt" | "always" | "never";
|
|
15
|
+
confirmOverwrite?: (filePath: string) => Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateModelOperationResult {
|
|
19
|
+
createdFiles: string[];
|
|
20
|
+
skippedFiles: string[];
|
|
21
|
+
updatedIndex: boolean;
|
|
22
|
+
connectorType?: "rdb" | "api";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateModelTemplate(modelName: string): string {
|
|
26
|
+
const pascalName = toPascalCase(modelName);
|
|
27
|
+
|
|
28
|
+
return `import { z } from 'zod/v4';
|
|
29
|
+
|
|
30
|
+
// ${pascalName} model (Zod official pattern: same name for value and type)
|
|
31
|
+
export const ${pascalName} = z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
name: z.string().min(1),
|
|
34
|
+
createdAt: z.string().optional(),
|
|
35
|
+
updatedAt: z.string().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}>;
|
|
39
|
+
|
|
40
|
+
// Display name for UI
|
|
41
|
+
export const displayName = '${pascalName}';
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generateConnectorModelTemplate(modelName: string, connectorName: string, connectorType: "rdb" | "api"): string {
|
|
46
|
+
const pascalName = toPascalCase(modelName);
|
|
47
|
+
const kebabName = modelName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
48
|
+
const pluralName = kebabName.endsWith("s") ? kebabName : `${kebabName}s`;
|
|
49
|
+
|
|
50
|
+
const schema = `import { z } from 'zod/v4';
|
|
51
|
+
|
|
52
|
+
// ${pascalName} model (Zod official pattern: same name for value and type)
|
|
53
|
+
export const ${pascalName} = z.object({
|
|
54
|
+
id: z.string(),
|
|
55
|
+
name: z.string().min(1),
|
|
56
|
+
createdAt: z.string().optional(),
|
|
57
|
+
updatedAt: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}>;
|
|
61
|
+
|
|
62
|
+
// Display name for UI
|
|
63
|
+
export const displayName = '${pascalName}';
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
if (connectorType === "rdb") {
|
|
67
|
+
return `${schema}
|
|
68
|
+
// SwallowKit Connector Metadata
|
|
69
|
+
export const connectorConfig = {
|
|
70
|
+
connector: '${connectorName}',
|
|
71
|
+
operations: ['getAll', 'getById'] as const,
|
|
72
|
+
table: '${pluralName}',
|
|
73
|
+
idColumn: 'id',
|
|
74
|
+
};
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return `${schema}
|
|
79
|
+
// SwallowKit Connector Metadata
|
|
80
|
+
export const connectorConfig = {
|
|
81
|
+
connector: '${connectorName}',
|
|
82
|
+
operations: ['getAll', 'getById', 'create', 'update'] as const,
|
|
83
|
+
endpoints: {
|
|
84
|
+
getAll: 'GET /${pluralName}',
|
|
85
|
+
getById: 'GET /${pluralName}/{id}',
|
|
86
|
+
create: 'POST /${pluralName}',
|
|
87
|
+
update: 'PATCH /${pluralName}/{id}',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function updateSharedIndex(kebabName: string, pascalName: string, session: FileOperationSession): boolean {
|
|
94
|
+
const indexPath = path.join(process.cwd(), "shared", "index.ts");
|
|
95
|
+
|
|
96
|
+
if (!session.fileExists(indexPath)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = session.readFile(indexPath);
|
|
101
|
+
const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
|
|
102
|
+
|
|
103
|
+
if (content.includes(exportLine)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
session.writeFile(indexPath, `${content}${exportLine}\n`, {
|
|
108
|
+
ownership: "extension-point",
|
|
109
|
+
generator: CREATE_MODEL_GENERATOR,
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function createModelOperation(options: CreateModelOperationOptions): Promise<CreateModelOperationResult> {
|
|
115
|
+
ensureSwallowKitProject("create-model");
|
|
116
|
+
|
|
117
|
+
let connectorType: "rdb" | "api" | undefined;
|
|
118
|
+
if (options.connector) {
|
|
119
|
+
const config = loadConfig();
|
|
120
|
+
const connectorDefinition = config.connectors?.[options.connector];
|
|
121
|
+
if (!connectorDefinition) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Connector '${options.connector}' not found in swallowkit.config.js. ` +
|
|
124
|
+
`Run 'swallowkit add-connector ${options.connector} --type=<rdb|api>' first.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
connectorType = connectorDefinition.type;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const modelsDir = options.modelsDir || "shared/models";
|
|
132
|
+
|
|
133
|
+
const session = getActiveFileSession() ?? new FileOperationSession("commit");
|
|
134
|
+
// jest 等の環境で path.resolve の内部 cwd キャッシュが process.chdir と食い違うことが
|
|
135
|
+
// あるため、cwd を明示的に固定して絶対パスを組み立てる。
|
|
136
|
+
const projectRoot = process.cwd();
|
|
137
|
+
const resolveFromRoot = (relativePath: string) =>
|
|
138
|
+
path.isAbsolute(relativePath) ? relativePath : path.join(projectRoot, relativePath);
|
|
139
|
+
|
|
140
|
+
const createdFiles: string[] = [];
|
|
141
|
+
const skippedFiles: string[] = [];
|
|
142
|
+
let updatedIndex = false;
|
|
143
|
+
|
|
144
|
+
for (const name of options.names) {
|
|
145
|
+
const kebabName = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
146
|
+
const filePath = path.join(modelsDir, `${kebabName}.ts`);
|
|
147
|
+
const pascalName = toPascalCase(name);
|
|
148
|
+
|
|
149
|
+
if (session.fileExists(resolveFromRoot(filePath))) {
|
|
150
|
+
const overwriteMode = options.overwriteMode || "prompt";
|
|
151
|
+
const shouldOverwrite = overwriteMode === "always"
|
|
152
|
+
? true
|
|
153
|
+
: overwriteMode === "never"
|
|
154
|
+
? false
|
|
155
|
+
: options.confirmOverwrite
|
|
156
|
+
? await options.confirmOverwrite(filePath)
|
|
157
|
+
: false;
|
|
158
|
+
|
|
159
|
+
if (!shouldOverwrite) {
|
|
160
|
+
skippedFiles.push(filePath.replace(/\\/g, "/"));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = options.connector && connectorType
|
|
166
|
+
? generateConnectorModelTemplate(name, options.connector, connectorType)
|
|
167
|
+
: generateModelTemplate(name);
|
|
168
|
+
session.writeFile(resolveFromRoot(filePath), content, {
|
|
169
|
+
ownership: "user-owned",
|
|
170
|
+
generator: CREATE_MODEL_GENERATOR,
|
|
171
|
+
sourceModel: pascalName,
|
|
172
|
+
});
|
|
173
|
+
createdFiles.push(filePath.replace(/\\/g, "/"));
|
|
174
|
+
|
|
175
|
+
if (updateSharedIndex(kebabName, pascalName, session)) {
|
|
176
|
+
updatedIndex = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (session.mode === "commit") {
|
|
181
|
+
recordSessionOperations(session.operations, {});
|
|
182
|
+
await syncProjectManifest();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
createdFiles,
|
|
187
|
+
skippedFiles,
|
|
188
|
+
updatedIndex,
|
|
189
|
+
...(connectorType ? { connectorType } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileOperationSession — SwallowKit generator 由来のファイル書き込みを一元管理する抽象。
|
|
3
|
+
*
|
|
4
|
+
* - mode "commit": 従来どおりディスクへ書き込みつつ、操作を記録する。
|
|
5
|
+
* - mode "collect": ディスクへは一切書き込まず、仮想オーバーレイ上で操作を収集する(Plan / dry-run 用)。
|
|
6
|
+
*
|
|
7
|
+
* すべての生成物には Artifact Ownership を付与し、Plan/Apply/Drift 検出の基礎データとする。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as crypto from "crypto";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
export type FileSessionMode = "commit" | "collect";
|
|
15
|
+
|
|
16
|
+
export type ArtifactOwnership =
|
|
17
|
+
| "managed"
|
|
18
|
+
| "generated-once"
|
|
19
|
+
| "user-owned"
|
|
20
|
+
| "extension-point"
|
|
21
|
+
| "metadata";
|
|
22
|
+
|
|
23
|
+
export type FileSessionActionKind = "create" | "modify" | "append" | "delete" | "skip";
|
|
24
|
+
|
|
25
|
+
export interface FileWriteMeta {
|
|
26
|
+
ownership: ArtifactOwnership;
|
|
27
|
+
generator: string;
|
|
28
|
+
sourceModel?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RecordedFileOperation {
|
|
32
|
+
/** Project-relative POSIX path */
|
|
33
|
+
path: string;
|
|
34
|
+
absolutePath: string;
|
|
35
|
+
action: FileSessionActionKind;
|
|
36
|
+
ownership: ArtifactOwnership;
|
|
37
|
+
generator: string;
|
|
38
|
+
sourceModel?: string;
|
|
39
|
+
existedBefore: boolean;
|
|
40
|
+
/** sha256 (LF normalized) of on-disk content before the session touched the file */
|
|
41
|
+
previousHash: string | null;
|
|
42
|
+
/** sha256 (LF normalized) of the new content; null for delete */
|
|
43
|
+
newHash: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeContentForHash(content: string): string {
|
|
47
|
+
return content.replace(/\r\n/g, "\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hashContent(content: string): string {
|
|
51
|
+
return crypto.createHash("sha256").update(normalizeContentForHash(content), "utf-8").digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hashFileIfExists(absolutePath: string): string | null {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return hashContent(fs.readFileSync(absolutePath, "utf-8"));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toProjectRelativePath(rootDirectory: string, absolutePath: string): string {
|
|
66
|
+
const relative = path.relative(rootDirectory, absolutePath);
|
|
67
|
+
return relative && !relative.startsWith("..")
|
|
68
|
+
? relative.replace(/\\/g, "/")
|
|
69
|
+
: absolutePath.replace(/\\/g, "/");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class FileOperationSession {
|
|
73
|
+
readonly mode: FileSessionMode;
|
|
74
|
+
readonly rootDirectory: string;
|
|
75
|
+
readonly warnings: string[] = [];
|
|
76
|
+
|
|
77
|
+
private readonly operationsByPath = new Map<string, RecordedFileOperation>();
|
|
78
|
+
private readonly inputFingerprintsByPath = new Map<string, string | null>();
|
|
79
|
+
/** overlay: absolute normalized path -> content (null = deleted in session) */
|
|
80
|
+
private readonly overlay = new Map<string, string | null>();
|
|
81
|
+
|
|
82
|
+
constructor(mode: FileSessionMode, rootDirectory: string = process.cwd()) {
|
|
83
|
+
this.mode = mode;
|
|
84
|
+
this.rootDirectory = rootDirectory;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get operations(): RecordedFileOperation[] {
|
|
88
|
+
return Array.from(this.operationsByPath.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get inputFingerprints(): Record<string, string | null> {
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
Array.from(this.inputFingerprintsByPath.entries()).sort(([left], [right]) => left.localeCompare(right))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
addWarning(message: string): void {
|
|
98
|
+
if (!this.warnings.includes(message)) {
|
|
99
|
+
this.warnings.push(message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
registerInput(absolutePath: string): void {
|
|
104
|
+
const relativePath = toProjectRelativePath(this.rootDirectory, absolutePath);
|
|
105
|
+
this.inputFingerprintsByPath.set(relativePath, hashFileIfExists(absolutePath));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private overlayKey(absolutePath: string): string {
|
|
109
|
+
return path.resolve(absolutePath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fileExists(absolutePath: string): boolean {
|
|
113
|
+
const key = this.overlayKey(absolutePath);
|
|
114
|
+
if (this.overlay.has(key)) {
|
|
115
|
+
return this.overlay.get(key) !== null;
|
|
116
|
+
}
|
|
117
|
+
return fs.existsSync(absolutePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
readFile(absolutePath: string): string {
|
|
121
|
+
const key = this.overlayKey(absolutePath);
|
|
122
|
+
if (this.overlay.has(key)) {
|
|
123
|
+
const content = this.overlay.get(key);
|
|
124
|
+
if (content === null || content === undefined) {
|
|
125
|
+
throw new Error(`File was deleted in this session: ${absolutePath}`);
|
|
126
|
+
}
|
|
127
|
+
return content;
|
|
128
|
+
}
|
|
129
|
+
return fs.readFileSync(absolutePath, "utf-8");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
writeFile(absolutePath: string, content: string, meta: FileWriteMeta): RecordedFileOperation {
|
|
133
|
+
const key = this.overlayKey(absolutePath);
|
|
134
|
+
const relativePath = toProjectRelativePath(this.rootDirectory, absolutePath);
|
|
135
|
+
const existing = this.operationsByPath.get(relativePath);
|
|
136
|
+
|
|
137
|
+
const existedBefore = existing ? existing.existedBefore : fs.existsSync(absolutePath);
|
|
138
|
+
const previousHash = existing ? existing.previousHash : hashFileIfExists(absolutePath);
|
|
139
|
+
const newHash = hashContent(content);
|
|
140
|
+
|
|
141
|
+
let action: FileSessionActionKind;
|
|
142
|
+
if (!existedBefore) {
|
|
143
|
+
action = "create";
|
|
144
|
+
} else if (newHash === previousHash) {
|
|
145
|
+
action = "skip";
|
|
146
|
+
} else if (meta.ownership === "extension-point") {
|
|
147
|
+
action = "append";
|
|
148
|
+
} else {
|
|
149
|
+
action = "modify";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const operation: RecordedFileOperation = {
|
|
153
|
+
path: relativePath,
|
|
154
|
+
absolutePath,
|
|
155
|
+
action,
|
|
156
|
+
ownership: meta.ownership,
|
|
157
|
+
generator: meta.generator,
|
|
158
|
+
sourceModel: meta.sourceModel ?? existing?.sourceModel,
|
|
159
|
+
existedBefore,
|
|
160
|
+
previousHash,
|
|
161
|
+
newHash,
|
|
162
|
+
};
|
|
163
|
+
this.operationsByPath.set(relativePath, operation);
|
|
164
|
+
this.overlay.set(key, content);
|
|
165
|
+
|
|
166
|
+
if (this.mode === "commit" && action !== "skip") {
|
|
167
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
168
|
+
fs.writeFileSync(absolutePath, content, "utf-8");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return operation;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
deleteFile(absolutePath: string, meta: FileWriteMeta): RecordedFileOperation | null {
|
|
175
|
+
if (!this.fileExists(absolutePath)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const key = this.overlayKey(absolutePath);
|
|
180
|
+
const relativePath = toProjectRelativePath(this.rootDirectory, absolutePath);
|
|
181
|
+
const existing = this.operationsByPath.get(relativePath);
|
|
182
|
+
|
|
183
|
+
const operation: RecordedFileOperation = {
|
|
184
|
+
path: relativePath,
|
|
185
|
+
absolutePath,
|
|
186
|
+
action: "delete",
|
|
187
|
+
ownership: meta.ownership,
|
|
188
|
+
generator: meta.generator,
|
|
189
|
+
sourceModel: meta.sourceModel ?? existing?.sourceModel,
|
|
190
|
+
existedBefore: existing ? existing.existedBefore : true,
|
|
191
|
+
previousHash: existing ? existing.previousHash : hashFileIfExists(absolutePath),
|
|
192
|
+
newHash: null,
|
|
193
|
+
};
|
|
194
|
+
this.operationsByPath.set(relativePath, operation);
|
|
195
|
+
this.overlay.set(key, null);
|
|
196
|
+
|
|
197
|
+
if (this.mode === "commit" && fs.existsSync(absolutePath)) {
|
|
198
|
+
fs.unlinkSync(absolutePath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return operation;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let activeSession: FileOperationSession | null = null;
|
|
206
|
+
|
|
207
|
+
export function getActiveFileSession(): FileOperationSession | null {
|
|
208
|
+
return activeSession;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function runWithFileSession<T>(
|
|
212
|
+
session: FileOperationSession,
|
|
213
|
+
action: () => Promise<T>
|
|
214
|
+
): Promise<T> {
|
|
215
|
+
const previous = activeSession;
|
|
216
|
+
activeSession = session;
|
|
217
|
+
try {
|
|
218
|
+
return await action();
|
|
219
|
+
} finally {
|
|
220
|
+
activeSession = previous;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan / Apply Provision — Azure プロビジョニングの事前計画と承認付き実行。
|
|
3
|
+
*
|
|
4
|
+
* - plan provision: ローカルの決定論的プリフライト(Bicep 解析・az CLI 有無)のみ。
|
|
5
|
+
* ネットワークアクセスや az login を必要とする操作は行わない。
|
|
6
|
+
* --what-if を明示した場合のみ az deployment group what-if を実行する。
|
|
7
|
+
* - apply provision: 常に承認(--approve)必須。承認なしでは requires-human で停止する。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as crypto from "crypto";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
import { MachineCommandError } from "../../machine/errors";
|
|
14
|
+
import { getSwallowKitVersion } from "../../version";
|
|
15
|
+
import { inspectInfra, InfraInspection } from "../project/infra";
|
|
16
|
+
import { deletePlanState, loadPlanState, savePlanState } from "../project/state";
|
|
17
|
+
import { hashFileIfExists } from "./file-session";
|
|
18
|
+
|
|
19
|
+
const RESOURCE_GROUP_PATTERN = /^[A-Za-z0-9._()-]{1,90}$/;
|
|
20
|
+
const LOCATION_PATTERN = /^[a-z0-9]{3,30}$/;
|
|
21
|
+
const SUBSCRIPTION_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
22
|
+
|
|
23
|
+
export interface PlanProvisionOptions {
|
|
24
|
+
resourceGroup: string;
|
|
25
|
+
location: string;
|
|
26
|
+
swaLocation: string;
|
|
27
|
+
subscription?: string;
|
|
28
|
+
/** 明示指定時のみ az deployment group what-if を実行(az login 済みが前提) */
|
|
29
|
+
whatIf?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProvisionPlanData {
|
|
33
|
+
planId: string;
|
|
34
|
+
planType: "provision";
|
|
35
|
+
resourceGroup: string;
|
|
36
|
+
location: string;
|
|
37
|
+
swaLocation: string;
|
|
38
|
+
subscription?: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
swallowkitVersion: string;
|
|
41
|
+
azCliAvailable: boolean;
|
|
42
|
+
infra: InfraInspection;
|
|
43
|
+
/** apply 時に実行されるコマンド(レビュー用) */
|
|
44
|
+
commands: string[];
|
|
45
|
+
whatIfResult?: string;
|
|
46
|
+
warnings: string[];
|
|
47
|
+
/** provision は常に人間の承認が必要 */
|
|
48
|
+
requiresApproval: true;
|
|
49
|
+
fingerprints: Record<string, string | null>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateProvisionInputs(options: PlanProvisionOptions): void {
|
|
53
|
+
if (!RESOURCE_GROUP_PATTERN.test(options.resourceGroup)) {
|
|
54
|
+
throw new MachineCommandError(
|
|
55
|
+
"invalid-arguments",
|
|
56
|
+
`Invalid resource group name: ${options.resourceGroup}`,
|
|
57
|
+
undefined,
|
|
58
|
+
"failed"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!LOCATION_PATTERN.test(options.location) || !LOCATION_PATTERN.test(options.swaLocation)) {
|
|
62
|
+
throw new MachineCommandError(
|
|
63
|
+
"invalid-arguments",
|
|
64
|
+
"Locations must be lowercase Azure region names (e.g. japaneast, eastasia).",
|
|
65
|
+
undefined,
|
|
66
|
+
"failed"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (options.subscription && !SUBSCRIPTION_PATTERN.test(options.subscription)) {
|
|
70
|
+
throw new MachineCommandError(
|
|
71
|
+
"invalid-arguments",
|
|
72
|
+
"Subscription must be a GUID.",
|
|
73
|
+
undefined,
|
|
74
|
+
"failed"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isAzCliAvailable(): boolean {
|
|
80
|
+
try {
|
|
81
|
+
const result = spawnSync("az", ["--version"], { shell: true, stdio: "ignore", timeout: 30_000 });
|
|
82
|
+
return result.status === 0;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildProvisionCommands(options: PlanProvisionOptions, mainBicepPath: string, parametersPath: string): string[] {
|
|
89
|
+
const commands: string[] = [];
|
|
90
|
+
if (options.subscription) {
|
|
91
|
+
commands.push(`az account set --subscription ${options.subscription}`);
|
|
92
|
+
}
|
|
93
|
+
commands.push(`az group create --name ${options.resourceGroup} --location ${options.location}`);
|
|
94
|
+
commands.push(
|
|
95
|
+
`az deployment group create --resource-group ${options.resourceGroup} --template-file "${mainBicepPath}" --parameters "${parametersPath}" --parameters location=${options.location} --parameters swaLocation=${options.swaLocation}`
|
|
96
|
+
);
|
|
97
|
+
return commands;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function planProvisionOperation(options: PlanProvisionOptions): Promise<ProvisionPlanData> {
|
|
101
|
+
const projectRoot = process.cwd();
|
|
102
|
+
validateProvisionInputs(options);
|
|
103
|
+
|
|
104
|
+
const infra = inspectInfra(projectRoot);
|
|
105
|
+
const warnings = [...infra.warnings];
|
|
106
|
+
|
|
107
|
+
if (!infra.mainBicep.exists) {
|
|
108
|
+
throw new MachineCommandError(
|
|
109
|
+
"infra-not-found",
|
|
110
|
+
'infra/main.bicep not found. Run "swallowkit init" to generate infrastructure files.',
|
|
111
|
+
undefined,
|
|
112
|
+
"blocked"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (!infra.parametersFile.exists) {
|
|
116
|
+
warnings.push("infra/main.parameters.json not found; deployment will fail until it is created.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const azCliAvailable = isAzCliAvailable();
|
|
120
|
+
if (!azCliAvailable) {
|
|
121
|
+
warnings.push("az-cli-not-found: Azure CLI is not installed. Install it before applying: https://aka.ms/azure-cli");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const mainBicepPath = path.join(projectRoot, "infra", "main.bicep");
|
|
125
|
+
const parametersPath = path.join(projectRoot, "infra", "main.parameters.json");
|
|
126
|
+
const commands = buildProvisionCommands(options, mainBicepPath, parametersPath);
|
|
127
|
+
|
|
128
|
+
let whatIfResult: string | undefined;
|
|
129
|
+
if (options.whatIf) {
|
|
130
|
+
if (!azCliAvailable) {
|
|
131
|
+
throw new MachineCommandError(
|
|
132
|
+
"az-cli-not-found",
|
|
133
|
+
"Azure CLI is required for --what-if. Install it first: https://aka.ms/azure-cli",
|
|
134
|
+
undefined,
|
|
135
|
+
"blocked"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const whatIfArgs = [
|
|
139
|
+
"deployment", "group", "what-if",
|
|
140
|
+
"--resource-group", options.resourceGroup,
|
|
141
|
+
"--template-file", `"${mainBicepPath}"`,
|
|
142
|
+
"--parameters", `"${parametersPath}"`,
|
|
143
|
+
"--parameters", `location=${options.location}`,
|
|
144
|
+
"--parameters", `swaLocation=${options.swaLocation}`,
|
|
145
|
+
"--no-pretty-print",
|
|
146
|
+
];
|
|
147
|
+
const result = spawnSync("az", whatIfArgs, { shell: true, encoding: "utf-8", timeout: 300_000 });
|
|
148
|
+
if (result.status !== 0) {
|
|
149
|
+
throw new MachineCommandError(
|
|
150
|
+
"what-if-failed",
|
|
151
|
+
`az deployment group what-if failed: ${(result.stderr || result.stdout || "").trim().slice(0, 2000)}`,
|
|
152
|
+
undefined,
|
|
153
|
+
"blocked"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
whatIfResult = (result.stdout || "").trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fingerprints: Record<string, string | null> = {
|
|
160
|
+
"infra/main.bicep": hashFileIfExists(mainBicepPath),
|
|
161
|
+
"infra/main.parameters.json": hashFileIfExists(parametersPath),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const planId = crypto
|
|
165
|
+
.createHash("sha256")
|
|
166
|
+
.update(
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
resourceGroup: options.resourceGroup,
|
|
169
|
+
location: options.location,
|
|
170
|
+
swaLocation: options.swaLocation,
|
|
171
|
+
subscription: options.subscription,
|
|
172
|
+
fingerprints,
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
.digest("hex")
|
|
176
|
+
.slice(0, 12);
|
|
177
|
+
|
|
178
|
+
const plan: ProvisionPlanData = {
|
|
179
|
+
planId,
|
|
180
|
+
planType: "provision",
|
|
181
|
+
resourceGroup: options.resourceGroup,
|
|
182
|
+
location: options.location,
|
|
183
|
+
swaLocation: options.swaLocation,
|
|
184
|
+
...(options.subscription ? { subscription: options.subscription } : {}),
|
|
185
|
+
createdAt: new Date().toISOString(),
|
|
186
|
+
swallowkitVersion: getSwallowKitVersion(),
|
|
187
|
+
azCliAvailable,
|
|
188
|
+
infra,
|
|
189
|
+
commands,
|
|
190
|
+
...(whatIfResult !== undefined ? { whatIfResult } : {}),
|
|
191
|
+
warnings,
|
|
192
|
+
requiresApproval: true,
|
|
193
|
+
fingerprints,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
savePlanState(plan, projectRoot);
|
|
197
|
+
return plan;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ApplyProvisionOptions {
|
|
201
|
+
planId: string;
|
|
202
|
+
approve?: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface ApplyProvisionResult {
|
|
206
|
+
planId: string;
|
|
207
|
+
resourceGroup: string;
|
|
208
|
+
location: string;
|
|
209
|
+
swaLocation: string;
|
|
210
|
+
executedCommands: string[];
|
|
211
|
+
deploymentOutputs?: Record<string, unknown>;
|
|
212
|
+
warnings: string[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function applyProvisionOperation(options: ApplyProvisionOptions): Promise<ApplyProvisionResult> {
|
|
216
|
+
const projectRoot = process.cwd();
|
|
217
|
+
|
|
218
|
+
const plan = loadPlanState<ProvisionPlanData>(options.planId, projectRoot);
|
|
219
|
+
if (!plan) {
|
|
220
|
+
throw new MachineCommandError(
|
|
221
|
+
"plan-not-found",
|
|
222
|
+
`Plan "${options.planId}" was not found. Run "plan provision" to create a new plan.`,
|
|
223
|
+
{ planId: options.planId },
|
|
224
|
+
"blocked"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Bicep が plan 後に変更されていないか確認
|
|
229
|
+
const changedFiles: string[] = [];
|
|
230
|
+
for (const [relativePath, expectedHash] of Object.entries(plan.fingerprints)) {
|
|
231
|
+
const currentHash = hashFileIfExists(path.join(projectRoot, relativePath));
|
|
232
|
+
if (currentHash !== expectedHash) changedFiles.push(relativePath);
|
|
233
|
+
}
|
|
234
|
+
if (changedFiles.length > 0) {
|
|
235
|
+
throw new MachineCommandError(
|
|
236
|
+
"stale-plan",
|
|
237
|
+
`Plan "${options.planId}" is stale: ${changedFiles.join(", ")} changed after the plan was created. Re-run "plan provision".`,
|
|
238
|
+
{ planId: options.planId, changedFiles },
|
|
239
|
+
"blocked"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!options.approve) {
|
|
244
|
+
throw new MachineCommandError(
|
|
245
|
+
"approval-required",
|
|
246
|
+
"Provisioning creates billable Azure resources and always requires human approval. Review plan.commands and re-run with --approve.",
|
|
247
|
+
{ planId: plan.planId, commands: plan.commands },
|
|
248
|
+
"requires-human"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!isAzCliAvailable()) {
|
|
253
|
+
throw new MachineCommandError(
|
|
254
|
+
"az-cli-not-found",
|
|
255
|
+
"Azure CLI is not installed. Install it first: https://aka.ms/azure-cli",
|
|
256
|
+
undefined,
|
|
257
|
+
"blocked"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const executedCommands: string[] = [];
|
|
262
|
+
const warnings = [...plan.warnings];
|
|
263
|
+
let deploymentOutputs: Record<string, unknown> | undefined;
|
|
264
|
+
|
|
265
|
+
for (const command of plan.commands) {
|
|
266
|
+
const result = spawnSync(command, [], { shell: true, encoding: "utf-8" });
|
|
267
|
+
executedCommands.push(command);
|
|
268
|
+
if (result.status !== 0) {
|
|
269
|
+
throw new MachineCommandError(
|
|
270
|
+
"provision-failed",
|
|
271
|
+
`Command failed: ${command}\n${(result.stderr || result.stdout || "").trim().slice(0, 4000)}`,
|
|
272
|
+
{ executedCommands },
|
|
273
|
+
"failed"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (command.includes("deployment group create")) {
|
|
277
|
+
try {
|
|
278
|
+
const deployment = JSON.parse(result.stdout || "{}");
|
|
279
|
+
deploymentOutputs = deployment.properties?.outputs;
|
|
280
|
+
} catch {
|
|
281
|
+
warnings.push("Could not parse deployment output JSON.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
deletePlanState(plan.planId, projectRoot);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
planId: plan.planId,
|
|
290
|
+
resourceGroup: plan.resourceGroup,
|
|
291
|
+
location: plan.location,
|
|
292
|
+
swaLocation: plan.swaLocation,
|
|
293
|
+
executedCommands,
|
|
294
|
+
...(deploymentOutputs ? { deploymentOutputs } : {}),
|
|
295
|
+
warnings,
|
|
296
|
+
};
|
|
297
|
+
}
|