@provartesting/provardx-cli 1.5.0-dev.2 → 1.5.0

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