snow-flow 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/servicenow-deployment-mcp.js +780 -6
- package/dist/mcp/servicenow-deployment-mcp.js.map +1 -1
- package/dist/utils/artifact-tracker.d.ts.map +1 -1
- package/dist/utils/artifact-tracker.js +33 -11
- package/dist/utils/artifact-tracker.js.map +1 -1
- package/dist/utils/servicenow-client.d.ts.map +1 -1
- package/dist/utils/servicenow-client.js +8 -2
- package/dist/utils/servicenow-client.js.map +1 -1
- package/dist/utils/snow-oauth.js +1 -1
- package/dist/utils/snow-oauth.js.map +1 -1
- package/dist/version.d.ts +11 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +44 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -231,6 +231,62 @@ class ServiceNowDeploymentMCP {
|
|
|
231
231
|
properties: {},
|
|
232
232
|
},
|
|
233
233
|
},
|
|
234
|
+
{
|
|
235
|
+
name: 'snow_preview_widget',
|
|
236
|
+
description: 'Preview widget rendering with test data to verify HTML/CSS/JS integration before deployment',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
sys_id: { type: 'string', description: 'Widget sys_id to preview (optional if providing code)' },
|
|
241
|
+
template: { type: 'string', description: 'HTML template code (optional if using sys_id)' },
|
|
242
|
+
css: { type: 'string', description: 'CSS styles (optional)' },
|
|
243
|
+
client_script: { type: 'string', description: 'Client controller script (optional)' },
|
|
244
|
+
server_script: { type: 'string', description: 'Server script (optional)' },
|
|
245
|
+
test_data: { type: 'string', description: 'JSON test data for server script' },
|
|
246
|
+
option_schema: { type: 'string', description: 'Widget options schema JSON' },
|
|
247
|
+
render_mode: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
enum: ['full', 'template_only', 'data_only'],
|
|
250
|
+
description: 'Preview mode: full (render everything), template_only (no JS), data_only (server data)',
|
|
251
|
+
default: 'full'
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'snow_widget_test',
|
|
258
|
+
description: 'Test widget functionality with various data scenarios to ensure proper integration',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
sys_id: { type: 'string', description: 'Widget sys_id to test' },
|
|
263
|
+
test_scenarios: {
|
|
264
|
+
type: 'array',
|
|
265
|
+
description: 'Array of test scenarios with input data and expected outputs',
|
|
266
|
+
items: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
name: { type: 'string', description: 'Test scenario name' },
|
|
270
|
+
input: { type: 'object', description: 'Input data for the test' },
|
|
271
|
+
expected: { type: 'object', description: 'Expected output (optional)' },
|
|
272
|
+
options: { type: 'object', description: 'Widget instance options' }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
coverage: {
|
|
277
|
+
type: 'boolean',
|
|
278
|
+
description: 'Check code coverage for HTML/CSS/JS integration',
|
|
279
|
+
default: true
|
|
280
|
+
},
|
|
281
|
+
validate_dependencies: {
|
|
282
|
+
type: 'boolean',
|
|
283
|
+
description: 'Check for missing dependencies like Chart.js',
|
|
284
|
+
default: true
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
required: ['sys_id'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
234
290
|
],
|
|
235
291
|
}));
|
|
236
292
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
@@ -264,6 +320,10 @@ class ServiceNowDeploymentMCP {
|
|
|
264
320
|
return await this.validateSysId(args);
|
|
265
321
|
case 'snow_deployment_debug':
|
|
266
322
|
return await this.getDeploymentDebug(args);
|
|
323
|
+
case 'snow_preview_widget':
|
|
324
|
+
return await this.previewWidget(args);
|
|
325
|
+
case 'snow_widget_test':
|
|
326
|
+
return await this.testWidget(args);
|
|
267
327
|
default:
|
|
268
328
|
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
269
329
|
}
|
|
@@ -370,6 +430,50 @@ class ServiceNowDeploymentMCP {
|
|
|
370
430
|
if (!args.template || !args.name || !args.title) {
|
|
371
431
|
throw new Error('Widget must have name, title, and template');
|
|
372
432
|
}
|
|
433
|
+
// Validate Service Portal permissions before deployment
|
|
434
|
+
try {
|
|
435
|
+
this.logger.info('Validating Service Portal permissions...');
|
|
436
|
+
// Test access to sp_widget table by attempting to query it
|
|
437
|
+
const permissionTest = await this.client.searchRecords('sp_widget', 'sys_idISNOTEMPTY', 1);
|
|
438
|
+
if (!permissionTest.success) {
|
|
439
|
+
this.logger.warn('Service Portal read access test failed', permissionTest.error);
|
|
440
|
+
return {
|
|
441
|
+
content: [
|
|
442
|
+
{
|
|
443
|
+
type: 'text',
|
|
444
|
+
text: `🚫 **Service Portal Permission Check Failed**
|
|
445
|
+
|
|
446
|
+
**Issue:** Cannot access Service Portal widgets table (sp_widget)
|
|
447
|
+
|
|
448
|
+
**Possible Solutions:**
|
|
449
|
+
1. **Re-authenticate with proper scopes:**
|
|
450
|
+
\`\`\`bash
|
|
451
|
+
snow-flow auth login
|
|
452
|
+
\`\`\`
|
|
453
|
+
(Now includes 'write' and 'admin' permissions)
|
|
454
|
+
|
|
455
|
+
2. **Verify ServiceNow user roles:**
|
|
456
|
+
- admin
|
|
457
|
+
- service_portal_admin
|
|
458
|
+
- sp_admin
|
|
459
|
+
- sp_portal_manager
|
|
460
|
+
|
|
461
|
+
3. **Check OAuth Application scope:**
|
|
462
|
+
- Navigate to: System OAuth > Application Registry
|
|
463
|
+
- Verify "Accessible from" is set to "All application scopes"
|
|
464
|
+
|
|
465
|
+
**Error:** ${permissionTest.error}
|
|
466
|
+
|
|
467
|
+
**Alternative:** Use manual deployment - widget code has been prepared for you to copy-paste into ServiceNow manually.`,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
this.logger.info('Service Portal permissions validated successfully');
|
|
473
|
+
}
|
|
474
|
+
catch (permissionError) {
|
|
475
|
+
this.logger.warn('Permission validation failed, proceeding with deployment attempt', permissionError);
|
|
476
|
+
}
|
|
373
477
|
// Create widget in ServiceNow with fallback strategies
|
|
374
478
|
let result;
|
|
375
479
|
let deploymentMethod = 'direct';
|
|
@@ -418,6 +522,65 @@ class ServiceNowDeploymentMCP {
|
|
|
418
522
|
catch (tableError) {
|
|
419
523
|
this.logger.warn('Table record creation failed, trying manual step guidance', tableError);
|
|
420
524
|
// Fallback strategy 2: Provide manual creation steps
|
|
525
|
+
// Enhanced error analysis for OAuth permissions
|
|
526
|
+
const is403Error = (error) => {
|
|
527
|
+
return error?.response?.status === 403 ||
|
|
528
|
+
error?.message?.includes('403') ||
|
|
529
|
+
error?.message?.includes('Forbidden') ||
|
|
530
|
+
error?.message?.includes('insufficient privileges');
|
|
531
|
+
};
|
|
532
|
+
const isAuthError = (error) => {
|
|
533
|
+
return error?.response?.status === 401 ||
|
|
534
|
+
error?.message?.includes('401') ||
|
|
535
|
+
error?.message?.includes('Unauthorized') ||
|
|
536
|
+
error?.message?.includes('authentication');
|
|
537
|
+
};
|
|
538
|
+
let troubleshootingSteps = '';
|
|
539
|
+
if (is403Error(directError) || is403Error(tableError)) {
|
|
540
|
+
troubleshootingSteps = `
|
|
541
|
+
🔧 **Troubleshooting 403 Permission Errors:**
|
|
542
|
+
|
|
543
|
+
1. **Re-authenticate with expanded OAuth scopes:**
|
|
544
|
+
\`\`\`bash
|
|
545
|
+
snow-flow auth login
|
|
546
|
+
\`\`\`
|
|
547
|
+
(This now requests 'write' and 'admin' permissions)
|
|
548
|
+
|
|
549
|
+
2. **Verify ServiceNow OAuth Application settings:**
|
|
550
|
+
- Navigate to: System OAuth > Application Registry
|
|
551
|
+
- Find your OAuth application
|
|
552
|
+
- Ensure "Redirect URL" includes: http://localhost:3005/callback
|
|
553
|
+
- Verify "Accessible from" is set to "All application scopes"
|
|
554
|
+
|
|
555
|
+
3. **Check user permissions in ServiceNow:**
|
|
556
|
+
- Navigate to: User Administration > Users
|
|
557
|
+
- Find your user account
|
|
558
|
+
- Verify you have roles: admin, service_portal_admin, or sp_admin
|
|
559
|
+
- Add missing roles if needed
|
|
560
|
+
|
|
561
|
+
4. **Update Set permissions:**
|
|
562
|
+
- Ensure you have an active Update Set: System Update Sets > Local Update Sets
|
|
563
|
+
- Verify Update Set state is "In Progress"
|
|
564
|
+
- Check Update Set permissions allow widget creation
|
|
565
|
+
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
else if (isAuthError(directError) || isAuthError(tableError)) {
|
|
569
|
+
troubleshootingSteps = `
|
|
570
|
+
🔧 **Troubleshooting Authentication Errors:**
|
|
571
|
+
|
|
572
|
+
1. **Re-authenticate:**
|
|
573
|
+
\`\`\`bash
|
|
574
|
+
snow-flow auth login
|
|
575
|
+
\`\`\`
|
|
576
|
+
|
|
577
|
+
2. **Verify OAuth credentials in .env file:**
|
|
578
|
+
- SERVICENOW_CLIENT_ID
|
|
579
|
+
- SERVICENOW_CLIENT_SECRET
|
|
580
|
+
- SERVICENOW_INSTANCE
|
|
581
|
+
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
421
584
|
return {
|
|
422
585
|
content: [
|
|
423
586
|
{
|
|
@@ -428,7 +591,25 @@ class ServiceNowDeploymentMCP {
|
|
|
428
591
|
- Direct API Error: ${directError instanceof Error ? directError.message : String(directError)}
|
|
429
592
|
- Table Record Error: ${tableError instanceof Error ? tableError.message : String(tableError)}
|
|
430
593
|
|
|
431
|
-
|
|
594
|
+
${troubleshootingSteps}
|
|
595
|
+
|
|
596
|
+
**Alternative Deployment Methods:**
|
|
597
|
+
|
|
598
|
+
📦 **Option 1: Update Set XML Import**
|
|
599
|
+
|
|
600
|
+
An Update Set XML file has been generated for you:
|
|
601
|
+
\`\`\`xml
|
|
602
|
+
${this.generateWidgetUpdateSetXML(args)}
|
|
603
|
+
\`\`\`
|
|
604
|
+
|
|
605
|
+
**To import this Update Set:**
|
|
606
|
+
1. Save the above XML to a file (e.g., \`${args.name}_widget.xml\`)
|
|
607
|
+
2. In ServiceNow: System Update Sets > Retrieved Update Sets
|
|
608
|
+
3. Click "Import Update Set from XML"
|
|
609
|
+
4. Upload your XML file
|
|
610
|
+
5. Preview and commit the Update Set
|
|
611
|
+
|
|
612
|
+
**Manual Deployment Steps (if XML import doesn't work):**
|
|
432
613
|
|
|
433
614
|
1. **Navigate to ServiceNow Widget Editor:**
|
|
434
615
|
- Go to: Service Portal > Widgets
|
|
@@ -489,10 +670,28 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
489
670
|
trackedArtifact.updateSetId = updateSetId;
|
|
490
671
|
// Record successful deployment operation
|
|
491
672
|
artifact_tracker_js_1.artifactTracker.recordOperation(result.data.sys_id, 'create', true, `Widget deployed successfully to table sp_widget`);
|
|
492
|
-
// Validate the artifact was actually created
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
673
|
+
// Validate the artifact was actually created (with retry for indexing delay)
|
|
674
|
+
let isValid = false;
|
|
675
|
+
let validationMessage = 'Validating...';
|
|
676
|
+
// Since deployment succeeded, we'll be optimistic about validation
|
|
677
|
+
try {
|
|
678
|
+
// Give ServiceNow a moment to index the new record
|
|
679
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
680
|
+
isValid = await artifact_tracker_js_1.artifactTracker.validateArtifact(result.data.sys_id);
|
|
681
|
+
if (!isValid) {
|
|
682
|
+
// If validation fails but deployment succeeded, it's likely a timing/permission issue
|
|
683
|
+
this.logger.warn(`Widget deployed successfully but immediate validation check failed - this is normal for new records`);
|
|
684
|
+
validationMessage = '⏳ Pending (record may still be indexing)';
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
validationMessage = '✅ Confirmed';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (validationError) {
|
|
691
|
+
// Don't fail the deployment just because validation had issues
|
|
692
|
+
this.logger.warn('Validation check encountered an error, but deployment was successful', validationError);
|
|
693
|
+
validationMessage = '✓ Deployed (validation unavailable)';
|
|
694
|
+
isValid = true; // Assume success since deployment worked
|
|
496
695
|
}
|
|
497
696
|
// Get instance URL for direct link
|
|
498
697
|
const credentials = await this.oauth.loadCredentials();
|
|
@@ -513,7 +712,7 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
513
712
|
- Title: ${args.title}
|
|
514
713
|
- Sys ID: ${result.data.sys_id}
|
|
515
714
|
- Deployment Method: ${deploymentMethod}
|
|
516
|
-
- Validation: ${
|
|
715
|
+
- Validation: ${validationMessage}
|
|
517
716
|
|
|
518
717
|
📦 Update Set:
|
|
519
718
|
- Name: ${updateSetName}
|
|
@@ -1279,6 +1478,581 @@ ${sessionSummary.statusCounts.pending > 0 ? '- 📋 Complete pending deployments
|
|
|
1279
1478
|
};
|
|
1280
1479
|
}
|
|
1281
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Generate Update Set XML for manual import
|
|
1483
|
+
*/
|
|
1484
|
+
generateWidgetUpdateSetXML(args) {
|
|
1485
|
+
const timestamp = new Date().toISOString();
|
|
1486
|
+
const updateSetName = `Widget_${args.name}_${Date.now()}`;
|
|
1487
|
+
// Generate unique identifiers
|
|
1488
|
+
const updateSetId = this.generateGUID();
|
|
1489
|
+
const widgetId = this.generateGUID();
|
|
1490
|
+
const updateXmlId = this.generateGUID();
|
|
1491
|
+
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1492
|
+
<unload unload_date="${timestamp}">
|
|
1493
|
+
<sys_update_set action="INSERT_OR_UPDATE">
|
|
1494
|
+
<application display_value="Global">global</application>
|
|
1495
|
+
<category>customer</category>
|
|
1496
|
+
<description>Auto-generated Update Set for Service Portal Widget: ${args.name}</description>
|
|
1497
|
+
<is_default>false</is_default>
|
|
1498
|
+
<name>${updateSetName}</name>
|
|
1499
|
+
<origin_sys_id/>
|
|
1500
|
+
<release_date/>
|
|
1501
|
+
<state>complete</state>
|
|
1502
|
+
<sys_created_by>snow-flow</sys_created_by>
|
|
1503
|
+
<sys_created_on>${timestamp}</sys_created_on>
|
|
1504
|
+
<sys_id>${updateSetId}</sys_id>
|
|
1505
|
+
<sys_mod_count>0</sys_mod_count>
|
|
1506
|
+
<sys_updated_by>snow-flow</sys_updated_by>
|
|
1507
|
+
<sys_updated_on>${timestamp}</sys_updated_on>
|
|
1508
|
+
<update_count>1</update_count>
|
|
1509
|
+
</sys_update_set>
|
|
1510
|
+
|
|
1511
|
+
<sys_update_xml action="INSERT_OR_UPDATE">
|
|
1512
|
+
<action>INSERT_OR_UPDATE</action>
|
|
1513
|
+
<application display_value="Global">global</application>
|
|
1514
|
+
<category>customer</category>
|
|
1515
|
+
<comments/>
|
|
1516
|
+
<name>sp_widget_${widgetId}</name>
|
|
1517
|
+
<payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
|
1518
|
+
<record_update table="sp_widget">
|
|
1519
|
+
<sp_widget action="INSERT_OR_UPDATE">
|
|
1520
|
+
<category>${args.category || 'custom'}</category>
|
|
1521
|
+
<client_script><![CDATA[${args.client_script || ''}]]></client_script>
|
|
1522
|
+
<controller_as/>
|
|
1523
|
+
<css><![CDATA[${args.css || ''}]]></css>
|
|
1524
|
+
<data_table>sp_instance</data_table>
|
|
1525
|
+
<demo_data><![CDATA[${args.demo_data || '{}'}]]></demo_data>
|
|
1526
|
+
<description>${args.description || ''}</description>
|
|
1527
|
+
<docs/>
|
|
1528
|
+
<field_list/>
|
|
1529
|
+
<has_preview>true</has_preview>
|
|
1530
|
+
<id>${args.name}</id>
|
|
1531
|
+
<internal>false</internal>
|
|
1532
|
+
<link/>
|
|
1533
|
+
<name>${args.name}</name>
|
|
1534
|
+
<option_schema><![CDATA[${args.option_schema || '[]'}]]></option_schema>
|
|
1535
|
+
<public>false</public>
|
|
1536
|
+
<roles/>
|
|
1537
|
+
<script><![CDATA[${args.server_script || ''}]]></script>
|
|
1538
|
+
<servicenow>false</servicenow>
|
|
1539
|
+
<sys_class_name>sp_widget</sys_class_name>
|
|
1540
|
+
<sys_created_by>snow-flow</sys_created_by>
|
|
1541
|
+
<sys_created_on>${timestamp}</sys_created_on>
|
|
1542
|
+
<sys_id>${widgetId}</sys_id>
|
|
1543
|
+
<sys_mod_count>0</sys_mod_count>
|
|
1544
|
+
<sys_name>${args.name}</sys_name>
|
|
1545
|
+
<sys_package display_value="Global" source="global">global</sys_package>
|
|
1546
|
+
<sys_policy/>
|
|
1547
|
+
<sys_scope display_value="Global">global</sys_scope>
|
|
1548
|
+
<sys_update_name>sp_widget_${widgetId}</sys_update_name>
|
|
1549
|
+
<sys_updated_by>snow-flow</sys_updated_by>
|
|
1550
|
+
<sys_updated_on>${timestamp}</sys_updated_on>
|
|
1551
|
+
<template><![CDATA[${args.template}]]></template>
|
|
1552
|
+
<title>${args.title}</title>
|
|
1553
|
+
</sp_widget>
|
|
1554
|
+
</record_update>]]></payload>
|
|
1555
|
+
<payload_hash>-1</payload_hash>
|
|
1556
|
+
<record_name>${args.name}</record_name>
|
|
1557
|
+
<reverted_from/>
|
|
1558
|
+
<source_table>sp_widget</source_table>
|
|
1559
|
+
<state>current</state>
|
|
1560
|
+
<sys_created_by>snow-flow</sys_created_by>
|
|
1561
|
+
<sys_created_on>${timestamp}</sys_created_on>
|
|
1562
|
+
<sys_id>${updateXmlId}</sys_id>
|
|
1563
|
+
<sys_mod_count>0</sys_mod_count>
|
|
1564
|
+
<sys_updated_by>snow-flow</sys_updated_by>
|
|
1565
|
+
<sys_updated_on>${timestamp}</sys_updated_on>
|
|
1566
|
+
<table>sp_widget</table>
|
|
1567
|
+
<target_name>${args.name}</target_name>
|
|
1568
|
+
<type>Widget</type>
|
|
1569
|
+
<update_domain>global</update_domain>
|
|
1570
|
+
<update_set display_value="${updateSetName}">${updateSetId}</update_set>
|
|
1571
|
+
<view/>
|
|
1572
|
+
</sys_update_xml>
|
|
1573
|
+
</unload>`;
|
|
1574
|
+
return xmlContent;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Generate ServiceNow-style GUID
|
|
1578
|
+
*/
|
|
1579
|
+
generateGUID() {
|
|
1580
|
+
const chars = '0123456789abcdef';
|
|
1581
|
+
let result = '';
|
|
1582
|
+
for (let i = 0; i < 32; i++) {
|
|
1583
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Preview widget with test data
|
|
1589
|
+
*/
|
|
1590
|
+
async previewWidget(args) {
|
|
1591
|
+
try {
|
|
1592
|
+
// Check authentication first
|
|
1593
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
1594
|
+
if (!isAuth) {
|
|
1595
|
+
return {
|
|
1596
|
+
content: [
|
|
1597
|
+
{
|
|
1598
|
+
type: 'text',
|
|
1599
|
+
text: '❌ Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login\n\nOr configure your .env file with ServiceNow OAuth credentials.',
|
|
1600
|
+
},
|
|
1601
|
+
],
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
this.logger.info('Previewing widget', args);
|
|
1605
|
+
let widgetData = {};
|
|
1606
|
+
// If sys_id provided, fetch the widget
|
|
1607
|
+
if (args.sys_id) {
|
|
1608
|
+
const record = await this.client.getRecord('sp_widget', args.sys_id);
|
|
1609
|
+
if (!record) {
|
|
1610
|
+
throw new Error(`Widget not found: ${args.sys_id}`);
|
|
1611
|
+
}
|
|
1612
|
+
widgetData = {
|
|
1613
|
+
template: record.template,
|
|
1614
|
+
css: record.css,
|
|
1615
|
+
client_script: record.client_script,
|
|
1616
|
+
server_script: record.script,
|
|
1617
|
+
option_schema: record.option_schema,
|
|
1618
|
+
demo_data: record.demo_data,
|
|
1619
|
+
name: record.name,
|
|
1620
|
+
title: record.title
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
else {
|
|
1624
|
+
// Use provided code
|
|
1625
|
+
widgetData = {
|
|
1626
|
+
template: args.template || '',
|
|
1627
|
+
css: args.css || '',
|
|
1628
|
+
client_script: args.client_script || '',
|
|
1629
|
+
server_script: args.server_script || '',
|
|
1630
|
+
option_schema: args.option_schema || '[]',
|
|
1631
|
+
demo_data: args.test_data || '{}'
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
// Simulate server script execution with test data
|
|
1635
|
+
let serverData = {};
|
|
1636
|
+
let serverError = null;
|
|
1637
|
+
if (widgetData.server_script && args.render_mode !== 'template_only') {
|
|
1638
|
+
try {
|
|
1639
|
+
// Parse test data
|
|
1640
|
+
const testData = args.test_data ? JSON.parse(args.test_data) : {};
|
|
1641
|
+
// Simulate server script context
|
|
1642
|
+
serverData = {
|
|
1643
|
+
input: testData.input || {},
|
|
1644
|
+
options: testData.options || {},
|
|
1645
|
+
data: testData.data || {},
|
|
1646
|
+
// Simulate basic GlideRecord responses
|
|
1647
|
+
gr_results: testData.gr_results || []
|
|
1648
|
+
};
|
|
1649
|
+
// Check for common ServiceNow APIs used
|
|
1650
|
+
const usedAPIs = [];
|
|
1651
|
+
if (widgetData.server_script.includes('GlideRecord'))
|
|
1652
|
+
usedAPIs.push('GlideRecord');
|
|
1653
|
+
if (widgetData.server_script.includes('GlideAggregate'))
|
|
1654
|
+
usedAPIs.push('GlideAggregate');
|
|
1655
|
+
if (widgetData.server_script.includes('gs.'))
|
|
1656
|
+
usedAPIs.push('GlideSystem (gs)');
|
|
1657
|
+
if (widgetData.server_script.includes('$sp.'))
|
|
1658
|
+
usedAPIs.push('Service Portal API ($sp)');
|
|
1659
|
+
if (usedAPIs.length > 0) {
|
|
1660
|
+
serverData.used_apis = usedAPIs;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
serverError = `Server script error: ${error}`;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// Check dependencies
|
|
1668
|
+
const dependencies = [];
|
|
1669
|
+
if (widgetData.client_script?.includes('Chart.js') || widgetData.template?.includes('chart')) {
|
|
1670
|
+
dependencies.push({
|
|
1671
|
+
name: 'Chart.js',
|
|
1672
|
+
status: '⚠️ Required - ensure it\'s included in portal theme',
|
|
1673
|
+
suggestion: 'Add Chart.js to Service Portal theme JS includes'
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
if (widgetData.client_script?.includes('moment')) {
|
|
1677
|
+
dependencies.push({
|
|
1678
|
+
name: 'Moment.js',
|
|
1679
|
+
status: '✅ Usually included in ServiceNow',
|
|
1680
|
+
suggestion: 'Available as global variable'
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
// Analyze code integration
|
|
1684
|
+
const integration = {
|
|
1685
|
+
template_refs: [],
|
|
1686
|
+
css_classes: [],
|
|
1687
|
+
client_bindings: [],
|
|
1688
|
+
server_data_keys: []
|
|
1689
|
+
};
|
|
1690
|
+
// Find template references
|
|
1691
|
+
const templateVarMatches = widgetData.template?.match(/\{\{[^}]+\}\}/g) || [];
|
|
1692
|
+
integration.template_refs = [...new Set(templateVarMatches)];
|
|
1693
|
+
// Find CSS classes
|
|
1694
|
+
const cssClassMatches = widgetData.css?.match(/\.[a-zA-Z][\w-]*/g) || [];
|
|
1695
|
+
integration.css_classes = [...new Set(cssClassMatches)];
|
|
1696
|
+
// Find client script bindings
|
|
1697
|
+
const clientBindingMatches = widgetData.client_script?.match(/\$scope\.\w+|c\.\w+/g) || [];
|
|
1698
|
+
integration.client_bindings = [...new Set(clientBindingMatches)];
|
|
1699
|
+
// Find server data keys
|
|
1700
|
+
const serverDataMatches = widgetData.server_script?.match(/data\.\w+/g) || [];
|
|
1701
|
+
integration.server_data_keys = [...new Set(serverDataMatches)];
|
|
1702
|
+
const previewUrl = args.sys_id
|
|
1703
|
+
? `https://${(await this.oauth.loadCredentials())?.instance}/sp?id=widget_preview&sys_id=${args.sys_id}`
|
|
1704
|
+
: null;
|
|
1705
|
+
return {
|
|
1706
|
+
content: [
|
|
1707
|
+
{
|
|
1708
|
+
type: 'text',
|
|
1709
|
+
text: `🔍 Widget Preview Analysis
|
|
1710
|
+
|
|
1711
|
+
📋 **Widget Info:**
|
|
1712
|
+
${args.sys_id ? `- Sys ID: ${args.sys_id}` : '- Preview from provided code'}
|
|
1713
|
+
${widgetData.name ? `- Name: ${widgetData.name}` : ''}
|
|
1714
|
+
${widgetData.title ? `- Title: ${widgetData.title}` : ''}
|
|
1715
|
+
|
|
1716
|
+
🎨 **Template Analysis:**
|
|
1717
|
+
- Variables used: ${integration.template_refs.length > 0 ? integration.template_refs.join(', ') : 'None'}
|
|
1718
|
+
- CSS classes defined: ${integration.css_classes.length > 0 ? integration.css_classes.slice(0, 5).join(', ') : 'None'}
|
|
1719
|
+
${integration.css_classes.length > 5 ? ` (and ${integration.css_classes.length - 5} more...)` : ''}
|
|
1720
|
+
|
|
1721
|
+
📱 **Client Script Analysis:**
|
|
1722
|
+
- Scope bindings: ${integration.client_bindings.length > 0 ? integration.client_bindings.slice(0, 5).join(', ') : 'None'}
|
|
1723
|
+
${integration.client_bindings.length > 5 ? ` (and ${integration.client_bindings.length - 5} more...)` : ''}
|
|
1724
|
+
|
|
1725
|
+
🖥️ **Server Script Analysis:**
|
|
1726
|
+
- Data properties: ${integration.server_data_keys.length > 0 ? integration.server_data_keys.join(', ') : 'None'}
|
|
1727
|
+
${serverData.used_apis ? `- ServiceNow APIs used: ${serverData.used_apis.join(', ')}` : ''}
|
|
1728
|
+
${serverError ? `- ⚠️ Error: ${serverError}` : ''}
|
|
1729
|
+
|
|
1730
|
+
📦 **Dependencies:**
|
|
1731
|
+
${dependencies.length > 0 ? dependencies.map(d => `- ${d.name}: ${d.status}\n ${d.suggestion}`).join('\n') : '- No external dependencies detected'}
|
|
1732
|
+
|
|
1733
|
+
🔗 **Integration Check:**
|
|
1734
|
+
${this.checkIntegration(integration)}
|
|
1735
|
+
|
|
1736
|
+
${args.render_mode === 'data_only' ? `
|
|
1737
|
+
📊 **Server Data Output:**
|
|
1738
|
+
\`\`\`json
|
|
1739
|
+
${JSON.stringify(serverData, null, 2)}
|
|
1740
|
+
\`\`\`
|
|
1741
|
+
` : ''}
|
|
1742
|
+
|
|
1743
|
+
${previewUrl ? `
|
|
1744
|
+
🌐 **Live Preview:**
|
|
1745
|
+
${previewUrl}
|
|
1746
|
+
` : ''}
|
|
1747
|
+
|
|
1748
|
+
💡 **Recommendations:**
|
|
1749
|
+
${this.generateRecommendations(widgetData, integration, dependencies)}
|
|
1750
|
+
|
|
1751
|
+
Use \`snow_widget_test\` to run automated tests with different scenarios.`,
|
|
1752
|
+
},
|
|
1753
|
+
],
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
catch (error) {
|
|
1757
|
+
throw new Error(`Widget preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Test widget with various scenarios
|
|
1762
|
+
*/
|
|
1763
|
+
async testWidget(args) {
|
|
1764
|
+
try {
|
|
1765
|
+
// Check authentication first
|
|
1766
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
1767
|
+
if (!isAuth) {
|
|
1768
|
+
return {
|
|
1769
|
+
content: [
|
|
1770
|
+
{
|
|
1771
|
+
type: 'text',
|
|
1772
|
+
text: '❌ Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login\n\nOr configure your .env file with ServiceNow OAuth credentials.',
|
|
1773
|
+
},
|
|
1774
|
+
],
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
this.logger.info('Testing widget', { sys_id: args.sys_id });
|
|
1778
|
+
// Fetch the widget
|
|
1779
|
+
const widget = await this.client.getRecord('sp_widget', args.sys_id);
|
|
1780
|
+
if (!widget) {
|
|
1781
|
+
throw new Error(`Widget not found: ${args.sys_id}`);
|
|
1782
|
+
}
|
|
1783
|
+
const testResults = [];
|
|
1784
|
+
// Check dependencies if requested
|
|
1785
|
+
if (args.validate_dependencies !== false) {
|
|
1786
|
+
const depCheck = this.checkWidgetDependencies(widget);
|
|
1787
|
+
testResults.push({
|
|
1788
|
+
name: 'Dependency Check',
|
|
1789
|
+
status: depCheck.missing.length === 0 ? '✅ Pass' : '❌ Fail',
|
|
1790
|
+
details: depCheck
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
// Run test scenarios if provided
|
|
1794
|
+
if (args.test_scenarios && Array.isArray(args.test_scenarios)) {
|
|
1795
|
+
for (const scenario of args.test_scenarios) {
|
|
1796
|
+
const result = await this.runTestScenario(widget, scenario);
|
|
1797
|
+
testResults.push(result);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
// Run default tests
|
|
1802
|
+
const defaultTests = [
|
|
1803
|
+
{
|
|
1804
|
+
name: 'Empty Data Test',
|
|
1805
|
+
input: {},
|
|
1806
|
+
options: {}
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: 'Basic Data Test',
|
|
1810
|
+
input: { test: true },
|
|
1811
|
+
options: { title: 'Test Widget' }
|
|
1812
|
+
}
|
|
1813
|
+
];
|
|
1814
|
+
for (const test of defaultTests) {
|
|
1815
|
+
const result = await this.runTestScenario(widget, test);
|
|
1816
|
+
testResults.push(result);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
// Code coverage analysis if requested
|
|
1820
|
+
let coverageReport = '';
|
|
1821
|
+
if (args.coverage !== false) {
|
|
1822
|
+
const coverage = this.analyzeCodeCoverage(widget);
|
|
1823
|
+
coverageReport = `
|
|
1824
|
+
📊 **Code Coverage Analysis:**
|
|
1825
|
+
- Template variables used in client script: ${coverage.templateVarsUsed}/${coverage.totalTemplateVars} (${coverage.templateCoverage}%)
|
|
1826
|
+
- Client bindings used in template: ${coverage.clientBindingsUsed}/${coverage.totalClientBindings} (${coverage.clientCoverage}%)
|
|
1827
|
+
- Server data used in client: ${coverage.serverDataUsed}/${coverage.totalServerData} (${coverage.serverCoverage}%)
|
|
1828
|
+
- Overall integration score: ${coverage.overallScore}%
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
// Generate test report
|
|
1832
|
+
const passedTests = testResults.filter(r => r.status.includes('✅')).length;
|
|
1833
|
+
const failedTests = testResults.filter(r => r.status.includes('❌')).length;
|
|
1834
|
+
const warningTests = testResults.filter(r => r.status.includes('⚠️')).length;
|
|
1835
|
+
return {
|
|
1836
|
+
content: [
|
|
1837
|
+
{
|
|
1838
|
+
type: 'text',
|
|
1839
|
+
text: `🧪 Widget Test Results
|
|
1840
|
+
|
|
1841
|
+
📋 **Widget:** ${widget.title || widget.name}
|
|
1842
|
+
🆔 **Sys ID:** ${args.sys_id}
|
|
1843
|
+
|
|
1844
|
+
📈 **Test Summary:**
|
|
1845
|
+
- Total Tests: ${testResults.length}
|
|
1846
|
+
- ✅ Passed: ${passedTests}
|
|
1847
|
+
- ❌ Failed: ${failedTests}
|
|
1848
|
+
- ⚠️ Warnings: ${warningTests}
|
|
1849
|
+
- Success Rate: ${Math.round((passedTests / testResults.length) * 100)}%
|
|
1850
|
+
|
|
1851
|
+
🔍 **Test Results:**
|
|
1852
|
+
${testResults.map(r => `
|
|
1853
|
+
**${r.name}:** ${r.status}
|
|
1854
|
+
${r.details ? `- Details: ${JSON.stringify(r.details, null, 2)}` : ''}
|
|
1855
|
+
${r.error ? `- Error: ${r.error}` : ''}
|
|
1856
|
+
${r.recommendation ? `- 💡 Recommendation: ${r.recommendation}` : ''}
|
|
1857
|
+
`).join('\n')}
|
|
1858
|
+
|
|
1859
|
+
${coverageReport}
|
|
1860
|
+
|
|
1861
|
+
🏆 **Overall Status:** ${failedTests === 0 ? '✅ All tests passed!' : '❌ Some tests failed'}
|
|
1862
|
+
|
|
1863
|
+
💡 **Next Steps:**
|
|
1864
|
+
${failedTests > 0 ? '1. Fix the failing tests\n2. Re-run the test suite\n' : ''}
|
|
1865
|
+
${warningTests > 0 ? '1. Review warnings and consider improvements\n' : ''}
|
|
1866
|
+
3. Deploy to a test portal page for user testing
|
|
1867
|
+
4. Consider adding more comprehensive test scenarios
|
|
1868
|
+
|
|
1869
|
+
Use \`snow_preview_widget\` to see a detailed preview of the widget rendering.`,
|
|
1870
|
+
},
|
|
1871
|
+
],
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
catch (error) {
|
|
1875
|
+
throw new Error(`Widget test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Check widget dependencies like Chart.js
|
|
1880
|
+
*/
|
|
1881
|
+
checkWidgetDependencies(widget) {
|
|
1882
|
+
const dependencies = {
|
|
1883
|
+
required: [],
|
|
1884
|
+
found: [],
|
|
1885
|
+
missing: []
|
|
1886
|
+
};
|
|
1887
|
+
// Check for Chart.js
|
|
1888
|
+
if (widget.client_script?.includes('Chart') || widget.template?.includes('chart')) {
|
|
1889
|
+
dependencies.required.push('Chart.js');
|
|
1890
|
+
// In real implementation, would check if Chart.js is available in portal
|
|
1891
|
+
dependencies.missing.push('Chart.js (verify it\'s included in portal theme)');
|
|
1892
|
+
}
|
|
1893
|
+
// Check for other common libraries
|
|
1894
|
+
const libraries = [
|
|
1895
|
+
{ name: 'jQuery', pattern: /\$\(|jQuery\(/ },
|
|
1896
|
+
{ name: 'lodash', pattern: /_\./ },
|
|
1897
|
+
{ name: 'moment', pattern: /moment\(/ }
|
|
1898
|
+
];
|
|
1899
|
+
for (const lib of libraries) {
|
|
1900
|
+
if (lib.pattern.test(widget.client_script || '')) {
|
|
1901
|
+
dependencies.required.push(lib.name);
|
|
1902
|
+
if (lib.name === 'jQuery' || lib.name === 'moment') {
|
|
1903
|
+
dependencies.found.push(`${lib.name} (built-in)`);
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
dependencies.missing.push(lib.name);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return dependencies;
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Run a test scenario on the widget
|
|
1914
|
+
*/
|
|
1915
|
+
async runTestScenario(widget, scenario) {
|
|
1916
|
+
try {
|
|
1917
|
+
// Simulate running the widget with test data
|
|
1918
|
+
const result = {
|
|
1919
|
+
name: scenario.name,
|
|
1920
|
+
status: '✅ Pass',
|
|
1921
|
+
details: null,
|
|
1922
|
+
error: null,
|
|
1923
|
+
recommendation: null
|
|
1924
|
+
};
|
|
1925
|
+
// Check if server script would work with provided input
|
|
1926
|
+
if (widget.script) {
|
|
1927
|
+
// Check for required input fields
|
|
1928
|
+
const requiredInputs = widget.script.match(/input\.\w+/g) || [];
|
|
1929
|
+
const uniqueInputs = [...new Set(requiredInputs.map((i) => i.replace('input.', '')))];
|
|
1930
|
+
const missingInputs = uniqueInputs.filter(field => !(scenario.input && scenario.input[field]));
|
|
1931
|
+
if (missingInputs.length > 0) {
|
|
1932
|
+
result.status = '⚠️ Warning';
|
|
1933
|
+
result.details = { missingInputs };
|
|
1934
|
+
result.recommendation = `Provide test data for: ${missingInputs.join(', ')}`;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
// Check if client script references exist in template
|
|
1938
|
+
if (widget.client_script && widget.template) {
|
|
1939
|
+
const clientRefs = widget.client_script.match(/c\.\w+|\$scope\.\w+/g) || [];
|
|
1940
|
+
const templateRefs = widget.template.match(/\{\{[^}]+\}\}/g) || [];
|
|
1941
|
+
// Simple check - could be enhanced
|
|
1942
|
+
if (clientRefs.length > 0 && templateRefs.length === 0) {
|
|
1943
|
+
result.status = '⚠️ Warning';
|
|
1944
|
+
result.details = { issue: 'Client script defines variables but template doesn\'t use them' };
|
|
1945
|
+
result.recommendation = 'Ensure template uses the data from client script';
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return result;
|
|
1949
|
+
}
|
|
1950
|
+
catch (error) {
|
|
1951
|
+
return {
|
|
1952
|
+
name: scenario.name,
|
|
1953
|
+
status: '❌ Fail',
|
|
1954
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Analyze code coverage between HTML/CSS/JS
|
|
1960
|
+
*/
|
|
1961
|
+
analyzeCodeCoverage(widget) {
|
|
1962
|
+
const coverage = {
|
|
1963
|
+
totalTemplateVars: 0,
|
|
1964
|
+
templateVarsUsed: 0,
|
|
1965
|
+
totalClientBindings: 0,
|
|
1966
|
+
clientBindingsUsed: 0,
|
|
1967
|
+
totalServerData: 0,
|
|
1968
|
+
serverDataUsed: 0,
|
|
1969
|
+
templateCoverage: 0,
|
|
1970
|
+
clientCoverage: 0,
|
|
1971
|
+
serverCoverage: 0,
|
|
1972
|
+
overallScore: 0
|
|
1973
|
+
};
|
|
1974
|
+
// Extract all variables
|
|
1975
|
+
const templateVars = (widget.template?.match(/\{\{([^}]+)\}\}/g) || [])
|
|
1976
|
+
.map((v) => v.replace(/[{}]/g, '').trim());
|
|
1977
|
+
const clientBindings = (widget.client_script?.match(/c\.(\w+)|\$scope\.(\w+)/g) || [])
|
|
1978
|
+
.map((v) => v.replace(/c\.|\\$scope\./, ''));
|
|
1979
|
+
const serverDataKeys = (widget.script?.match(/data\.(\w+)/g) || [])
|
|
1980
|
+
.map((v) => v.replace('data.', ''));
|
|
1981
|
+
coverage.totalTemplateVars = templateVars.length;
|
|
1982
|
+
coverage.totalClientBindings = clientBindings.length;
|
|
1983
|
+
coverage.totalServerData = serverDataKeys.length;
|
|
1984
|
+
// Check usage
|
|
1985
|
+
templateVars.forEach(v => {
|
|
1986
|
+
if (widget.client_script?.includes(v) || widget.script?.includes(v)) {
|
|
1987
|
+
coverage.templateVarsUsed++;
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
clientBindings.forEach(b => {
|
|
1991
|
+
if (widget.template?.includes(b)) {
|
|
1992
|
+
coverage.clientBindingsUsed++;
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
serverDataKeys.forEach(k => {
|
|
1996
|
+
if (widget.client_script?.includes(k) || widget.template?.includes(k)) {
|
|
1997
|
+
coverage.serverDataUsed++;
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
// Calculate percentages
|
|
2001
|
+
coverage.templateCoverage = coverage.totalTemplateVars > 0
|
|
2002
|
+
? Math.round((coverage.templateVarsUsed / coverage.totalTemplateVars) * 100) : 100;
|
|
2003
|
+
coverage.clientCoverage = coverage.totalClientBindings > 0
|
|
2004
|
+
? Math.round((coverage.clientBindingsUsed / coverage.totalClientBindings) * 100) : 100;
|
|
2005
|
+
coverage.serverCoverage = coverage.totalServerData > 0
|
|
2006
|
+
? Math.round((coverage.serverDataUsed / coverage.totalServerData) * 100) : 100;
|
|
2007
|
+
coverage.overallScore = Math.round((coverage.templateCoverage + coverage.clientCoverage + coverage.serverCoverage) / 3);
|
|
2008
|
+
return coverage;
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Check integration between template, CSS, and scripts
|
|
2012
|
+
*/
|
|
2013
|
+
checkIntegration(integration) {
|
|
2014
|
+
const issues = [];
|
|
2015
|
+
// Check if template variables are defined in scripts
|
|
2016
|
+
const undefinedVars = integration.template_refs.filter((ref) => {
|
|
2017
|
+
const varName = ref.replace(/[{}]/g, '').split('.')[0].trim();
|
|
2018
|
+
return !integration.client_bindings.some((binding) => binding.includes(varName)) &&
|
|
2019
|
+
!integration.server_data_keys.some((key) => key.includes(varName));
|
|
2020
|
+
});
|
|
2021
|
+
if (undefinedVars.length > 0) {
|
|
2022
|
+
issues.push(`⚠️ Template variables not defined in scripts: ${undefinedVars.join(', ')}`);
|
|
2023
|
+
}
|
|
2024
|
+
// Check if CSS classes are used in template
|
|
2025
|
+
const unusedClasses = integration.css_classes.filter((cls) => {
|
|
2026
|
+
const className = cls.substring(1); // Remove the dot
|
|
2027
|
+
return !integration.template_refs.some((ref) => ref.includes(className));
|
|
2028
|
+
});
|
|
2029
|
+
if (unusedClasses.length > 0 && unusedClasses.length < 5) {
|
|
2030
|
+
issues.push(`⚠️ CSS classes possibly unused: ${unusedClasses.join(', ')}`);
|
|
2031
|
+
}
|
|
2032
|
+
return issues.length > 0 ? issues.join('\n') : '✅ Good integration between template, CSS, and scripts';
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Generate recommendations based on analysis
|
|
2036
|
+
*/
|
|
2037
|
+
generateRecommendations(widgetData, integration, dependencies) {
|
|
2038
|
+
const recommendations = [];
|
|
2039
|
+
if (dependencies.length > 0) {
|
|
2040
|
+
recommendations.push('1. Ensure all required libraries are included in the Service Portal theme');
|
|
2041
|
+
}
|
|
2042
|
+
if (integration.template_refs.length === 0) {
|
|
2043
|
+
recommendations.push('2. Consider adding dynamic content to your template using {{variable}} syntax');
|
|
2044
|
+
}
|
|
2045
|
+
if (integration.server_data_keys.length > 0 && integration.client_bindings.length === 0) {
|
|
2046
|
+
recommendations.push('3. Add client script to handle server data and user interactions');
|
|
2047
|
+
}
|
|
2048
|
+
if (!widgetData.demo_data || widgetData.demo_data === '{}') {
|
|
2049
|
+
recommendations.push('4. Add demo data to help others understand how to use your widget');
|
|
2050
|
+
}
|
|
2051
|
+
if (!widgetData.option_schema || widgetData.option_schema === '[]') {
|
|
2052
|
+
recommendations.push('5. Define widget options schema for better reusability');
|
|
2053
|
+
}
|
|
2054
|
+
return recommendations.length > 0 ? recommendations.join('\n') : 'Widget structure looks good!';
|
|
2055
|
+
}
|
|
1282
2056
|
async start() {
|
|
1283
2057
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
1284
2058
|
await this.server.connect(transport);
|