@provartesting/provardx-cli 1.4.7 → 1.5.0-dev.1
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.md +264 -9
- package/lib/commands/provar/automation/config/validate.js.map +1 -1
- package/lib/commands/provar/automation/project/validate.d.ts +14 -0
- package/lib/commands/provar/automation/project/validate.js +69 -0
- package/lib/commands/provar/automation/project/validate.js.map +1 -0
- package/lib/commands/provar/mcp/start.d.ts +16 -0
- package/lib/commands/provar/mcp/start.js +62 -0
- package/lib/commands/provar/mcp/start.js.map +1 -0
- package/lib/commands/provar/quality-hub/connect.d.ts +5 -0
- package/lib/commands/provar/quality-hub/connect.js +12 -0
- package/lib/commands/provar/quality-hub/connect.js.map +1 -0
- package/lib/commands/provar/quality-hub/display.d.ts +5 -0
- package/lib/commands/provar/quality-hub/display.js +12 -0
- package/lib/commands/provar/quality-hub/display.js.map +1 -0
- package/lib/commands/provar/quality-hub/open.d.ts +5 -0
- package/lib/commands/provar/quality-hub/open.js +12 -0
- package/lib/commands/provar/quality-hub/open.js.map +1 -0
- package/lib/commands/provar/quality-hub/test/run/abort.d.ts +5 -0
- package/lib/commands/provar/quality-hub/test/run/abort.js +12 -0
- package/lib/commands/provar/quality-hub/test/run/abort.js.map +1 -0
- package/lib/commands/provar/quality-hub/test/run/report.d.ts +5 -0
- package/lib/commands/provar/quality-hub/test/run/report.js +12 -0
- package/lib/commands/provar/quality-hub/test/run/report.js.map +1 -0
- package/lib/commands/provar/quality-hub/test/run.d.ts +5 -0
- package/lib/commands/provar/quality-hub/test/run.js +12 -0
- package/lib/commands/provar/quality-hub/test/run.js.map +1 -0
- package/lib/commands/provar/quality-hub/testcase/retrieve.d.ts +5 -0
- package/lib/commands/provar/quality-hub/testcase/retrieve.js +12 -0
- package/lib/commands/provar/quality-hub/testcase/retrieve.js.map +1 -0
- package/lib/mcp/licensing/algasClient.d.ts +19 -0
- package/lib/mcp/licensing/algasClient.js +144 -0
- package/lib/mcp/licensing/algasClient.js.map +1 -0
- package/lib/mcp/licensing/ideDetection.d.ts +34 -0
- package/lib/mcp/licensing/ideDetection.js +179 -0
- package/lib/mcp/licensing/ideDetection.js.map +1 -0
- package/lib/mcp/licensing/index.d.ts +5 -0
- package/lib/mcp/licensing/index.js +10 -0
- package/lib/mcp/licensing/index.js.map +1 -0
- package/lib/mcp/licensing/licenseCache.d.ts +20 -0
- package/lib/mcp/licensing/licenseCache.js +79 -0
- package/lib/mcp/licensing/licenseCache.js.map +1 -0
- package/lib/mcp/licensing/licenseError.d.ts +4 -0
- package/lib/mcp/licensing/licenseError.js +15 -0
- package/lib/mcp/licensing/licenseError.js.map +1 -0
- package/lib/mcp/licensing/licenseValidator.d.ts +33 -0
- package/lib/mcp/licensing/licenseValidator.js +103 -0
- package/lib/mcp/licensing/licenseValidator.js.map +1 -0
- package/lib/mcp/logging/logger.d.ts +7 -0
- package/lib/mcp/logging/logger.js +22 -0
- package/lib/mcp/logging/logger.js.map +1 -0
- package/lib/mcp/rules/page_object_validation_rules.json +344 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +3192 -0
- package/lib/mcp/schemas/common.d.ts +20 -0
- package/lib/mcp/schemas/common.js +16 -0
- package/lib/mcp/schemas/common.js.map +1 -0
- package/lib/mcp/security/pathPolicy.d.ts +14 -0
- package/lib/mcp/security/pathPolicy.js +38 -0
- package/lib/mcp/security/pathPolicy.js.map +1 -0
- package/lib/mcp/server.d.ts +5 -0
- package/lib/mcp/server.js +59 -0
- package/lib/mcp/server.js.map +1 -0
- package/lib/mcp/tools/antTools.d.ts +21 -0
- package/lib/mcp/tools/antTools.js +602 -0
- package/lib/mcp/tools/antTools.js.map +1 -0
- package/lib/mcp/tools/automationTools.d.ts +14 -0
- package/lib/mcp/tools/automationTools.js +386 -0
- package/lib/mcp/tools/automationTools.js.map +1 -0
- package/lib/mcp/tools/bestPracticesEngine.d.ts +30 -0
- package/lib/mcp/tools/bestPracticesEngine.js +632 -0
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -0
- package/lib/mcp/tools/defectTools.d.ts +15 -0
- package/lib/mcp/tools/defectTools.js +199 -0
- package/lib/mcp/tools/defectTools.js.map +1 -0
- package/lib/mcp/tools/hierarchyValidate.d.ts +139 -0
- package/lib/mcp/tools/hierarchyValidate.js +540 -0
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.d.ts +3 -0
- package/lib/mcp/tools/pageObjectGenerate.js +153 -0
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -0
- package/lib/mcp/tools/pageObjectValidate.d.ts +18 -0
- package/lib/mcp/tools/pageObjectValidate.js +420 -0
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -0
- package/lib/mcp/tools/projectInspect.d.ts +3 -0
- package/lib/mcp/tools/projectInspect.js +694 -0
- package/lib/mcp/tools/projectInspect.js.map +1 -0
- package/lib/mcp/tools/projectValidateFromPath.d.ts +3 -0
- package/lib/mcp/tools/projectValidateFromPath.js +153 -0
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -0
- package/lib/mcp/tools/propertiesTools.d.ts +7 -0
- package/lib/mcp/tools/propertiesTools.js +314 -0
- package/lib/mcp/tools/propertiesTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.d.ts +8 -0
- package/lib/mcp/tools/qualityHubTools.js +178 -0
- package/lib/mcp/tools/qualityHubTools.js.map +1 -0
- package/lib/mcp/tools/rcaTools.d.ts +4 -0
- package/lib/mcp/tools/rcaTools.js +620 -0
- package/lib/mcp/tools/rcaTools.js.map +1 -0
- package/lib/mcp/tools/sfSpawn.d.ts +28 -0
- package/lib/mcp/tools/sfSpawn.js +50 -0
- package/lib/mcp/tools/sfSpawn.js.map +1 -0
- package/lib/mcp/tools/testCaseGenerate.d.ts +3 -0
- package/lib/mcp/tools/testCaseGenerate.js +221 -0
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -0
- package/lib/mcp/tools/testCaseValidate.d.ts +20 -0
- package/lib/mcp/tools/testCaseValidate.js +227 -0
- package/lib/mcp/tools/testCaseValidate.js.map +1 -0
- package/lib/mcp/tools/testPlanTools.d.ts +6 -0
- package/lib/mcp/tools/testPlanTools.js +311 -0
- package/lib/mcp/tools/testPlanTools.js.map +1 -0
- package/lib/mcp/tools/testPlanValidate.d.ts +2 -0
- package/lib/mcp/tools/testPlanValidate.js +75 -0
- package/lib/mcp/tools/testPlanValidate.js.map +1 -0
- package/lib/mcp/tools/testSuiteValidate.d.ts +2 -0
- package/lib/mcp/tools/testSuiteValidate.js +63 -0
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -0
- package/lib/services/projectValidation.d.ts +119 -0
- package/lib/services/projectValidation.js +678 -0
- package/lib/services/projectValidation.js.map +1 -0
- package/messages/sf.provar.automation.project.validate.md +52 -0
- package/messages/sf.provar.mcp.start.md +74 -0
- package/oclif.manifest.json +1298 -1
- package/package.json +46 -23
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Provar Limited.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* Licensed under the BSD 3-Clause license.
|
|
5
|
+
* For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable camelcase */
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
|
|
12
|
+
import { makeError, makeRequestId } from '../schemas/common.js';
|
|
13
|
+
import { log } from '../logging/logger.js';
|
|
14
|
+
export function registerProjectInspect(server, config) {
|
|
15
|
+
server.tool('provar.project.inspect', [
|
|
16
|
+
'Inspect a Provar project folder and return a structured inventory.',
|
|
17
|
+
'Returns: provardx-properties.json config files (for ProvarDX CLI runs),',
|
|
18
|
+
'ANT build files (build.xml etc in ANT/ dirs, for CLI/pipeline runs),',
|
|
19
|
+
'source page object directories with Java file counts (src/pageobjects — compiled bin/ dirs excluded),',
|
|
20
|
+
'.testcase files found recursively under tests/,',
|
|
21
|
+
'count of custom test step files in src/customapis/,',
|
|
22
|
+
'count of data source files (CSV/XLSX/JSON) in data/ and templates/ dirs,',
|
|
23
|
+
'test plan coverage showing which test cases are covered vs uncovered,',
|
|
24
|
+
'and connection + environment overview parsed from the .testproject file',
|
|
25
|
+
'(Salesforce, UI Testing, Web Services, Quality Hub, Database, and other connection types).',
|
|
26
|
+
].join(' '), {
|
|
27
|
+
project_path: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe('Absolute or relative path to the Provar project root directory'),
|
|
30
|
+
}, ({ project_path }) => {
|
|
31
|
+
const requestId = makeRequestId();
|
|
32
|
+
log('info', 'provar.project.inspect', { requestId, project_path });
|
|
33
|
+
try {
|
|
34
|
+
assertPathAllowed(project_path, config.allowedPaths);
|
|
35
|
+
const resolved = path.resolve(project_path);
|
|
36
|
+
if (!fs.existsSync(resolved)) {
|
|
37
|
+
const err = makeError('PATH_NOT_FOUND', `Project path does not exist: ${resolved}`, requestId);
|
|
38
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
|
|
39
|
+
}
|
|
40
|
+
const result = buildProjectInventory(resolved, requestId);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
43
|
+
structuredContent: result,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const error = err;
|
|
48
|
+
const errResult = makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'INSPECT_ERROR'), error.message, requestId, false);
|
|
49
|
+
log('error', 'provar.project.inspect failed', { requestId, error: error.message });
|
|
50
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// ─── inventory builder ────────────────────────────────────────────────────────
|
|
55
|
+
function buildProjectInventory(projectPath, requestId) {
|
|
56
|
+
const provardxPropertiesFiles = [];
|
|
57
|
+
const antBuildFiles = [];
|
|
58
|
+
const sourcePageObjectDirs = [];
|
|
59
|
+
const testCaseFilesDisplay = []; // capped at 500 for API display
|
|
60
|
+
const allTestCasePaths = new Set(); // uncapped — used for coverage
|
|
61
|
+
let customTestStepFileCount = 0;
|
|
62
|
+
let dataSourceFileCount = 0;
|
|
63
|
+
const dataSourceDirs = [];
|
|
64
|
+
walkDir(projectPath, (filePath, isDir, name) => {
|
|
65
|
+
const rel = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
66
|
+
if (isDir) {
|
|
67
|
+
if (name === 'bin')
|
|
68
|
+
return false;
|
|
69
|
+
if (name === 'plans')
|
|
70
|
+
return false; // handled by buildPlanCoverage (needs dot-file visibility)
|
|
71
|
+
if (name === 'pageobjects') {
|
|
72
|
+
const parentRel = path.relative(projectPath, path.dirname(filePath)).replace(/\\/g, '/');
|
|
73
|
+
if (parentRel === 'src' || parentRel.endsWith('/src')) {
|
|
74
|
+
// Count .java files now — don't rely on recursion (we return false below)
|
|
75
|
+
const javaCount = countFilesRecursive(filePath, (n) => n.endsWith('.java'));
|
|
76
|
+
sourcePageObjectDirs.push({ path: rel, java_file_count: javaCount });
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (name === 'customapis') {
|
|
81
|
+
customTestStepFileCount += countFilesRecursive(filePath, isSourceFile);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (name === 'data' || name === 'templates') {
|
|
85
|
+
dataSourceDirs.push(rel);
|
|
86
|
+
dataSourceFileCount += countFilesRecursive(filePath, isDataFile);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (name === 'provardx-properties.json') {
|
|
92
|
+
provardxPropertiesFiles.push(rel);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (name.endsWith('.testcase') && (rel.startsWith('tests/') || rel.includes('/tests/'))) {
|
|
96
|
+
allTestCasePaths.add(rel);
|
|
97
|
+
if (testCaseFilesDisplay.length < 500)
|
|
98
|
+
testCaseFilesDisplay.push(rel);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (rel.startsWith('ANT/') || rel.includes('/ANT/')) {
|
|
102
|
+
if (name.endsWith('.xml') || name.endsWith('.properties'))
|
|
103
|
+
antBuildFiles.push(rel);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
const { provarHome, provarHomeSource } = detectProvarHome(projectPath, provardxPropertiesFiles, antBuildFiles);
|
|
109
|
+
const testSuiteDirs = getTopLevelTestSuites(projectPath);
|
|
110
|
+
const planCoverage = buildPlanCoverage(projectPath, allTestCasePaths);
|
|
111
|
+
const testProject = parseTestProject(projectPath);
|
|
112
|
+
const secretsValidation = validateSecretsFile(projectPath, testProject);
|
|
113
|
+
return {
|
|
114
|
+
requestId,
|
|
115
|
+
project_path: projectPath,
|
|
116
|
+
provar_home: provarHome,
|
|
117
|
+
provar_home_source: provarHomeSource,
|
|
118
|
+
provardx_properties_files: provardxPropertiesFiles,
|
|
119
|
+
ant_build_files: antBuildFiles,
|
|
120
|
+
source_page_object_dirs: sourcePageObjectDirs,
|
|
121
|
+
test_suite_dirs: testSuiteDirs,
|
|
122
|
+
test_case_files: testCaseFilesDisplay,
|
|
123
|
+
custom_test_step_file_count: customTestStepFileCount,
|
|
124
|
+
data_source_dirs: dataSourceDirs,
|
|
125
|
+
data_source_file_count: dataSourceFileCount,
|
|
126
|
+
test_plan_coverage: planCoverage,
|
|
127
|
+
test_project: testProject,
|
|
128
|
+
secrets_validation: secretsValidation,
|
|
129
|
+
summary: {
|
|
130
|
+
provardx_properties_count: provardxPropertiesFiles.length,
|
|
131
|
+
ant_build_file_count: antBuildFiles.length,
|
|
132
|
+
source_page_object_dir_count: sourcePageObjectDirs.length,
|
|
133
|
+
page_object_file_count: sourcePageObjectDirs.reduce((s, d) => s + d.java_file_count, 0),
|
|
134
|
+
test_suite_count: testSuiteDirs.length,
|
|
135
|
+
test_case_count: allTestCasePaths.size,
|
|
136
|
+
custom_test_step_count: customTestStepFileCount,
|
|
137
|
+
data_source_count: dataSourceFileCount,
|
|
138
|
+
test_plan_count: planCoverage.test_plan_count,
|
|
139
|
+
test_suites_in_plans_count: planCoverage.test_suite_count,
|
|
140
|
+
test_instance_count: planCoverage.test_instance_count,
|
|
141
|
+
coverage_percent: planCoverage.coverage_percent,
|
|
142
|
+
environment_count: testProject.environments.length,
|
|
143
|
+
connection_count: testProject.connections.summary['total'] ?? 0,
|
|
144
|
+
secrets_encrypted: secretsValidation.all_values_encrypted,
|
|
145
|
+
unencrypted_secret_count: secretsValidation.unencrypted_key_count,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ─── .testproject parser ──────────────────────────────────────────────────────
|
|
150
|
+
/**
|
|
151
|
+
* Parses the .testproject file (a hidden dot-file at the project root).
|
|
152
|
+
* Extracts environments and connection overview.
|
|
153
|
+
*
|
|
154
|
+
* Connection class → label mapping (verified across 136 POC projects):
|
|
155
|
+
* - sf → Salesforce (sub-type detection: communities / portal / standard)
|
|
156
|
+
* - ui → UI Testing (browser/Selenium connections)
|
|
157
|
+
* - testmanager → Provar Quality Hub
|
|
158
|
+
* - webservice → Web Service REST (url starts with "restservice:") or SOAP
|
|
159
|
+
* - google → Google (Gmail / Google Workspace)
|
|
160
|
+
* - msexc → Microsoft (Exchange / Outlook via EWS — url starts with "exchange:")
|
|
161
|
+
* - database → Database (Oracle, SQL Server, DB2, MySQL, PostgreSQL — rare in field)
|
|
162
|
+
* - zephyr / zephyrScale / zephyrServer → Zephyr (Cloud & Server)
|
|
163
|
+
* - sso → SSO
|
|
164
|
+
* - anything else → other (raw class name preserved for forward-compatibility)
|
|
165
|
+
*/
|
|
166
|
+
function parseTestProject(projectPath) {
|
|
167
|
+
const testProjectPath = path.join(projectPath, '.testproject');
|
|
168
|
+
const empty = {
|
|
169
|
+
found: false,
|
|
170
|
+
file_path: null,
|
|
171
|
+
environments: [],
|
|
172
|
+
connections: emptyConnectionOverview(),
|
|
173
|
+
};
|
|
174
|
+
if (!fs.existsSync(testProjectPath))
|
|
175
|
+
return empty;
|
|
176
|
+
let content;
|
|
177
|
+
try {
|
|
178
|
+
content = fs.readFileSync(testProjectPath, 'utf-8');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return empty;
|
|
182
|
+
}
|
|
183
|
+
const environments = parseEnvironments(content);
|
|
184
|
+
const connections = parseConnectionClasses(content);
|
|
185
|
+
return {
|
|
186
|
+
found: true,
|
|
187
|
+
file_path: '.testproject',
|
|
188
|
+
environments,
|
|
189
|
+
connections,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// ─── .secrets validator ───────────────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Reads the .secrets file (a Java properties file) and checks that every
|
|
195
|
+
* credential value is wrapped in an ENC1() block.
|
|
196
|
+
*
|
|
197
|
+
* Security note: only key NAMES are returned — plaintext values are never
|
|
198
|
+
* included in the output, even for unencrypted entries.
|
|
199
|
+
*
|
|
200
|
+
* The secrets file path comes from <secureStoragePath> in .testproject
|
|
201
|
+
* (defaults to ".secrets" at the project root).
|
|
202
|
+
*
|
|
203
|
+
* File format:
|
|
204
|
+
* - `# comment`
|
|
205
|
+
* - `<uuid>.<field>=ENC1(base64...)` — correctly encrypted
|
|
206
|
+
* - `<uuid>.<field>=plaintextpassword` — VIOLATION
|
|
207
|
+
* - `Encryptor.check=ENC1(...)` — sentinel (present when password is configured)
|
|
208
|
+
*/
|
|
209
|
+
function validateSecretsFile(projectPath, testProject) {
|
|
210
|
+
// Resolve path from .testproject <secureStoragePath>, defaulting to ".secrets"
|
|
211
|
+
let secretsRelPath = '.secrets';
|
|
212
|
+
if (testProject.found) {
|
|
213
|
+
try {
|
|
214
|
+
const raw = fs.readFileSync(path.join(projectPath, '.testproject'), 'utf-8');
|
|
215
|
+
const match = raw.match(/<secureStoragePath>([^<]+)<\/secureStoragePath>/);
|
|
216
|
+
if (match?.[1])
|
|
217
|
+
secretsRelPath = match[1].trim();
|
|
218
|
+
}
|
|
219
|
+
catch { /* use default */ }
|
|
220
|
+
}
|
|
221
|
+
const notFound = {
|
|
222
|
+
secrets_file: secretsRelPath,
|
|
223
|
+
found: false,
|
|
224
|
+
encryptor_check_present: false,
|
|
225
|
+
all_values_encrypted: false,
|
|
226
|
+
total_entries: 0,
|
|
227
|
+
encrypted_count: 0,
|
|
228
|
+
unencrypted_key_count: 0,
|
|
229
|
+
unencrypted_keys: [],
|
|
230
|
+
};
|
|
231
|
+
const secretsPath = path.join(projectPath, secretsRelPath);
|
|
232
|
+
if (!fs.existsSync(secretsPath))
|
|
233
|
+
return notFound;
|
|
234
|
+
let content;
|
|
235
|
+
try {
|
|
236
|
+
content = fs.readFileSync(secretsPath, 'utf-8');
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return notFound;
|
|
240
|
+
}
|
|
241
|
+
let encryptorCheckPresent = false;
|
|
242
|
+
let totalEntries = 0;
|
|
243
|
+
let encryptedCount = 0;
|
|
244
|
+
const unencryptedKeys = [];
|
|
245
|
+
for (const rawLine of content.split('\n')) {
|
|
246
|
+
const line = rawLine.trimEnd();
|
|
247
|
+
// Skip blank lines and comment lines (Java properties # or ! prefix)
|
|
248
|
+
if (!line || line.startsWith('#') || line.startsWith('!'))
|
|
249
|
+
continue;
|
|
250
|
+
// Split on the FIRST unescaped '=' — everything before is the key
|
|
251
|
+
const eqIdx = line.indexOf('=');
|
|
252
|
+
if (eqIdx === -1)
|
|
253
|
+
continue;
|
|
254
|
+
const key = line.slice(0, eqIdx).trim();
|
|
255
|
+
const value = line.slice(eqIdx + 1); // do NOT trim — value may legitimately start with a space
|
|
256
|
+
if (key === 'Encryptor.check') {
|
|
257
|
+
// Sentinel entry — indicates Provar Secrets Password is configured
|
|
258
|
+
encryptorCheckPresent = true;
|
|
259
|
+
// Count it but don't add to unencrypted list even if somehow unwrapped
|
|
260
|
+
totalEntries++;
|
|
261
|
+
if (value.startsWith('ENC1('))
|
|
262
|
+
encryptedCount++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
totalEntries++;
|
|
266
|
+
if (value.startsWith('ENC1(')) {
|
|
267
|
+
encryptedCount++;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Only record the KEY name — never the plaintext value
|
|
271
|
+
unencryptedKeys.push(key);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
secrets_file: secretsRelPath,
|
|
276
|
+
found: true,
|
|
277
|
+
encryptor_check_present: encryptorCheckPresent,
|
|
278
|
+
all_values_encrypted: unencryptedKeys.length === 0,
|
|
279
|
+
total_entries: totalEntries,
|
|
280
|
+
encrypted_count: encryptedCount,
|
|
281
|
+
unencrypted_key_count: unencryptedKeys.length,
|
|
282
|
+
unencrypted_keys: unencryptedKeys,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function parseEnvironments(content) {
|
|
286
|
+
const names = [];
|
|
287
|
+
const section = content.match(/<environments>([\s\S]*?)<\/environments>/);
|
|
288
|
+
if (!section)
|
|
289
|
+
return names;
|
|
290
|
+
const pattern = /<environment\s[^>]*\bname="([^"]+)"/g;
|
|
291
|
+
let m;
|
|
292
|
+
while ((m = pattern.exec(section[1])) !== null)
|
|
293
|
+
names.push(m[1]);
|
|
294
|
+
return names;
|
|
295
|
+
}
|
|
296
|
+
function parseConnectionClasses(content) {
|
|
297
|
+
const result = emptyConnectionOverview();
|
|
298
|
+
const classesBlock = content.match(/<connectionClasses>([\s\S]*?)<\/connectionClasses>/);
|
|
299
|
+
if (!classesBlock)
|
|
300
|
+
return result;
|
|
301
|
+
const classPattern = /<connectionClass\s+name="([^"]+)">([\s\S]*?)<\/connectionClass>/g;
|
|
302
|
+
let classMatch;
|
|
303
|
+
while ((classMatch = classPattern.exec(classesBlock[1])) !== null) {
|
|
304
|
+
const className = classMatch[1];
|
|
305
|
+
const classContent = classMatch[2];
|
|
306
|
+
const connPattern = /<connection\s[^>]*\bname="([^"]+)"[^>]*>([\s\S]*?)<\/connection>/g;
|
|
307
|
+
let connMatch;
|
|
308
|
+
while ((connMatch = connPattern.exec(classContent)) !== null) {
|
|
309
|
+
const connName = connMatch[1];
|
|
310
|
+
const connContent = connMatch[2];
|
|
311
|
+
// Collect all url="..." values for this connection (may have per-environment overrides)
|
|
312
|
+
const urlPattern = /\burl="([^"]+)"/g;
|
|
313
|
+
const urls = [];
|
|
314
|
+
let urlMatch;
|
|
315
|
+
while ((urlMatch = urlPattern.exec(connContent)) !== null)
|
|
316
|
+
urls.push(urlMatch[1]);
|
|
317
|
+
categoriseConnection(result, className, connName, urls);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Build summary totals
|
|
321
|
+
const sfCommunities = result.salesforce.filter((c) => c.sub_type === 'communities').length;
|
|
322
|
+
const sfPortal = result.salesforce.filter((c) => c.sub_type === 'portal').length;
|
|
323
|
+
const sfLoginAs = result.salesforce.filter((c) => c.auth_method === 'login-as').length;
|
|
324
|
+
const sfOAuth = result.salesforce.filter((c) => c.auth_method === 'oauth').length;
|
|
325
|
+
const sfBasic = result.salesforce.filter((c) => c.auth_method === 'basic').length;
|
|
326
|
+
const sfDirectLogin = result.salesforce.filter((c) => c.is_direct_login).length;
|
|
327
|
+
result.summary = {
|
|
328
|
+
salesforce: result.salesforce.length,
|
|
329
|
+
salesforce_standard: result.salesforce.length - sfCommunities - sfPortal,
|
|
330
|
+
salesforce_communities: sfCommunities,
|
|
331
|
+
salesforce_portal: sfPortal,
|
|
332
|
+
salesforce_auth_login_as: sfLoginAs,
|
|
333
|
+
salesforce_auth_oauth: sfOAuth,
|
|
334
|
+
salesforce_auth_basic: sfBasic,
|
|
335
|
+
salesforce_direct_login: sfDirectLogin, // review these — may be admin users
|
|
336
|
+
ui_testing: result.ui_testing.length,
|
|
337
|
+
quality_hub: result.quality_hub.length,
|
|
338
|
+
web_service_rest: result.web_service_rest.length,
|
|
339
|
+
web_service_soap: result.web_service_soap.length,
|
|
340
|
+
database: result.database.length,
|
|
341
|
+
google: result.google.length,
|
|
342
|
+
microsoft: result.microsoft.length,
|
|
343
|
+
zephyr: result.zephyr.length,
|
|
344
|
+
sso: result.sso.length,
|
|
345
|
+
other: result.other.length,
|
|
346
|
+
total: result.salesforce.length +
|
|
347
|
+
result.ui_testing.length +
|
|
348
|
+
result.quality_hub.length +
|
|
349
|
+
result.web_service_rest.length +
|
|
350
|
+
result.web_service_soap.length +
|
|
351
|
+
result.database.length +
|
|
352
|
+
result.google.length +
|
|
353
|
+
result.microsoft.length +
|
|
354
|
+
result.zephyr.length +
|
|
355
|
+
result.sso.length +
|
|
356
|
+
result.other.length,
|
|
357
|
+
};
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
// eslint-disable-next-line complexity
|
|
361
|
+
function categoriseConnection(result, className, name, urls) {
|
|
362
|
+
// Use the first URL for type inference; env-override URLs share the same class
|
|
363
|
+
const primaryUrl = urls[0] ?? '';
|
|
364
|
+
switch (className) {
|
|
365
|
+
case 'sf': {
|
|
366
|
+
// ── sub-type ──────────────────────────────────────────────────────────
|
|
367
|
+
const isCommunities = primaryUrl.includes('userType=COMMUNITIES');
|
|
368
|
+
const isPortal = !isCommunities && primaryUrl.includes('portal=');
|
|
369
|
+
const subType = isCommunities
|
|
370
|
+
? 'communities'
|
|
371
|
+
: isPortal
|
|
372
|
+
? 'portal'
|
|
373
|
+
: 'standard';
|
|
374
|
+
// ── auth method ───────────────────────────────────────────────────────
|
|
375
|
+
const logonAsMatch = primaryUrl.match(/logonAsConnection=([^;]+)/);
|
|
376
|
+
const isLogonAs = logonAsMatch !== null;
|
|
377
|
+
const isOAuth = primaryUrl.includes('authenticationType=OAUTH');
|
|
378
|
+
const isBasic = primaryUrl.includes('password=');
|
|
379
|
+
const authMethod = isLogonAs
|
|
380
|
+
? 'login-as'
|
|
381
|
+
: isOAuth
|
|
382
|
+
? 'oauth'
|
|
383
|
+
: isBasic
|
|
384
|
+
? 'basic'
|
|
385
|
+
: 'unknown';
|
|
386
|
+
// ── SF environment (org type) ─────────────────────────────────────────
|
|
387
|
+
const envMatch = primaryUrl.match(/(?:^|;)environment=([^;]+)/);
|
|
388
|
+
const envValue = envMatch?.[1]?.toUpperCase();
|
|
389
|
+
const sfEnvironment = envValue === 'SANDBOX' ? 'sandbox' :
|
|
390
|
+
envValue === 'PROD_DEV' ? 'production-developer' :
|
|
391
|
+
envValue ? 'other' :
|
|
392
|
+
null;
|
|
393
|
+
result.salesforce.push({
|
|
394
|
+
name,
|
|
395
|
+
sub_type: subType,
|
|
396
|
+
auth_method: authMethod,
|
|
397
|
+
sf_environment: sfEnvironment,
|
|
398
|
+
logon_as_connection: logonAsMatch?.[1] ?? null,
|
|
399
|
+
is_direct_login: !isLogonAs,
|
|
400
|
+
});
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case 'ui':
|
|
404
|
+
result.ui_testing.push(name);
|
|
405
|
+
break;
|
|
406
|
+
case 'testmanager':
|
|
407
|
+
result.quality_hub.push(name);
|
|
408
|
+
break;
|
|
409
|
+
case 'webservice':
|
|
410
|
+
if (primaryUrl.startsWith('restservice:')) {
|
|
411
|
+
result.web_service_rest.push(name);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
result.web_service_soap.push(name);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
case 'database':
|
|
418
|
+
result.database.push(name);
|
|
419
|
+
break;
|
|
420
|
+
case 'google':
|
|
421
|
+
result.google.push(name);
|
|
422
|
+
break;
|
|
423
|
+
case 'msexc': // Microsoft Exchange / Outlook (EWS)
|
|
424
|
+
case 'microsoft': // kept as fallback in case variant exists
|
|
425
|
+
result.microsoft.push(name);
|
|
426
|
+
break;
|
|
427
|
+
case 'zephyr':
|
|
428
|
+
case 'zephyrScale':
|
|
429
|
+
case 'zephyrServer':
|
|
430
|
+
result.zephyr.push(name);
|
|
431
|
+
break;
|
|
432
|
+
case 'sso':
|
|
433
|
+
result.sso.push(name);
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
result.other.push({ class: className, name });
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function emptyConnectionOverview() {
|
|
441
|
+
return {
|
|
442
|
+
salesforce: [],
|
|
443
|
+
ui_testing: [],
|
|
444
|
+
quality_hub: [],
|
|
445
|
+
web_service_rest: [],
|
|
446
|
+
web_service_soap: [],
|
|
447
|
+
database: [],
|
|
448
|
+
google: [],
|
|
449
|
+
microsoft: [],
|
|
450
|
+
zephyr: [],
|
|
451
|
+
sso: [],
|
|
452
|
+
other: [],
|
|
453
|
+
summary: {},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// ─── test plan coverage ───────────────────────────────────────────────────────
|
|
457
|
+
/**
|
|
458
|
+
* Builds a map of testcase UUID (registryId / id / guid) → project-relative path.
|
|
459
|
+
* Used as a UUID-based fallback when testCasePath in a .testinstance doesn't match
|
|
460
|
+
* the on-disk path exactly (e.g. different separators or moved files).
|
|
461
|
+
*/
|
|
462
|
+
function buildProjectTestCaseIdMap(projectPath) {
|
|
463
|
+
const testsDir = path.join(projectPath, 'tests');
|
|
464
|
+
const idMap = new Map();
|
|
465
|
+
if (!fs.existsSync(testsDir))
|
|
466
|
+
return idMap;
|
|
467
|
+
function walk(dir) {
|
|
468
|
+
try {
|
|
469
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
470
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
471
|
+
continue;
|
|
472
|
+
const fullPath = path.join(dir, entry.name);
|
|
473
|
+
if (entry.isDirectory()) {
|
|
474
|
+
walk(fullPath);
|
|
475
|
+
}
|
|
476
|
+
else if (entry.name.endsWith('.testcase')) {
|
|
477
|
+
try {
|
|
478
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
479
|
+
const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
480
|
+
for (const attr of ['registryId', 'id', 'guid']) {
|
|
481
|
+
const m = content.match(new RegExp(`${attr}=["']([^"']+)["']`));
|
|
482
|
+
if (m?.[1] && !idMap.has(m[1]))
|
|
483
|
+
idMap.set(m[1], rel);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch { /* skip */ }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch { /* skip */ }
|
|
491
|
+
}
|
|
492
|
+
walk(testsDir);
|
|
493
|
+
return idMap;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Plans structure:
|
|
497
|
+
* - plans/
|
|
498
|
+
* - plans/{PlanName}/ — test plan directory
|
|
499
|
+
* - plans/{PlanName}/.planitem — plan definition (hidden dot-file)
|
|
500
|
+
* - plans/{PlanName}/{SuiteName}/ — test suite directory
|
|
501
|
+
* - plans/{PlanName}/{SuiteName}/.planitem — suite definition
|
|
502
|
+
* - plans/{PlanName}/{SuiteName}/{Name}.testinstance — references .testcase via testCasePath
|
|
503
|
+
*
|
|
504
|
+
* Depth (relative to plans/):
|
|
505
|
+
* - split('/').length === 2 → plan-level .planitem
|
|
506
|
+
* - split('/').length >= 3 → suite-level .planitem
|
|
507
|
+
*/
|
|
508
|
+
function buildPlanCoverage(projectPath, allTestCasePaths) {
|
|
509
|
+
const plansDir = path.join(projectPath, 'plans');
|
|
510
|
+
const noCoverage = {
|
|
511
|
+
test_plan_count: 0,
|
|
512
|
+
test_suite_count: 0,
|
|
513
|
+
test_instance_count: 0,
|
|
514
|
+
covered_test_case_paths: [],
|
|
515
|
+
uncovered_test_case_paths: [...allTestCasePaths].sort(),
|
|
516
|
+
coverage_percent: 0,
|
|
517
|
+
};
|
|
518
|
+
if (!fs.existsSync(plansDir))
|
|
519
|
+
return noCoverage;
|
|
520
|
+
// Build UUID fallback map: registryId/id/guid → project-relative path
|
|
521
|
+
const testCaseIdMap = buildProjectTestCaseIdMap(projectPath);
|
|
522
|
+
let testPlanCount = 0;
|
|
523
|
+
let testSuiteCount = 0;
|
|
524
|
+
let testInstanceCount = 0;
|
|
525
|
+
const referencedPaths = new Set();
|
|
526
|
+
walkPlansDir(plansDir, (filePath, isDir, name) => {
|
|
527
|
+
if (isDir)
|
|
528
|
+
return true;
|
|
529
|
+
const relToPlans = path.relative(plansDir, filePath).replace(/\\/g, '/');
|
|
530
|
+
const depth = relToPlans.split('/').length;
|
|
531
|
+
if (name === '.planitem') {
|
|
532
|
+
if (depth === 2)
|
|
533
|
+
testPlanCount++;
|
|
534
|
+
else if (depth >= 3)
|
|
535
|
+
testSuiteCount++;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (name.endsWith('.testinstance')) {
|
|
539
|
+
testInstanceCount++;
|
|
540
|
+
try {
|
|
541
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
542
|
+
// Primary: path-based match
|
|
543
|
+
const pathMatch = content.match(/testCasePath=["']([^"']+)["']/);
|
|
544
|
+
if (pathMatch?.[1])
|
|
545
|
+
referencedPaths.add(pathMatch[1].replace(/\\/g, '/'));
|
|
546
|
+
// Fallback: UUID match via testCaseId → testcase registryId/id/guid
|
|
547
|
+
const idMatch = content.match(/testCaseId=["']([^"']+)["']/);
|
|
548
|
+
if (idMatch?.[1]) {
|
|
549
|
+
const resolvedPath = testCaseIdMap.get(idMatch[1]);
|
|
550
|
+
if (resolvedPath)
|
|
551
|
+
referencedPaths.add(resolvedPath);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { /* skip */ }
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
});
|
|
559
|
+
const coveredPaths = [];
|
|
560
|
+
const uncoveredPaths = [];
|
|
561
|
+
for (const tc of [...allTestCasePaths].sort()) {
|
|
562
|
+
(referencedPaths.has(tc) ? coveredPaths : uncoveredPaths).push(tc);
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
test_plan_count: testPlanCount,
|
|
566
|
+
test_suite_count: testSuiteCount,
|
|
567
|
+
test_instance_count: testInstanceCount,
|
|
568
|
+
covered_test_case_paths: coveredPaths,
|
|
569
|
+
uncovered_test_case_paths: uncoveredPaths,
|
|
570
|
+
coverage_percent: allTestCasePaths.size > 0 ? Math.round((coveredPaths.length / allTestCasePaths.size) * 100) : 0,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
// ─── provar home detection ────────────────────────────────────────────────────
|
|
574
|
+
function detectProvarHome(projectPath, provardxFiles, antFiles) {
|
|
575
|
+
// 1. Environment variable (set by CI or local Provar installer)
|
|
576
|
+
const envHome = process.env['PROVAR_HOME'];
|
|
577
|
+
if (envHome)
|
|
578
|
+
return { provarHome: envHome, provarHomeSource: 'PROVAR_HOME environment variable' };
|
|
579
|
+
// 2. provardx-properties.json — provarHome field
|
|
580
|
+
for (const rel of provardxFiles) {
|
|
581
|
+
try {
|
|
582
|
+
const props = JSON.parse(fs.readFileSync(path.join(projectPath, rel), 'utf-8'));
|
|
583
|
+
if (typeof props['provarHome'] === 'string') {
|
|
584
|
+
return { provarHome: props['provarHome'], provarHomeSource: `provardx-properties.json (${rel})` };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch { /* skip */ }
|
|
588
|
+
}
|
|
589
|
+
// 3. ANT build.xml — <property name="provarHome" value="..." />
|
|
590
|
+
for (const rel of antFiles) {
|
|
591
|
+
if (!rel.endsWith('.xml'))
|
|
592
|
+
continue;
|
|
593
|
+
try {
|
|
594
|
+
const content = fs.readFileSync(path.join(projectPath, rel), 'utf-8');
|
|
595
|
+
const match = content.match(/name=["']provarHome["'][^/]*value=["']([^"']+)["']/i) ??
|
|
596
|
+
content.match(/value=["']([^"']+)["'][^/]*name=["']provarHome["']/i);
|
|
597
|
+
if (match?.[1])
|
|
598
|
+
return { provarHome: match[1], provarHomeSource: `ANT build file (${rel})` };
|
|
599
|
+
}
|
|
600
|
+
catch { /* skip */ }
|
|
601
|
+
}
|
|
602
|
+
return { provarHome: null, provarHomeSource: null };
|
|
603
|
+
}
|
|
604
|
+
// ─── test suite folder structure ──────────────────────────────────────────────
|
|
605
|
+
function getTopLevelTestSuites(projectPath) {
|
|
606
|
+
const suites = [];
|
|
607
|
+
for (const candidate of ['tests', 'Tests']) {
|
|
608
|
+
const testsDir = path.join(projectPath, candidate);
|
|
609
|
+
if (!fs.existsSync(testsDir))
|
|
610
|
+
continue;
|
|
611
|
+
try {
|
|
612
|
+
const entries = fs.readdirSync(testsDir, { withFileTypes: true });
|
|
613
|
+
for (const entry of entries) {
|
|
614
|
+
if (entry.isDirectory() && !entry.name.startsWith('.'))
|
|
615
|
+
suites.push(`${candidate}/${entry.name}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch { /* skip */ }
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
return suites;
|
|
622
|
+
}
|
|
623
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
624
|
+
function isSourceFile(name) {
|
|
625
|
+
return name.endsWith('.java') || name.endsWith('.groovy') || name.endsWith('.jar');
|
|
626
|
+
}
|
|
627
|
+
function isDataFile(name) {
|
|
628
|
+
return name.endsWith('.xlsx') || name.endsWith('.xls') || name.endsWith('.csv') || name.endsWith('.json');
|
|
629
|
+
}
|
|
630
|
+
function countFilesRecursive(dir, filter) {
|
|
631
|
+
let count = 0;
|
|
632
|
+
try {
|
|
633
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
if (entry.name.startsWith('.'))
|
|
636
|
+
continue;
|
|
637
|
+
const full = path.join(dir, entry.name);
|
|
638
|
+
if (entry.isDirectory()) {
|
|
639
|
+
count += countFilesRecursive(full, filter);
|
|
640
|
+
}
|
|
641
|
+
else if (filter(entry.name)) {
|
|
642
|
+
count++;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch { /* skip */ }
|
|
647
|
+
return count;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* General project walker — skips dot-files and node_modules.
|
|
651
|
+
* Return false from visitor to skip recursion into a directory.
|
|
652
|
+
*/
|
|
653
|
+
function walkDir(dir, visitor, depth = 0) {
|
|
654
|
+
if (depth > 10)
|
|
655
|
+
return;
|
|
656
|
+
let entries;
|
|
657
|
+
try {
|
|
658
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
665
|
+
continue;
|
|
666
|
+
const fullPath = path.join(dir, entry.name);
|
|
667
|
+
const recurse = visitor(fullPath, entry.isDirectory(), entry.name);
|
|
668
|
+
if (entry.isDirectory() && recurse)
|
|
669
|
+
walkDir(fullPath, visitor, depth + 1);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Plans-directory walker — does NOT skip dot-files (.planitem starts with '.').
|
|
674
|
+
*/
|
|
675
|
+
function walkPlansDir(dir, visitor, depth = 0) {
|
|
676
|
+
if (depth > 8)
|
|
677
|
+
return;
|
|
678
|
+
let entries;
|
|
679
|
+
try {
|
|
680
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
for (const entry of entries) {
|
|
686
|
+
if (entry.name === 'node_modules')
|
|
687
|
+
continue;
|
|
688
|
+
const fullPath = path.join(dir, entry.name);
|
|
689
|
+
const recurse = visitor(fullPath, entry.isDirectory(), entry.name);
|
|
690
|
+
if (entry.isDirectory() && recurse)
|
|
691
|
+
walkPlansDir(fullPath, visitor, depth + 1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=projectInspect.js.map
|