@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.6a0bba7.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.
Files changed (32) hide show
  1. package/infrastructure/domains/database/migration-builder.js +199 -1
  2. package/infrastructure/domains/database/migration-builder.test.js +73 -0
  3. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  4. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  5. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +6 -0
  6. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +307 -1
  7. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +38 -5
  8. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
  9. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  10. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  11. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  12. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  13. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  14. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  15. package/infrastructure/domains/health/domain/entities/issue.js +50 -1
  16. package/infrastructure/domains/health/domain/entities/issue.test.js +111 -0
  17. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  18. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  19. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  20. package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
  21. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
  22. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  23. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  24. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  25. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  26. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  27. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +407 -20
  28. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +698 -26
  29. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
  30. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
  31. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
  32. package/package.json +6 -6
@@ -0,0 +1,345 @@
1
+ /**
2
+ * LogicalIdMapper - Map orphaned resources to their logical IDs in templates
3
+ *
4
+ * Purpose: Analyze orphaned resources and match them to the correct logical IDs
5
+ * from CloudFormation templates using tags, containment analysis, and template comparison.
6
+ */
7
+
8
+ const {
9
+ EC2Client,
10
+ DescribeSubnetsCommand,
11
+ DescribeSecurityGroupsCommand,
12
+ } = require('@aws-sdk/client-ec2');
13
+
14
+ class LogicalIdMapper {
15
+ constructor({ region = 'us-east-1' } = {}) {
16
+ this.ec2Client = new EC2Client({ region });
17
+ }
18
+
19
+ /**
20
+ * Map orphaned resources to their logical IDs in template
21
+ * @param {object} params - Mapping parameters
22
+ * @param {Array} params.orphanedResources - Orphaned resources to map
23
+ * @param {object} params.buildTemplate - Build template with logical IDs
24
+ * @param {object} params.deployedTemplate - Deployed template with hardcoded IDs
25
+ * @returns {Promise<Array>} Mappings with logical IDs
26
+ */
27
+ async mapOrphanedResourcesToLogicalIds({
28
+ orphanedResources,
29
+ buildTemplate,
30
+ deployedTemplate,
31
+ }) {
32
+ const mappings = [];
33
+
34
+ for (const orphan of orphanedResources) {
35
+ // Strategy 1: Check CloudFormation tags for logical ID
36
+ // Tags are stored in orphan.properties.tags (Resource entity structure)
37
+ const tags = orphan.properties?.tags || orphan.tags; // Support both formats
38
+ const logicalIdFromTag = this._getLogicalIdFromTags(tags);
39
+
40
+ if (logicalIdFromTag) {
41
+ mappings.push({
42
+ logicalId: logicalIdFromTag,
43
+ physicalId: orphan.physicalId,
44
+ resourceType: orphan.resourceType,
45
+ matchMethod: 'tag',
46
+ confidence: 'high',
47
+ });
48
+ continue;
49
+ }
50
+
51
+ // Strategy 2: Match by template comparison
52
+ if (orphan.resourceType === 'AWS::EC2::VPC') {
53
+ const logicalId = await this._matchVpcByContainedResources(
54
+ orphan,
55
+ buildTemplate,
56
+ deployedTemplate
57
+ );
58
+ if (logicalId) {
59
+ mappings.push({
60
+ logicalId,
61
+ physicalId: orphan.physicalId,
62
+ resourceType: orphan.resourceType,
63
+ matchMethod: 'contained-resources',
64
+ confidence: 'high',
65
+ });
66
+ continue;
67
+ }
68
+ }
69
+
70
+ if (orphan.resourceType === 'AWS::EC2::Subnet') {
71
+ const logicalId = await this._matchSubnetByVpcAndUsage(
72
+ orphan,
73
+ buildTemplate,
74
+ deployedTemplate
75
+ );
76
+ if (logicalId) {
77
+ mappings.push({
78
+ logicalId,
79
+ physicalId: orphan.physicalId,
80
+ resourceType: orphan.resourceType,
81
+ matchMethod: 'vpc-usage',
82
+ confidence: 'high',
83
+ });
84
+ continue;
85
+ }
86
+ }
87
+
88
+ if (orphan.resourceType === 'AWS::EC2::SecurityGroup') {
89
+ const logicalId = await this._matchSecurityGroupByUsage(
90
+ orphan,
91
+ buildTemplate,
92
+ deployedTemplate
93
+ );
94
+ if (logicalId) {
95
+ mappings.push({
96
+ logicalId,
97
+ physicalId: orphan.physicalId,
98
+ resourceType: orphan.resourceType,
99
+ matchMethod: 'usage',
100
+ confidence: 'medium',
101
+ });
102
+ continue;
103
+ }
104
+ }
105
+
106
+ // No match found - mark as unmapped
107
+ mappings.push({
108
+ logicalId: null,
109
+ physicalId: orphan.physicalId,
110
+ resourceType: orphan.resourceType,
111
+ matchMethod: 'none',
112
+ confidence: 'none',
113
+ });
114
+ }
115
+
116
+ return mappings;
117
+ }
118
+
119
+ /**
120
+ * Extract logical ID from CloudFormation tags
121
+ * Supports both formats:
122
+ * - AWS array format: [{Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC'}]
123
+ * - Parsed object format: {'aws:cloudformation:logical-id': 'FriggVPC'}
124
+ * @private
125
+ */
126
+ _getLogicalIdFromTags(tags) {
127
+ if (!tags) return null;
128
+
129
+ // Handle AWS array format [{Key, Value}]
130
+ if (Array.isArray(tags)) {
131
+ const logicalIdTag = tags.find(
132
+ (t) => t.Key === 'aws:cloudformation:logical-id'
133
+ );
134
+ return logicalIdTag ? logicalIdTag.Value : null;
135
+ }
136
+
137
+ // Handle parsed object format {key: value}
138
+ if (typeof tags === 'object') {
139
+ return tags['aws:cloudformation:logical-id'] || null;
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Match VPC by checking if it contains expected subnets from template
147
+ * @private
148
+ */
149
+ async _matchVpcByContainedResources(
150
+ vpc,
151
+ buildTemplate,
152
+ deployedTemplate
153
+ ) {
154
+ // Get expected subnet IDs from deployed template
155
+ const expectedSubnetIds = this._extractSubnetIdsFromTemplate(
156
+ deployedTemplate
157
+ );
158
+
159
+ if (expectedSubnetIds.length === 0) {
160
+ return null;
161
+ }
162
+
163
+ // Get actual subnets in this VPC
164
+ const actualSubnets = await this._getSubnetsInVpc(vpc.physicalId);
165
+
166
+ // Check if this VPC contains ALL expected subnets
167
+ const containsExpectedSubnets = expectedSubnetIds.every((expectedId) =>
168
+ actualSubnets.some((subnet) => subnet.SubnetId === expectedId)
169
+ );
170
+
171
+ if (containsExpectedSubnets) {
172
+ // Find VPC logical ID in build template
173
+ return this._findVpcLogicalIdInTemplate(buildTemplate);
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * Match subnet by VPC ownership and usage in Lambda functions
181
+ * @private
182
+ */
183
+ async _matchSubnetByVpcAndUsage(subnet, buildTemplate, deployedTemplate) {
184
+ // Extract subnet IDs from deployed template Lambda VPC configs
185
+ const templateSubnetIds = this._extractSubnetIdsFromTemplate(
186
+ deployedTemplate
187
+ );
188
+
189
+ // Check if this subnet is referenced in deployed template
190
+ if (!templateSubnetIds.includes(subnet.physicalId)) {
191
+ return null;
192
+ }
193
+
194
+ // Find the position of this subnet in the template's subnet list
195
+ const subnetIndex = templateSubnetIds.indexOf(subnet.physicalId);
196
+
197
+ // Extract subnet Refs from build template
198
+ const subnetRefs = this._extractSubnetRefsFromTemplate(buildTemplate);
199
+
200
+ // Return the corresponding logical ID based on position
201
+ return subnetRefs[subnetIndex] || null;
202
+ }
203
+
204
+ /**
205
+ * Match security group by usage in Lambda functions
206
+ * @private
207
+ */
208
+ async _matchSecurityGroupByUsage(sg, buildTemplate, deployedTemplate) {
209
+ // Extract security group IDs from deployed template
210
+ const templateSgIds = this._extractSecurityGroupIdsFromTemplate(
211
+ deployedTemplate
212
+ );
213
+
214
+ // Check if this SG is referenced in deployed template
215
+ if (!templateSgIds.includes(sg.physicalId)) {
216
+ return null;
217
+ }
218
+
219
+ // Find logical ID in build template
220
+ const sgRefs = this._extractSecurityGroupRefsFromTemplate(buildTemplate);
221
+ return sgRefs[0] || null; // Usually just one Lambda SG
222
+ }
223
+
224
+ /**
225
+ * Get subnets in a VPC from AWS
226
+ * @private
227
+ */
228
+ async _getSubnetsInVpc(vpcId) {
229
+ const response = await this.ec2Client.send(
230
+ new DescribeSubnetsCommand({
231
+ Filters: [{ Name: 'vpc-id', Values: [vpcId] }],
232
+ })
233
+ );
234
+ return response.Subnets || [];
235
+ }
236
+
237
+ /**
238
+ * Extract subnet IDs from deployed template (hardcoded values)
239
+ * @private
240
+ */
241
+ _extractSubnetIdsFromTemplate(template) {
242
+ const subnetIds = new Set();
243
+
244
+ // Traverse Lambda VpcConfig sections
245
+ Object.values(template.resources || {}).forEach((resource) => {
246
+ if (
247
+ resource.Type === 'AWS::Lambda::Function' &&
248
+ resource.Properties?.VpcConfig?.SubnetIds
249
+ ) {
250
+ resource.Properties.VpcConfig.SubnetIds.forEach((id) => {
251
+ if (typeof id === 'string' && id.startsWith('subnet-')) {
252
+ subnetIds.add(id);
253
+ }
254
+ });
255
+ }
256
+ });
257
+
258
+ return Array.from(subnetIds);
259
+ }
260
+
261
+ /**
262
+ * Extract security group IDs from deployed template (hardcoded values)
263
+ * @private
264
+ */
265
+ _extractSecurityGroupIdsFromTemplate(template) {
266
+ const sgIds = new Set();
267
+
268
+ Object.values(template.resources || {}).forEach((resource) => {
269
+ if (
270
+ resource.Type === 'AWS::Lambda::Function' &&
271
+ resource.Properties?.VpcConfig?.SecurityGroupIds
272
+ ) {
273
+ resource.Properties.VpcConfig.SecurityGroupIds.forEach((id) => {
274
+ if (typeof id === 'string' && id.startsWith('sg-')) {
275
+ sgIds.add(id);
276
+ }
277
+ });
278
+ }
279
+ });
280
+
281
+ return Array.from(sgIds);
282
+ }
283
+
284
+ /**
285
+ * Extract subnet Refs from build template
286
+ * @private
287
+ */
288
+ _extractSubnetRefsFromTemplate(template) {
289
+ const subnetRefs = [];
290
+
291
+ // Find Lambda functions and extract SubnetIds Refs
292
+ Object.values(template.resources || {}).forEach((resource) => {
293
+ if (
294
+ resource.Type === 'AWS::Lambda::Function' &&
295
+ resource.Properties?.VpcConfig?.SubnetIds
296
+ ) {
297
+ resource.Properties.VpcConfig.SubnetIds.forEach((ref) => {
298
+ if (ref.Ref && ref.Ref.includes('Subnet')) {
299
+ subnetRefs.push(ref.Ref);
300
+ }
301
+ });
302
+ }
303
+ });
304
+
305
+ return subnetRefs;
306
+ }
307
+
308
+ /**
309
+ * Extract security group Refs from build template
310
+ * @private
311
+ */
312
+ _extractSecurityGroupRefsFromTemplate(template) {
313
+ const sgRefs = [];
314
+
315
+ Object.values(template.resources || {}).forEach((resource) => {
316
+ if (
317
+ resource.Type === 'AWS::Lambda::Function' &&
318
+ resource.Properties?.VpcConfig?.SecurityGroupIds
319
+ ) {
320
+ resource.Properties.VpcConfig.SecurityGroupIds.forEach((ref) => {
321
+ if (ref.Ref && ref.Ref.includes('SecurityGroup')) {
322
+ sgRefs.push(ref.Ref);
323
+ }
324
+ });
325
+ }
326
+ });
327
+
328
+ return sgRefs;
329
+ }
330
+
331
+ /**
332
+ * Find VPC logical ID in build template
333
+ * @private
334
+ */
335
+ _findVpcLogicalIdInTemplate(template) {
336
+ const vpcResources = Object.entries(template.resources || {}).filter(
337
+ ([_, resource]) => resource.Type === 'AWS::EC2::VPC'
338
+ );
339
+
340
+ // Return first VPC logical ID (usually only one)
341
+ return vpcResources.length > 0 ? vpcResources[0][0] : null;
342
+ }
343
+ }
344
+
345
+ module.exports = { LogicalIdMapper };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * TemplateParser - Parse CloudFormation templates for resource extraction
3
+ *
4
+ * Purpose: Parse both build templates (.serverless/) and deployed templates (from AWS)
5
+ * to extract resource definitions, logical IDs, and Refs for import mapping.
6
+ */
7
+
8
+ class TemplateParser {
9
+ /**
10
+ * Parse CloudFormation template and extract resource definitions
11
+ * @param {string|object} template - Template path or parsed template object
12
+ * @returns {object} Parsed template with resources
13
+ */
14
+ parseTemplate(template) {
15
+ let parsedTemplate;
16
+
17
+ if (typeof template === 'string') {
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ if (!fs.existsSync(template)) {
22
+ throw new Error(`Template not found at path: ${template}`);
23
+ }
24
+
25
+ parsedTemplate = JSON.parse(fs.readFileSync(template, 'utf8'));
26
+ } else {
27
+ parsedTemplate = template;
28
+ }
29
+
30
+ return {
31
+ resources: parsedTemplate.Resources || {},
32
+ version: parsedTemplate.AWSTemplateFormatVersion,
33
+ description: parsedTemplate.Description,
34
+ outputs: parsedTemplate.Outputs || {},
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Extract VPC-related resource logical IDs from template
40
+ * @param {object} template - Parsed template object
41
+ * @returns {Array} VPC resources with logical IDs
42
+ */
43
+ getVpcResources(template) {
44
+ const vpcResourceTypes = [
45
+ 'AWS::EC2::VPC',
46
+ 'AWS::EC2::Subnet',
47
+ 'AWS::EC2::SecurityGroup',
48
+ 'AWS::EC2::InternetGateway',
49
+ 'AWS::EC2::NatGateway',
50
+ 'AWS::EC2::RouteTable',
51
+ 'AWS::EC2::VPCEndpoint',
52
+ ];
53
+
54
+ return Object.entries(template.resources)
55
+ .filter(([_, resource]) => vpcResourceTypes.includes(resource.Type))
56
+ .map(([logicalId, resource]) => ({
57
+ logicalId,
58
+ resourceType: resource.Type,
59
+ properties: resource.Properties || {},
60
+ }));
61
+ }
62
+
63
+ /**
64
+ * Extract hardcoded resource IDs from deployed template
65
+ * Finds physical IDs that are hardcoded instead of using Refs
66
+ * @param {object} template - Deployed CloudFormation template
67
+ * @returns {object} Extracted hardcoded IDs by type
68
+ */
69
+ extractHardcodedIds(template) {
70
+ const hardcodedIds = {
71
+ vpcIds: new Set(),
72
+ subnetIds: new Set(),
73
+ securityGroupIds: new Set(),
74
+ };
75
+
76
+ // Traverse template to find hardcoded IDs
77
+ Object.values(template.resources).forEach((resource) => {
78
+ this._extractIdsFromResource(resource, hardcodedIds);
79
+ });
80
+
81
+ return {
82
+ vpcIds: Array.from(hardcodedIds.vpcIds),
83
+ subnetIds: Array.from(hardcodedIds.subnetIds),
84
+ securityGroupIds: Array.from(hardcodedIds.securityGroupIds),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Extract Refs from build template
90
+ * Finds logical IDs that are referenced via {Ref: "LogicalId"}
91
+ * @param {object} template - Build template with Refs
92
+ * @returns {object} Logical IDs mapped to expected resource types
93
+ */
94
+ extractRefs(template) {
95
+ const refs = {
96
+ vpcRefs: new Set(),
97
+ subnetRefs: new Set(),
98
+ securityGroupRefs: new Set(),
99
+ };
100
+
101
+ // Traverse template to find Ref expressions
102
+ Object.values(template.resources).forEach((resource) => {
103
+ this._extractRefsFromResource(resource, refs);
104
+ });
105
+
106
+ return {
107
+ vpcRefs: Array.from(refs.vpcRefs),
108
+ subnetRefs: Array.from(refs.subnetRefs),
109
+ securityGroupRefs: Array.from(refs.securityGroupRefs),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Recursively extract hardcoded IDs from resource properties
115
+ * @private
116
+ */
117
+ _extractIdsFromResource(obj, hardcodedIds) {
118
+ if (typeof obj !== 'object' || obj === null) return;
119
+
120
+ Object.entries(obj).forEach(([key, value]) => {
121
+ // Check for VPC IDs
122
+ if (key === 'VpcId' && typeof value === 'string' && value.startsWith('vpc-')) {
123
+ hardcodedIds.vpcIds.add(value);
124
+ }
125
+
126
+ // Check for subnet IDs
127
+ if (
128
+ (key === 'SubnetIds' || key === 'SubnetId') &&
129
+ Array.isArray(value)
130
+ ) {
131
+ value.forEach((id) => {
132
+ if (typeof id === 'string' && id.startsWith('subnet-')) {
133
+ hardcodedIds.subnetIds.add(id);
134
+ }
135
+ });
136
+ } else if (
137
+ key === 'SubnetId' &&
138
+ typeof value === 'string' &&
139
+ value.startsWith('subnet-')
140
+ ) {
141
+ hardcodedIds.subnetIds.add(value);
142
+ }
143
+
144
+ // Check for security group IDs
145
+ if (key === 'SecurityGroupIds' && Array.isArray(value)) {
146
+ value.forEach((id) => {
147
+ if (typeof id === 'string' && id.startsWith('sg-')) {
148
+ hardcodedIds.securityGroupIds.add(id);
149
+ }
150
+ });
151
+ } else if (
152
+ key === 'GroupId' &&
153
+ typeof value === 'string' &&
154
+ value.startsWith('sg-')
155
+ ) {
156
+ hardcodedIds.securityGroupIds.add(value);
157
+ }
158
+
159
+ // Recurse into nested objects
160
+ if (typeof value === 'object') {
161
+ this._extractIdsFromResource(value, hardcodedIds);
162
+ }
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Recursively extract Refs from resource properties
168
+ * @private
169
+ */
170
+ _extractRefsFromResource(obj, refs) {
171
+ if (typeof obj !== 'object' || obj === null) return;
172
+
173
+ Object.entries(obj).forEach(([key, value]) => {
174
+ // Check for Ref expressions
175
+ if (key === 'Ref' && typeof value === 'string') {
176
+ // Determine ref type based on logical ID naming
177
+ if (value.includes('VPC') && !value.includes('Endpoint')) {
178
+ refs.vpcRefs.add(value);
179
+ } else if (value.includes('Subnet')) {
180
+ refs.subnetRefs.add(value);
181
+ } else if (value.includes('SecurityGroup')) {
182
+ refs.securityGroupRefs.add(value);
183
+ }
184
+ }
185
+
186
+ // Recurse into nested objects and arrays
187
+ if (typeof value === 'object') {
188
+ this._extractRefsFromResource(value, refs);
189
+ }
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Find logical ID for a physical ID by comparing templates
195
+ * @param {string} physicalId - Physical resource ID from AWS
196
+ * @param {object} deployedTemplate - Template with hardcoded IDs
197
+ * @param {object} buildTemplate - Template with Refs
198
+ * @returns {string|null} Matching logical ID or null
199
+ */
200
+ findLogicalIdForPhysicalId(physicalId, deployedTemplate, buildTemplate) {
201
+ // Extract hardcoded IDs and their context
202
+ const hardcodedIds = this.extractHardcodedIds(deployedTemplate);
203
+ const refs = this.extractRefs(buildTemplate);
204
+
205
+ // Determine resource type from physical ID
206
+ let logicalIdCandidates = [];
207
+ if (physicalId.startsWith('vpc-')) {
208
+ logicalIdCandidates = refs.vpcRefs;
209
+ } else if (physicalId.startsWith('subnet-')) {
210
+ logicalIdCandidates = refs.subnetRefs;
211
+ } else if (physicalId.startsWith('sg-')) {
212
+ logicalIdCandidates = refs.securityGroupRefs;
213
+ }
214
+
215
+ // For now, return first candidate (will enhance with position matching)
216
+ return logicalIdCandidates[0] || null;
217
+ }
218
+
219
+ /**
220
+ * Get build template path from project directory
221
+ * @param {string} projectPath - Project root path
222
+ * @returns {string} Path to build template
223
+ */
224
+ static getBuildTemplatePath(projectPath = process.cwd()) {
225
+ const path = require('path');
226
+ return path.join(
227
+ projectPath,
228
+ '.serverless',
229
+ 'cloudformation-template-update-stack.json'
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Check if build template exists
235
+ * @param {string} projectPath - Project root path
236
+ * @returns {boolean} True if template exists
237
+ */
238
+ static buildTemplateExists(projectPath = process.cwd()) {
239
+ const fs = require('fs');
240
+ const templatePath = this.getBuildTemplatePath(projectPath);
241
+ return fs.existsSync(templatePath);
242
+ }
243
+ }
244
+
245
+ module.exports = { TemplateParser };