@aifabrix/builder 2.31.1 → 2.32.2

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -107,12 +107,79 @@ function handleStatusCodeError(statusCode, errorData) {
107
107
  return handleSpecificClientErrors(statusCode, errorData);
108
108
  }
109
109
  // Handle 5xx server errors
110
+ // Use generic "Server error" message for consistency (specific details shown in formatted output)
110
111
  if (statusCode >= 500) {
111
112
  return createErrorResult('server', 'Server error', formatServerError(errorData), errorData);
112
113
  }
113
114
  return null;
114
115
  }
115
116
 
117
+ /**
118
+ * Checks if error response is a plain string (not JSON)
119
+ * @param {string|Object} errorResponse - Error response
120
+ * @returns {boolean} True if plain string
121
+ */
122
+ function isPlainStringError(errorResponse) {
123
+ if (typeof errorResponse !== 'string') {
124
+ return false;
125
+ }
126
+ const trimmed = errorResponse.trim();
127
+ return !(trimmed.startsWith('{') || trimmed.startsWith('['));
128
+ }
129
+
130
+ /**
131
+ * Handles null or undefined error response
132
+ * @param {number} statusCode - HTTP status code
133
+ * @returns {Object} Error result object
134
+ */
135
+ function handleNullError(statusCode) {
136
+ return createErrorResult('generic', 'Unknown error', formatGenericError({ message: 'Unknown error' }, statusCode), { message: 'Unknown error' });
137
+ }
138
+
139
+ /**
140
+ * Handles non-string, non-object error response
141
+ * @param {*} errorResponse - Error response
142
+ * @param {number} statusCode - HTTP status code
143
+ * @returns {Object} Error result object
144
+ */
145
+ function handleNonStringObjectError(errorResponse, statusCode) {
146
+ const errorMessage = String(errorResponse);
147
+ return createErrorResult('generic', errorMessage, formatGenericError({ message: errorMessage }, statusCode), { message: errorMessage });
148
+ }
149
+
150
+ /**
151
+ * Handles network errors
152
+ * @param {Object} errorData - Error data object
153
+ * @param {string|Object} errorResponse - Original error response
154
+ * @returns {Object} Error result object
155
+ */
156
+ function handleNetworkError(errorData, errorResponse) {
157
+ const errorMessage = errorData.message || errorResponse || 'Network error';
158
+ return createErrorResult('network', errorMessage, formatNetworkError(errorMessage, errorData), errorData);
159
+ }
160
+
161
+ /**
162
+ * Handles plain string errors without specific status code handling
163
+ * @param {Object} errorData - Error data object
164
+ * @param {string} errorResponse - Original error response
165
+ * @param {number} statusCode - HTTP status code
166
+ * @returns {Object} Error result object
167
+ */
168
+ function handlePlainStringError(errorData, errorResponse, statusCode) {
169
+ const errorMessage = errorData.message || errorResponse || 'Unknown error';
170
+ return createErrorResult('generic', errorMessage, formatGenericError(errorData, statusCode), errorData);
171
+ }
172
+
173
+ /**
174
+ * Handles generic errors
175
+ * @param {Object} errorData - Error data object
176
+ * @param {number} statusCode - HTTP status code
177
+ * @returns {Object} Error result object
178
+ */
179
+ function handleGenericError(errorData, statusCode) {
180
+ return createErrorResult('generic', errorData.message || errorData.error || 'Unknown error', formatGenericError(errorData, statusCode), errorData);
181
+ }
182
+
116
183
  /**
117
184
  * Parses error response and determines error type
118
185
  * @param {string|Object} errorResponse - Error response (string or parsed JSON)
@@ -121,6 +188,17 @@ function handleStatusCodeError(statusCode, errorData) {
121
188
  * @returns {Object} Parsed error object with type, message, and formatted output
122
189
  */
