@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
@@ -6,6 +6,7 @@
6
6
  */
7
7
  /* eslint-disable camelcase */
8
8
  import fs from 'node:fs';
9
+ import os from 'node:os';
9
10
  import path from 'node:path';
10
11
  import { z } from 'zod';
11
12
  import { propertyFileContent } from '@provartesting/provardx-plugins-utils';
@@ -38,10 +39,18 @@ function validateProperties(props) {
38
39
  if (meta && typeof meta === 'object') {
39
40
  for (const f of METADATA_REQUIRED) {
40
41
  if (!meta[f])
41
- errors.push({ field: `metadata.${f}`, message: `Required field "metadata.${f}" is missing`, severity: 'error' });
42
+ errors.push({
43
+ field: `metadata.${f}`,
44
+ message: `Required field "metadata.${f}" is missing`,
45
+ severity: 'error',
46
+ });
42
47
  }
43
48
  if (meta['metadataLevel'] && !VALID_METADATA_LEVELS.includes(meta['metadataLevel'])) {
44
- errors.push({ field: 'metadata.metadataLevel', message: `metadata.metadataLevel must be one of: ${VALID_METADATA_LEVELS.join(', ')}`, severity: 'error' });
49
+ errors.push({
50
+ field: 'metadata.metadataLevel',
51
+ message: `metadata.metadataLevel must be one of: ${VALID_METADATA_LEVELS.join(', ')}`,
52
+ severity: 'error',
53
+ });
45
54
  }
46
55
  }
47
56
  // environment object
@@ -49,27 +58,58 @@ function validateProperties(props) {
49
58
  if (env && typeof env === 'object') {
50
59
  for (const f of ENV_REQUIRED) {
51
60
  if (!env[f])
52
- errors.push({ field: `environment.${f}`, message: `Required field "environment.${f}" is missing`, severity: 'error' });
61
+ errors.push({
62
+ field: `environment.${f}`,
63
+ message: `Required field "environment.${f}" is missing`,
64
+ severity: 'error',
65
+ });
53
66
  }
54
67
  if (env['webBrowser'] && !VALID_BROWSERS.includes(env['webBrowser'])) {
55
- errors.push({ field: 'environment.webBrowser', message: `webBrowser must be one of: ${VALID_BROWSERS.join(', ')}`, severity: 'error' });
68
+ errors.push({
69
+ field: 'environment.webBrowser',
70
+ message: `webBrowser must be one of: ${VALID_BROWSERS.join(', ')}`,
71
+ severity: 'error',
72
+ });
56
73
  }
57
74
  }
58
75
  // Optional enum fields
59
- if (props['resultsPathDisposition'] && !VALID_RESULTS_DISPOSITION.includes(props['resultsPathDisposition'])) {
60
- errors.push({ field: 'resultsPathDisposition', message: `Must be one of: ${VALID_RESULTS_DISPOSITION.join(', ')}`, severity: 'error' });
76
+ if (props['resultsPathDisposition'] &&
77
+ !VALID_RESULTS_DISPOSITION.includes(props['resultsPathDisposition'])) {
78
+ errors.push({
79
+ field: 'resultsPathDisposition',
80
+ message: `Must be one of: ${VALID_RESULTS_DISPOSITION.join(', ')}`,
81
+ severity: 'error',
82
+ });
61
83
  }
62
84
  if (props['testOutputLevel'] && !VALID_OUTPUT_LEVELS.includes(props['testOutputLevel'])) {
63
- errors.push({ field: 'testOutputLevel', message: `Must be one of: ${VALID_OUTPUT_LEVELS.join(', ')}`, severity: 'error' });
85
+ errors.push({
86
+ field: 'testOutputLevel',
87
+ message: `Must be one of: ${VALID_OUTPUT_LEVELS.join(', ')}`,
88
+ severity: 'error',
89
+ });
64
90
  }
65
91
  if (props['pluginOutputlevel'] && !VALID_PLUGIN_LEVELS.includes(props['pluginOutputlevel'])) {
66
- errors.push({ field: 'pluginOutputlevel', message: `Must be one of: ${VALID_PLUGIN_LEVELS.join(', ')}`, severity: 'error' });
92
+ errors.push({
93
+ field: 'pluginOutputlevel',
94
+ message: `Must be one of: ${VALID_PLUGIN_LEVELS.join(', ')}`,
95
+ severity: 'error',
96
+ });
67
97
  }
68
98
  // Warn about placeholder values still in file
69
- const placeholders = ['${PROVAR_HOME}', '${PROVAR_PROJECT_PATH}', '${PROVAR_RESULTS_PATH}', '${PROVAR_TEST_ENVIRONMENT}', '${PROVAR_TEST_PROJECT_SECRETS}'];
99
+ const placeholders = [
100
+ '${PROVAR_HOME}',
101
+ '${PROVAR_PROJECT_PATH}',
102
+ '${PROVAR_RESULTS_PATH}',
103
+ '${PROVAR_TEST_ENVIRONMENT}',
104
+ '${PROVAR_TEST_PROJECT_SECRETS}',
105
+ ];
70
106
  for (const [key, value] of Object.entries(props)) {
71
107
  if (typeof value === 'string' && placeholders.includes(value)) {
72
- errors.push({ field: key, message: `Field "${key}" still contains placeholder value "${value}" — replace with actual path or use an environment variable`, severity: 'warning' });
108
+ errors.push({
109
+ field: key,
110
+ message: `Field "${key}" still contains placeholder value "${value}" — replace with actual path or use an environment variable`,
111
+ severity: 'warning',
112
+ });
73
113
  }
74
114
  }
75
115
  return errors;
@@ -78,8 +118,12 @@ function validateProperties(props) {
78
118
  function deepMerge(target, source) {
79
119
  const result = { ...target };
80
120
  for (const [key, val] of Object.entries(source)) {
81
- if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
82
- result[key] !== null && typeof result[key] === 'object' && !Array.isArray(result[key])) {
121
+ if (val !== null &&
122
+ typeof val === 'object' &&
123
+ !Array.isArray(val) &&
124
+ result[key] !== null &&
125
+ typeof result[key] === 'object' &&
126
+ !Array.isArray(result[key])) {
83
127
  result[key] = deepMerge(result[key], val);
84
128
  }
85
129
  else {
@@ -88,31 +132,55 @@ function deepMerge(target, source) {
88
132
  }
89
133
  return result;
90
134
  }
91
- // ── provar.properties.generate ────────────────────────────────────────────────
135
+ // ── provar_properties_generate ────────────────────────────────────────────────
92
136
  export function registerPropertiesGenerate(server, config) {
93
- server.tool('provar.properties.generate', [
94
- 'Generate a provardx-properties.json file from the standard template.',
95
- 'Optionally pre-fills projectPath and provarHome if provided.',
96
- 'The generated file uses ${PLACEHOLDER} values that must be replaced before running tests.',
97
- 'Use provar.properties.set afterwards to update specific fields.',
98
- ].join(' '), {
99
- output_path: z.string().describe('Where to write the file (e.g. /path/to/project/provardx-properties.json)'),
100
- project_path: z.string().optional().describe('Pre-fill the projectPath field with this value'),
101
- provar_home: z.string().optional().describe('Pre-fill the provarHome field with this value'),
102
- results_path: z.string().optional().describe('Pre-fill the resultsPath field with this value'),
103
- overwrite: z.boolean().optional().default(false).describe('Overwrite the file if it already exists (default: false)'),
104
- dry_run: z.boolean().optional().default(false).describe('Return the content without writing (default: false)'),
137
+ server.registerTool('provar_properties_generate', {
138
+ title: 'Generate ProvarDX Properties File',
139
+ description: [
140
+ 'Generate a provardx-properties.json file from the standard template.',
141
+ 'Optionally pre-fills projectPath and provarHome if provided.',
142
+ 'The generated file uses ${PLACEHOLDER} values that must be replaced before running tests.',
143
+ 'Use provar_properties_set afterwards to update specific fields.',
144
+ ].join(' '),
145
+ inputSchema: {
146
+ output_path: z.string().describe('Where to write the file (e.g. /path/to/project/provardx-properties.json)'),
147
+ project_path: z.string().optional().describe('Pre-fill the projectPath field with this value'),
148
+ provar_home: z.string().optional().describe('Pre-fill the provarHome field with this value'),
149
+ results_path: z.string().optional().describe('Pre-fill the resultsPath field with this value'),
150
+ overwrite: z
151
+ .boolean()
152
+ .optional()
153
+ .default(false)
154
+ .describe('Overwrite the file if it already exists (default: false)'),
155
+ dry_run: z.boolean().optional().default(false).describe('Return the content without writing (default: false)'),
156
+ },
105
157
  }, ({ output_path, project_path, provar_home, results_path, overwrite, dry_run }) => {
106
158
  const requestId = makeRequestId();
107
- log('info', 'provar.properties.generate', { requestId, output_path });
159
+ log('info', 'provar_properties_generate', { requestId, output_path });
108
160
  try {
109
161
  assertPathAllowed(output_path, config.allowedPaths);
110
162
  const resolved = path.resolve(output_path);
111
163
  if (!overwrite && fs.existsSync(resolved)) {
112
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_EXISTS', `File already exists: ${resolved}. Set overwrite: true to replace it.`, requestId)) }] };
164
+ return {
165
+ isError: true,
166
+ content: [
167
+ {
168
+ type: 'text',
169
+ text: JSON.stringify(makeError('FILE_EXISTS', `File already exists: ${resolved}. Set overwrite: true to replace it.`, requestId)),
170
+ },
171
+ ],
172
+ };
113
173
  }
114
174
  if (!resolved.endsWith('.json')) {
115
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('INVALID_PATH', 'output_path must end with .json', requestId)) }] };
175
+ return {
176
+ isError: true,
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: JSON.stringify(makeError('INVALID_PATH', 'output_path must end with .json', requestId)),
181
+ },
182
+ ],
183
+ };
116
184
  }
117
185
  // Start from the template and apply any provided overrides
118
186
  const content = { ...propertyFileContent };
@@ -128,9 +196,16 @@ export function registerPropertiesGenerate(server, config) {
128
196
  fs.writeFileSync(resolved, json, 'utf-8');
129
197
  }
130
198
  const nextSteps = dry_run
131
- ? 'Review the content, write to disk, then run provar.automation.config.load to register this file before compiling or running tests.'
132
- : `Run provar.automation.config.load with properties_path "${resolved}" to register this configuration. Required before provar.automation.compile or provar.automation.testrun will work.`;
133
- const response = { requestId, file_path: resolved, written: !dry_run, dry_run: dry_run ?? false, content, next_steps: nextSteps };
199
+ ? 'Review the content, write to disk, then run provar_automation_config_load to register this file before compiling or running tests.'
200
+ : `Run provar_automation_config_load with properties_path "${resolved}" to register this configuration. Required before provar_automation_compile or provar_automation_testrun will work.`;
201
+ const response = {
202
+ requestId,
203
+ file_path: resolved,
204
+ written: !dry_run,
205
+ dry_run: dry_run ?? false,
206
+ content,
207
+ next_steps: nextSteps,
208
+ };
134
209
  return {
135
210
  content: [{ type: 'text', text: JSON.stringify(response) }],
136
211
  structuredContent: response,
@@ -138,22 +213,81 @@ export function registerPropertiesGenerate(server, config) {
138
213
  }
139
214
  catch (err) {
140
215
  const error = err;
141
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'GENERATE_ERROR'), error.message, requestId)) }] };
216
+ return {
217
+ isError: true,
218
+ content: [
219
+ {
220
+ type: 'text',
221
+ text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId)),
222
+ },
223
+ ],
224
+ };
142
225
  }
