@provartesting/provardx-cli 1.5.0-beta.1 → 1.5.0-beta.10

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 (78) hide show
  1. package/README.md +137 -13
  2. package/lib/commands/provar/auth/clear.d.ts +7 -0
  3. package/lib/commands/provar/auth/clear.js +36 -0
  4. package/lib/commands/provar/auth/clear.js.map +1 -0
  5. package/lib/commands/provar/auth/login.d.ts +10 -0
  6. package/lib/commands/provar/auth/login.js +90 -0
  7. package/lib/commands/provar/auth/login.js.map +1 -0
  8. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  9. package/lib/commands/provar/auth/rotate.js +42 -0
  10. package/lib/commands/provar/auth/rotate.js.map +1 -0
  11. package/lib/commands/provar/auth/status.d.ts +7 -0
  12. package/lib/commands/provar/auth/status.js +107 -0
  13. package/lib/commands/provar/auth/status.js.map +1 -0
  14. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  15. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  16. package/lib/mcp/licensing/licenseValidator.js +4 -4
  17. package/lib/mcp/prompts/index.d.ts +2 -0
  18. package/lib/mcp/prompts/index.js +19 -0
  19. package/lib/mcp/prompts/index.js.map +1 -0
  20. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  21. package/lib/mcp/prompts/loopPrompts.js +435 -0
  22. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  23. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  24. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  25. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  26. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  27. package/lib/mcp/server.js +45 -1
  28. package/lib/mcp/server.js.map +1 -1
  29. package/lib/mcp/tools/antTools.d.ts +15 -0
  30. package/lib/mcp/tools/antTools.js +204 -50
  31. package/lib/mcp/tools/antTools.js.map +1 -1
  32. package/lib/mcp/tools/automationTools.d.ts +36 -3
  33. package/lib/mcp/tools/automationTools.js +335 -42
  34. package/lib/mcp/tools/automationTools.js.map +1 -1
  35. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  36. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  37. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  38. package/lib/mcp/tools/connectionTools.js +168 -0
  39. package/lib/mcp/tools/connectionTools.js.map +1 -0
  40. package/lib/mcp/tools/nitroXTools.d.ts +22 -0
  41. package/lib/mcp/tools/nitroXTools.js +750 -0
  42. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  43. package/lib/mcp/tools/pageObjectGenerate.js +103 -35
  44. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  45. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  46. package/lib/mcp/tools/propertiesTools.js +277 -39
  47. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  48. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  49. package/lib/mcp/tools/qualityHubApiTools.js +134 -0
  50. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  51. package/lib/mcp/tools/qualityHubTools.js +127 -19
  52. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  53. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  54. package/lib/mcp/tools/rcaTools.js +145 -20
  55. package/lib/mcp/tools/rcaTools.js.map +1 -1
  56. package/lib/mcp/tools/testCaseGenerate.js +88 -59
  57. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  58. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  59. package/lib/mcp/tools/testCaseStepTools.js +221 -0
  60. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  61. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  62. package/lib/mcp/tools/testCaseValidate.js +146 -19
  63. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  64. package/lib/services/auth/credentials.d.ts +21 -0
  65. package/lib/services/auth/credentials.js +75 -0
  66. package/lib/services/auth/credentials.js.map +1 -0
  67. package/lib/services/auth/loginFlow.d.ts +68 -0
  68. package/lib/services/auth/loginFlow.js +216 -0
  69. package/lib/services/auth/loginFlow.js.map +1 -0
  70. package/lib/services/qualityHub/client.d.ts +161 -0
  71. package/lib/services/qualityHub/client.js +226 -0
  72. package/lib/services/qualityHub/client.js.map +1 -0
  73. package/messages/sf.provar.auth.clear.md +16 -0
  74. package/messages/sf.provar.auth.login.md +31 -0
  75. package/messages/sf.provar.auth.rotate.md +23 -0
  76. package/messages/sf.provar.auth.status.md +16 -0
  77. package/oclif.manifest.json +214 -1
  78. package/package.json +8 -4