123
190
  function parseErrorResponse(errorResponse, statusCode, isNetworkError) {
191
+ // For null, undefined, treat as generic regardless of status code
192
+ if (errorResponse === null || errorResponse === undefined) {
193
+ return handleNullError(statusCode);
194
+ }
195
+
196
+ // For non-string, non-object errors (numbers, booleans, etc.), treat as generic
197
+ if (typeof errorResponse !== 'string' && typeof errorResponse !== 'object') {
198
+ return handleNonStringObjectError(errorResponse, statusCode);
199
+ }
200
+
201
+ const isPlainString = isPlainStringError(errorResponse);
124
202
  let errorData = parseErrorData(errorResponse);
125
203
 
126
204
  // Handle nested response structure (some APIs wrap errors in data field)
@@ -130,18 +208,26 @@ function parseErrorResponse(errorResponse, statusCode, isNetworkError) {
130
208
 
131
209
  // Handle network errors
132
210
  if (isNetworkError) {
133
- const errorMessage = errorData.message || errorResponse || 'Network error';
134
- return createErrorResult('network', errorMessage, formatNetworkError(errorMessage, errorData), errorData);
211
+ return handleNetworkError(errorData, errorResponse);
135
212
  }
136
213
 
137
- // Handle HTTP status codes
214
+ // Handle HTTP status codes first (even for plain strings, status code takes precedence)
138
215
  const statusError = handleStatusCodeError(statusCode, errorData);
139
216
  if (statusError) {
217
+ // For plain string errors, preserve the original message instead of formatted message
218
+ if (isPlainString && typeof errorResponse === 'string') {
219
+ return createErrorResult(statusError.type, errorResponse, statusError.formatted, statusError.data);
220
+ }
140
221
  return statusError;
141
222
  }
142
223
 
224
+ // For plain string errors (not JSON) without specific status code handling, treat as generic
225
+ if (isPlainString) {
226
+ return handlePlainStringError(errorData, errorResponse, statusCode);
227
+ }
228
+
143
229
  // Generic error
144
- return createErrorResult('generic', errorData.message || errorData.error || 'Unknown error', formatGenericError(errorData, statusCode), errorData);
230
+ return handleGenericError(errorData, statusCode);
145
231
  }
146
232
 
147
233
  module.exports = {
@@ -21,17 +21,26 @@ const chalk = require('chalk');
21
21
  * @param {string} [errorData.correlationId] - Correlation ID
22
22
  * @returns {string} Formatted authentication error message
23
23
  */
24
- function formatAuthenticationError(errorData) {
25
- const lines = [];
26
- lines.push(chalk.red('❌ Authentication Failed\n'));
27
-
28
- // Show controller URL prominently if available
24
+ /**
25
+ * Adds controller URL information to error lines
26
+ * @function addControllerUrlInfo
27
+ * @param {string[]} lines - Error message lines
28
+ * @param {Object} errorData - Error data
29
+ */
30
+ function addControllerUrlInfo(lines, errorData) {
29
31
  if (errorData.controllerUrl) {
30
32
  lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
31
33
  lines.push('');
32
34
  }
35
+ }
33
36
 
34
- // Show attempted URLs if multiple were tried
37
+ /**
38
+ * Adds attempted URLs information
39
+ * @function addAttemptedUrlsInfo
40
+ * @param {string[]} lines - Error message lines
41
+ * @param {Object} errorData - Error data
42
+ */
43
+ function addAttemptedUrlsInfo(lines, errorData) {
35
44
  if (errorData.attemptedUrls && errorData.attemptedUrls.length > 1) {
36
45
  lines.push(chalk.gray('Attempted controller URLs:'));
37
46
  errorData.attemptedUrls.forEach(url => {
@@ -39,28 +48,46 @@ function formatAuthenticationError(errorData) {
39
48
  });
40
49
  lines.push('');
41
50
  }
51
+ }
42
52
 
43
- // Check if error message contains specific information
44
- const errorMessage = errorData.message || errorData.error || errorData.detail || '';
53
+ /**
54
+ * Checks if error message is generic
55
+ * @function isGenericErrorMessage
56
+ * @param {string} errorMessage - Error message
57
+ * @returns {boolean} True if generic
58
+ */
59
+ function isGenericErrorMessage(errorMessage) {
45
60
  const lowerMessage = errorMessage.toLowerCase();
61
+ return lowerMessage.includes('authentication required') ||
62
+ lowerMessage.includes('unauthorized') ||
63
+ lowerMessage === '';
64
+ }
46
65
 
47
- // Only show error message if it provides useful information beyond generic messages
48
- const isGenericMessage = lowerMessage.includes('authentication required') ||
49
- lowerMessage.includes('unauthorized') ||
50
- lowerMessage === '';
51
-
52
- if (errorMessage && !isGenericMessage) {
53
- // Show specific error message if it provides useful details
66
+ /**
67
+ * Adds error message if not generic
68
+ * @function addErrorMessageIfNotGeneric
69
+ * @param {string[]} lines - Error message lines
70
+ * @param {Object} errorData - Error data
71
+ */
72
+ function addErrorMessageIfNotGeneric(lines, errorData) {
73
+ const errorMessage = errorData.message || errorData.error || errorData.detail || '';
74
+ if (errorMessage && !isGenericErrorMessage(errorMessage)) {
54
75
  lines.push(chalk.yellow(errorMessage));
55
76
  lines.push('');
56
77
  }
78
+ }
57
79
 
58
- // Always show general, actionable guidance
80
+ /**
81
+ * Adds authentication guidance
82
+ * @function addAuthenticationGuidance
83
+ * @param {string[]} lines - Error message lines
84
+ * @param {Object} errorData - Error data
85
+ */
86
+ function addAuthenticationGuidance(lines, errorData) {
59
87
  lines.push(chalk.gray('Your authentication token is invalid or has expired.'));
60
88
  lines.push('');
61
89
  lines.push(chalk.gray('To authenticate, run:'));
62
90
 
63
- // Use real controller URL if provided, otherwise show placeholder
64
91
  const controllerUrl = errorData.controllerUrl;
65
92
  if (controllerUrl && controllerUrl.trim()) {
66
93
  lines.push(chalk.gray(` aifabrix login --method device --controller ${controllerUrl}`));
@@ -72,6 +99,16 @@ function formatAuthenticationError(errorData) {
72
99
  lines.push('');
73
100
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
74
101
  }
102
+ }
103
+
104
+ function formatAuthenticationError(errorData) {
105
+ const lines = [];
106
+ lines.push(chalk.red('❌ Authentication Failed\n'));
107
+
108
+ addControllerUrlInfo(lines, errorData);
109
+ addAttemptedUrlsInfo(lines, errorData);
110
+ addErrorMessageIfNotGeneric(lines, errorData);
111
+ addAuthenticationGuidance(lines, errorData);
75
112
 
76
113
  return lines.join('\n');
77
114
  }
@@ -11,51 +11,128 @@
11
11
  const chalk = require('chalk');
12
12
 
13
13
  /**
14
- * Formats network error
14
+ * Normalizes error message to string
15
+ * @function normalizeErrorMessage
15
16
  * @param {string} errorMessage - Error message
16
- * @param {Object} errorData - Error response data (optional)
17
- * @param {string} [errorData.controllerUrl] - Controller URL
18
- * @returns {string} Formatted network error message
17
+ * @returns {string} Normalized error message
19
18
  */
20
- function formatNetworkError(errorMessage, errorData) {
21
- const lines = [];
22
- lines.push(chalk.red('❌ Network Error\n'));
19
+ function normalizeErrorMessage(errorMessage) {
20
+ return typeof errorMessage === 'string' ? errorMessage : String(errorMessage || 'Network error');
21
+ }
23
22
 
24
- // Show controller URL prominently if available
23
+ /**
24
+ * Adds controller URL header to error lines
25
+ * @function addControllerUrlHeader
26
+ * @param {Array<string>} lines - Error message lines
27
+ * @param {Object} errorData - Error response data
28
+ */
29
+ function addControllerUrlHeader(lines, errorData) {
25
30
  if (errorData && errorData.controllerUrl) {
26
31
  lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
27
32
  lines.push('');
28
33
  }
34
+ }
35
+
36
+ /**
37
+ * Adds controller URL to error message
38
+ * @function addControllerUrlToMessage
39
+ * @param {Array<string>} lines - Error message lines
40
+ * @param {Object} errorData - Error response data
41
+ */
42
+ function addControllerUrlToMessage(lines, errorData) {
43
+ if (errorData && errorData.controllerUrl) {
44
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Formats connection refused error
50
+ * @function formatConnectionRefusedError
51
+ * @param {Array<string>} lines - Error message lines
52
+ * @param {Object} errorData - Error response data
53
+ */
54
+ function formatConnectionRefusedError(lines, errorData) {
55
+ lines.push(chalk.yellow('Cannot connect to controller.'));
56
+ addControllerUrlToMessage(lines, errorData);
57
+ lines.push(chalk.gray('Check if the controller is running.'));
58
+ }
59
+
60
+ /**
61
+ * Formats hostname not found error
62
+ * @function formatHostnameNotFoundError
63
+ * @param {Array<string>} lines - Error message lines
64
+ * @param {Object} errorData - Error response data
65
+ */
66
+ function formatHostnameNotFoundError(lines, errorData) {
67
+ lines.push(chalk.yellow('Controller hostname not found.'));
68
+ addControllerUrlToMessage(lines, errorData);
69
+ lines.push(chalk.gray('Check your controller URL.'));
70
+ }
29
71
 
30
- // Ensure errorMessage is a string
31
- const message = typeof errorMessage === 'string' ? errorMessage : String(errorMessage || 'Network error');
72
+ /**
73
+ * Formats timeout error
74
+ * @function formatTimeoutError
75
+ * @param {Array<string>} lines - Error message lines
76
+ * @param {Object} errorData - Error response data
77
+ */
78
+ function formatTimeoutError(lines, errorData) {
79
+ lines.push(chalk.yellow('Request timed out.'));
80
+ addControllerUrlToMessage(lines, errorData);
81
+ lines.push(chalk.gray('The controller may be overloaded.'));
82
+ }
32
83
 
33
- if (message.includes('ECONNREFUSED') || message.includes('Cannot connect')) {
34
- lines.push(chalk.yellow('Cannot connect to controller.'));
35
- if (errorData && errorData.controllerUrl) {
36
- lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
37
- }
38
- lines.push(chalk.gray('Check if the controller is running.'));
84
+ /**
85
+ * Formats error message based on error type
86
+ * @function formatErrorMessageByType
87
+ * @param {string} message - Normalized error message
88
+ * @param {Array<string>} lines - Error message lines
89
+ * @param {Object} errorData - Error response data
90
+ */
91
+ function formatErrorMessageByType(message, lines, errorData) {
92
+ if (!message || message.trim() === '') {
93
+ // Empty message gets normalized to 'Network error' and displayed as generic error
94
+ lines.push(chalk.yellow('Network error'));
95
+ } else if (message.includes('ECONNREFUSED') || message.includes('Cannot connect')) {
96
+ formatConnectionRefusedError(lines, errorData);
39
97
  } else if (message.includes('ENOTFOUND') || message.includes('hostname not found')) {
40
- lines.push(chalk.yellow('Controller hostname not found.'));
41
- if (errorData && errorData.controllerUrl) {
42
- lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
43
- }
44
- lines.push(chalk.gray('Check your controller URL.'));
98
+ formatHostnameNotFoundError(lines, errorData);
45
99
  } else if (message.includes('timeout') || message.includes('timed out')) {
46
- lines.push(chalk.yellow('Request timed out.'));
47
- if (errorData && errorData.controllerUrl) {
48
- lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
49
- }
50
- lines.push(chalk.gray('The controller may be overloaded.'));
100
+ formatTimeoutError(lines, errorData);
51
101
  } else {
52
102
  lines.push(chalk.yellow(message));
53
103
  }
104
+ }
54
105
 
106
+ /**
107
+ * Adds correlation ID if present
108
+ * @function addCorrelationId
109
+ * @param {Array<string>} lines - Error message lines
110
+ * @param {Object} errorData - Error response data
111
+ */
112
+ function addCorrelationId(lines, errorData) {
55
113
  if (errorData && errorData.correlationId) {
56
114
  lines.push('');
57
115
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
58
116
  }
117
+ }
118
+
119
+ /**
120
+ * Formats network error
121
+ * @param {string} errorMessage - Error message
122
+ * @param {Object} errorData - Error response data (optional)
123
+ * @param {string} [errorData.controllerUrl] - Controller URL
124
+ * @returns {string} Formatted network error message
125
+ */
126
+ function formatNetworkError(errorMessage, errorData) {
127
+ const lines = [];
128
+ lines.push(chalk.red('❌ Network Error\n'));
129
+
130
+ addControllerUrlHeader(lines, errorData);
131
+
132
+ const message = normalizeErrorMessage(errorMessage);
133
+ formatErrorMessageByType(message, lines, errorData);
134
+
135
+ addCorrelationId(lines, errorData);
59
136
 
60
137
  return lines.join('\n');
61
138
  }