143
226
  });
144
227
  }
145
- // ── provar.properties.read ────────────────────────────────────────────────────
228
+ // ── Runtime divergence detection ──────────────────────────────────────────────
229
+ // Overrideable in tests so we don't touch the real ~/.sf directory
230
+ let sfConfigDirOverride = null;
231
+ /** Exposed for testing only — override the directory that contains config.json. Pass null to reset. */
232
+ export function setSfConfigDirForTesting(dir) {
233
+ sfConfigDirOverride = dir;
234
+ }
235
+ /**
236
+ * Returns the properties file path registered via `sf provar automation config load`,
237
+ * or null if the sf config cannot be read.
238
+ */
239
+ function readActivePropertiesPath() {
240
+ try {
241
+ const sfDir = sfConfigDirOverride ?? path.join(os.homedir(), '.sf');
242
+ const sfConfigPath = path.join(sfDir, 'config.json');
243
+ if (!fs.existsSync(sfConfigPath))
244
+ return null;
245
+ const sfConfig = JSON.parse(fs.readFileSync(sfConfigPath, 'utf-8'));
246
+ return sfConfig['PROVARDX_PROPERTIES_FILE_PATH'] ?? null;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * Compare two properties objects on the keys most likely to cause silent bugs.
254
+ * Returns a human-readable description of any divergent keys, or null if all match.
255
+ */
256
+ function buildDivergenceWarning(diskPath, diskContent, activePath, activeContent) {
257
+ const KEY_FIELDS = ['provarHome', 'projectPath', 'resultsPath'];
258
+ const divergent = KEY_FIELDS.filter((k) => JSON.stringify(diskContent[k]) !== JSON.stringify(activeContent[k]));
259
+ if (divergent.length === 0)
260
+ return null;
261
+ const details = divergent
262
+ .map((k) => `${k}: disk="${String(diskContent[k])}" vs active="${String(activeContent[k])}"`)
263
+ .join(', ');
264
+ return (`The file you read (${diskPath}) differs from the active sf config (${activePath}) on: ${details}. ` +
265
+ 'Test runs use the active config values — run provar_automation_config_load with the correct file to sync.');
266
+ }
267
+ // ── provar_properties_read ────────────────────────────────────────────────────
146
268
  export function registerPropertiesRead(server, config) {
147
- server.tool('provar.properties.read', 'Read and parse a provardx-properties.json file. Returns the parsed content so you can inspect current settings before making changes with provar.properties.set.', {
148
- file_path: z.string().describe('Path to the provardx-properties.json file'),
269
+ server.registerTool('provar_properties_read', {
270
+ title: 'Read Properties File',
271
+ description: 'Read and parse a provardx-properties.json file. Returns the parsed content so you can inspect current settings before making changes with provar_properties_set.',
272
+ inputSchema: {
273
+ file_path: z.string().describe('Path to the provardx-properties.json file'),
274
+ },
149
275
  }, ({ file_path }) => {
150
276
  const requestId = makeRequestId();
151
- log('info', 'provar.properties.read', { requestId, file_path });
277
+ log('info', 'provar_properties_read', { requestId, file_path });
152
278
  try {
153
279
  assertPathAllowed(file_path, config.allowedPaths);
154
280
  const resolved = path.resolve(file_path);
155
281
  if (!fs.existsSync(resolved)) {
156
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)) }] };
282
+ return {
283
+ isError: true,
284
+ content: [
285
+ {
286
+ type: 'text',
287
+ text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `Properties file not found: ${resolved}. Use provar_properties_generate to create it.`, requestId)),
288
+ },
289
+ ],
290
+ };
157
291
  }
158
292
  const raw = fs.readFileSync(resolved, 'utf-8');
159
293
  let parsed;
@@ -161,9 +295,34 @@ export function registerPropertiesRead(server, config) {
161
295
  parsed = JSON.parse(raw);
162
296
  }
163
297
  catch {
164
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MALFORMED_JSON', 'File is not valid JSON', requestId)) }] };
298
+ return {
299
+ isError: true,
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ text: JSON.stringify(makeError('MALFORMED_JSON', 'File is not valid JSON', requestId)),
304
+ },
305
+ ],
306
+ };
307
+ }
308
+ // Check whether the file being read matches what's registered as active in the sf config.
309
+ // If they differ on critical fields, surface a warning so the agent doesn't silently use stale values.
310
+ let divergenceWarning;
311
+ const activePath = readActivePropertiesPath();
312
+ if (activePath && path.resolve(activePath) !== resolved) {
313
+ try {
314
+ // Guard: only read the active file if it is within the session's allowed paths.
315
+ assertPathAllowed(activePath, config.allowedPaths);
316
+ const activeContent = JSON.parse(fs.readFileSync(activePath, 'utf-8'));
317
+ divergenceWarning = buildDivergenceWarning(resolved, parsed, activePath, activeContent) ?? undefined;
318
+ }
319
+ catch {
320
+ // Ignore — active path may be outside allowed paths or unreadable
321
+ }
165
322
  }
