agileflow 2.89.1 → 2.89.3
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/CHANGELOG.md +10 -0
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +449 -0
- package/lib/validate.js +165 -11
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/tools/cli/commands/config.js +3 -3
- package/tools/cli/commands/doctor.js +30 -2
- package/tools/cli/commands/list.js +2 -2
- package/tools/cli/commands/uninstall.js +3 -3
- package/tools/cli/installers/core/installer.js +62 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
|
@@ -33,6 +33,13 @@ const {
|
|
|
33
33
|
countSkills,
|
|
34
34
|
getCounts,
|
|
35
35
|
} = require('../../../scripts/lib/counter');
|
|
36
|
+
const {
|
|
37
|
+
sanitize,
|
|
38
|
+
sanitizeAgentData,
|
|
39
|
+
sanitizeCommandData,
|
|
40
|
+
validatePlaceholderValue,
|
|
41
|
+
detectInjectionAttempt,
|
|
42
|
+
} = require('../../../lib/content-sanitizer');
|
|
36
43
|
|
|
37
44
|
// =============================================================================
|
|
38
45
|
// List Generation Functions
|
|
@@ -41,12 +48,14 @@ const {
|
|
|
41
48
|
/**
|
|
42
49
|
* Validate that a file path is within the expected directory.
|
|
43
50
|
* Prevents reading files outside the expected scope.
|
|
51
|
+
* Security: Symlinks are NOT allowed to prevent escape attacks.
|
|
44
52
|
* @param {string} filePath - File path to validate
|
|
45
53
|
* @param {string} baseDir - Expected base directory
|
|
46
54
|
* @returns {boolean} True if path is safe
|
|
47
55
|
*/
|
|
48
56
|
function isPathSafe(filePath, baseDir) {
|
|
49
|
-
|
|
57
|
+
// Security hardening (US-0104): Symlinks disabled to prevent escape attacks
|
|
58
|
+
const result = validatePath(filePath, baseDir, { allowSymlinks: false });
|
|
50
59
|
return result.ok;
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -76,19 +85,32 @@ function generateAgentList(agentsDir) {
|
|
|
76
85
|
continue;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
// Sanitize agent data to prevent injection attacks
|
|
89
|
+
const rawAgent = {
|
|
80
90
|
name: frontmatter.name || path.basename(file, '.md'),
|
|
81
91
|
description: frontmatter.description || '',
|
|
82
92
|
tools: normalizeTools(frontmatter.tools),
|
|
83
93
|
model: frontmatter.model || 'haiku',
|
|
84
|
-
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const sanitizedAgent = sanitizeAgentData(rawAgent);
|
|
97
|
+
|
|
98
|
+
// Skip if sanitization produced invalid data
|
|
99
|
+
if (!sanitizedAgent.name || sanitizedAgent.name === 'unknown') {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
agents.push(sanitizedAgent);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
// Sanitize the count value
|
|
109
|
+
const safeCount = sanitize.count(agents.length);
|
|
110
|
+
let output = `**AVAILABLE AGENTS (${safeCount} total)**:\n\n`;
|
|
90
111
|
|
|
91
112
|
agents.forEach((agent, index) => {
|
|
113
|
+
// All values are already sanitized by sanitizeAgentData
|
|
92
114
|
output += `${index + 1}. **${agent.name}** (model: ${agent.model})\n`;
|
|
93
115
|
output += ` - **Purpose**: ${agent.description}\n`;
|
|
94
116
|
output += ` - **Tools**: ${agent.tools.join(', ')}\n`;
|
|
@@ -127,11 +149,19 @@ function generateCommandList(commandsDir) {
|
|
|
127
149
|
continue;
|
|
128
150
|
}
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
// Sanitize command data to prevent injection attacks
|
|
153
|
+
const rawCommand = {
|
|
131
154
|
name: cmdName,
|
|
132
155
|
description: frontmatter.description || '',
|
|
133
156
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
134
|
-
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const sanitizedCommand = sanitizeCommandData(rawCommand);
|
|
160
|
+
if (!sanitizedCommand.name || sanitizedCommand.name === 'unknown') {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
commands.push(sanitizedCommand);
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
// Scan subdirectories (e.g., session/)
|
|
@@ -163,20 +193,31 @@ function generateCommandList(commandsDir) {
|
|
|
163
193
|
continue;
|
|
164
194
|
}
|
|
165
195
|
|
|
166
|
-
|
|
196
|
+
// Sanitize command data
|
|
197
|
+
const rawCommand = {
|
|
167
198
|
name: cmdName,
|
|
168
199
|
description: frontmatter.description || '',
|
|
169
200
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
170
|
-
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const sanitizedCommand = sanitizeCommandData(rawCommand);
|
|
204
|
+
if (!sanitizedCommand.name || sanitizedCommand.name === 'unknown') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
commands.push(sanitizedCommand);
|
|
171
209
|
}
|
|
172
210
|
}
|
|
173
211
|
}
|
|
174
212
|
|
|
175
213
|
commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
176
214
|
|
|
177
|
-
|
|
215
|
+
// Sanitize the count value
|
|
216
|
+
const safeCount = sanitize.count(commands.length);
|
|
217
|
+
let output = `Available commands (${safeCount} total):\n`;
|
|
178
218
|
|
|
179
219
|
commands.forEach(cmd => {
|
|
220
|
+
// All values are already sanitized by sanitizeCommandData
|
|
180
221
|
const argHint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';
|
|
181
222
|
output += `- \`/agileflow:${cmd.name}${argHint}\` - ${cmd.description}\n`;
|
|
182
223
|
});
|
|
@@ -208,16 +249,28 @@ function injectContent(content, context = {}) {
|
|
|
208
249
|
counts = getCounts(coreDir);
|
|
209
250
|
}
|
|
210
251
|
|
|
252
|
+
// Validate and sanitize all placeholder values before injection
|
|
253
|
+
const safeCommandCount = validatePlaceholderValue('COMMAND_COUNT', counts.commands).sanitized;
|
|
254
|
+
const safeAgentCount = validatePlaceholderValue('AGENT_COUNT', counts.agents).sanitized;
|
|
255
|
+
const safeSkillCount = validatePlaceholderValue('SKILL_COUNT', counts.skills).sanitized;
|
|
256
|
+
const safeVersion = validatePlaceholderValue('VERSION', version).sanitized;
|
|
257
|
+
const safeDate = validatePlaceholderValue('INSTALL_DATE', new Date()).sanitized;
|
|
258
|
+
const safeAgileflowFolder = validatePlaceholderValue(
|
|
259
|
+
'agileflow_folder',
|
|
260
|
+
agileflowFolder
|
|
261
|
+
).sanitized;
|
|
262
|
+
|
|
211
263
|
// Replace count placeholders (both formats: {{X}} and <!-- {{X}} -->)
|
|
212
|
-
result = result.replace(/\{\{COMMAND_COUNT\}\}/g, String(
|
|
213
|
-
result = result.replace(/\{\{AGENT_COUNT\}\}/g, String(
|
|
214
|
-
result = result.replace(/\{\{SKILL_COUNT\}\}/g, String(
|
|
264
|
+
result = result.replace(/\{\{COMMAND_COUNT\}\}/g, String(safeCommandCount));
|
|
265
|
+
result = result.replace(/\{\{AGENT_COUNT\}\}/g, String(safeAgentCount));
|
|
266
|
+
result = result.replace(/\{\{SKILL_COUNT\}\}/g, String(safeSkillCount));
|
|
215
267
|
|
|
216
268
|
// Replace metadata placeholders
|
|
217
|
-
result = result.replace(/\{\{VERSION\}\}/g,
|
|
218
|
-
result = result.replace(/\{\{INSTALL_DATE\}\}/g,
|
|
269
|
+
result = result.replace(/\{\{VERSION\}\}/g, safeVersion);
|
|
270
|
+
result = result.replace(/\{\{INSTALL_DATE\}\}/g, safeDate);
|
|
219
271
|
|
|
220
272
|
// Replace list placeholders (only if core directory available)
|
|
273
|
+
// List generation already includes sanitization via sanitizeAgentData/sanitizeCommandData
|
|
221
274
|
if (coreDir && fs.existsSync(coreDir)) {
|
|
222
275
|
if (result.includes('{{AGENT_LIST}}')) {
|
|
223
276
|
const agentList = generateAgentList(path.join(coreDir, 'agents'));
|
|
@@ -232,8 +285,8 @@ function injectContent(content, context = {}) {
|
|
|
232
285
|
}
|
|
233
286
|
}
|
|
234
287
|
|
|
235
|
-
// Replace folder placeholders
|
|
236
|
-
result = result.replace(/\{agileflow_folder\}/g,
|
|
288
|
+
// Replace folder placeholders with sanitized values
|
|
289
|
+
result = result.replace(/\{agileflow_folder\}/g, safeAgileflowFolder);
|
|
237
290
|
result = result.replace(/\{project-root\}/g, '{project-root}'); // Keep as-is for runtime
|
|
238
291
|
|
|
239
292
|
return result;
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* Provides specific error types for common IDE setup failures.
|
|
5
5
|
* These errors carry context about what failed and why,
|
|
6
6
|
* enabling better error handling and user feedback.
|
|
7
|
+
*
|
|
8
|
+
* Integration with error-codes.js:
|
|
9
|
+
* - All IDE errors now have errorCode, severity, category metadata
|
|
10
|
+
* - Use formatError() from error-codes.js for consistent display
|
|
11
|
+
* - isRecoverable() works with these errors
|
|
7
12
|
*/
|
|
8
13
|
|
|
14
|
+
const { ErrorCodes, Severity, Category } = require('../../../lib/error-codes');
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Base error class for IDE-related errors.
|
|
11
18
|
* All IDE errors extend this class.
|
|
@@ -15,13 +22,22 @@ class IdeError extends Error {
|
|
|
15
22
|
* @param {string} message - Error description
|
|
16
23
|
* @param {string} ideName - Name of the IDE (e.g., 'Claude Code', 'Cursor')
|
|
17
24
|
* @param {Object} [context={}] - Additional context about the error
|
|
25
|
+
* @param {string} [errorCode='EUNKNOWN'] - Error code from error-codes.js
|
|
18
26
|
*/
|
|
19
|
-
constructor(message, ideName, context = {}) {
|
|
27
|
+
constructor(message, ideName, context = {}, errorCode = 'EUNKNOWN') {
|
|
20
28
|
super(message);
|
|
21
29
|
this.name = this.constructor.name;
|
|
22
30
|
this.ideName = ideName;
|
|
23
31
|
this.context = context;
|
|
24
32
|
Error.captureStackTrace(this, this.constructor);
|
|
33
|
+
|
|
34
|
+
// Attach error code metadata from unified error codes
|
|
35
|
+
const codeData = ErrorCodes[errorCode] || ErrorCodes.EUNKNOWN;
|
|
36
|
+
this.errorCode = codeData.code;
|
|
37
|
+
this.severity = codeData.severity;
|
|
38
|
+
this.category = codeData.category;
|
|
39
|
+
this.recoverable = codeData.recoverable;
|
|
40
|
+
this.autoFix = codeData.autoFix || null;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
/**
|
|
@@ -31,6 +47,16 @@ class IdeError extends Error {
|
|
|
31
47
|
getUserMessage() {
|
|
32
48
|
return `${this.ideName}: ${this.message}`;
|
|
33
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get suggested action to fix the error
|
|
53
|
+
* Override in subclasses for specific suggestions
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
getSuggestedAction() {
|
|
57
|
+
const codeData = ErrorCodes[this.errorCode] || ErrorCodes.EUNKNOWN;
|
|
58
|
+
return codeData.suggestedFix;
|
|
59
|
+
}
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
/**
|
|
@@ -44,10 +70,12 @@ class IdeConfigNotFoundError extends IdeError {
|
|
|
44
70
|
* @param {Object} [context={}] - Additional context
|
|
45
71
|
*/
|
|
46
72
|
constructor(ideName, configPath, context = {}) {
|
|
47
|
-
super(
|
|
48
|
-
configPath
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
super(
|
|
74
|
+
`Configuration directory not found: ${configPath}`,
|
|
75
|
+
ideName,
|
|
76
|
+
{ configPath, ...context },
|
|
77
|
+
'ENODIR' // Use unified error code
|
|
78
|
+
);
|
|
51
79
|
this.configPath = configPath;
|
|
52
80
|
}
|
|
53
81
|
|
|
@@ -72,11 +100,23 @@ class CommandInstallationError extends IdeError {
|
|
|
72
100
|
* @param {Object} [context={}] - Additional context
|
|
73
101
|
*/
|
|
74
102
|
constructor(ideName, commandName, reason, context = {}) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
103
|
+
// Detect appropriate error code from reason
|
|
104
|
+
let errorCode = 'ESTATE';
|
|
105
|
+
if (reason.toLowerCase().includes('permission')) {
|
|
106
|
+
errorCode = 'EACCES';
|
|
107
|
+
} else if (
|
|
108
|
+
reason.toLowerCase().includes('not found') ||
|
|
109
|
+
reason.toLowerCase().includes('no such')
|
|
110
|
+
) {
|
|
111
|
+
errorCode = 'ENOENT';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
super(
|
|
115
|
+
`Failed to install command '${commandName}': ${reason}`,
|
|
116
|
+
ideName,
|
|
117
|
+
{ commandName, reason, ...context },
|
|
118
|
+
errorCode
|
|
119
|
+
);
|
|
80
120
|
this.commandName = commandName;
|
|
81
121
|
this.reason = reason;
|
|
82
122
|
}
|
|
@@ -108,11 +148,12 @@ class FilePermissionError extends IdeError {
|
|
|
108
148
|
* @param {Object} [context={}] - Additional context
|
|
109
149
|
*/
|
|
110
150
|
constructor(ideName, filePath, operation, context = {}) {
|
|
111
|
-
super(
|
|
112
|
-
filePath
|
|
113
|
-
|
|
114
|
-
...context,
|
|
115
|
-
|
|
151
|
+
super(
|
|
152
|
+
`Permission denied: cannot ${operation} '${filePath}'`,
|
|
153
|
+
ideName,
|
|
154
|
+
{ filePath, operation, ...context },
|
|
155
|
+
'EACCES' // Use unified error code
|
|
156
|
+
);
|
|
116
157
|
this.filePath = filePath;
|
|
117
158
|
this.operation = operation;
|
|
118
159
|
}
|
|
@@ -138,14 +179,23 @@ class ContentInjectionError extends IdeError {
|
|
|
138
179
|
* @param {Object} [context={}] - Additional context
|
|
139
180
|
*/
|
|
140
181
|
constructor(ideName, templateFile, reason, context = {}) {
|
|
141
|
-
super(
|
|
142
|
-
templateFile
|
|
143
|
-
|
|
144
|
-
...context,
|
|
145
|
-
|
|
182
|
+
super(
|
|
183
|
+
`Content injection failed for '${templateFile}': ${reason}`,
|
|
184
|
+
ideName,
|
|
185
|
+
{ templateFile, reason, ...context },
|
|
186
|
+
'ECONFIG' // Use unified error code - configuration/template issue
|
|
187
|
+
);
|
|
146
188
|
this.templateFile = templateFile;
|
|
147
189
|
this.reason = reason;
|
|
148
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get suggested action to fix the error
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
getSuggestedAction() {
|
|
197
|
+
return `Check the template file '${this.templateFile}' for valid placeholders. Run "npx agileflow doctor --fix" to repair.`;
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
|
|
151
201
|
/**
|
|
@@ -160,14 +210,34 @@ class CleanupError extends IdeError {
|
|
|
160
210
|
* @param {Object} [context={}] - Additional context
|
|
161
211
|
*/
|
|
162
212
|
constructor(ideName, targetPath, reason, context = {}) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
213
|
+
// Detect appropriate error code from reason
|
|
214
|
+
let errorCode = 'ESTATE';
|
|
215
|
+
if (reason.toLowerCase().includes('lock') || reason.toLowerCase().includes('busy')) {
|
|
216
|
+
errorCode = 'ELOCK';
|
|
217
|
+
} else if (reason.toLowerCase().includes('permission')) {
|
|
218
|
+
errorCode = 'EACCES';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
super(
|
|
222
|
+
`Cleanup failed for '${targetPath}': ${reason}`,
|
|
223
|
+
ideName,
|
|
224
|
+
{ targetPath, reason, ...context },
|
|
225
|
+
errorCode
|
|
226
|
+
);
|
|
168
227
|
this.targetPath = targetPath;
|
|
169
228
|
this.reason = reason;
|
|
170
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get suggested action to fix the error
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
getSuggestedAction() {
|
|
236
|
+
if (this.errorCode === 'ELOCK') {
|
|
237
|
+
return `Close any applications using files in '${this.targetPath}' and try again.`;
|
|
238
|
+
}
|
|
239
|
+
return `Check permissions on '${this.targetPath}' or remove it manually.`;
|
|
240
|
+
}
|
|
171
241
|
}
|
|
172
242
|
|
|
173
243
|
/**
|
|
@@ -181,12 +251,29 @@ class IdeDetectionError extends IdeError {
|
|
|
181
251
|
* @param {Object} [context={}] - Additional context
|
|
182
252
|
*/
|
|
183
253
|
constructor(ideName, reason, context = {}) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
254
|
+
// Detect appropriate error code
|
|
255
|
+
let errorCode = 'ECONFLICT';
|
|
256
|
+
if (
|
|
257
|
+
reason.toLowerCase().includes('not found') ||
|
|
258
|
+
reason.toLowerCase().includes('not installed')
|
|
259
|
+
) {
|
|
260
|
+
errorCode = 'ENOENT';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
super(`IDE detection failed: ${reason}`, ideName, { reason, ...context }, errorCode);
|
|
188
264
|
this.reason = reason;
|
|
189
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get suggested action to fix the error
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
getSuggestedAction() {
|
|
272
|
+
if (this.errorCode === 'ENOENT') {
|
|
273
|
+
return `Install ${this.ideName} and run it at least once to initialize configuration.`;
|
|
274
|
+
}
|
|
275
|
+
return `Check IDE configuration and resolve any conflicts. Run "npx agileflow doctor" for details.`;
|
|
276
|
+
}
|
|
190
277
|
}
|
|
191
278
|
|
|
192
279
|
/**
|
|
@@ -220,6 +307,52 @@ function isIdeError(error) {
|
|
|
220
307
|
return error instanceof IdeError;
|
|
221
308
|
}
|
|
222
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Format an IDE error for display using unified error code format
|
|
312
|
+
* @param {IdeError} error - IDE error to format
|
|
313
|
+
* @param {Object} [options={}] - Format options
|
|
314
|
+
* @param {boolean} [options.includeStack=false] - Include stack trace
|
|
315
|
+
* @param {boolean} [options.includeSuggestion=true] - Include suggested action
|
|
316
|
+
* @returns {string} Formatted error string
|
|
317
|
+
*/
|
|
318
|
+
function formatIdeError(error, options = {}) {
|
|
319
|
+
const { includeStack = false, includeSuggestion = true } = options;
|
|
320
|
+
|
|
321
|
+
if (!error) return 'Unknown error';
|
|
322
|
+
|
|
323
|
+
const lines = [];
|
|
324
|
+
|
|
325
|
+
// Main error line with IDE name and error code
|
|
326
|
+
lines.push(`[${error.errorCode}] ${error.ideName}: ${error.message}`);
|
|
327
|
+
|
|
328
|
+
// Severity and category
|
|
329
|
+
lines.push(` Severity: ${error.severity} | Category: ${error.category}`);
|
|
330
|
+
|
|
331
|
+
// Suggested action (IDE-specific takes precedence)
|
|
332
|
+
if (includeSuggestion) {
|
|
333
|
+
const suggestion = error.getSuggestedAction?.() || error.suggestedFix;
|
|
334
|
+
if (suggestion) {
|
|
335
|
+
lines.push(` Fix: ${suggestion}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Recoverable status
|
|
340
|
+
lines.push(` Recoverable: ${error.recoverable ? 'Yes' : 'No'}`);
|
|
341
|
+
|
|
342
|
+
// Auto-fix availability
|
|
343
|
+
if (error.autoFix) {
|
|
344
|
+
lines.push(` Auto-fix available: npx agileflow doctor --fix`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Stack trace
|
|
348
|
+
if (includeStack && error.stack) {
|
|
349
|
+
lines.push('');
|
|
350
|
+
lines.push(error.stack);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return lines.join('\n');
|
|
354
|
+
}
|
|
355
|
+
|
|
223
356
|
module.exports = {
|
|
224
357
|
IdeError,
|
|
225
358
|
IdeConfigNotFoundError,
|
|
@@ -230,4 +363,5 @@ module.exports = {
|
|
|
230
363
|
IdeDetectionError,
|
|
231
364
|
withPermissionHandling,
|
|
232
365
|
isIdeError,
|
|
366
|
+
formatIdeError,
|
|
233
367
|
};
|