@polymorphism-tech/morph-spec 1.0.4 → 2.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/CLAUDE.md +1381 -0
- package/LICENSE +72 -0
- package/README.md +89 -6
- package/bin/detect-agents.js +225 -0
- package/bin/morph-spec.js +120 -0
- package/bin/render-template.js +302 -0
- package/bin/semantic-detect-agents.js +246 -0
- package/bin/validate-agents-skills.js +239 -0
- package/bin/validate-agents.js +69 -0
- package/bin/validate-phase.js +263 -0
- package/content/.azure/README.md +293 -0
- package/content/.azure/docs/azure-devops-setup.md +454 -0
- package/content/.azure/docs/branch-strategy.md +398 -0
- package/content/.azure/docs/local-development.md +515 -0
- package/content/.azure/pipelines/pipeline-variables.yml +34 -0
- package/content/.azure/pipelines/prod-pipeline.yml +319 -0
- package/content/.azure/pipelines/staging-pipeline.yml +234 -0
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -0
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -0
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -0
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -0
- package/content/.claude/commands/morph-apply.md +118 -26
- package/content/.claude/commands/morph-archive.md +9 -9
- package/content/.claude/commands/morph-clarify.md +184 -0
- package/content/.claude/commands/morph-design.md +275 -0
- package/content/.claude/commands/morph-proposal.md +56 -15
- package/content/.claude/commands/morph-setup.md +100 -0
- package/content/.claude/commands/morph-status.md +47 -32
- package/content/.claude/commands/morph-tasks.md +319 -0
- package/content/.claude/commands/morph-uiux.md +211 -0
- package/content/.claude/skills/specialists/ai-system-architect.md +604 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +143 -89
- package/content/.claude/skills/specialists/ui-ux-designer.md +744 -9
- package/content/.claude/skills/stacks/dotnet-blazor.md +244 -8
- package/content/.claude/skills/stacks/dotnet-nextjs.md +2 -2
- package/content/.morph/.morphversion +5 -0
- package/content/.morph/config/agents.json +101 -8
- package/content/.morph/config/azure-pricing.json +70 -0
- package/content/.morph/config/azure-pricing.schema.json +50 -0
- package/content/.morph/config/config.template.json +15 -3
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -0
- package/content/.morph/hooks/README.md +239 -0
- package/content/.morph/hooks/pre-commit-agents.sh +24 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -0
- package/content/.morph/hooks/pre-commit-costs.sh +91 -0
- package/content/.morph/hooks/pre-commit-specs.sh +49 -0
- package/content/.morph/hooks/pre-commit-tests.sh +60 -0
- package/content/.morph/project.md +5 -4
- package/content/.morph/schemas/agent.schema.json +296 -0
- package/content/.morph/standards/agent-framework-setup.md +453 -0
- package/content/.morph/standards/architecture.md +142 -7
- package/content/.morph/standards/azure.md +218 -23
- package/content/.morph/standards/coding.md +47 -12
- package/content/.morph/standards/dotnet10-migration.md +494 -0
- package/content/.morph/standards/fluent-ui-setup.md +590 -0
- package/content/.morph/standards/migration-guide.md +514 -0
- package/content/.morph/standards/passkeys-auth.md +423 -0
- package/content/.morph/standards/vector-search-rag.md +536 -0
- package/content/.morph/state.json +18 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -0
- package/content/.morph/templates/MudTheme.cs +281 -0
- package/content/.morph/templates/contracts.cs +55 -55
- package/content/.morph/templates/decisions.md +4 -4
- package/content/.morph/templates/design-system.css +226 -0
- package/content/.morph/templates/infra/.dockerignore.example +89 -0
- package/content/.morph/templates/infra/Dockerfile.example +82 -0
- package/content/.morph/templates/infra/README.md +286 -0
- package/content/.morph/templates/infra/app-service.bicep +164 -0
- package/content/.morph/templates/infra/deploy.ps1 +229 -0
- package/content/.morph/templates/infra/deploy.sh +208 -0
- package/content/.morph/templates/infra/main.bicep +41 -7
- package/content/.morph/templates/infra/parameters.dev.json +6 -0
- package/content/.morph/templates/infra/parameters.prod.json +6 -0
- package/content/.morph/templates/infra/parameters.staging.json +29 -0
- package/content/.morph/templates/proposal.md +3 -3
- package/content/.morph/templates/recap.md +3 -3
- package/content/.morph/templates/spec.md +9 -8
- package/content/.morph/templates/sprint-status.yaml +68 -0
- package/content/.morph/templates/state.template.json +222 -0
- package/content/.morph/templates/story.md +143 -0
- package/content/.morph/templates/tasks.md +1 -1
- package/content/.morph/templates/ui-components.md +276 -0
- package/content/.morph/templates/ui-design-system.md +286 -0
- package/content/.morph/templates/ui-flows.md +336 -0
- package/content/.morph/templates/ui-mockups.md +133 -0
- package/content/.morph/test-infra/example.bicep +59 -0
- package/content/CLAUDE.md +124 -0
- package/content/README.md +79 -0
- package/detectors/config-detector.js +223 -0
- package/detectors/conversation-analyzer.js +163 -0
- package/detectors/index.js +84 -0
- package/detectors/standards-generator.js +275 -0
- package/detectors/structure-detector.js +221 -0
- package/docs/README.md +149 -0
- package/docs/api/cost-calculator.js.html +513 -0
- package/docs/api/design-system-generator.js.html +382 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/api/global.html +5263 -0
- package/docs/api/index.html +96 -0
- package/docs/api/scripts/collapse.js +39 -0
- package/docs/api/scripts/commonNav.js +28 -0
- package/docs/api/scripts/linenumber.js +25 -0
- package/docs/api/scripts/nav.js +12 -0
- package/docs/api/scripts/polyfill.js +4 -0
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/api/scripts/prettify/lang-css.js +2 -0
- package/docs/api/scripts/prettify/prettify.js +28 -0
- package/docs/api/scripts/search.js +99 -0
- package/docs/api/state-manager.js.html +423 -0
- package/docs/api/styles/jsdoc.css +776 -0
- package/docs/api/styles/prettify.css +80 -0
- package/docs/examples.md +328 -0
- package/docs/getting-started.md +302 -0
- package/docs/installation.md +361 -0
- package/docs/templates.md +418 -0
- package/docs/validation-checklist.md +266 -0
- package/package.json +39 -12
- package/src/commands/cost.js +181 -0
- package/src/commands/create-story.js +283 -0
- package/src/commands/detect.js +104 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/generate.js +149 -0
- package/src/commands/init.js +69 -45
- package/src/commands/shard-spec.js +224 -0
- package/src/commands/sprint-status.js +250 -0
- package/src/commands/state.js +333 -0
- package/src/commands/sync.js +167 -0
- package/src/commands/update-pricing.js +206 -0
- package/src/commands/update.js +88 -13
- package/src/lib/complexity-analyzer.js +292 -0
- package/src/lib/cost-calculator.js +429 -0
- package/src/lib/design-system-generator.js +298 -0
- package/src/lib/state-manager.js +340 -0
- package/src/utils/file-copier.js +59 -0
- package/src/utils/version-checker.js +175 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Cost Calculator Library
|
|
3
|
+
*
|
|
4
|
+
* Calculates Azure infrastructure costs from Bicep files.
|
|
5
|
+
* Used both by CLI commands and internal automation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { globSync } from 'glob';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// PRICING TABLE LOADER
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load Azure pricing from JSON file
|
|
22
|
+
* @param {string} pricingPath - Optional custom pricing file path
|
|
23
|
+
* @returns {Object} Pricing table
|
|
24
|
+
*/
|
|
25
|
+
export function loadAzurePricing(pricingPath = null) {
|
|
26
|
+
const defaultPath = join(__dirname, '..', '..', 'content', '.morph', 'config', 'azure-pricing.json');
|
|
27
|
+
const pathToTry = pricingPath || defaultPath;
|
|
28
|
+
|
|
29
|
+
if (!existsSync(pathToTry)) {
|
|
30
|
+
// Fallback to hardcoded pricing (backwards compatibility)
|
|
31
|
+
return AZURE_PRICING_FALLBACK;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = readFileSync(pathToTry, 'utf8');
|
|
36
|
+
const pricingData = JSON.parse(content);
|
|
37
|
+
return pricingData.resources || {};
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn(`Warning: Could not load pricing from ${pathToTry}: ${err.message}`);
|
|
40
|
+
return AZURE_PRICING_FALLBACK;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fallback pricing table (hardcoded for backwards compatibility)
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
const AZURE_PRICING_FALLBACK = {
|
|
49
|
+
// Azure SQL Database
|
|
50
|
+
'Microsoft.Sql/servers/databases': {
|
|
51
|
+
'Free': 0,
|
|
52
|
+
'Basic': 4.99,
|
|
53
|
+
'S0': 14.72,
|
|
54
|
+
'S1': 29.44,
|
|
55
|
+
'S2': 73.61,
|
|
56
|
+
'S3': 147.22,
|
|
57
|
+
'S4': 294.44,
|
|
58
|
+
'P1': 465.00,
|
|
59
|
+
'P2': 930.00,
|
|
60
|
+
'GP_Gen5_2': 328.00,
|
|
61
|
+
'GP_Gen5_4': 656.00,
|
|
62
|
+
'BC_Gen5_2': 1092.00,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Azure Container Apps
|
|
66
|
+
'Microsoft.App/containerApps': {
|
|
67
|
+
'Consumption': 0,
|
|
68
|
+
'Consumption_Active': 5.00,
|
|
69
|
+
'Dedicated_D2': 55.00,
|
|
70
|
+
'Dedicated_D4': 110.00,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Azure Storage Account
|
|
74
|
+
'Microsoft.Storage/storageAccounts': {
|
|
75
|
+
'Standard_LRS': 0.02,
|
|
76
|
+
'Standard_GRS': 0.05,
|
|
77
|
+
'Premium_LRS': 0.15,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Azure Application Insights
|
|
81
|
+
'Microsoft.Insights/components': {
|
|
82
|
+
'Free': 0,
|
|
83
|
+
'Basic': 2.30,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Azure Key Vault
|
|
87
|
+
'Microsoft.KeyVault/vaults': {
|
|
88
|
+
'Standard': 0.03,
|
|
89
|
+
'Premium': 1.00,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Azure Cosmos DB
|
|
93
|
+
'Microsoft.DocumentDB/databaseAccounts': {
|
|
94
|
+
'Serverless': 0,
|
|
95
|
+
'Provisioned_400RU': 23.36,
|
|
96
|
+
'Provisioned_1000RU': 58.40,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Azure App Service
|
|
100
|
+
'Microsoft.Web/serverfarms': {
|
|
101
|
+
'F1': 0,
|
|
102
|
+
'D1': 9.67,
|
|
103
|
+
'B1': 13.14,
|
|
104
|
+
'B2': 26.28,
|
|
105
|
+
'S1': 69.35,
|
|
106
|
+
'P1v2': 146.00,
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Azure Service Bus
|
|
110
|
+
'Microsoft.ServiceBus/namespaces': {
|
|
111
|
+
'Basic': 0.05,
|
|
112
|
+
'Standard': 10.00,
|
|
113
|
+
'Premium': 677.00,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Azure Redis Cache
|
|
117
|
+
'Microsoft.Cache/redis': {
|
|
118
|
+
'C0': 16.06,
|
|
119
|
+
'C1': 61.32,
|
|
120
|
+
'C2': 122.63,
|
|
121
|
+
'P1': 486.00,
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Azure Functions (Consumption Plan)
|
|
125
|
+
'Microsoft.Web/sites': {
|
|
126
|
+
'Consumption': 0,
|
|
127
|
+
'Premium_EP1': 146.00,
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// PRICING TABLE - Azure (East US 2 / Brazil South)
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Azure pricing table (loaded from JSON)
|
|
137
|
+
* @deprecated Use loadAzurePricing() for custom pricing paths
|
|
138
|
+
*/
|
|
139
|
+
export const AZURE_PRICING = loadAzurePricing();
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// DEFAULT CONFIGURATION
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
export const DEFAULT_LIMITS = {
|
|
146
|
+
freeTierOnly: 0,
|
|
147
|
+
withApproval: 10,
|
|
148
|
+
requiresADR: 10
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// CONFIGURATION LOADER
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load cost configuration from config.json
|
|
157
|
+
* @param {string} configPath - Optional custom config path
|
|
158
|
+
* @returns {Object} Cost configuration
|
|
159
|
+
*/
|
|
160
|
+
export function loadCostConfig(configPath = null) {
|
|
161
|
+
const defaultPath = join(process.cwd(), '.morph/config/config.json');
|
|
162
|
+
const templatePath = join(process.cwd(), 'content/.morph/config/config.template.json');
|
|
163
|
+
|
|
164
|
+
const pathsToTry = [
|
|
165
|
+
configPath,
|
|
166
|
+
defaultPath,
|
|
167
|
+
templatePath
|
|
168
|
+
].filter(Boolean);
|
|
169
|
+
|
|
170
|
+
for (const path of pathsToTry) {
|
|
171
|
+
if (existsSync(path)) {
|
|
172
|
+
try {
|
|
173
|
+
const content = readFileSync(path, 'utf8');
|
|
174
|
+
const config = JSON.parse(content);
|
|
175
|
+
|
|
176
|
+
if (config.costs && config.costs.limits) {
|
|
177
|
+
return {
|
|
178
|
+
limits: config.costs.limits,
|
|
179
|
+
enforcement: config.costs.enforcement || {},
|
|
180
|
+
currency: config.costs.currency || 'USD',
|
|
181
|
+
region: config.costs.pricing?.region || 'eastus2'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// Continue to next path
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fallback to defaults
|
|
191
|
+
return {
|
|
192
|
+
limits: DEFAULT_LIMITS,
|
|
193
|
+
enforcement: {
|
|
194
|
+
blockCommitsAboveLimit: true,
|
|
195
|
+
requireADRInDecisionsMd: true,
|
|
196
|
+
alertOnApproach: 0.8
|
|
197
|
+
},
|
|
198
|
+
currency: 'USD',
|
|
199
|
+
region: 'eastus2'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// BICEP PARSER
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse Bicep file and extract resources with SKUs
|
|
209
|
+
* @param {string} filePath - Path to Bicep file
|
|
210
|
+
* @returns {Array} Array of resource objects
|
|
211
|
+
*/
|
|
212
|
+
export function parseBicepFile(filePath) {
|
|
213
|
+
if (!existsSync(filePath)) {
|
|
214
|
+
throw new Error(`File not found: ${filePath}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = readFileSync(filePath, 'utf8');
|
|
218
|
+
const resources = [];
|
|
219
|
+
|
|
220
|
+
// Regex para extrair resources com conteúdo completo (inclui blocos aninhados)
|
|
221
|
+
// Formato: resource <name> '<type>@<apiVersion>' = { ... }
|
|
222
|
+
const resourceRegex = /resource\s+(\w+)\s+'([^']+)@[^']+'\s*=\s*\{/g;
|
|
223
|
+
|
|
224
|
+
let match;
|
|
225
|
+
while ((match = resourceRegex.exec(content)) !== null) {
|
|
226
|
+
const [, name, type] = match;
|
|
227
|
+
const startIndex = match.index + match[0].length;
|
|
228
|
+
|
|
229
|
+
// Encontrar o fechamento do bloco { }
|
|
230
|
+
let braceCount = 1;
|
|
231
|
+
let endIndex = startIndex;
|
|
232
|
+
|
|
233
|
+
while (braceCount > 0 && endIndex < content.length) {
|
|
234
|
+
if (content[endIndex] === '{') braceCount++;
|
|
235
|
+
if (content[endIndex] === '}') braceCount--;
|
|
236
|
+
endIndex++;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const body = content.substring(startIndex, endIndex - 1);
|
|
240
|
+
|
|
241
|
+
// Extrair SKU (dentro de bloco sku: { name: '...' })
|
|
242
|
+
const skuBlockMatch = /sku:\s*\{([^}]+)\}/s.exec(body);
|
|
243
|
+
let sku = null;
|
|
244
|
+
|
|
245
|
+
if (skuBlockMatch) {
|
|
246
|
+
const skuBlock = skuBlockMatch[1];
|
|
247
|
+
const skuNameMatch = /name:\s*'([^']+)'/s.exec(skuBlock);
|
|
248
|
+
sku = skuNameMatch ? skuNameMatch[1] : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Extrair tier (alternativa, fora ou dentro do bloco sku)
|
|
252
|
+
if (!sku) {
|
|
253
|
+
const tierMatch = /tier:\s*'([^']+)'/s.exec(body);
|
|
254
|
+
sku = tierMatch ? tierMatch[1] : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Extrair minReplicas (Container Apps, deep nested)
|
|
258
|
+
const minReplicasMatch = /minReplicas:\s*(\d+)/s.exec(body);
|
|
259
|
+
const minReplicas = minReplicasMatch ? parseInt(minReplicasMatch[1], 10) : null;
|
|
260
|
+
|
|
261
|
+
resources.push({
|
|
262
|
+
name,
|
|
263
|
+
type,
|
|
264
|
+
sku,
|
|
265
|
+
minReplicas,
|
|
266
|
+
file: filePath
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return resources;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse multiple Bicep files (supports glob patterns)
|
|
275
|
+
* @param {string|Array} patterns - File path(s) or glob pattern(s)
|
|
276
|
+
* @returns {Array} Array of all resources
|
|
277
|
+
*/
|
|
278
|
+
export function parseBicepFiles(patterns) {
|
|
279
|
+
const patternArray = Array.isArray(patterns) ? patterns : [patterns];
|
|
280
|
+
const allResources = [];
|
|
281
|
+
|
|
282
|
+
for (const pattern of patternArray) {
|
|
283
|
+
// Se for glob pattern, expandir
|
|
284
|
+
const files = pattern.includes('*')
|
|
285
|
+
? globSync(pattern, { absolute: true })
|
|
286
|
+
: [pattern];
|
|
287
|
+
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
try {
|
|
290
|
+
const resources = parseBicepFile(file);
|
|
291
|
+
allResources.push(...resources);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
// Silently skip files with errors (let caller handle)
|
|
294
|
+
console.warn(`Warning: Could not parse ${file}: ${err.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return allResources;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// COST CALCULATOR
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Calculate cost of a single resource
|
|
308
|
+
* @param {Object} resource - Resource object with type, sku, minReplicas
|
|
309
|
+
* @returns {Object} Cost result with cost, warning, estimated, details
|
|
310
|
+
*/
|
|
311
|
+
export function calculateResourceCost(resource) {
|
|
312
|
+
const pricing = AZURE_PRICING[resource.type];
|
|
313
|
+
|
|
314
|
+
if (!pricing) {
|
|
315
|
+
return {
|
|
316
|
+
cost: 0,
|
|
317
|
+
warning: `Unknown resource type: ${resource.type}`,
|
|
318
|
+
estimated: false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Special case: Container Apps with minReplicas
|
|
323
|
+
if (resource.type === 'Microsoft.App/containerApps') {
|
|
324
|
+
const sku = resource.minReplicas > 0 ? 'Consumption_Active' : 'Consumption';
|
|
325
|
+
const cost = pricing[sku] || 0;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
cost,
|
|
329
|
+
warning: null,
|
|
330
|
+
estimated: true,
|
|
331
|
+
details: `minReplicas: ${resource.minReplicas}, scale-to-zero: ${resource.minReplicas === 0}`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// General case: lookup by SKU
|
|
336
|
+
if (resource.sku && pricing[resource.sku] !== undefined) {
|
|
337
|
+
return {
|
|
338
|
+
cost: pricing[resource.sku],
|
|
339
|
+
warning: null,
|
|
340
|
+
estimated: true
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// SKU not found
|
|
345
|
+
return {
|
|
346
|
+
cost: 0,
|
|
347
|
+
warning: `Unknown SKU '${resource.sku}' for ${resource.type}`,
|
|
348
|
+
estimated: false
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Calculate total costs of all resources
|
|
354
|
+
* @param {Array} resources - Array of resource objects
|
|
355
|
+
* @param {Object} costConfig - Cost configuration
|
|
356
|
+
* @returns {Object} Cost calculation result
|
|
357
|
+
*/
|
|
358
|
+
export function calculateTotalCosts(resources, costConfig) {
|
|
359
|
+
let total = 0;
|
|
360
|
+
const breakdown = [];
|
|
361
|
+
const warnings = [];
|
|
362
|
+
|
|
363
|
+
for (const resource of resources) {
|
|
364
|
+
const result = calculateResourceCost(resource);
|
|
365
|
+
|
|
366
|
+
total += result.cost;
|
|
367
|
+
|
|
368
|
+
breakdown.push({
|
|
369
|
+
name: resource.name,
|
|
370
|
+
type: resource.type,
|
|
371
|
+
sku: resource.sku || 'N/A',
|
|
372
|
+
cost: result.cost,
|
|
373
|
+
file: resource.file,
|
|
374
|
+
estimated: result.estimated,
|
|
375
|
+
details: result.details
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (result.warning) {
|
|
379
|
+
warnings.push(result.warning);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Warning para recursos caros
|
|
383
|
+
if (result.cost > costConfig.limits.withApproval) {
|
|
384
|
+
warnings.push(
|
|
385
|
+
`Resource '${resource.name}' (${resource.type} ${resource.sku}) costs $${result.cost.toFixed(2)}/month - exceeds approval threshold!`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
monthly: parseFloat(total.toFixed(2)),
|
|
392
|
+
currency: costConfig.currency,
|
|
393
|
+
region: costConfig.region,
|
|
394
|
+
breakdown,
|
|
395
|
+
warnings,
|
|
396
|
+
limits: costConfig.limits,
|
|
397
|
+
requiresApproval: total > costConfig.limits.freeTierOnly,
|
|
398
|
+
requiresADR: total > costConfig.limits.requiresADR,
|
|
399
|
+
isFreeTier: total === 0
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Calculate costs from Bicep file(s) - Main entry point
|
|
405
|
+
* @param {string|Array} bicepFiles - Bicep file path(s) or glob pattern(s)
|
|
406
|
+
* @param {Object} options - Options
|
|
407
|
+
* @param {string} options.configPath - Optional custom config path
|
|
408
|
+
* @returns {Object} Complete cost calculation result
|
|
409
|
+
*/
|
|
410
|
+
export function calculateBicepCost(bicepFiles, options = {}) {
|
|
411
|
+
const costConfig = loadCostConfig(options.configPath);
|
|
412
|
+
const resources = parseBicepFiles(bicepFiles);
|
|
413
|
+
|
|
414
|
+
if (resources.length === 0) {
|
|
415
|
+
return {
|
|
416
|
+
monthly: 0,
|
|
417
|
+
currency: costConfig.currency,
|
|
418
|
+
region: costConfig.region,
|
|
419
|
+
breakdown: [],
|
|
420
|
+
warnings: ['No resources found in Bicep files'],
|
|
421
|
+
limits: costConfig.limits,
|
|
422
|
+
requiresApproval: false,
|
|
423
|
+
requiresADR: false,
|
|
424
|
+
isFreeTier: true
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return calculateTotalCosts(resources, costConfig);
|
|
429
|
+
}
|