166
323
  const response = { requestId, file_path: resolved, content: parsed };
324
+ if (divergenceWarning)
325
+ response['details'] = { warning: divergenceWarning };
167
326
  return {
168
327
  content: [{ type: 'text', text: JSON.stringify(response) }],
169
328
  structuredContent: response,
@@ -171,64 +330,116 @@ export function registerPropertiesRead(server, config) {
171
330
  }
172
331
  catch (err) {
173
332
  const error = err;
174
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'READ_ERROR'), error.message, requestId)) }] };
333
+ return {
334
+ isError: true,
335
+ content: [
336
+ {
337
+ type: 'text',
338
+ text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'READ_ERROR', error.message, requestId)),
339
+ },
340
+ ],
341
+ };
175
342
  }
176
343
  });
177
344
  }
178
- // ── provar.properties.set ─────────────────────────────────────────────────────
179
- const updatesSchema = z.object({
345
+ // ── provar_properties_set ─────────────────────────────────────────────────────
346
+ const updatesSchema = z
347
+ .object({
180
348
  provarHome: z.string().optional().describe('Path to Provar installation directory'),
181
349
  projectPath: z.string().optional().describe('Path to the Provar test project root'),
182
350
  resultsPath: z.string().optional().describe('Path where test results will be written'),
183
- resultsPathDisposition: z.enum(['Increment', 'Replace', 'Fail']).optional().describe('What to do if results path already exists'),
351
+ resultsPathDisposition: z
352
+ .enum(['Increment', 'Replace', 'Fail'])
353
+ .optional()
354
+ .describe('What to do if results path already exists'),
184
355
  testOutputLevel: z.enum(['BASIC', 'DETAILED', 'DIAGNOSTIC']).optional().describe('Amount of test output logged'),
185
- pluginOutputlevel: z.enum(['SEVERE', 'WARNING', 'INFO', 'FINE', 'FINER', 'FINEST']).optional().describe('Amount of plugin output logged'),
356
+ pluginOutputlevel: z
357
+ .enum(['SEVERE', 'WARNING', 'INFO', 'FINE', 'FINER', 'FINEST'])
358
+ .optional()
359
+ .describe('Amount of plugin output logged'),
186
360
  stopOnError: z.boolean().optional().describe('Abort test run on first failure'),
187
361
  excludeCallable: z.boolean().optional().describe('Omit callable test cases from execution'),
188
- testprojectSecrets: z.string().optional().describe('Test project secrets encryption password'),
189
- environment: z.object({
362
+ testprojectSecrets: z
363
+ .string()
364
+ .optional()
365
+ .describe('Encryption key (password string) used to decrypt the .secrets file in the Provar project root. ' +
366
+ 'This is the key itself — NOT a file path. Omit this field unless your project uses secrets encryption.'),
367
+ environment: z
368
+ .object({
190
369
  testEnvironment: z.string().optional().describe('Name of the test environment to run against'),
191
370
  webBrowser: z.enum(['Chrome', 'Safari', 'Edge', 'Edge_Legacy', 'Firefox', 'IE', 'Chrome_Headless']).optional(),
192
371
  webBrowserConfig: z.string().optional(),
193
372
  webBrowserProviderName: z.string().optional(),
194
373
  webBrowserDeviceName: z.string().optional(),
195
- }).optional().describe('Test execution environment settings'),
196
- metadata: z.object({
374
+ })
375
+ .optional()
376
+ .describe('Test execution environment settings'),
377
+ metadata: z
378
+ .object({
197
379
  metadataLevel: z.enum(['Reuse', 'Reload', 'Refresh']).optional().describe('Salesforce metadata cache strategy'),
198
380
  cachePath: z.string().optional().describe('Path for the metadata cache'),
199
- }).optional().describe('Salesforce metadata settings'),
200
- testCase: z.array(z.string()).optional().describe('Specific test case file paths to run (relative to projectPath/tests/). NOTE: <dataTable> data-driven iteration does NOT work in this mode — data table variables resolve as null. To run data-driven tests, add the test case to a plan with provar.testplan.add-instance and run via testPlan instead.'),
381
+ })
382
+ .optional()
383
+ .describe('Salesforce metadata settings'),
384
+ testCase: z
385
+ .array(z.string())
386
+ .optional()
387
+ .describe('Specific test case file paths to run (relative to projectPath/tests/). NOTE: <dataTable> data-driven iteration does NOT work in this mode — data table variables resolve as null. To run data-driven tests, add the test case to a plan with provar_testplan_add-instance and run via testPlan instead.'),
201
388
  testPlan: z.array(z.string()).optional().describe('Test plan names to run (wildcards permitted)'),
202
- connectionOverride: z.array(z.object({
389
+ connectionOverride: z
390
+ .array(z.object({
203
391
  connection: z.string().describe('Provar connection name'),
204
392
  username: z.string().describe('SFDX username or alias to substitute'),
205
- })).optional().describe('Override Provar connections with SFDX usernames'),
206
- }).describe('Fields to update in the properties file — only provided fields are changed');
393
+ }))
394
+ .optional()
395
+ .describe('Override Provar connections with SFDX usernames'),
396
+ })
397
+ .describe('Fields to update in the properties file — only provided fields are changed');
207
398
  export function registerPropertiesSet(server, config) {
208
- server.tool('provar.properties.set', [
209
- 'Update one or more fields in a provardx-properties.json file.',
210
- 'Only the provided fields are changed — all other fields are preserved.',
211
- 'Object fields (environment, metadata) are deep-merged.',
212
- 'Array fields (testCase, testPlan, connectionOverride) replace the existing value entirely.',
213
- 'Use provar.properties.read first to inspect the current state.',
214
- ].join(' '), {
215
- file_path: z.string().describe('Path to the provardx-properties.json file to update'),
216
- updates: updatesSchema,
399
+ server.registerTool('provar_properties_set', {
400
+ title: 'Set Property Value',
401
+ description: [
402
+ 'Update one or more fields in a provardx-properties.json file.',
403
+ 'Only the provided fields are changed all other fields are preserved.',
404
+ 'Object fields (environment, metadata) are deep-merged.',
405
+ 'Array fields (testCase, testPlan, connectionOverride) replace the existing value entirely.',
406
+ 'Use provar_properties_read first to inspect the current state.',
407
+ ].join(' '),
408
+ inputSchema: {
409
+ file_path: z.string().describe('Path to the provardx-properties.json file to update'),
410
+ updates: updatesSchema,
411
+ },
217
412
  }, ({ file_path, updates }) => {
218
413
  const requestId = makeRequestId();
219
- log('info', 'provar.properties.set', { requestId, file_path });
414
+ log('info', 'provar_properties_set', { requestId, file_path });
220
415
  try {
221
416
  assertPathAllowed(file_path, config.allowedPaths);
222
417
  const resolved = path.resolve(file_path);
223
418
  if (!fs.existsSync(resolved)) {
224
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}. Use provar.properties.generate to create it first.`, requestId)) }] };
419
+ return {
420
+ isError: true,
421
+ content: [
422
+ {
423
+ type: 'text',
424
+ text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `File not found: ${resolved}. Use provar_properties_generate to create it first.`, requestId)),
425
+ },
426
+ ],
427
+ };
225
428
  }
226
429
  let current;
227
430
  try {
228
431
  current = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
229
432
  }
230
433
  catch {
231
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MALFORMED_JSON', 'Existing file is not valid JSON', requestId)) }] };
434
+ return {
435
+ isError: true,
436
+ content: [
437
+ {
438
+ type: 'text',
439
+ text: JSON.stringify(makeError('MALFORMED_JSON', 'Existing file is not valid JSON', requestId)),
440
+ },
441
+ ],
442
+ };
232
443
  }
233
444
  const updatesRecord = updates;
234
445
  const merged = deepMerge(current, updatesRecord);
@@ -242,24 +453,44 @@ export function registerPropertiesSet(server, config) {
242
453
  }
243
454
  catch (err) {
244
455
  const error = err;
245
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'SET_ERROR'), error.message, requestId)) }] };
456
+ return {
457
+ isError: true,
458
+ content: [
459
+ {
460
+ type: 'text',
461
+ text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'SET_ERROR', error.message, requestId)),
462
+ },
463
+ ],
464
+ };
246
465
  }
247
466
  });
248
467
  }
249
- // ── provar.properties.validate ────────────────────────────────────────────────
468
+ // ── provar_properties_validate ────────────────────────────────────────────────
250
469
  export function registerPropertiesValidate(server, config) {
251
- server.tool('provar.properties.validate', [
252
- 'Validate a provardx-properties.json file against the ProvarDX schema.',
253
- 'Checks required fields, valid enum values, and warns about unfilled placeholder values.',
254
- 'Accepts either a file path or inline JSON content.',
255
- ].join(' '), {
256
- file_path: z.string().optional().describe('Path to the provardx-properties.json file to validate'),
257
- content: z.string().optional().describe('Inline JSON string to validate (alternative to file_path)'),
470
+ server.registerTool('provar_properties_validate', {
471
+ title: 'Validate ProvarDX Properties File',
472
+ description: [
473
+ 'Validate a provardx-properties.json file against the ProvarDX schema.',
474
+ 'Checks required fields, valid enum values, and warns about unfilled placeholder values.',
475
+ 'Accepts either a file path or inline JSON content.',
476
+ ].join(' '),
477
+ inputSchema: {
478
+ file_path: z.string().optional().describe('Path to the provardx-properties.json file to validate'),
479
+ content: z.string().optional().describe('Inline JSON string to validate (alternative to file_path)'),
480
+ },
258
481
  }, ({ file_path, content }) => {
259
482
  const requestId = makeRequestId();
260
- log('info', 'provar.properties.validate', { requestId, file_path });
483
+ log('info', 'provar_properties_validate', { requestId, file_path });
261
484
  if (!file_path && !content) {
262
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MISSING_INPUT', 'Provide either file_path or content', requestId)) }] };
485
+ return {
486
+ isError: true,
487
+ content: [
488
+ {
489
+ type: 'text',
490
+ text: JSON.stringify(makeError('MISSING_INPUT', 'Provide either file_path or content', requestId)),
491
+ },
492
+ ],
493
+ };
263
494
  }
264
495
  try {
265
496
  let rawJson;
@@ -267,7 +498,15 @@ export function registerPropertiesValidate(server, config) {
267
498
  assertPathAllowed(file_path, config.allowedPaths);
268
499
  const resolved = path.resolve(file_path);
269
500
  if (!fs.existsSync(resolved)) {
270
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)) }] };
501
+ return {
502
+ isError: true,
503
+ content: [
504
+ {
505
+ type: 'text',
506
+ text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)),
507
+ },
508
+ ],
509
+ };
271
510
  }
272
511
  rawJson = fs.readFileSync(resolved, 'utf-8');
273
512
  }
@@ -279,7 +518,14 @@ export function registerPropertiesValidate(server, config) {
279
518
  parsed = JSON.parse(rawJson);
280
519
  }
281
520
  catch {
282
- const response = { requestId, is_valid: false, error_count: 1, warning_count: 0, errors: [{ field: '(root)', message: 'File is not valid JSON', severity: 'error' }], warnings: [] };
521
+ const response = {
522
+ requestId,
523
+ is_valid: false,
524
+ error_count: 1,
525
+ warning_count: 0,
526
+ errors: [{ field: '(root)', message: 'File is not valid JSON', severity: 'error' }],
527
+ warnings: [],
528
+ };
283
529
  return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
284
530
  }
285
531
  const allIssues = validateProperties(parsed);
@@ -300,7 +546,15 @@ export function registerPropertiesValidate(server, config) {
300
546
  }
301
547
  catch (err) {
302
548
  const error = err;
303
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'VALIDATE_ERROR'), error.message, requestId)) }] };
549
+ return {
550
+ isError: true,
551
+ content: [
552
+ {
553
+ type: 'text',
554
+ text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'VALIDATE_ERROR', error.message, requestId)),
555
+ },
556
+ ],
557
+ };
304
558
  }
305
559
  });
306
560
  }