@@ -0,0 +1,750 @@
1
+ /*
2
+ * Copyright (c) 2024 Provar Limited.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ /* eslint-disable camelcase */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { randomUUID } from 'node:crypto';
12
+ import { z } from 'zod';
13
+ import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
14
+ import { makeError, makeRequestId } from '../schemas/common.js';
15
+ import { log } from '../logging/logger.js';
16
+ function isObj(v) {
17
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
18
+ }
19
+ // ── Directory Utilities ───────────────────────────────────────────────────────
20
+ const SKIP_DIRS = new Set(['node_modules', '.git']);
21
+ /**
22
+ * Recursively walk directories looking for .testproject marker files.
23
+ * Skips node_modules, .git, and hidden dirs (names starting with '.').
24
+ */
25
+ function findProvarProjects(roots, maxDepth) {
26
+ const projects = [];
27
+ function walk(dir, depth) {
28
+ if (depth > maxDepth)
29
+ return;
30
+ if (fs.existsSync(path.join(dir, '.testproject'))) {
31
+ projects.push(dir);
32
+ return; // don't recurse into a found project
33
+ }
34
+ let entries;
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory())
43
+ continue;
44
+ if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name))
45
+ continue;
46
+ walk(path.join(dir, entry.name), depth + 1);
47
+ }
48
+ }
49
+ for (const root of roots) {
50
+ try {
51
+ if (!fs.existsSync(root))
52
+ continue;
53
+ walk(root, 0);
54
+ }
55
+ catch {
56
+ // Root inaccessible — skip gracefully
57
+ }
58
+ }
59
+ return projects;
60
+ }
61
+ /** Collect all *.po.json files under a directory, recursively. */
62
+ function collectPoJsonFiles(dir) {
63
+ const files = [];
64
+ function walk(d) {
65
+ let entries;
66
+ try {
67
+ entries = fs.readdirSync(d, { withFileTypes: true });
68
+ }
69
+ catch {
70
+ return;
71
+ }
72
+ for (const entry of entries) {
73
+ if (entry.isDirectory()) {
74
+ walk(path.join(d, entry.name));
75
+ }
76
+ else if (entry.isFile() && entry.name.endsWith('.po.json')) {
77
+ files.push(path.join(d, entry.name));
78
+ }
79
+ }
80
+ }
81
+ walk(dir);
82
+ return files;
83
+ }
84
+ // ── NitroX Validator ─────────────────────────────────────────────────────────
85
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
86
+ const VALID_COMPARISON_TYPES = ['equals', 'starts-with', 'contains'];
87
+ const INTERACTION_NAME_RE = /^[A-Za-z0-9\s]*$/;
88
+ /** Validate root-level scalar properties (NX001, NX002, NX003, NX010). */
89
+ function validateRootProperties(obj, issues) {
90
+ // NX001: componentId must be present and a valid UUID
91
+ if (obj['componentId'] === undefined || obj['componentId'] === null) {
92
+ issues.push({
93
+ rule_id: 'NX001', severity: 'ERROR',
94
+ message: 'componentId is required.',
95
+ applies_to: 'root', field: 'componentId',
96
+ });
97
+ }
98
+ else if (typeof obj['componentId'] !== 'string' || !UUID_RE.test(obj['componentId'])) {
99
+ issues.push({
100
+ rule_id: 'NX001', severity: 'ERROR',
101
+ message: `componentId must be a valid UUID, got: "${String(obj['componentId'])}".`,
102
+ applies_to: 'root', field: 'componentId',
103
+ });
104
+ }
105
+ // NX002: Root (no parentId) requires name, type, pageStructureElement, fieldDetailsElement
106
+ const hasParentId = obj['parentId'] !== undefined && obj['parentId'] !== null;
107
+ if (!hasParentId) {
108
+ for (const field of ['name', 'type', 'pageStructureElement', 'fieldDetailsElement']) {
109
+ if (obj[field] === undefined || obj[field] === null) {
110
+ issues.push({
111
+ rule_id: 'NX002', severity: 'ERROR',
112
+ message: `Root component requires "${field}".`,
113
+ applies_to: 'root', field,
114
+ suggestion: `Add a "${field}" property to the root component object.`,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ // NX003: tagName must not contain whitespace
120
+ if (typeof obj['tagName'] === 'string' && /\s/.test(obj['tagName'])) {
121
+ issues.push({
122
+ rule_id: 'NX003', severity: 'ERROR',
123
+ message: 'tagName should not contain spaces.',
124
+ applies_to: 'root', field: 'tagName',
125
+ suggestion: 'Remove whitespace from tagName.',
126
+ });
127
+ }
128
+ // NX010: bodyTagName (if present) must not contain whitespace
129
+ if (typeof obj['bodyTagName'] === 'string' && /\s/.test(obj['bodyTagName'])) {
130
+ issues.push({
131
+ rule_id: 'NX010', severity: 'INFO',
132
+ message: 'bodyTagName should not contain spaces.',
133
+ applies_to: 'root', field: 'bodyTagName',
134
+ suggestion: 'Remove whitespace from bodyTagName.',
135
+ });
136
+ }
137
+ }
138
+ /** Validate a parsed NitroX .po.json object against schema-derived rules. */
139
+ export function validateNitroXContent(obj) {
140
+ const issues = [];
141
+ validateRootProperties(obj, issues);
142
+ // Validate root-level parameters
143
+ if (Array.isArray(obj['parameters'])) {
144
+ for (const param of obj['parameters']) {
145
+ if (isObj(param))
146
+ validateParameter(param, 'root', issues);
147
+ }
148
+ }
149
+ // Validate root-level interactions
150
+ if (Array.isArray(obj['interactions'])) {
151
+ for (const interaction of obj['interactions']) {
152
+ if (isObj(interaction))
153
+ validateInteraction(interaction, 'root', issues);
154
+ }
155
+ }
156
+ // Validate root-level selectors
157
+ if (Array.isArray(obj['selectors'])) {
158
+ for (const sel of obj['selectors']) {
159
+ if (isObj(sel))
160
+ validateSelector(sel, issues);
161
+ }
162
+ }
163
+ // Validate elements recursively
164
+ if (Array.isArray(obj['elements'])) {
165
+ for (const el of obj['elements']) {
166
+ if (isObj(el))
167
+ validateElement(el, issues);
168
+ }
169
+ }
170
+ const errorCount = issues.filter((i) => i.severity === 'ERROR').length;
171
+ const warningCount = issues.filter((i) => i.severity === 'WARNING').length;
172
+ const infoCount = issues.filter((i) => i.severity === 'INFO').length;
173
+ const score = Math.max(0, 100 - 20 * errorCount - 5 * warningCount - 1 * infoCount);
174
+ return { valid: errorCount === 0, score, issue_count: issues.length, issues };
175
+ }
176
+ function validateElement(el, issues) {
177
+ // NX007: Element should have type
178
+ if (!el['type']) {
179
+ issues.push({
180
+ rule_id: 'NX007', severity: 'WARNING',
181
+ message: 'Element is missing required "type".',
182
+ applies_to: 'element',
183
+ suggestion: 'Add a "type" field to the element (e.g. "content" or "component::UUID").',
184
+ });
185
+ }
186
+ if (Array.isArray(el['selectors'])) {
187
+ for (const sel of el['selectors']) {
188
+ if (isObj(sel))
189
+ validateSelector(sel, issues);
190
+ }
191
+ }
192
+ if (Array.isArray(el['interactions'])) {
193
+ for (const interaction of el['interactions']) {
194
+ if (isObj(interaction))
195
+ validateInteraction(interaction, 'element', issues);
196
+ }
197
+ }
198
+ if (Array.isArray(el['parameters'])) {
199
+ for (const param of el['parameters']) {
200
+ if (isObj(param))
201
+ validateParameter(param, 'element', issues);
202
+ }
203
+ }
204
+ if (Array.isArray(el['elements'])) {
205
+ for (const nested of el['elements']) {
206
+ if (isObj(nested))
207
+ validateElement(nested, issues);
208
+ }
209
+ }
210
+ }
211
+ function validateInteraction(interaction, context, issues) {
212
+ // NX004: required fields
213
+ for (const field of ['defaultInteraction', 'interactionType', 'name', 'testStepTitlePattern', 'title']) {
214
+ if (interaction[field] === undefined || interaction[field] === null) {
215
+ issues.push({
216
+ rule_id: 'NX004', severity: 'ERROR',
217
+ message: `Interaction in ${context} missing required field "${field}".`,
218
+ applies_to: 'interaction', field,
219
+ });
220
+ }
221
+ }
222
+ if (!Array.isArray(interaction['implementations']) || interaction['implementations'].length === 0) {
223
+ issues.push({
224
+ rule_id: 'NX004', severity: 'ERROR',
225
+ message: `Interaction in ${context} must have at least one implementation.`,
226
+ applies_to: 'interaction', field: 'implementations',
227
+ });
228
+ }
229
+ else {
230
+ for (const impl of interaction['implementations']) {
231
+ if (isObj(impl))
232
+ validateImplementation(impl, context, issues);
233
+ }
234
+ }
235
+ // NX009: name should match ^[A-Za-z0-9\s]*$
236
+ if (typeof interaction['name'] === 'string' && !INTERACTION_NAME_RE.test(interaction['name'])) {
237
+ issues.push({
238
+ rule_id: 'NX009', severity: 'INFO',
239
+ message: `Interaction name "${interaction['name']}" should contain only alphanumeric characters and spaces.`,
240
+ applies_to: 'interaction', field: 'name',
241
+ suggestion: 'Remove special characters from the interaction name.',
242
+ });
243
+ }
244
+ }
245
+ function validateImplementation(impl, context, issues) {
246
+ // NX005: must have javaScriptSnippet
247
+ if (!impl['javaScriptSnippet']) {
248
+ issues.push({
249
+ rule_id: 'NX005', severity: 'ERROR',
250
+ message: `Implementation in ${context} missing required "javaScriptSnippet".`,
251
+ applies_to: 'implementation', field: 'javaScriptSnippet',
252
+ });
253
+ }
254
+ }
255
+ function validateSelector(sel, issues) {
256
+ // NX006: must have xpath
257
+ if (!sel['xpath']) {
258
+ issues.push({
259
+ rule_id: 'NX006', severity: 'ERROR',
260
+ message: 'Selector missing required "xpath".',
261
+ applies_to: 'selector', field: 'xpath',
262
+ suggestion: 'Add an "xpath" property to the selector.',
263
+ });
264
+ }
265
+ }
266
+ function validateParameter(param, context, issues) {
267
+ // NX008: comparisonType must be one of valid enum values
268
+ if (param['comparisonType'] !== undefined && !VALID_COMPARISON_TYPES.includes(String(param['comparisonType']))) {
269
+ issues.push({
270
+ rule_id: 'NX008', severity: 'WARNING',
271
+ message: `Parameter in ${context} has invalid comparisonType "${String(param['comparisonType'])}". Must be one of: ${VALID_COMPARISON_TYPES.join(', ')}.`,
272
+ applies_to: 'parameter', field: 'comparisonType',
273
+ suggestion: `Use one of: ${VALID_COMPARISON_TYPES.join(', ')}`,
274
+ });
275
+ }
276
+ }
277
+ function buildNitroXJson(input) {
278
+ const result = {
279
+ componentId: randomUUID(),
280
+ name: input.name,
281
+ tagName: input.tag_name,
282
+ type: input.type,
283
+ pageStructureElement: input.page_structure_element,
284
+ fieldDetailsElement: input.field_details_element,
285
+ };
286
+ if (input.parameters && input.parameters.length > 0) {
287
+ result['parameters'] = input.parameters.map((p, i) => ({
288
+ name: p.name,
289
+ value: p.value,
290
+ ...(p.comparisonType !== undefined && { comparisonType: p.comparisonType }),
291
+ ...(p.default !== undefined && { default: p.default }),
292
+ index: i,
293
+ }));
294
+ }
295
+ if (input.elements && input.elements.length > 0) {
296
+ result['elements'] = input.elements.map((el) => buildElement(el));
297
+ }
298
+ return result;
299
+ }
300
+ function buildElement(el) {
301
+ const element = {
302
+ componentId: randomUUID(),
303
+ type: el.type_ref,
304
+ label: el.label,
305
+ };
306
+ if (el.tag_name) {
307
+ element['elementTagName'] = el.tag_name;
308
+ }
309
+ if (el.parameters && el.parameters.length > 0) {
310
+ element['parameters'] = el.parameters.map((p, i) => ({
311
+ name: p.name,
312
+ value: p.value,
313
+ ...(p.comparisonType !== undefined && { comparisonType: p.comparisonType }),
314
+ ...(p.default !== undefined && { default: p.default }),
315
+ index: i,
316
+ }));
317
+ }
318
+ if (el.selector_xpath) {
319
+ element['selectors'] = [{ xpath: el.selector_xpath }];
320
+ }
321
+ return element;
322
+ }
323
+ // ── RFC 7396 Merge-Patch ──────────────────────────────────────────────────────
324
+ function applyMergePatch(target, patch) {
325
+ const result = { ...target };
326
+ for (const [key, value] of Object.entries(patch)) {
327
+ if (value === null) {
328
+ delete result[key];
329
+ }
330
+ else if (isObj(value) && isObj(result[key])) {
331
+ result[key] = applyMergePatch(result[key], value);
332
+ }
333
+ else {
334
+ result[key] = value;
335
+ }
336
+ }
337
+ return result;
338
+ }
339
+ // ── Tool Registrations ────────────────────────────────────────────────────────
340
+ export function registerNitroXDiscover(server) {
341
+ server.tool('provar.nitrox.discover', [
342
+ 'Discover Provar projects containing NitroX (Hybrid Model) page objects.',
343
+ 'Scans directories for .testproject marker files, then inventories nitroX/ and nitroXPackages/ directories.',
344
+ 'NitroX is Provar\'s Hybrid Model for locators — component-based page objects for LWC,',
345
+ 'Screen Flow, Industry Components, Experience Cloud, and HTML5 components.',
346
+ 'Results provide file paths and package info for use with provar.nitrox.read, validate, and generate.',
347
+ ].join(' '), {
348
+ search_roots: z
349
+ .array(z.string())
350
+ .optional()
351
+ .describe('Directories to scan (default: cwd; if empty, falls back to ~/git and ~/Provar)'),
352
+ max_depth: z
353
+ .number()
354
+ .int()
355
+ .min(1)
356
+ .max(20)
357
+ .default(6)
358
+ .describe('Maximum directory depth for .testproject search'),
359
+ include_packages: z
360
+ .boolean()
361
+ .default(true)
362
+ .describe('Include nitroXPackages/ package.json metadata in results'),
363
+ }, ({ search_roots, max_depth, include_packages }) => {
364
+ const requestId = makeRequestId();
365
+ log('info', 'provar.nitrox.discover', { requestId, search_roots, max_depth });
366
+ try {
367
+ let roots = search_roots && search_roots.length > 0 ? search_roots : [process.cwd()];
368
+ let projects = findProvarProjects(roots, max_depth);
369
+ // If no .testproject found in cwd, widen to home-dir defaults
370
+ if (projects.length === 0 && (!search_roots || search_roots.length === 0)) {
371
+ const fallbackRoots = [
372
+ path.join(os.homedir(), 'git'),
373
+ path.join(os.homedir(), 'Provar'),
374
+ ];
375
+ const fallbackProjects = findProvarProjects(fallbackRoots, max_depth);
376
+ if (fallbackProjects.length > 0) {
377
+ projects = fallbackProjects;
378
+ roots = fallbackRoots;
379
+ }
380
+ }
381
+ const projectResults = projects.map((projectPath) => {
382
+ const nitroxDir = path.join(projectPath, 'nitroX');
383
+ const packagesDir = path.join(projectPath, 'nitroXPackages');
384
+ const hasNitrox = fs.existsSync(nitroxDir);
385
+ const hasPackages = fs.existsSync(packagesDir);
386
+ const nitroxFiles = hasNitrox ? collectPoJsonFiles(nitroxDir) : [];
387
+ let packages = [];
388
+ if (include_packages && hasPackages) {
389
+ try {
390
+ packages = fs
391
+ .readdirSync(packagesDir, { withFileTypes: true })
392
+ .filter((e) => e.isDirectory())
393
+ .map((e) => {
394
+ const pkgDir = path.join(packagesDir, e.name);
395
+ const pkgJson = path.join(pkgDir, 'package.json');
396
+ if (!fs.existsSync(pkgJson))
397
+ return { path: pkgDir };
398
+ try {
399
+ const parsed = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
400
+ return { path: pkgDir, name: String(parsed['name'] ?? '') };
401
+ }
402
+ catch {
403
+ return { path: pkgDir, error: 'invalid JSON' };
404
+ }
405
+ });
406
+ }
407
+ catch {
408
+ // packagesDir inaccessible — return empty packages
409
+ }
410
+ }
411
+ return {
412
+ project_path: projectPath,
413
+ nitrox_dir: hasNitrox ? nitroxDir : null,
414
+ nitrox_file_count: nitroxFiles.length,
415
+ nitrox_files: nitroxFiles,
416
+ packages_dir: hasPackages ? packagesDir : null,
417
+ packages,
418
+ };
419
+ });
420
+ const result = { requestId, projects: projectResults, searched_roots: roots };
421
+ return {
422
+ content: [{ type: 'text', text: JSON.stringify(result) }],
423
+ structuredContent: result,
424
+ };
425
+ }
426
+ catch (err) {
427
+ const error = err;
428
+ const errResult = makeError('DISCOVER_ERROR', error.message, requestId, false);
429
+ log('error', 'provar.nitrox.discover failed', { requestId, error: error.message });
430
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
431
+ }
432
+ });
433
+ }
434
+ export function registerNitroXRead(server, config) {
435
+ server.tool('provar.nitrox.read', [
436
+ 'Read one or more NitroX .po.json (Hybrid Model page object) files and return their parsed content.',
437
+ 'Use this to load examples before generating or validating.',
438
+ 'Provide file_paths for specific files, or project_path to read all .po.json files from a project\'s nitroX/ directory.',
439
+ ].join(' '), {
440
+ file_paths: z.array(z.string()).optional().describe('Specific .po.json file paths to read'),
441
+ project_path: z
442
+ .string()
443
+ .optional()
444
+ .describe('Provar project path — reads all .po.json files from nitroX/ directory'),
445
+ max_files: z
446
+ .number()
447
+ .int()
448
+ .min(1)
449
+ .max(100)
450
+ .default(20)
451
+ .describe('Maximum number of files to return (prevents context overflow)'),
452
+ }, ({ file_paths, project_path, max_files }) => {
453
+ const requestId = makeRequestId();
454
+ log('info', 'provar.nitrox.read', {
455
+ requestId,
456
+ file_count: file_paths?.length,
457
+ project_path,
458
+ });
459
+ try {
460
+ if (!file_paths?.length && !project_path) {
461
+ const err = makeError('MISSING_INPUT', 'Provide either file_paths or project_path.', requestId);
462
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
463
+ }
464
+ let targets = [];
465
+ if (file_paths?.length) {
466
+ targets = file_paths;
467
+ }
468
+ else if (project_path) {
469
+ assertPathAllowed(project_path, config.allowedPaths);
470
+ const resolved = path.resolve(project_path);
471
+ if (!fs.existsSync(resolved)) {
472
+ const err = makeError('FILE_NOT_FOUND', `Project path not found: ${resolved}`, requestId);
473
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
474
+ }
475
+ const nitroxDir = path.join(resolved, 'nitroX');
476
+ if (!fs.existsSync(nitroxDir)) {
477
+ const err = makeError('FILE_NOT_FOUND', `No nitroX/ directory found in: ${resolved}`, requestId);
478
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
479
+ }
480
+ targets = collectPoJsonFiles(nitroxDir);
481
+ }
482
+ const truncated = targets.length > max_files;
483
+ const toRead = targets.slice(0, max_files);
484
+ const files = toRead.map((filePath) => {
485
+ const resolved = path.resolve(filePath);
486
+ try {
487
+ assertPathAllowed(resolved, config.allowedPaths);
488
+ }
489
+ catch (e) {
490
+ const policyErr = e;
491
+ return { file_path: resolved, error: policyErr.message, content: null, size_bytes: 0 };
492
+ }
493
+ if (!fs.existsSync(resolved)) {
494
+ return { file_path: resolved, error: 'FILE_NOT_FOUND', content: null, size_bytes: 0 };
495
+ }
496
+ try {
497
+ const raw = fs.readFileSync(resolved, 'utf-8');
498
+ const content = JSON.parse(raw);
499
+ return { file_path: resolved, content, size_bytes: raw.length };
500
+ }
501
+ catch {
502
+ return { file_path: resolved, error: 'PARSE_ERROR', content: null, size_bytes: 0 };
503
+ }
504
+ });
505
+ const result = { requestId, files, truncated, total_found: targets.length };
506
+ return {
507
+ content: [{ type: 'text', text: JSON.stringify(result) }],
508
+ structuredContent: result,
509
+ };
510
+ }
511
+ catch (err) {
512
+ const error = err;
513
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : 'READ_ERROR', error.message, requestId, false);
514
+ log('error', 'provar.nitrox.read failed', { requestId, error: error.message });
515
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
516
+ }
517
+ });
518
+ }
519
+ export function registerNitroXValidate(server, config) {
520
+ server.tool('provar.nitrox.validate', [
521
+ 'Validate a NitroX .po.json (Hybrid Model component page object) against schema rules.',
522
+ 'Works for any NitroX-mapped component type: LWC, Screen Flow, Industry Components, Experience Cloud, HTML5.',
523
+ 'Returns a quality score (0–100) and a list of issues with rule IDs (NX001–NX010), severity, and suggestions.',
524
+ 'Score formula: 100 − (20 × errors) − (5 × warnings) − (1 × infos).',
525
+ ].join(' '), {
526
+ content: z.string().optional().describe('JSON string of the .po.json content to validate'),
527
+ file_path: z.string().optional().describe('Path to a .po.json file to validate'),
528
+ }, ({ content, file_path }) => {
529
+ const requestId = makeRequestId();
530
+ log('info', 'provar.nitrox.validate', { requestId, has_content: !!content, file_path });
531
+ try {
532
+ let source = content;
533
+ if (!source && file_path) {
534
+ assertPathAllowed(file_path, config.allowedPaths);
535
+ const resolved = path.resolve(file_path);
536
+ if (!fs.existsSync(resolved)) {
537
+ const err = makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId);
538
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
539
+ }
540
+ source = fs.readFileSync(resolved, 'utf-8');
541
+ }
542
+ if (!source) {
543
+ const err = makeError('MISSING_INPUT', 'Provide either content or file_path.', requestId);
544
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
545
+ }
546
+ let parsed;
547
+ try {
548
+ parsed = JSON.parse(source);
549
+ }
550
+ catch {
551
+ const err = makeError('NX000', 'Invalid JSON: could not parse content.', requestId);
552
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
553
+ }
554
+ if (!isObj(parsed)) {
555
+ const err = makeError('NX000', 'Content must be a JSON object.', requestId);
556
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
557
+ }
558
+ const validation = validateNitroXContent(parsed);
559
+ const result = { requestId, ...validation };
560
+ return {
561
+ content: [{ type: 'text', text: JSON.stringify(result) }],
562
+ structuredContent: result,
563
+ };
564
+ }
565
+ catch (err) {
566
+ const error = err;
567
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : 'VALIDATE_ERROR', error.message, requestId, false);
568
+ log('error', 'provar.nitrox.validate failed', { requestId, error: error.message });
569
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
570
+ }
571
+ });
572
+ }
573
+ const ParameterInputSchema = z.object({
574
+ name: z.string().describe('Parameter/qualifier name'),
575
+ value: z.string().describe('Parameter value'),
576
+ comparisonType: z.enum(['equals', 'starts-with', 'contains']).optional(),
577
+ default: z.boolean().optional().describe('Whether this is the default parameter value'),
578
+ });
579
+ const ElementInputSchema = z.object({
580
+ label: z.string().describe('Human-readable element label'),
581
+ type_ref: z
582
+ .string()
583
+ .describe('Component type reference (e.g. "component::UUID" or "content")'),
584
+ tag_name: z.string().optional().describe('Optional HTML/LWC tag name override'),
585
+ parameters: z.array(ParameterInputSchema).optional(),
586
+ selector_xpath: z.string().optional().describe('XPath selector for this element'),
587
+ });
588
+ export function registerNitroXGenerate(server, config) {
589
+ server.tool('provar.nitrox.generate', [
590
+ 'Generate a new NitroX .po.json (Hybrid Model page object) from a component description.',
591
+ 'Applicable to any component type supported by Provar\'s Hybrid Model:',
592
+ 'LWC, Screen Flow, Industry Components, Experience Cloud, HTML5.',
593
+ 'All componentId fields are assigned fresh UUIDs. Returns JSON content;',
594
+ 'writes to disk only when dry_run=false.',
595
+ ].join(' '), {
596
+ name: z
597
+ .string()
598
+ .describe('Path-like component name, e.g. /com/force/myapp/ButtonComponent'),
599
+ tag_name: z
600
+ .string()
601
+ .describe('LWC or HTML tag name, e.g. lightning-button or c-my-component'),
602
+ type: z.enum(['Block', 'Page']).default('Block').describe('Component type'),
603
+ page_structure_element: z
604
+ .boolean()
605
+ .default(true)
606
+ .describe('Whether this is a page structure element'),
607
+ field_details_element: z
608
+ .boolean()
609
+ .default(false)
610
+ .describe('Whether this is a field details element'),
611
+ parameters: z.array(ParameterInputSchema).optional().describe('Component parameters/qualifiers'),
612
+ elements: z.array(ElementInputSchema).optional().describe('Child elements'),
613
+ output_path: z
614
+ .string()
615
+ .optional()
616
+ .describe('File path to write (requires dry_run=false)'),
617
+ overwrite: z.boolean().default(false).describe('Overwrite if output_path already exists'),
618
+ dry_run: z
619
+ .boolean()
620
+ .default(true)
621
+ .describe('Return JSON without writing to disk (default)'),
622
+ }, (input) => {
623
+ const requestId = makeRequestId();
624
+ log('info', 'provar.nitrox.generate', { requestId, name: input.name, dry_run: input.dry_run });
625
+ try {
626
+ const generated = buildNitroXJson({
627
+ name: input.name,
628
+ tag_name: input.tag_name,
629
+ type: input.type,
630
+ page_structure_element: input.page_structure_element,
631
+ field_details_element: input.field_details_element,
632
+ parameters: input.parameters,
633
+ elements: input.elements,
634
+ });
635
+ const content = JSON.stringify(generated, null, 2);
636
+ let filePath;
637
+ let written = false;
638
+ if (input.output_path && !input.dry_run) {
639
+ filePath = path.resolve(input.output_path);
640
+ assertPathAllowed(filePath, config.allowedPaths);
641
+ if (fs.existsSync(filePath) && !input.overwrite) {
642
+ const err = makeError('FILE_EXISTS', `File already exists: ${filePath}. Set overwrite=true to replace.`, requestId);
643
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
644
+ }
645
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
646
+ fs.writeFileSync(filePath, content, 'utf-8');
647
+ written = true;
648
+ log('info', 'provar.nitrox.generate: wrote file', { requestId, filePath });
649
+ }
650
+ const result = { requestId, content, file_path: filePath, written, dry_run: input.dry_run };
651
+ return {
652
+ content: [{ type: 'text', text: JSON.stringify(result) }],
653
+ structuredContent: result,
654
+ };
655
+ }
656
+ catch (err) {
657
+ const error = err;
658
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'GENERATE_ERROR'), error.message, requestId, false);
659
+ log('error', 'provar.nitrox.generate failed', { requestId, error: error.message });
660
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
661
+ }
662
+ });
663
+ }
664
+ export function registerNitroXPatch(server, config) {
665
+ server.tool('provar.nitrox.patch', [
666
+ 'Apply a JSON merge-patch (RFC 7396) to an existing NitroX .po.json file.',
667
+ 'Reads the file, merges the patch (null values remove keys, other values replace or recurse into objects),',
668
+ 'optionally validates the merged result, and writes back.',
669
+ 'Use dry_run=true (default) to preview the merged output without writing.',
670
+ ].join(' '), {
671
+ file_path: z.string().describe('Path to the existing .po.json file to patch'),
672
+ patch: z
673
+ .record(z.unknown())
674
+ .describe('JSON merge-patch to apply (RFC 7396: null removes key, any other value replaces)'),
675
+ dry_run: z
676
+ .boolean()
677
+ .default(true)
678
+ .describe('Return merged result without writing to disk (default)'),
679
+ validate_after: z
680
+ .boolean()
681
+ .default(true)
682
+ .describe('Run NX validation on merged result; blocks write if errors found'),
683
+ }, ({ file_path, patch, dry_run, validate_after }) => {
684
+ const requestId = makeRequestId();
685
+ log('info', 'provar.nitrox.patch', { requestId, file_path, dry_run });
686
+ try {
687
+ assertPathAllowed(file_path, config.allowedPaths);
688
+ const resolved = path.resolve(file_path);
689
+ if (!fs.existsSync(resolved)) {
690
+ const err = makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId);
691
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
692
+ }
693
+ let original;
694
+ try {
695
+ original = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
696
+ }
697
+ catch {
698
+ const err = makeError('PARSE_ERROR', 'File contains invalid JSON.', requestId);
699
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
700
+ }
701
+ if (!isObj(original)) {
702
+ const err = makeError('PARSE_ERROR', 'File content must be a JSON object.', requestId);
703
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
704
+ }
705
+ const merged = applyMergePatch(original, patch);
706
+ const content = JSON.stringify(merged, null, 2);
707
+ let validation;
708
+ if (validate_after) {
709
+ validation = validateNitroXContent(merged);
710
+ if (!dry_run && !validation.valid) {
711
+ const errCount = validation.issues.filter((i) => i.severity === 'ERROR').length;
712
+ const err = makeError('VALIDATION_FAILED', `Patched content has ${errCount} error(s). Fix issues or set validate_after=false to skip.`, requestId, false, { validation });
713
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
714
+ }
715
+ }
716
+ let written = false;
717
+ if (!dry_run) {
718
+ fs.writeFileSync(resolved, content, 'utf-8');
719
+ written = true;
720
+ log('info', 'provar.nitrox.patch: wrote file', { requestId, filePath: resolved });
721
+ }
722
+ const result = {
723
+ requestId,
724
+ content,
725
+ file_path: resolved,
726
+ written,
727
+ dry_run,
728
+ ...(validation !== undefined && { validation }),
729
+ };
730
+ return {
731
+ content: [{ type: 'text', text: JSON.stringify(result) }],
732
+ structuredContent: result,
733
+ };
734
+ }
735
+ catch (err) {
736
+ const error = err;
737
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'PATCH_ERROR'), error.message, requestId, false);
738
+ log('error', 'provar.nitrox.patch failed', { requestId, error: error.message });
739
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
740
+ }
741
+ });
742
+ }
743
+ export function registerAllNitroXTools(server, config) {
744
+ registerNitroXDiscover(server);
745
+ registerNitroXRead(server, config);
746
+ registerNitroXValidate(server, config);
747
+ registerNitroXGenerate(server, config);
748
+ registerNitroXPatch(server, config);
749
+ }
750
+ //# sourceMappingURL=nitroXTools.js.map