@provartesting/provardx-cli 1.5.0-dev.2 → 1.5.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 +163 -12
- package/bin/mcp-start.js +74 -0
- package/lib/commands/provar/auth/clear.d.ts +7 -0
- package/lib/commands/provar/auth/clear.js +36 -0
- package/lib/commands/provar/auth/clear.js.map +1 -0
- package/lib/commands/provar/auth/login.d.ts +10 -0
- package/lib/commands/provar/auth/login.js +90 -0
- package/lib/commands/provar/auth/login.js.map +1 -0
- package/lib/commands/provar/auth/rotate.d.ts +7 -0
- package/lib/commands/provar/auth/rotate.js +42 -0
- package/lib/commands/provar/auth/rotate.js.map +1 -0
- package/lib/commands/provar/auth/status.d.ts +7 -0
- package/lib/commands/provar/auth/status.js +107 -0
- package/lib/commands/provar/auth/status.js.map +1 -0
- package/lib/commands/provar/mcp/start.d.ts +2 -0
- package/lib/commands/provar/mcp/start.js +14 -1
- package/lib/commands/provar/mcp/start.js.map +1 -1
- package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
- package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
- package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +187 -0
- package/lib/mcp/licensing/algasClient.js +14 -5
- package/lib/mcp/licensing/algasClient.js.map +1 -1
- package/lib/mcp/licensing/ideDetection.d.ts +0 -12
- package/lib/mcp/licensing/ideDetection.js +1 -73
- package/lib/mcp/licensing/ideDetection.js.map +1 -1
- package/lib/mcp/licensing/licenseCache.js +7 -1
- package/lib/mcp/licensing/licenseCache.js.map +1 -1
- package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
- package/lib/mcp/licensing/licenseValidator.js +11 -4
- package/lib/mcp/licensing/licenseValidator.js.map +1 -1
- package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
- package/lib/mcp/prompts/guidePrompts.js +334 -0
- package/lib/mcp/prompts/guidePrompts.js.map +1 -0
- package/lib/mcp/prompts/index.d.ts +2 -0
- package/lib/mcp/prompts/index.js +23 -0
- package/lib/mcp/prompts/index.js.map +1 -0
- package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
- package/lib/mcp/prompts/loopPrompts.js +435 -0
- package/lib/mcp/prompts/loopPrompts.js.map +1 -0
- package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
- package/lib/mcp/prompts/migrationPrompts.js +207 -0
- package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
- package/lib/mcp/security/pathPolicy.d.ts +5 -0
- package/lib/mcp/security/pathPolicy.js +58 -3
- package/lib/mcp/security/pathPolicy.js.map +1 -1
- package/lib/mcp/server.d.ts +18 -0
- package/lib/mcp/server.js +232 -19
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +369 -170
- package/lib/mcp/tools/antTools.js.map +1 -1
- package/lib/mcp/tools/automationTools.d.ts +18 -8
- package/lib/mcp/tools/automationTools.js +333 -176
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +161 -23
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/connectionTools.d.ts +4 -0
- package/lib/mcp/tools/connectionTools.js +242 -0
- package/lib/mcp/tools/connectionTools.js.map +1 -0
- package/lib/mcp/tools/defectTools.d.ts +1 -1
- package/lib/mcp/tools/defectTools.js +61 -50
- package/lib/mcp/tools/defectTools.js.map +1 -1
- package/lib/mcp/tools/descHelper.d.ts +5 -0
- package/lib/mcp/tools/descHelper.js +14 -0
- package/lib/mcp/tools/descHelper.js.map +1 -0
- package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
- package/lib/mcp/tools/hierarchyValidate.js +127 -42
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/nitroXTools.d.ts +23 -0
- package/lib/mcp/tools/nitroXTools.js +863 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +150 -57
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/pageObjectValidate.js +143 -46
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
- package/lib/mcp/tools/projectInspect.js +79 -32
- package/lib/mcp/tools/projectInspect.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +185 -58
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +358 -78
- package/lib/mcp/tools/propertiesTools.js.map +1 -1
- package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
- package/lib/mcp/tools/qualityHubApiTools.js +139 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +292 -72
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +194 -56
- package/lib/mcp/tools/rcaTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +25 -3
- package/lib/mcp/tools/sfSpawn.js +154 -6
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +285 -78
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
- package/lib/mcp/tools/testCaseStepTools.js +244 -0
- package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
- package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
- package/lib/mcp/tools/testCaseValidate.js +381 -46
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanTools.d.ts +1 -0
- package/lib/mcp/tools/testPlanTools.js +316 -59
- package/lib/mcp/tools/testPlanTools.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +114 -23
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +130 -15
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/update/updateChecker.d.ts +14 -0
- package/lib/mcp/update/updateChecker.js +228 -0
- package/lib/mcp/update/updateChecker.js.map +1 -0
- package/lib/mcp/utils/detailLevel.d.ts +9 -0
- package/lib/mcp/utils/detailLevel.js +20 -0
- package/lib/mcp/utils/detailLevel.js.map +1 -0
- package/lib/mcp/utils/fieldMask.d.ts +17 -0
- package/lib/mcp/utils/fieldMask.js +75 -0
- package/lib/mcp/utils/fieldMask.js.map +1 -0
- package/lib/mcp/utils/tokenMeta.d.ts +40 -0
- package/lib/mcp/utils/tokenMeta.js +90 -0
- package/lib/mcp/utils/tokenMeta.js.map +1 -0
- package/lib/mcp/utils/validationDiff.d.ts +57 -0
- package/lib/mcp/utils/validationDiff.js +191 -0
- package/lib/mcp/utils/validationDiff.js.map +1 -0
- package/lib/mcp/utils/validationScore.d.ts +15 -0
- package/lib/mcp/utils/validationScore.js +31 -0
- package/lib/mcp/utils/validationScore.js.map +1 -0
- package/lib/services/auth/credentials.d.ts +21 -0
- package/lib/services/auth/credentials.js +75 -0
- package/lib/services/auth/credentials.js.map +1 -0
- package/lib/services/auth/loginFlow.d.ts +68 -0
- package/lib/services/auth/loginFlow.js +216 -0
- package/lib/services/auth/loginFlow.js.map +1 -0
- package/lib/services/projectValidation.d.ts +5 -2
- package/lib/services/projectValidation.js +83 -31
- package/lib/services/projectValidation.js.map +1 -1
- package/lib/services/qualityHub/client.d.ts +161 -0
- package/lib/services/qualityHub/client.js +226 -0
- package/lib/services/qualityHub/client.js.map +1 -0
- package/messages/sf.provar.auth.clear.md +16 -0
- package/messages/sf.provar.auth.login.md +31 -0
- package/messages/sf.provar.auth.rotate.md +23 -0
- package/messages/sf.provar.auth.status.md +16 -0
- package/messages/sf.provar.mcp.start.md +83 -48
- package/oclif.manifest.json +325 -28
- package/package.json +35 -12
|
@@ -11,221 +11,359 @@ import path from 'node:path';
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { makeError, makeRequestId } from '../schemas/common.js';
|
|
13
13
|
import { log } from '../logging/logger.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function getSfCommonPaths() {
|
|
21
|
-
const home = os.homedir();
|
|
22
|
-
if (process.platform === 'win32') {
|
|
23
|
-
const appData = process.env['APPDATA'] ?? path.join(home, 'AppData', 'Roaming');
|
|
24
|
-
return [
|
|
25
|
-
path.join(appData, 'npm', 'sf.cmd'),
|
|
26
|
-
path.join('C:', 'Program Files', 'nodejs', 'sf.cmd'),
|
|
27
|
-
path.join('C:', 'Program Files (x86)', 'nodejs', 'sf.cmd'),
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
const candidates = [
|
|
31
|
-
'/usr/local/bin/sf',
|
|
32
|
-
path.join(home, '.npm-global', 'bin', 'sf'),
|
|
33
|
-
path.join(home, '.local', 'bin', 'sf'),
|
|
34
|
-
path.join(home, '.volta', 'bin', 'sf'),
|
|
35
|
-
];
|
|
36
|
-
// nvm — scan the three most-recently installed Node versions
|
|
37
|
-
const nvmBinDir = path.join(process.env['NVM_DIR'] ?? path.join(home, '.nvm'), 'versions', 'node');
|
|
38
|
-
if (fs.existsSync(nvmBinDir)) {
|
|
39
|
-
try {
|
|
40
|
-
for (const v of fs.readdirSync(nvmBinDir).sort().reverse().slice(0, 3)) {
|
|
41
|
-
candidates.push(path.join(nvmBinDir, v, 'bin', 'sf'));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch { /* skip */ }
|
|
45
|
-
}
|
|
46
|
-
return candidates;
|
|
47
|
-
}
|
|
48
|
-
// ── Shared spawn helper ───────────────────────────────────────────────────────
|
|
49
|
-
const MAX_BUFFER = 50 * 1024 * 1024; // 50 MB — prevents ENOBUFS on verbose Provar runs
|
|
50
|
-
// Proactively resolve the sf executable path once on first use and cache it.
|
|
51
|
-
// This ensures sf is always found even when ENOENT is masked by other errors (e.g. ENOBUFS).
|
|
52
|
-
let cachedSfPath; // undefined = not yet probed
|
|
53
|
-
/** Exposed for testing only — pre-seeds the cached sf executable path, bypassing the probe spawn. */
|
|
54
|
-
export function setSfPathCacheForTesting(value) {
|
|
55
|
-
cachedSfPath = value;
|
|
56
|
-
}
|
|
57
|
-
function resolveSfExecutable() {
|
|
58
|
-
if (cachedSfPath !== undefined)
|
|
59
|
-
return cachedSfPath;
|
|
60
|
-
// Check PATH first via a cheap version probe
|
|
61
|
-
const probe = sfSpawnHelper.spawnSync('sf', ['--version'], { encoding: 'utf-8', shell: false, maxBuffer: 1024 * 1024 });
|
|
62
|
-
if (!probe.error) {
|
|
63
|
-
cachedSfPath = 'sf';
|
|
64
|
-
return cachedSfPath;
|
|
65
|
-
}
|
|
66
|
-
// Fall back to common install locations
|
|
67
|
-
for (const candidate of getSfCommonPaths()) {
|
|
68
|
-
if (fs.existsSync(candidate)) {
|
|
69
|
-
cachedSfPath = candidate;
|
|
70
|
-
return cachedSfPath;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
cachedSfPath = null;
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
class SfNotFoundError extends Error {
|
|
77
|
-
code = 'SF_NOT_FOUND';
|
|
78
|
-
constructor(sfPath) {
|
|
79
|
-
const where = sfPath
|
|
80
|
-
? `at explicit path "${sfPath}"`
|
|
81
|
-
: 'in PATH or common npm/nvm install locations';
|
|
82
|
-
super(`sf CLI not found ${where}. ` +
|
|
83
|
-
'Install Salesforce CLI (npm install -g @salesforce/cli) and ensure the install directory is in your PATH, ' +
|
|
84
|
-
'or pass sf_path pointing to the sf executable directly ' +
|
|
85
|
-
'(e.g. "~/.nvm/versions/node/v22.0.0/bin/sf").');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function runSfCommand(args, sfPath) {
|
|
89
|
-
// Use explicit path if provided; otherwise use cached probe result
|
|
90
|
-
const executable = sfPath ?? resolveSfExecutable();
|
|
91
|
-
if (!executable)
|
|
92
|
-
throw new SfNotFoundError();
|
|
93
|
-
const result = sfSpawnHelper.spawnSync(executable, args, { encoding: 'utf-8', shell: false, maxBuffer: MAX_BUFFER });
|
|
94
|
-
if (result.error) {
|
|
95
|
-
const err = result.error;
|
|
96
|
-
if (err.code === 'ENOENT') {
|
|
97
|
-
throw new SfNotFoundError(sfPath);
|
|
98
|
-
}
|
|
99
|
-
throw result.error;
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
stdout: result.stdout ?? '',
|
|
103
|
-
stderr: result.stderr ?? '',
|
|
104
|
-
exitCode: result.status ?? 1,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
14
|
+
import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
|
|
15
|
+
import { parseJUnitResults } from './antTools.js';
|
|
16
|
+
import { runSfCommand } from './sfSpawn.js';
|
|
17
|
+
import { desc } from './descHelper.js';
|
|
18
|
+
// Re-export sf resolution helpers so existing test imports from automationTools continue to work
|
|
19
|
+
export { getSfCommonPaths, needsWindowsShell, setSfPathCacheForTesting, setSfPlatformForTesting } from './sfSpawn.js';
|
|
107
20
|
function handleSpawnError(err, requestId, toolName) {
|
|
108
21
|
const error = err;
|
|
109
22
|
log('error', `${toolName} failed`, { requestId, error: error.message });
|
|
110
23
|
return {
|
|
111
24
|
isError: true,
|
|
112
|
-
content: [
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify(makeError(error.code ?? 'SF_ERROR', error.message, requestId, false)),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
113
31
|
};
|
|
114
32
|
}
|
|
115
|
-
// ── Tool:
|
|
116
|
-
export function registerAutomationConfigLoad(server) {
|
|
117
|
-
server.
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
33
|
+
// ── Tool: provar_automation_config_load ──────────────────────────────────────
|
|
34
|
+
export function registerAutomationConfigLoad(server, config) {
|
|
35
|
+
server.registerTool('provar_automation_config_load', {
|
|
36
|
+
title: 'Load Automation Config',
|
|
37
|
+
description: desc([
|
|
38
|
+
'Register a provardx-properties.json file as the active Provar configuration.',
|
|
39
|
+
'Invokes `sf provar automation config load --properties-file <path>`, writing the path to ~/.sf/config.json.',
|
|
40
|
+
'REQUIRED before provar_automation_compile or provar_automation_testrun — without this step those commands fail with MISSING_FILE.',
|
|
41
|
+
'Typical workflow: provar_automation_config_load → provar_automation_compile → provar_automation_testrun.',
|
|
42
|
+
].join(' '), 'Register a provardx-properties.json as active config; required before compile/testrun.'),
|
|
43
|
+
inputSchema: {
|
|
44
|
+
properties_path: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe(desc('Absolute path to the provardx-properties.json file to register as active configuration', 'string, absolute path to provardx-properties.json')),
|
|
47
|
+
sf_path: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(desc('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")', 'string, optional; path to sf CLI')),
|
|
51
|
+
},
|
|
125
52
|
}, ({ properties_path, sf_path }) => {
|
|
126
53
|
const requestId = makeRequestId();
|
|
127
|
-
log('info', '
|
|
54
|
+
log('info', 'provar_automation_config_load', { requestId, properties_path });
|
|
128
55
|
try {
|
|
56
|
+
assertPathAllowed(properties_path, config.allowedPaths);
|
|
129
57
|
const result = runSfCommand(['provar', 'automation', 'config', 'load', '--properties-file', properties_path], sf_path);
|
|
130
|
-
const response = {
|
|
58
|
+
const response = {
|
|
59
|
+
requestId,
|
|
60
|
+
exitCode: result.exitCode,
|
|
61
|
+
stdout: result.stdout,
|
|
62
|
+
stderr: result.stderr,
|
|
63
|
+
properties_path,
|
|
64
|
+
};
|
|
131
65
|
if (result.exitCode !== 0) {
|
|
132
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
isError: true,
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: JSON.stringify(makeError('AUTOMATION_CONFIG_LOAD_FAILED', result.stderr || result.stdout, requestId)),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
133
75
|
}
|
|
134
76
|
return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
|
|
135
77
|
}
|
|
136
78
|
catch (err) {
|
|
137
|
-
|
|
79
|
+
if (err instanceof PathPolicyError) {
|
|
80
|
+
return {
|
|
81
|
+
isError: true,
|
|
82
|
+
content: [
|
|
83
|
+
{ type: 'text', text: JSON.stringify(makeError(err.code, err.message, requestId, false)) },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return handleSpawnError(err, requestId, 'provar_automation_config_load');
|
|
138
88
|
}
|
|
139
89
|
});
|
|
140
90
|
}
|
|
141
|
-
// ──
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
91
|
+
// ── Testrun output filter ─────────────────────────────────────────────────────
|
|
92
|
+
const NOISE_PATTERNS = [/com\.networknt\.schema/, /SEVERE.*Failed to configure logger.*\.lck/];
|
|
93
|
+
/**
|
|
94
|
+
* Strip Java schema-validator debug lines and stale logger-lock SEVERE warnings
|
|
95
|
+
* from Provar testrun output. These two patterns account for the bulk of output
|
|
96
|
+
* volume and cause MCP responses to be truncated before the pass/fail lines.
|
|
97
|
+
*
|
|
98
|
+
* Everything else (including real SEVERE failures) passes through unchanged.
|
|
99
|
+
* Collapses runs of blank lines to a single blank to keep the output readable.
|
|
100
|
+
* Returns the filtered text and the count of suppressed lines.
|
|
101
|
+
*/
|
|
102
|
+
export function filterTestRunOutput(raw) {
|
|
103
|
+
const lines = raw.split(/\r?\n/);
|
|
104
|
+
const kept = [];
|
|
105
|
+
let suppressed = 0;
|
|
106
|
+
let lastKeptWasBlank = false;
|
|
107
|
+
for (const rawLine of lines) {
|
|
108
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
109
|
+
if (NOISE_PATTERNS.some((p) => p.test(line))) {
|
|
110
|
+
suppressed++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const isBlank = line.trim() === '';
|
|
114
|
+
if (isBlank && lastKeptWasBlank)
|
|
115
|
+
continue; // collapse blank runs
|
|
116
|
+
kept.push(line);
|
|
117
|
+
lastKeptWasBlank = isBlank;
|
|
118
|
+
}
|
|
119
|
+
let filtered = kept.join('\n');
|
|
120
|
+
if (suppressed > 0) {
|
|
121
|
+
filtered += `\n[testrun: ${suppressed} lines suppressed (schema validator / logger noise) — use provar_testrun_rca for full results]`;
|
|
122
|
+
}
|
|
123
|
+
return { filtered, suppressed };
|
|
124
|
+
}
|
|
125
|
+
// ── JUnit results enrichment ──────────────────────────────────────────────────
|
|
126
|
+
// Overrideable in tests — bypasses the sf config file read
|
|
127
|
+
let sfResultsPathOverride;
|
|
128
|
+
/** Exposed for testing only — set the results path returned by the sf config reader. Pass undefined to reset. */
|
|
129
|
+
export function setSfResultsPathForTesting(p) {
|
|
130
|
+
sfResultsPathOverride = p;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolves the actual results directory for the latest run.
|
|
134
|
+
* Provar's Increment disposition creates Results(1), Results(2)… as siblings of Results/.
|
|
135
|
+
* Returns the latest sibling dir, or the base path if no siblings exist.
|
|
136
|
+
*/
|
|
137
|
+
function resolveLatestResultsDir(resultsBase) {
|
|
138
|
+
const parent = path.dirname(resultsBase);
|
|
139
|
+
const base = path.basename(resultsBase);
|
|
140
|
+
try {
|
|
141
|
+
const safeName = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
+
const pattern = new RegExp(`^${safeName}\\((\\d+)\\)$`);
|
|
143
|
+
const indices = fs
|
|
144
|
+
.readdirSync(parent, { withFileTypes: true })
|
|
145
|
+
.filter((e) => e.isDirectory() && pattern.test(e.name))
|
|
146
|
+
.map((e) => parseInt(pattern.exec(e.name)[1], 10));
|
|
147
|
+
if (indices.length > 0) {
|
|
148
|
+
const maxIdx = indices.reduce((a, b) => (a > b ? a : b), 0);
|
|
149
|
+
return path.join(parent, `${base}(${maxIdx})`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// ignore filesystem errors
|
|
154
|
+
}
|
|
155
|
+
return resultsBase;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Reads resultsPath from the currently active provardx-properties.json (via ~/.sf/config.json),
|
|
159
|
+
* then resolves to the latest Increment-mode sibling directory.
|
|
160
|
+
* Returns null when the sf config or properties file cannot be read or is outside allowed paths.
|
|
161
|
+
*/
|
|
162
|
+
function readResultsPathFromSfConfig(config) {
|
|
163
|
+
if (sfResultsPathOverride !== undefined)
|
|
164
|
+
return sfResultsPathOverride;
|
|
165
|
+
try {
|
|
166
|
+
const sfConfigPath = path.join(os.homedir(), '.sf', 'config.json');
|
|
167
|
+
if (!fs.existsSync(sfConfigPath))
|
|
168
|
+
return null;
|
|
169
|
+
const sfConfig = JSON.parse(fs.readFileSync(sfConfigPath, 'utf-8'));
|
|
170
|
+
const propFilePath = sfConfig['PROVARDX_PROPERTIES_FILE_PATH'];
|
|
171
|
+
if (!propFilePath || !fs.existsSync(propFilePath))
|
|
172
|
+
return null;
|
|
173
|
+
// Guard: only read the properties file if it is within the session's allowed paths.
|
|
174
|
+
assertPathAllowed(propFilePath, config.allowedPaths);
|
|
175
|
+
const props = JSON.parse(fs.readFileSync(propFilePath, 'utf-8'));
|
|
176
|
+
const resultsBase = props['resultsPath'];
|
|
177
|
+
if (!resultsBase)
|
|
178
|
+
return null;
|
|
179
|
+
const resultsDir = resolveLatestResultsDir(resultsBase);
|
|
180
|
+
// Guard: only read the results directory if it is within the session's allowed paths.
|
|
181
|
+
assertPathAllowed(resultsDir, config.allowedPaths);
|
|
182
|
+
return resultsDir;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ── Tool: provar_automation_testrun ───────────────────────────────────────────
|
|
189
|
+
export function registerAutomationTestRun(server, config) {
|
|
190
|
+
server.registerTool('provar_automation_testrun', {
|
|
191
|
+
title: 'Run Tests',
|
|
192
|
+
description: desc([
|
|
193
|
+
'Trigger a LOCAL Provar automation test run using installed Provar binaries. Invokes `sf provar automation test run`.',
|
|
194
|
+
'PREREQUISITE: Run provar_automation_config_load first to register a provardx-properties.json — without this the command fails with MISSING_FILE.',
|
|
195
|
+
'Requires Provar to be installed locally and provarHome set correctly in the properties file.',
|
|
196
|
+
'Use provar_automation_setup first if Provar is not yet installed.',
|
|
197
|
+
'For grid/CI execution via Provar Quality Hub instead of running locally, use provar_qualityhub_testrun.',
|
|
198
|
+
'Output buffer: a 50 MB maxBuffer is set so ENOBUFS on verbose Provar runs is now rare.',
|
|
199
|
+
'If ENOBUFS still occurs (extremely verbose logging), run `sf provar automation test run --json` directly in the terminal and pipe or tail the output instead of retrying this tool.',
|
|
200
|
+
'Typical local AI loop: config.load → compile → testrun → inspect results.',
|
|
201
|
+
].join(' '), 'Run local Provar tests via sf CLI; requires config_load first.'),
|
|
202
|
+
inputSchema: {
|
|
203
|
+
flags: z
|
|
204
|
+
.array(z.string())
|
|
205
|
+
.optional()
|
|
206
|
+
.default([])
|
|
207
|
+
.describe(desc('Raw CLI flags to forward (e.g. ["--project-path", "/path/to/project"])', 'array, optional; raw CLI flags')),
|
|
208
|
+
sf_path: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe(desc('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")', 'string, optional; path to sf CLI')),
|
|
212
|
+
},
|
|
153
213
|
}, ({ flags, sf_path }) => {
|
|
154
214
|
const requestId = makeRequestId();
|
|
155
|
-
log('info', '
|
|
215
|
+
log('info', 'provar_automation_testrun', { requestId });
|
|
156
216
|
try {
|
|
157
217
|
const result = runSfCommand(['provar', 'automation', 'test', 'run', ...flags], sf_path);
|
|
158
|
-
const
|
|
218
|
+
const { filtered, suppressed } = filterTestRunOutput(result.stdout);
|
|
219
|
+
// Attempt to enrich the response with structured step data from JUnit XML
|
|
220
|
+
const resultsPath = readResultsPathFromSfConfig(config);
|
|
221
|
+
const { steps, warning: junitWarning } = resultsPath
|
|
222
|
+
? parseJUnitResults(resultsPath)
|
|
223
|
+
: { steps: [], warning: undefined };
|
|
159
224
|
if (result.exitCode !== 0) {
|
|
160
|
-
|
|
225
|
+
const { filtered: filteredErr, suppressed: suppressedErr } = filterTestRunOutput(result.stderr || result.stdout);
|
|
226
|
+
const errBody = {
|
|
227
|
+
...makeError('AUTOMATION_TESTRUN_FAILED', filteredErr, requestId),
|
|
228
|
+
...(suppressedErr > 0 ? { output_lines_suppressed: suppressedErr } : {}),
|
|
229
|
+
};
|
|
230
|
+
if (steps.length > 0)
|
|
231
|
+
errBody['steps'] = steps;
|
|
232
|
+
if (!resultsPath || junitWarning) {
|
|
233
|
+
errBody['details'] = {
|
|
234
|
+
warning: junitWarning ??
|
|
235
|
+
'Could not locate results directory — step-level output unavailable. Run provar_automation_config_load first.',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(errBody) }] };
|
|
161
239
|
}
|
|
240
|
+
const response = {
|
|
241
|
+
requestId,
|
|
242
|
+
exitCode: result.exitCode,
|
|
243
|
+
stdout: filtered,
|
|
244
|
+
stderr: result.stderr,
|
|
245
|
+
};
|
|
246
|
+
if (suppressed > 0)
|
|
247
|
+
response['output_lines_suppressed'] = suppressed;
|
|
248
|
+
if (steps.length > 0)
|
|
249
|
+
response['steps'] = steps;
|
|
250
|
+
if (junitWarning)
|
|
251
|
+
response['details'] = { warning: junitWarning };
|
|
162
252
|
return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
|
|
163
253
|
}
|
|
164
254
|
catch (err) {
|
|
165
|
-
return handleSpawnError(err, requestId, '
|
|
255
|
+
return handleSpawnError(err, requestId, 'provar_automation_testrun');
|
|
166
256
|
}
|
|
167
257
|
});
|
|
168
258
|
}
|
|
169
|
-
// ── Tool:
|
|
259
|
+
// ── Tool: provar_automation_compile ───────────────────────────────────────────
|
|
170
260
|
export function registerAutomationCompile(server) {
|
|
171
|
-
server.
|
|
172
|
-
'Compile
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
261
|
+
server.registerTool('provar_automation_compile', {
|
|
262
|
+
title: 'Compile Test Assets',
|
|
263
|
+
description: desc([
|
|
264
|
+
'Compile a Provar automation project. Invokes `sf provar automation project compile`.',
|
|
265
|
+
'PREREQUISITE: Run provar_automation_config_load first to register a provardx-properties.json — without this the command fails with MISSING_FILE.',
|
|
266
|
+
'Run this before triggering a test run after modifying test cases.',
|
|
267
|
+
].join(' '), 'Compile a Provar project; requires config_load first.'),
|
|
268
|
+
inputSchema: {
|
|
269
|
+
flags: z
|
|
270
|
+
.array(z.string())
|
|
271
|
+
.optional()
|
|
272
|
+
.default([])
|
|
273
|
+
.describe(desc('Raw CLI flags to forward (e.g. ["--project-path", "/path/to/project"])', 'array, optional; raw CLI flags')),
|
|
274
|
+
sf_path: z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe(desc('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")', 'string, optional; path to sf CLI')),
|
|
278
|
+
},
|
|
178
279
|
}, ({ flags, sf_path }) => {
|
|
179
280
|
const requestId = makeRequestId();
|
|
180
|
-
log('info', '
|
|
281
|
+
log('info', 'provar_automation_compile', { requestId });
|
|
181
282
|
try {
|
|
182
283
|
const result = runSfCommand(['provar', 'automation', 'project', 'compile', ...flags], sf_path);
|
|
183
284
|
const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
184
285
|
if (result.exitCode !== 0) {
|
|
185
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
isError: true,
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: 'text',
|
|
291
|
+
text: JSON.stringify(makeError('AUTOMATION_COMPILE_FAILED', result.stderr || result.stdout, requestId)),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
};
|
|
186
295
|
}
|
|
187
296
|
return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
|
|
188
297
|
}
|
|
189
298
|
catch (err) {
|
|
190
|
-
return handleSpawnError(err, requestId, '
|
|
299
|
+
return handleSpawnError(err, requestId, 'provar_automation_compile');
|
|
191
300
|
}
|
|
192
301
|
});
|
|
193
302
|
}
|
|
194
|
-
// ── Tool:
|
|
303
|
+
// ── Tool: provar_automation_metadata_download ─────────────────────────────────
|
|
304
|
+
const DOWNLOAD_ERROR_SUGGESTION = 'A [DOWNLOAD_ERROR] almost always means a Salesforce authentication failure for the connection being used. ' +
|
|
305
|
+
'Check: (1) the connection credentials in the Provar project .secrets file are current and not expired; ' +
|
|
306
|
+
'(2) the named connection exists in the project and the name is spelled correctly (case-sensitive); ' +
|
|
307
|
+
'(3) if using a scratch org, confirm it has not expired (`sf org list`); ' +
|
|
308
|
+
'(4) if testprojectSecrets is set in provardx-properties.json, it must be the encryption key string used to decrypt .secrets — not a file path.';
|
|
195
309
|
export function registerAutomationMetadataDownload(server) {
|
|
196
|
-
server.
|
|
197
|
-
|
|
198
|
-
|
|
310
|
+
server.registerTool('provar_automation_metadata_download', {
|
|
311
|
+
title: 'Download Salesforce Metadata',
|
|
312
|
+
description: desc([
|
|
313
|
+
'Download Salesforce metadata for one or more connections into a Provar project.',
|
|
314
|
+
'Invokes `sf provar automation metadata download`.',
|
|
315
|
+
'PREREQUISITE: Call provar_automation_config_load first — without it the command fails with MISSING_FILE.',
|
|
316
|
+
'Use the -c flag to specify connections: flags: ["-c", "ConnectionName1,ConnectionName2"].',
|
|
317
|
+
'Connection names are case-sensitive and must match the names defined in the Provar project.',
|
|
318
|
+
'If the download fails with [DOWNLOAD_ERROR], this is almost always a Salesforce authentication issue —',
|
|
319
|
+
'check that the credentials in the project .secrets file are current and that any referenced scratch orgs have not expired.',
|
|
320
|
+
].join(' '), 'Download Salesforce metadata for project connections; requires config_load first.'),
|
|
321
|
+
inputSchema: {
|
|
322
|
+
flags: z
|
|
323
|
+
.array(z.string())
|
|
324
|
+
.optional()
|
|
325
|
+
.default([])
|
|
326
|
+
.describe(desc('Raw CLI flags to forward. Use ["-c", "Name1,Name2"] (or the equivalent --connections form) to target specific connections. Example: ["-c", "MyOrg,SandboxOrg"]', 'array, optional; raw CLI flags e.g. ["-c", "ConnName"]')),
|
|
327
|
+
sf_path: z
|
|
328
|
+
.string()
|
|
329
|
+
.optional()
|
|
330
|
+
.describe(desc('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")', 'string, optional; path to sf CLI')),
|
|
331
|
+
},
|
|
199
332
|
}, ({ flags, sf_path }) => {
|
|
200
333
|
const requestId = makeRequestId();
|
|
201
|
-
log('info', '
|
|
334
|
+
log('info', 'provar_automation_metadata_download', { requestId });
|
|
202
335
|
try {
|
|
203
336
|
const result = runSfCommand(['provar', 'automation', 'metadata', 'download', ...flags], sf_path);
|
|
204
|
-
const
|
|
337
|
+
const message = result.stderr || result.stdout;
|
|
205
338
|
if (result.exitCode !== 0) {
|
|
206
|
-
|
|
339
|
+
const isDownloadError = message.includes('[DOWNLOAD_ERROR]');
|
|
340
|
+
const details = isDownloadError
|
|
341
|
+
? { suggestion: DOWNLOAD_ERROR_SUGGESTION }
|
|
342
|
+
: undefined;
|
|
343
|
+
return {
|
|
344
|
+
isError: true,
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: JSON.stringify(makeError('AUTOMATION_METADATA_FAILED', message, requestId, false, details)),
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
207
352
|
}
|
|
353
|
+
const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
208
354
|
return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
|
|
209
355
|
}
|
|
210
356
|
catch (err) {
|
|
211
|
-
return handleSpawnError(err, requestId, '
|
|
357
|
+
return handleSpawnError(err, requestId, 'provar_automation_metadata_download');
|
|
212
358
|
}
|
|
213
359
|
});
|
|
214
360
|
}
|
|
215
|
-
// ── Tool:
|
|
361
|
+
// ── Tool: provar_automation_setup ─────────────────────────────────────────────
|
|
216
362
|
/** Known system-level Provar install paths per platform. */
|
|
217
363
|
const SYSTEM_INSTALL_BASES = {
|
|
218
|
-
win32: [
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
],
|
|
222
|
-
darwin: [
|
|
223
|
-
'/Applications',
|
|
224
|
-
],
|
|
225
|
-
linux: [
|
|
226
|
-
'/opt',
|
|
227
|
-
'/usr/local',
|
|
228
|
-
],
|
|
364
|
+
win32: ['C:/Program Files', 'C:/Program Files (x86)'],
|
|
365
|
+
darwin: ['/Applications'],
|
|
366
|
+
linux: ['/opt', '/usr/local'],
|
|
229
367
|
};
|
|
230
368
|
/** Try to read a Provar version string from well-known files inside an install dir. */
|
|
231
369
|
function readProvarVersion(installPath) {
|
|
@@ -299,22 +437,36 @@ function findExistingInstallations() {
|
|
|
299
437
|
return found;
|
|
300
438
|
}
|
|
301
439
|
export function registerAutomationSetup(server) {
|
|
302
|
-
server.
|
|
303
|
-
'
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
440
|
+
server.registerTool('provar_automation_setup', {
|
|
441
|
+
title: 'Install Provar Automation',
|
|
442
|
+
description: desc([
|
|
443
|
+
'Download and install Provar Automation binaries locally. Invokes `sf provar automation setup`.',
|
|
444
|
+
'Before downloading, checks for existing Provar installations in:',
|
|
445
|
+
' • PROVAR_HOME environment variable',
|
|
446
|
+
' • ./ProvarHome (default CLI install location)',
|
|
447
|
+
' • C:\\Program Files\\Provar* (Windows system installs)',
|
|
448
|
+
' • /Applications/Provar* (macOS app installs)',
|
|
449
|
+
'If an existing installation is found, returns its path so you can set provarHome in the properties file — skipping the download unless force is true.',
|
|
450
|
+
'After a successful install, update the provarHome property in provardx-properties.json to the returned install_path using provar_properties_set.',
|
|
451
|
+
].join(' '), 'Download and install Provar Automation binaries; skips if already installed.'),
|
|
452
|
+
inputSchema: {
|
|
453
|
+
version: z
|
|
454
|
+
.string()
|
|
455
|
+
.optional()
|
|
456
|
+
.describe(desc('Specific Provar Automation version to install, e.g. "2.12.0". Omit to install the latest release.', 'string, optional; version to install e.g. "2.12.0"')),
|
|
457
|
+
force: z
|
|
458
|
+
.boolean()
|
|
459
|
+
.optional()
|
|
460
|
+
.default(false)
|
|
461
|
+
.describe(desc('Force a fresh download even if an existing installation is already detected (default: false).', 'bool, optional; force re-download')),
|
|
462
|
+
sf_path: z
|
|
463
|
+
.string()
|
|
464
|
+
.optional()
|
|
465
|
+
.describe(desc('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")', 'string, optional; path to sf CLI')),
|
|
466
|
+
},
|
|
315
467
|
}, ({ version, force, sf_path }) => {
|
|
316
468
|
const requestId = makeRequestId();
|
|
317
|
-
log('info', '
|
|
469
|
+
log('info', 'provar_automation_setup', { requestId, version, force });
|
|
318
470
|
try {
|
|
319
471
|
// ── 1. Check for existing installations ──────────────────────────────
|
|
320
472
|
const existing = findExistingInstallations();
|
|
@@ -343,12 +495,17 @@ export function registerAutomationSetup(server) {
|
|
|
343
495
|
if (result.exitCode !== 0) {
|
|
344
496
|
return {
|
|
345
497
|
isError: true,
|
|
346
|
-
content: [
|
|
498
|
+
content: [
|
|
499
|
+
{
|
|
500
|
+
type: 'text',
|
|
501
|
+
text: JSON.stringify(makeError('AUTOMATION_SETUP_FAILED', result.stderr || result.stdout, requestId)),
|
|
502
|
+
},
|
|
503
|
+
],
|
|
347
504
|
};
|
|
348
505
|
}
|
|
349
506
|
// ── 3. Locate the freshly installed ProvarHome ───────────────────────
|
|
350
507
|
const freshInstalls = findExistingInstallations();
|
|
351
|
-
const localInstall = freshInstalls.find(i => i.source === 'local') ?? freshInstalls[0];
|
|
508
|
+
const localInstall = freshInstalls.find((i) => i.source === 'local') ?? freshInstalls[0];
|
|
352
509
|
const installPath = localInstall?.path ?? path.resolve(process.cwd(), 'ProvarHome');
|
|
353
510
|
const detectedVersion = localInstall?.version ?? version ?? null;
|
|
354
511
|
const response = {
|
|
@@ -362,7 +519,7 @@ export function registerAutomationSetup(server) {
|
|
|
362
519
|
version: detectedVersion,
|
|
363
520
|
message: [
|
|
364
521
|
`Provar Automation installed successfully at: ${installPath}.`,
|
|
365
|
-
'Update provarHome in your provardx-properties.json to this path using
|
|
522
|
+
'Update provarHome in your provardx-properties.json to this path using provar_properties_set.',
|
|
366
523
|
].join(' '),
|
|
367
524
|
};
|
|
368
525
|
return {
|
|
@@ -371,15 +528,15 @@ export function registerAutomationSetup(server) {
|
|
|
371
528
|
};
|
|
372
529
|
}
|
|
373
530
|
catch (err) {
|
|
374
|
-
return handleSpawnError(err, requestId, '
|
|
531
|
+
return handleSpawnError(err, requestId, 'provar_automation_setup');
|
|
375
532
|
}
|
|
376
533
|
});
|
|
377
534
|
}
|
|
378
535
|
// ── Bulk registration ─────────────────────────────────────────────────────────
|
|
379
|
-
export function registerAllAutomationTools(server) {
|
|
536
|
+
export function registerAllAutomationTools(server, config) {
|
|
380
537
|
registerAutomationSetup(server);
|
|
381
|
-
registerAutomationConfigLoad(server);
|
|
382
|
-
registerAutomationTestRun(server);
|
|
538
|
+
registerAutomationConfigLoad(server, config);
|
|
539
|
+
registerAutomationTestRun(server, config);
|
|
383
540
|
registerAutomationCompile(server);
|
|
384
541
|
registerAutomationMetadataDownload(server);
|
|
385
542
|
}
|