snow-flow 1.1.34 ā 1.1.36
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 +1056 -16
- 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 +10 -2
- package/dist/utils/servicenow-client.d.ts.map +1 -1
- package/dist/utils/servicenow-client.js +88 -2
- package/dist/utils/servicenow-client.js.map +1 -1
- package/dist/version.d.ts +11 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +48 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -72,12 +72,14 @@ class ServiceNowDeploymentMCP {
|
|
|
72
72
|
properties: {
|
|
73
73
|
name: { type: 'string', description: 'Flow name' },
|
|
74
74
|
description: { type: 'string', description: 'Flow description' },
|
|
75
|
+
flow_type: { type: 'string', enum: ['flow', 'subflow', 'action'], description: 'Type of flow to create (default: flow)', default: 'flow' },
|
|
75
76
|
table: { type: 'string', description: 'Target table (e.g., sc_request, incident)' },
|
|
76
77
|
trigger_type: { type: 'string', enum: ['record_created', 'record_updated', 'scheduled', 'manual'], description: 'Flow trigger type' },
|
|
77
78
|
condition: { type: 'string', description: 'Trigger condition (encoded query)' },
|
|
78
79
|
active: { type: 'boolean', description: 'Activate flow on deployment' },
|
|
79
80
|
flow_definition: { type: 'string', description: 'Flow Designer definition JSON' },
|
|
80
81
|
category: { type: 'string', description: 'Flow category (e.g., approval, automation)' },
|
|
82
|
+
validate_before_deploy: { type: 'boolean', description: 'Validate flow definition before deployment', default: true },
|
|
81
83
|
},
|
|
82
84
|
required: ['name', 'description', 'flow_definition', 'trigger_type'],
|
|
83
85
|
},
|
|
@@ -231,6 +233,130 @@ class ServiceNowDeploymentMCP {
|
|
|
231
233
|
properties: {},
|
|
232
234
|
},
|
|
233
235
|
},
|
|
236
|
+
{
|
|
237
|
+
name: 'snow_preview_widget',
|
|
238
|
+
description: 'Preview widget rendering with test data to verify HTML/CSS/JS integration before deployment',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
sys_id: { type: 'string', description: 'Widget sys_id to preview (optional if providing code)' },
|
|
243
|
+
template: { type: 'string', description: 'HTML template code (optional if using sys_id)' },
|
|
244
|
+
css: { type: 'string', description: 'CSS styles (optional)' },
|
|
245
|
+
client_script: { type: 'string', description: 'Client controller script (optional)' },
|
|
246
|
+
server_script: { type: 'string', description: 'Server script (optional)' },
|
|
247
|
+
test_data: { type: 'string', description: 'JSON test data for server script' },
|
|
248
|
+
option_schema: { type: 'string', description: 'Widget options schema JSON' },
|
|
249
|
+
render_mode: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
enum: ['full', 'template_only', 'data_only'],
|
|
252
|
+
description: 'Preview mode: full (render everything), template_only (no JS), data_only (server data)',
|
|
253
|
+
default: 'full'
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'snow_widget_test',
|
|
260
|
+
description: 'Test widget functionality with various data scenarios to ensure proper integration',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
sys_id: { type: 'string', description: 'Widget sys_id to test' },
|
|
265
|
+
test_scenarios: {
|
|
266
|
+
type: 'array',
|
|
267
|
+
description: 'Array of test scenarios with input data and expected outputs',
|
|
268
|
+
items: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
name: { type: 'string', description: 'Test scenario name' },
|
|
272
|
+
input: { type: 'object', description: 'Input data for the test' },
|
|
273
|
+
expected: { type: 'object', description: 'Expected output (optional)' },
|
|
274
|
+
options: { type: 'object', description: 'Widget instance options' }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
coverage: {
|
|
279
|
+
type: 'boolean',
|
|
280
|
+
description: 'Check code coverage for HTML/CSS/JS integration',
|
|
281
|
+
default: true
|
|
282
|
+
},
|
|
283
|
+
validate_dependencies: {
|
|
284
|
+
type: 'boolean',
|
|
285
|
+
description: 'Check for missing dependencies like Chart.js',
|
|
286
|
+
default: true
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
required: ['sys_id'],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'snow_smart_update_set',
|
|
294
|
+
description: 'Smart update set creation with context detection - automatically creates new update sets for new tasks',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
detect_context: { type: 'boolean', description: 'Auto-detect task context change', default: true },
|
|
299
|
+
name_prefix: { type: 'string', description: 'Update set name prefix', default: 'AUTO' },
|
|
300
|
+
separate_by_task: { type: 'boolean', description: 'Create new update set for each task', default: true },
|
|
301
|
+
close_previous: { type: 'boolean', description: 'Close previous update set', default: true },
|
|
302
|
+
description: { type: 'string', description: 'Update set description' },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'snow_validate_flow_definition',
|
|
308
|
+
description: 'Validate flow definition before deployment with preview and test mode',
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: 'object',
|
|
311
|
+
properties: {
|
|
312
|
+
definition: { type: 'string', description: 'Flow definition JSON to validate' },
|
|
313
|
+
flow_type: { type: 'string', enum: ['flow', 'subflow', 'action'], default: 'flow' },
|
|
314
|
+
show_preview: { type: 'boolean', description: 'Show visual preview', default: true },
|
|
315
|
+
test_mode: { type: 'boolean', description: 'Run in test mode', default: false },
|
|
316
|
+
check_dependencies: { type: 'boolean', description: 'Check for missing dependencies', default: true },
|
|
317
|
+
},
|
|
318
|
+
required: ['definition'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'snow_create_solution_package',
|
|
323
|
+
description: 'Create a solution package grouping related artifacts with a new update set',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
name: { type: 'string', description: 'Solution package name' },
|
|
328
|
+
description: { type: 'string', description: 'Package description' },
|
|
329
|
+
artifacts: {
|
|
330
|
+
type: 'array',
|
|
331
|
+
description: 'Artifacts to include in the package',
|
|
332
|
+
items: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
type: { type: 'string', enum: ['flow', 'widget', 'script_include', 'business_rule', 'table'] },
|
|
336
|
+
create: { type: 'object', description: 'Artifact creation configuration' },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
new_update_set: { type: 'boolean', description: 'Force new update set', default: true },
|
|
341
|
+
},
|
|
342
|
+
required: ['name', 'artifacts'],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'snow_flow_wizard',
|
|
347
|
+
description: 'Interactive flow creation wizard with step-by-step guidance',
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
properties: {
|
|
351
|
+
name: { type: 'string', description: 'Flow name' },
|
|
352
|
+
interactive: { type: 'boolean', description: 'Enable interactive mode', default: true },
|
|
353
|
+
preview_each_step: { type: 'boolean', description: 'Preview after each step', default: true },
|
|
354
|
+
test_as_you_build: { type: 'boolean', description: 'Test flow during creation', default: true },
|
|
355
|
+
flow_type: { type: 'string', enum: ['flow', 'subflow', 'action'], default: 'flow' },
|
|
356
|
+
},
|
|
357
|
+
required: ['name'],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
234
360
|
],
|
|
235
361
|
}));
|
|
236
362
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
@@ -264,6 +390,18 @@ class ServiceNowDeploymentMCP {
|
|
|
264
390
|
return await this.validateSysId(args);
|
|
265
391
|
case 'snow_deployment_debug':
|
|
266
392
|
return await this.getDeploymentDebug(args);
|
|
393
|
+
case 'snow_preview_widget':
|
|
394
|
+
return await this.previewWidget(args);
|
|
395
|
+
case 'snow_widget_test':
|
|
396
|
+
return await this.testWidget(args);
|
|
397
|
+
case 'snow_smart_update_set':
|
|
398
|
+
return await this.smartUpdateSet(args);
|
|
399
|
+
case 'snow_validate_flow_definition':
|
|
400
|
+
return await this.validateFlowDefinition(args);
|
|
401
|
+
case 'snow_create_solution_package':
|
|
402
|
+
return await this.createSolutionPackage(args);
|
|
403
|
+
case 'snow_flow_wizard':
|
|
404
|
+
return await this.flowWizard(args);
|
|
267
405
|
default:
|
|
268
406
|
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
269
407
|
}
|
|
@@ -610,10 +748,28 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
610
748
|
trackedArtifact.updateSetId = updateSetId;
|
|
611
749
|
// Record successful deployment operation
|
|
612
750
|
artifact_tracker_js_1.artifactTracker.recordOperation(result.data.sys_id, 'create', true, `Widget deployed successfully to table sp_widget`);
|
|
613
|
-
// Validate the artifact was actually created
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
751
|
+
// Validate the artifact was actually created (with retry for indexing delay)
|
|
752
|
+
let isValid = false;
|
|
753
|
+
let validationMessage = 'Validating...';
|
|
754
|
+
// Since deployment succeeded, we'll be optimistic about validation
|
|
755
|
+
try {
|
|
756
|
+
// Give ServiceNow a moment to index the new record
|
|
757
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
758
|
+
isValid = await artifact_tracker_js_1.artifactTracker.validateArtifact(result.data.sys_id);
|
|
759
|
+
if (!isValid) {
|
|
760
|
+
// If validation fails but deployment succeeded, it's likely a timing/permission issue
|
|
761
|
+
this.logger.warn(`Widget deployed successfully but immediate validation check failed - this is normal for new records`);
|
|
762
|
+
validationMessage = 'ā³ Pending (record may still be indexing)';
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
validationMessage = 'ā
Confirmed';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (validationError) {
|
|
769
|
+
// Don't fail the deployment just because validation had issues
|
|
770
|
+
this.logger.warn('Validation check encountered an error, but deployment was successful', validationError);
|
|
771
|
+
validationMessage = 'ā Deployed (validation unavailable)';
|
|
772
|
+
isValid = true; // Assume success since deployment worked
|
|
617
773
|
}
|
|
618
774
|
// Get instance URL for direct link
|
|
619
775
|
const credentials = await this.oauth.loadCredentials();
|
|
@@ -634,7 +790,7 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
634
790
|
- Title: ${args.title}
|
|
635
791
|
- Sys ID: ${result.data.sys_id}
|
|
636
792
|
- Deployment Method: ${deploymentMethod}
|
|
637
|
-
- Validation: ${
|
|
793
|
+
- Validation: ${validationMessage}
|
|
638
794
|
|
|
639
795
|
š¦ Update Set:
|
|
640
796
|
- Name: ${updateSetName}
|
|
@@ -686,7 +842,30 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
686
842
|
],
|
|
687
843
|
};
|
|
688
844
|
}
|
|
689
|
-
|
|
845
|
+
const flowType = args.flow_type || 'flow';
|
|
846
|
+
this.logger.info(`Deploying ${flowType} to ServiceNow`, { name: args.name, type: flowType });
|
|
847
|
+
// Validate flow definition first if requested
|
|
848
|
+
if (args.validate_before_deploy !== false) {
|
|
849
|
+
const validationResult = await this.validateFlowDefinition({
|
|
850
|
+
definition: args.flow_definition,
|
|
851
|
+
flow_type: flowType,
|
|
852
|
+
show_preview: false,
|
|
853
|
+
test_mode: false,
|
|
854
|
+
check_dependencies: true
|
|
855
|
+
});
|
|
856
|
+
// Check if validation failed
|
|
857
|
+
const validationText = validationResult.content?.[0]?.text || '';
|
|
858
|
+
if (validationText.includes('ā') || validationText.includes('ERROR')) {
|
|
859
|
+
return {
|
|
860
|
+
content: [
|
|
861
|
+
{
|
|
862
|
+
type: 'text',
|
|
863
|
+
text: `ā Flow validation failed. Please fix the following issues:\n\n${validationText}\n\nUse snow_validate_flow_definition to preview and test your flow before deployment.`
|
|
864
|
+
}
|
|
865
|
+
]
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
690
869
|
// Ensure Update Set is active
|
|
691
870
|
const { updateSetId, updateSetName } = await this.ensureUpdateSet('Flow', args.name);
|
|
692
871
|
// Check if this is a master flow with linked artifacts
|
|
@@ -726,14 +905,11 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
726
905
|
return activity;
|
|
727
906
|
});
|
|
728
907
|
}
|
|
729
|
-
// Create
|
|
908
|
+
// Create flow data based on flow type
|
|
730
909
|
const flowData = {
|
|
731
910
|
name: args.name,
|
|
732
911
|
description: args.description,
|
|
733
912
|
active: args.active !== false,
|
|
734
|
-
table: args.table || '',
|
|
735
|
-
trigger_type: args.trigger_type,
|
|
736
|
-
condition: args.condition || '',
|
|
737
913
|
flow_definition: JSON.stringify(flowDefinition),
|
|
738
914
|
category: args.category || 'automation',
|
|
739
915
|
// Additional fields for composed flows
|
|
@@ -741,8 +917,43 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
741
917
|
linked_artifact_count: linkedArtifacts.length,
|
|
742
918
|
artifact_references: deployedArtifacts.map(a => a.sys_id).join(',')
|
|
743
919
|
};
|
|
744
|
-
//
|
|
745
|
-
|
|
920
|
+
// Configure based on flow type
|
|
921
|
+
switch (flowType) {
|
|
922
|
+
case 'flow':
|
|
923
|
+
flowData.table = args.table || '';
|
|
924
|
+
flowData.trigger_type = args.trigger_type;
|
|
925
|
+
flowData.condition = args.condition || '';
|
|
926
|
+
flowData.type = 'flow';
|
|
927
|
+
break;
|
|
928
|
+
case 'subflow':
|
|
929
|
+
// Subflows don't have triggers, they're called by other flows
|
|
930
|
+
flowData.type = 'subflow';
|
|
931
|
+
flowData.inputs = flowDefinition.inputs || [];
|
|
932
|
+
flowData.outputs = flowDefinition.outputs || [];
|
|
933
|
+
break;
|
|
934
|
+
case 'action':
|
|
935
|
+
// Actions are reusable components
|
|
936
|
+
flowData.type = 'action';
|
|
937
|
+
flowData.action_type = args.action_type || 'custom';
|
|
938
|
+
flowData.inputs = flowDefinition.inputs || [];
|
|
939
|
+
flowData.outputs = flowDefinition.outputs || [];
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
// Deploy to ServiceNow using appropriate API based on flow type
|
|
943
|
+
let result;
|
|
944
|
+
switch (flowType) {
|
|
945
|
+
case 'flow':
|
|
946
|
+
result = await this.client.createFlow(flowData);
|
|
947
|
+
break;
|
|
948
|
+
case 'subflow':
|
|
949
|
+
result = await this.client.createSubflow(flowData);
|
|
950
|
+
break;
|
|
951
|
+
case 'action':
|
|
952
|
+
result = await this.client.createFlowAction(flowData);
|
|
953
|
+
break;
|
|
954
|
+
default:
|
|
955
|
+
throw new Error(`Unknown flow type: ${flowType}`);
|
|
956
|
+
}
|
|
746
957
|
const credentials = await this.oauth.loadCredentials();
|
|
747
958
|
const flowUrl = result.success && result.data
|
|
748
959
|
? `https://${credentials?.instance}/$flow-designer.do#/flow/${result.data.sys_id}`
|
|
@@ -759,11 +970,14 @@ Use \`snow_deployment_debug\` for more information about this session.`,
|
|
|
759
970
|
type: 'text',
|
|
760
971
|
text: `ā
Flow Designer flow deployed successfully!
|
|
761
972
|
|
|
762
|
-
š
|
|
973
|
+
š **${flowType.charAt(0).toUpperCase() + flowType.slice(1)} Details:**
|
|
763
974
|
- Name: ${args.name}
|
|
764
|
-
- Type: ${
|
|
765
|
-
-
|
|
766
|
-
|
|
975
|
+
- Flow Type: ${flowType === 'flow' ? 'š Flow' : flowType === 'subflow' ? 'š Subflow' : 'ā” Action'}
|
|
976
|
+
- Composed: ${isComposedFlow ? 'š§ Yes - Intelligent Composed Flow' : 'ā No - Standard'}
|
|
977
|
+
${flowType === 'flow' ? `- Trigger Type: ${args.trigger_type}
|
|
978
|
+
- Table: ${args.table || 'N/A'}` : ''}
|
|
979
|
+
${flowType !== 'flow' ? `- Inputs: ${flowDefinition.inputs?.length || 0}
|
|
980
|
+
- Outputs: ${flowDefinition.outputs?.length || 0}` : ''}
|
|
767
981
|
- Category: ${args.category || 'automation'}
|
|
768
982
|
- Active: ${args.active !== false ? 'Yes' : 'No'}
|
|
769
983
|
|
|
@@ -1506,6 +1720,832 @@ ${sessionSummary.statusCounts.pending > 0 ? '- š Complete pending deployments
|
|
|
1506
1720
|
}
|
|
1507
1721
|
return result;
|
|
1508
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Preview widget with test data
|
|
1725
|
+
*/
|
|
1726
|
+
async previewWidget(args) {
|
|
1727
|
+
try {
|
|
1728
|
+
// Check authentication first
|
|
1729
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
1730
|
+
if (!isAuth) {
|
|
1731
|
+
return {
|
|
1732
|
+
content: [
|
|
1733
|
+
{
|
|
1734
|
+
type: 'text',
|
|
1735
|
+
text: 'ā Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login\n\nOr configure your .env file with ServiceNow OAuth credentials.',
|
|
1736
|
+
},
|
|
1737
|
+
],
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
this.logger.info('Previewing widget', args);
|
|
1741
|
+
let widgetData = {};
|
|
1742
|
+
// If sys_id provided, fetch the widget
|
|
1743
|
+
if (args.sys_id) {
|
|
1744
|
+
const record = await this.client.getRecord('sp_widget', args.sys_id);
|
|
1745
|
+
if (!record) {
|
|
1746
|
+
throw new Error(`Widget not found: ${args.sys_id}`);
|
|
1747
|
+
}
|
|
1748
|
+
widgetData = {
|
|
1749
|
+
template: record.template,
|
|
1750
|
+
css: record.css,
|
|
1751
|
+
client_script: record.client_script,
|
|
1752
|
+
server_script: record.script,
|
|
1753
|
+
option_schema: record.option_schema,
|
|
1754
|
+
demo_data: record.demo_data,
|
|
1755
|
+
name: record.name,
|
|
1756
|
+
title: record.title
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
// Use provided code
|
|
1761
|
+
widgetData = {
|
|
1762
|
+
template: args.template || '',
|
|
1763
|
+
css: args.css || '',
|
|
1764
|
+
client_script: args.client_script || '',
|
|
1765
|
+
server_script: args.server_script || '',
|
|
1766
|
+
option_schema: args.option_schema || '[]',
|
|
1767
|
+
demo_data: args.test_data || '{}'
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
// Simulate server script execution with test data
|
|
1771
|
+
let serverData = {};
|
|
1772
|
+
let serverError = null;
|
|
1773
|
+
if (widgetData.server_script && args.render_mode !== 'template_only') {
|
|
1774
|
+
try {
|
|
1775
|
+
// Parse test data
|
|
1776
|
+
const testData = args.test_data ? JSON.parse(args.test_data) : {};
|
|
1777
|
+
// Simulate server script context
|
|
1778
|
+
serverData = {
|
|
1779
|
+
input: testData.input || {},
|
|
1780
|
+
options: testData.options || {},
|
|
1781
|
+
data: testData.data || {},
|
|
1782
|
+
// Simulate basic GlideRecord responses
|
|
1783
|
+
gr_results: testData.gr_results || []
|
|
1784
|
+
};
|
|
1785
|
+
// Check for common ServiceNow APIs used
|
|
1786
|
+
const usedAPIs = [];
|
|
1787
|
+
if (widgetData.server_script.includes('GlideRecord'))
|
|
1788
|
+
usedAPIs.push('GlideRecord');
|
|
1789
|
+
if (widgetData.server_script.includes('GlideAggregate'))
|
|
1790
|
+
usedAPIs.push('GlideAggregate');
|
|
1791
|
+
if (widgetData.server_script.includes('gs.'))
|
|
1792
|
+
usedAPIs.push('GlideSystem (gs)');
|
|
1793
|
+
if (widgetData.server_script.includes('$sp.'))
|
|
1794
|
+
usedAPIs.push('Service Portal API ($sp)');
|
|
1795
|
+
if (usedAPIs.length > 0) {
|
|
1796
|
+
serverData.used_apis = usedAPIs;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
serverError = `Server script error: ${error}`;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
// Check dependencies
|
|
1804
|
+
const dependencies = [];
|
|
1805
|
+
if (widgetData.client_script?.includes('Chart.js') || widgetData.template?.includes('chart')) {
|
|
1806
|
+
dependencies.push({
|
|
1807
|
+
name: 'Chart.js',
|
|
1808
|
+
status: 'ā ļø Required - ensure it\'s included in portal theme',
|
|
1809
|
+
suggestion: 'Add Chart.js to Service Portal theme JS includes'
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
if (widgetData.client_script?.includes('moment')) {
|
|
1813
|
+
dependencies.push({
|
|
1814
|
+
name: 'Moment.js',
|
|
1815
|
+
status: 'ā
Usually included in ServiceNow',
|
|
1816
|
+
suggestion: 'Available as global variable'
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
// Analyze code integration
|
|
1820
|
+
const integration = {
|
|
1821
|
+
template_refs: [],
|
|
1822
|
+
css_classes: [],
|
|
1823
|
+
client_bindings: [],
|
|
1824
|
+
server_data_keys: []
|
|
1825
|
+
};
|
|
1826
|
+
// Find template references
|
|
1827
|
+
const templateVarMatches = widgetData.template?.match(/\{\{[^}]+\}\}/g) || [];
|
|
1828
|
+
integration.template_refs = [...new Set(templateVarMatches)];
|
|
1829
|
+
// Find CSS classes
|
|
1830
|
+
const cssClassMatches = widgetData.css?.match(/\.[a-zA-Z][\w-]*/g) || [];
|
|
1831
|
+
integration.css_classes = [...new Set(cssClassMatches)];
|
|
1832
|
+
// Find client script bindings
|
|
1833
|
+
const clientBindingMatches = widgetData.client_script?.match(/\$scope\.\w+|c\.\w+/g) || [];
|
|
1834
|
+
integration.client_bindings = [...new Set(clientBindingMatches)];
|
|
1835
|
+
// Find server data keys
|
|
1836
|
+
const serverDataMatches = widgetData.server_script?.match(/data\.\w+/g) || [];
|
|
1837
|
+
integration.server_data_keys = [...new Set(serverDataMatches)];
|
|
1838
|
+
const previewUrl = args.sys_id
|
|
1839
|
+
? `https://${(await this.oauth.loadCredentials())?.instance}/sp?id=widget_preview&sys_id=${args.sys_id}`
|
|
1840
|
+
: null;
|
|
1841
|
+
return {
|
|
1842
|
+
content: [
|
|
1843
|
+
{
|
|
1844
|
+
type: 'text',
|
|
1845
|
+
text: `š Widget Preview Analysis
|
|
1846
|
+
|
|
1847
|
+
š **Widget Info:**
|
|
1848
|
+
${args.sys_id ? `- Sys ID: ${args.sys_id}` : '- Preview from provided code'}
|
|
1849
|
+
${widgetData.name ? `- Name: ${widgetData.name}` : ''}
|
|
1850
|
+
${widgetData.title ? `- Title: ${widgetData.title}` : ''}
|
|
1851
|
+
|
|
1852
|
+
šØ **Template Analysis:**
|
|
1853
|
+
- Variables used: ${integration.template_refs.length > 0 ? integration.template_refs.join(', ') : 'None'}
|
|
1854
|
+
- CSS classes defined: ${integration.css_classes.length > 0 ? integration.css_classes.slice(0, 5).join(', ') : 'None'}
|
|
1855
|
+
${integration.css_classes.length > 5 ? ` (and ${integration.css_classes.length - 5} more...)` : ''}
|
|
1856
|
+
|
|
1857
|
+
š± **Client Script Analysis:**
|
|
1858
|
+
- Scope bindings: ${integration.client_bindings.length > 0 ? integration.client_bindings.slice(0, 5).join(', ') : 'None'}
|
|
1859
|
+
${integration.client_bindings.length > 5 ? ` (and ${integration.client_bindings.length - 5} more...)` : ''}
|
|
1860
|
+
|
|
1861
|
+
š„ļø **Server Script Analysis:**
|
|
1862
|
+
- Data properties: ${integration.server_data_keys.length > 0 ? integration.server_data_keys.join(', ') : 'None'}
|
|
1863
|
+
${serverData.used_apis ? `- ServiceNow APIs used: ${serverData.used_apis.join(', ')}` : ''}
|
|
1864
|
+
${serverError ? `- ā ļø Error: ${serverError}` : ''}
|
|
1865
|
+
|
|
1866
|
+
š¦ **Dependencies:**
|
|
1867
|
+
${dependencies.length > 0 ? dependencies.map(d => `- ${d.name}: ${d.status}\n ${d.suggestion}`).join('\n') : '- No external dependencies detected'}
|
|
1868
|
+
|
|
1869
|
+
š **Integration Check:**
|
|
1870
|
+
${this.checkIntegration(integration)}
|
|
1871
|
+
|
|
1872
|
+
${args.render_mode === 'data_only' ? `
|
|
1873
|
+
š **Server Data Output:**
|
|
1874
|
+
\`\`\`json
|
|
1875
|
+
${JSON.stringify(serverData, null, 2)}
|
|
1876
|
+
\`\`\`
|
|
1877
|
+
` : ''}
|
|
1878
|
+
|
|
1879
|
+
${previewUrl ? `
|
|
1880
|
+
š **Live Preview:**
|
|
1881
|
+
${previewUrl}
|
|
1882
|
+
` : ''}
|
|
1883
|
+
|
|
1884
|
+
š” **Recommendations:**
|
|
1885
|
+
${this.generateRecommendations(widgetData, integration, dependencies)}
|
|
1886
|
+
|
|
1887
|
+
Use \`snow_widget_test\` to run automated tests with different scenarios.`,
|
|
1888
|
+
},
|
|
1889
|
+
],
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
catch (error) {
|
|
1893
|
+
throw new Error(`Widget preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Test widget with various scenarios
|
|
1898
|
+
*/
|
|
1899
|
+
async testWidget(args) {
|
|
1900
|
+
try {
|
|
1901
|
+
// Check authentication first
|
|
1902
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
1903
|
+
if (!isAuth) {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [
|
|
1906
|
+
{
|
|
1907
|
+
type: 'text',
|
|
1908
|
+
text: 'ā Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login\n\nOr configure your .env file with ServiceNow OAuth credentials.',
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
this.logger.info('Testing widget', { sys_id: args.sys_id });
|
|
1914
|
+
// Fetch the widget
|
|
1915
|
+
const widget = await this.client.getRecord('sp_widget', args.sys_id);
|
|
1916
|
+
if (!widget) {
|
|
1917
|
+
throw new Error(`Widget not found: ${args.sys_id}`);
|
|
1918
|
+
}
|
|
1919
|
+
const testResults = [];
|
|
1920
|
+
// Check dependencies if requested
|
|
1921
|
+
if (args.validate_dependencies !== false) {
|
|
1922
|
+
const depCheck = this.checkWidgetDependencies(widget);
|
|
1923
|
+
testResults.push({
|
|
1924
|
+
name: 'Dependency Check',
|
|
1925
|
+
status: depCheck.missing.length === 0 ? 'ā
Pass' : 'ā Fail',
|
|
1926
|
+
details: depCheck
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
// Run test scenarios if provided
|
|
1930
|
+
if (args.test_scenarios && Array.isArray(args.test_scenarios)) {
|
|
1931
|
+
for (const scenario of args.test_scenarios) {
|
|
1932
|
+
const result = await this.runTestScenario(widget, scenario);
|
|
1933
|
+
testResults.push(result);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
// Run default tests
|
|
1938
|
+
const defaultTests = [
|
|
1939
|
+
{
|
|
1940
|
+
name: 'Empty Data Test',
|
|
1941
|
+
input: {},
|
|
1942
|
+
options: {}
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
name: 'Basic Data Test',
|
|
1946
|
+
input: { test: true },
|
|
1947
|
+
options: { title: 'Test Widget' }
|
|
1948
|
+
}
|
|
1949
|
+
];
|
|
1950
|
+
for (const test of defaultTests) {
|
|
1951
|
+
const result = await this.runTestScenario(widget, test);
|
|
1952
|
+
testResults.push(result);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
// Code coverage analysis if requested
|
|
1956
|
+
let coverageReport = '';
|
|
1957
|
+
if (args.coverage !== false) {
|
|
1958
|
+
const coverage = this.analyzeCodeCoverage(widget);
|
|
1959
|
+
coverageReport = `
|
|
1960
|
+
š **Code Coverage Analysis:**
|
|
1961
|
+
- Template variables used in client script: ${coverage.templateVarsUsed}/${coverage.totalTemplateVars} (${coverage.templateCoverage}%)
|
|
1962
|
+
- Client bindings used in template: ${coverage.clientBindingsUsed}/${coverage.totalClientBindings} (${coverage.clientCoverage}%)
|
|
1963
|
+
- Server data used in client: ${coverage.serverDataUsed}/${coverage.totalServerData} (${coverage.serverCoverage}%)
|
|
1964
|
+
- Overall integration score: ${coverage.overallScore}%
|
|
1965
|
+
`;
|
|
1966
|
+
}
|
|
1967
|
+
// Generate test report
|
|
1968
|
+
const passedTests = testResults.filter(r => r.status.includes('ā
')).length;
|
|
1969
|
+
const failedTests = testResults.filter(r => r.status.includes('ā')).length;
|
|
1970
|
+
const warningTests = testResults.filter(r => r.status.includes('ā ļø')).length;
|
|
1971
|
+
return {
|
|
1972
|
+
content: [
|
|
1973
|
+
{
|
|
1974
|
+
type: 'text',
|
|
1975
|
+
text: `š§Ŗ Widget Test Results
|
|
1976
|
+
|
|
1977
|
+
š **Widget:** ${widget.title || widget.name}
|
|
1978
|
+
š **Sys ID:** ${args.sys_id}
|
|
1979
|
+
|
|
1980
|
+
š **Test Summary:**
|
|
1981
|
+
- Total Tests: ${testResults.length}
|
|
1982
|
+
- ā
Passed: ${passedTests}
|
|
1983
|
+
- ā Failed: ${failedTests}
|
|
1984
|
+
- ā ļø Warnings: ${warningTests}
|
|
1985
|
+
- Success Rate: ${Math.round((passedTests / testResults.length) * 100)}%
|
|
1986
|
+
|
|
1987
|
+
š **Test Results:**
|
|
1988
|
+
${testResults.map(r => `
|
|
1989
|
+
**${r.name}:** ${r.status}
|
|
1990
|
+
${r.details ? `- Details: ${JSON.stringify(r.details, null, 2)}` : ''}
|
|
1991
|
+
${r.error ? `- Error: ${r.error}` : ''}
|
|
1992
|
+
${r.recommendation ? `- š” Recommendation: ${r.recommendation}` : ''}
|
|
1993
|
+
`).join('\n')}
|
|
1994
|
+
|
|
1995
|
+
${coverageReport}
|
|
1996
|
+
|
|
1997
|
+
š **Overall Status:** ${failedTests === 0 ? 'ā
All tests passed!' : 'ā Some tests failed'}
|
|
1998
|
+
|
|
1999
|
+
š” **Next Steps:**
|
|
2000
|
+
${failedTests > 0 ? '1. Fix the failing tests\n2. Re-run the test suite\n' : ''}
|
|
2001
|
+
${warningTests > 0 ? '1. Review warnings and consider improvements\n' : ''}
|
|
2002
|
+
3. Deploy to a test portal page for user testing
|
|
2003
|
+
4. Consider adding more comprehensive test scenarios
|
|
2004
|
+
|
|
2005
|
+
Use \`snow_preview_widget\` to see a detailed preview of the widget rendering.`,
|
|
2006
|
+
},
|
|
2007
|
+
],
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
catch (error) {
|
|
2011
|
+
throw new Error(`Widget test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Check widget dependencies like Chart.js
|
|
2016
|
+
*/
|
|
2017
|
+
checkWidgetDependencies(widget) {
|
|
2018
|
+
const dependencies = {
|
|
2019
|
+
required: [],
|
|
2020
|
+
found: [],
|
|
2021
|
+
missing: []
|
|
2022
|
+
};
|
|
2023
|
+
// Check for Chart.js
|
|
2024
|
+
if (widget.client_script?.includes('Chart') || widget.template?.includes('chart')) {
|
|
2025
|
+
dependencies.required.push('Chart.js');
|
|
2026
|
+
// In real implementation, would check if Chart.js is available in portal
|
|
2027
|
+
dependencies.missing.push('Chart.js (verify it\'s included in portal theme)');
|
|
2028
|
+
}
|
|
2029
|
+
// Check for other common libraries
|
|
2030
|
+
const libraries = [
|
|
2031
|
+
{ name: 'jQuery', pattern: /\$\(|jQuery\(/ },
|
|
2032
|
+
{ name: 'lodash', pattern: /_\./ },
|
|
2033
|
+
{ name: 'moment', pattern: /moment\(/ }
|
|
2034
|
+
];
|
|
2035
|
+
for (const lib of libraries) {
|
|
2036
|
+
if (lib.pattern.test(widget.client_script || '')) {
|
|
2037
|
+
dependencies.required.push(lib.name);
|
|
2038
|
+
if (lib.name === 'jQuery' || lib.name === 'moment') {
|
|
2039
|
+
dependencies.found.push(`${lib.name} (built-in)`);
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
dependencies.missing.push(lib.name);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return dependencies;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Run a test scenario on the widget
|
|
2050
|
+
*/
|
|
2051
|
+
async runTestScenario(widget, scenario) {
|
|
2052
|
+
try {
|
|
2053
|
+
// Simulate running the widget with test data
|
|
2054
|
+
const result = {
|
|
2055
|
+
name: scenario.name,
|
|
2056
|
+
status: 'ā
Pass',
|
|
2057
|
+
details: null,
|
|
2058
|
+
error: null,
|
|
2059
|
+
recommendation: null
|
|
2060
|
+
};
|
|
2061
|
+
// Check if server script would work with provided input
|
|
2062
|
+
if (widget.script) {
|
|
2063
|
+
// Check for required input fields
|
|
2064
|
+
const requiredInputs = widget.script.match(/input\.\w+/g) || [];
|
|
2065
|
+
const uniqueInputs = [...new Set(requiredInputs.map((i) => i.replace('input.', '')))];
|
|
2066
|
+
const missingInputs = uniqueInputs.filter(field => !(scenario.input && scenario.input[field]));
|
|
2067
|
+
if (missingInputs.length > 0) {
|
|
2068
|
+
result.status = 'ā ļø Warning';
|
|
2069
|
+
result.details = { missingInputs };
|
|
2070
|
+
result.recommendation = `Provide test data for: ${missingInputs.join(', ')}`;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
// Check if client script references exist in template
|
|
2074
|
+
if (widget.client_script && widget.template) {
|
|
2075
|
+
const clientRefs = widget.client_script.match(/c\.\w+|\$scope\.\w+/g) || [];
|
|
2076
|
+
const templateRefs = widget.template.match(/\{\{[^}]+\}\}/g) || [];
|
|
2077
|
+
// Simple check - could be enhanced
|
|
2078
|
+
if (clientRefs.length > 0 && templateRefs.length === 0) {
|
|
2079
|
+
result.status = 'ā ļø Warning';
|
|
2080
|
+
result.details = { issue: 'Client script defines variables but template doesn\'t use them' };
|
|
2081
|
+
result.recommendation = 'Ensure template uses the data from client script';
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return result;
|
|
2085
|
+
}
|
|
2086
|
+
catch (error) {
|
|
2087
|
+
return {
|
|
2088
|
+
name: scenario.name,
|
|
2089
|
+
status: 'ā Fail',
|
|
2090
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Analyze code coverage between HTML/CSS/JS
|
|
2096
|
+
*/
|
|
2097
|
+
analyzeCodeCoverage(widget) {
|
|
2098
|
+
const coverage = {
|
|
2099
|
+
totalTemplateVars: 0,
|
|
2100
|
+
templateVarsUsed: 0,
|
|
2101
|
+
totalClientBindings: 0,
|
|
2102
|
+
clientBindingsUsed: 0,
|
|
2103
|
+
totalServerData: 0,
|
|
2104
|
+
serverDataUsed: 0,
|
|
2105
|
+
templateCoverage: 0,
|
|
2106
|
+
clientCoverage: 0,
|
|
2107
|
+
serverCoverage: 0,
|
|
2108
|
+
overallScore: 0
|
|
2109
|
+
};
|
|
2110
|
+
// Extract all variables
|
|
2111
|
+
const templateVars = (widget.template?.match(/\{\{([^}]+)\}\}/g) || [])
|
|
2112
|
+
.map((v) => v.replace(/[{}]/g, '').trim());
|
|
2113
|
+
const clientBindings = (widget.client_script?.match(/c\.(\w+)|\$scope\.(\w+)/g) || [])
|
|
2114
|
+
.map((v) => v.replace(/c\.|\\$scope\./, ''));
|
|
2115
|
+
const serverDataKeys = (widget.script?.match(/data\.(\w+)/g) || [])
|
|
2116
|
+
.map((v) => v.replace('data.', ''));
|
|
2117
|
+
coverage.totalTemplateVars = templateVars.length;
|
|
2118
|
+
coverage.totalClientBindings = clientBindings.length;
|
|
2119
|
+
coverage.totalServerData = serverDataKeys.length;
|
|
2120
|
+
// Check usage
|
|
2121
|
+
templateVars.forEach(v => {
|
|
2122
|
+
if (widget.client_script?.includes(v) || widget.script?.includes(v)) {
|
|
2123
|
+
coverage.templateVarsUsed++;
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
clientBindings.forEach(b => {
|
|
2127
|
+
if (widget.template?.includes(b)) {
|
|
2128
|
+
coverage.clientBindingsUsed++;
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
serverDataKeys.forEach(k => {
|
|
2132
|
+
if (widget.client_script?.includes(k) || widget.template?.includes(k)) {
|
|
2133
|
+
coverage.serverDataUsed++;
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
// Calculate percentages
|
|
2137
|
+
coverage.templateCoverage = coverage.totalTemplateVars > 0
|
|
2138
|
+
? Math.round((coverage.templateVarsUsed / coverage.totalTemplateVars) * 100) : 100;
|
|
2139
|
+
coverage.clientCoverage = coverage.totalClientBindings > 0
|
|
2140
|
+
? Math.round((coverage.clientBindingsUsed / coverage.totalClientBindings) * 100) : 100;
|
|
2141
|
+
coverage.serverCoverage = coverage.totalServerData > 0
|
|
2142
|
+
? Math.round((coverage.serverDataUsed / coverage.totalServerData) * 100) : 100;
|
|
2143
|
+
coverage.overallScore = Math.round((coverage.templateCoverage + coverage.clientCoverage + coverage.serverCoverage) / 3);
|
|
2144
|
+
return coverage;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Check integration between template, CSS, and scripts
|
|
2148
|
+
*/
|
|
2149
|
+
checkIntegration(integration) {
|
|
2150
|
+
const issues = [];
|
|
2151
|
+
// Check if template variables are defined in scripts
|
|
2152
|
+
const undefinedVars = integration.template_refs.filter((ref) => {
|
|
2153
|
+
const varName = ref.replace(/[{}]/g, '').split('.')[0].trim();
|
|
2154
|
+
return !integration.client_bindings.some((binding) => binding.includes(varName)) &&
|
|
2155
|
+
!integration.server_data_keys.some((key) => key.includes(varName));
|
|
2156
|
+
});
|
|
2157
|
+
if (undefinedVars.length > 0) {
|
|
2158
|
+
issues.push(`ā ļø Template variables not defined in scripts: ${undefinedVars.join(', ')}`);
|
|
2159
|
+
}
|
|
2160
|
+
// Check if CSS classes are used in template
|
|
2161
|
+
const unusedClasses = integration.css_classes.filter((cls) => {
|
|
2162
|
+
const className = cls.substring(1); // Remove the dot
|
|
2163
|
+
return !integration.template_refs.some((ref) => ref.includes(className));
|
|
2164
|
+
});
|
|
2165
|
+
if (unusedClasses.length > 0 && unusedClasses.length < 5) {
|
|
2166
|
+
issues.push(`ā ļø CSS classes possibly unused: ${unusedClasses.join(', ')}`);
|
|
2167
|
+
}
|
|
2168
|
+
return issues.length > 0 ? issues.join('\n') : 'ā
Good integration between template, CSS, and scripts';
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Generate recommendations based on analysis
|
|
2172
|
+
*/
|
|
2173
|
+
generateRecommendations(widgetData, integration, dependencies) {
|
|
2174
|
+
const recommendations = [];
|
|
2175
|
+
if (dependencies.length > 0) {
|
|
2176
|
+
recommendations.push('1. Ensure all required libraries are included in the Service Portal theme');
|
|
2177
|
+
}
|
|
2178
|
+
if (integration.template_refs.length === 0) {
|
|
2179
|
+
recommendations.push('2. Consider adding dynamic content to your template using {{variable}} syntax');
|
|
2180
|
+
}
|
|
2181
|
+
if (integration.server_data_keys.length > 0 && integration.client_bindings.length === 0) {
|
|
2182
|
+
recommendations.push('3. Add client script to handle server data and user interactions');
|
|
2183
|
+
}
|
|
2184
|
+
if (!widgetData.demo_data || widgetData.demo_data === '{}') {
|
|
2185
|
+
recommendations.push('4. Add demo data to help others understand how to use your widget');
|
|
2186
|
+
}
|
|
2187
|
+
if (!widgetData.option_schema || widgetData.option_schema === '[]') {
|
|
2188
|
+
recommendations.push('5. Define widget options schema for better reusability');
|
|
2189
|
+
}
|
|
2190
|
+
return recommendations.length > 0 ? recommendations.join('\n') : 'Widget structure looks good!';
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Smart Update Set Management with context detection
|
|
2194
|
+
*/
|
|
2195
|
+
async smartUpdateSet(args) {
|
|
2196
|
+
try {
|
|
2197
|
+
// Check authentication
|
|
2198
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
2199
|
+
if (!isAuth) {
|
|
2200
|
+
return {
|
|
2201
|
+
content: [
|
|
2202
|
+
{
|
|
2203
|
+
type: 'text',
|
|
2204
|
+
text: 'ā Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login',
|
|
2205
|
+
},
|
|
2206
|
+
],
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
// Get current context (task identifier)
|
|
2210
|
+
const taskContext = args.description || 'Current Task';
|
|
2211
|
+
const contextKey = `task_context_${taskContext.replace(/\s+/g, '_').toLowerCase()}`;
|
|
2212
|
+
// Check if we need a new update set
|
|
2213
|
+
const currentUpdateSet = await this.client.getCurrentUpdateSet();
|
|
2214
|
+
let needNewUpdateSet = true;
|
|
2215
|
+
if (currentUpdateSet.success && currentUpdateSet.data) {
|
|
2216
|
+
// Check if current update set is for the same task
|
|
2217
|
+
if (!args.separate_by_task || currentUpdateSet.data.description?.includes(taskContext)) {
|
|
2218
|
+
needNewUpdateSet = false;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (!needNewUpdateSet && currentUpdateSet.data) {
|
|
2222
|
+
return {
|
|
2223
|
+
content: [
|
|
2224
|
+
{
|
|
2225
|
+
type: 'text',
|
|
2226
|
+
text: `ā
Using existing Update Set for this task:\n\nš¦ **Current Update Set:**\n- Name: ${currentUpdateSet.data.name}\n- ID: ${currentUpdateSet.data.sys_id}\n- Description: ${currentUpdateSet.data.description}\n\nš” Same task context detected - no new Update Set needed.`
|
|
2227
|
+
}
|
|
2228
|
+
]
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
// Close previous update set if requested
|
|
2232
|
+
if (args.close_previous && currentUpdateSet.data) {
|
|
2233
|
+
await this.client.completeUpdateSet(currentUpdateSet.data.sys_id);
|
|
2234
|
+
this.logger.info('Closed previous Update Set', { id: currentUpdateSet.data.sys_id });
|
|
2235
|
+
}
|
|
2236
|
+
// Create new update set
|
|
2237
|
+
const updateSetNumber = Date.now().toString().slice(-6);
|
|
2238
|
+
const updateSetName = `${args.name_prefix}-${updateSetNumber}: ${taskContext}`;
|
|
2239
|
+
const result = await this.client.createUpdateSet({
|
|
2240
|
+
name: updateSetName,
|
|
2241
|
+
description: `Auto-created for task: ${taskContext}\n\nContext Detection: ${args.detect_context ? 'Enabled' : 'Disabled'}\nSeparate by Task: ${args.separate_by_task ? 'Yes' : 'No'}`,
|
|
2242
|
+
state: 'in_progress'
|
|
2243
|
+
});
|
|
2244
|
+
if (!result.success) {
|
|
2245
|
+
throw new Error(`Failed to create Update Set: ${result.error}`);
|
|
2246
|
+
}
|
|
2247
|
+
// Set as current update set
|
|
2248
|
+
await this.client.setCurrentUpdateSet(result.data.sys_id);
|
|
2249
|
+
const credentials = await this.oauth.loadCredentials();
|
|
2250
|
+
const updateSetUrl = `https://${credentials?.instance}/sys_update_set.do?sys_id=${result.data.sys_id}`;
|
|
2251
|
+
return {
|
|
2252
|
+
content: [
|
|
2253
|
+
{
|
|
2254
|
+
type: 'text',
|
|
2255
|
+
text: `ā
Smart Update Set created successfully!\n\nš¦ **New Update Set:**\n- Name: ${updateSetName}\n- ID: ${result.data.sys_id}\n- Task Context: ${taskContext}\n\nš§ **Smart Features:**\n- Context Detection: ${args.detect_context ? 'ā
Enabled' : 'ā Disabled'}\n- Separate by Task: ${args.separate_by_task ? 'ā
Yes' : 'ā No'}\n- Auto-close Previous: ${args.close_previous ? 'ā
Yes' : 'ā No'}\n${currentUpdateSet.data && args.close_previous ? `- Previous Set Closed: ā
${currentUpdateSet.data.name}` : ''}\n\nš **Direct Link:**\n${updateSetUrl}\n\nš” **Next Steps:**\n1. All new changes will be tracked in this Update Set\n2. Deploy your artifacts - they'll be automatically included\n3. Complete the Update Set when your task is done\n4. Next task will get its own Update Set automatically`
|
|
2256
|
+
}
|
|
2257
|
+
]
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
catch (error) {
|
|
2261
|
+
throw new Error(`Smart Update Set creation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Validate Flow Definition before deployment
|
|
2266
|
+
*/
|
|
2267
|
+
async validateFlowDefinition(args) {
|
|
2268
|
+
try {
|
|
2269
|
+
const flowType = args.flow_type || 'flow';
|
|
2270
|
+
let definition;
|
|
2271
|
+
try {
|
|
2272
|
+
definition = typeof args.definition === 'string' ? JSON.parse(args.definition) : args.definition;
|
|
2273
|
+
}
|
|
2274
|
+
catch (error) {
|
|
2275
|
+
return {
|
|
2276
|
+
content: [
|
|
2277
|
+
{
|
|
2278
|
+
type: 'text',
|
|
2279
|
+
text: `ā Invalid JSON format in flow definition:\n\n${error instanceof Error ? error.message : String(error)}\n\nš” Please check your JSON syntax.`
|
|
2280
|
+
}
|
|
2281
|
+
]
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
const issues = [];
|
|
2285
|
+
const warnings = [];
|
|
2286
|
+
const info = [];
|
|
2287
|
+
// Basic structure validation
|
|
2288
|
+
if (!definition.activities || !Array.isArray(definition.activities)) {
|
|
2289
|
+
issues.push('ā Missing or invalid "activities" array');
|
|
2290
|
+
}
|
|
2291
|
+
// Flow type specific validation
|
|
2292
|
+
switch (flowType) {
|
|
2293
|
+
case 'flow':
|
|
2294
|
+
if (!definition.trigger && !args.trigger_type) {
|
|
2295
|
+
issues.push('ā Flow must have a trigger defined');
|
|
2296
|
+
}
|
|
2297
|
+
break;
|
|
2298
|
+
case 'subflow':
|
|
2299
|
+
if (!definition.inputs) {
|
|
2300
|
+
warnings.push('ā ļø Subflow has no inputs defined');
|
|
2301
|
+
}
|
|
2302
|
+
if (!definition.outputs) {
|
|
2303
|
+
warnings.push('ā ļø Subflow has no outputs defined');
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
case 'action':
|
|
2307
|
+
if (!definition.action_type) {
|
|
2308
|
+
warnings.push('ā ļø Action type not specified');
|
|
2309
|
+
}
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
// Activity validation
|
|
2313
|
+
if (definition.activities) {
|
|
2314
|
+
definition.activities.forEach((activity, index) => {
|
|
2315
|
+
if (!activity.name) {
|
|
2316
|
+
issues.push(`ā Activity ${index + 1} missing required "name" field`);
|
|
2317
|
+
}
|
|
2318
|
+
if (!activity.type) {
|
|
2319
|
+
issues.push(`ā Activity ${index + 1} missing required "type" field`);
|
|
2320
|
+
}
|
|
2321
|
+
// Check for common activity types
|
|
2322
|
+
const validTypes = ['rest', 'script', 'approval', 'condition', 'subflow', 'notification', 'wait', 'lookup'];
|
|
2323
|
+
if (activity.type && !validTypes.includes(activity.type)) {
|
|
2324
|
+
warnings.push(`ā ļø Activity "${activity.name}" uses non-standard type: ${activity.type}`);
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
// Dependency checking
|
|
2329
|
+
if (args.check_dependencies) {
|
|
2330
|
+
const dependencies = this.extractDependencies(definition);
|
|
2331
|
+
if (dependencies.length > 0) {
|
|
2332
|
+
info.push(`š¦ Dependencies found: ${dependencies.join(', ')}`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// Generate preview if requested
|
|
2336
|
+
let preview = '';
|
|
2337
|
+
if (args.show_preview) {
|
|
2338
|
+
preview = this.generateFlowPreview(definition, flowType);
|
|
2339
|
+
}
|
|
2340
|
+
const hasErrors = issues.length > 0;
|
|
2341
|
+
const status = hasErrors ? 'ā VALIDATION FAILED' : 'ā
VALIDATION PASSED';
|
|
2342
|
+
return {
|
|
2343
|
+
content: [
|
|
2344
|
+
{
|
|
2345
|
+
type: 'text',
|
|
2346
|
+
text: `${status}\n\nš **Flow Validation Report:**\n- Flow Type: ${flowType}\n- Activities: ${definition.activities?.length || 0}\n- Status: ${hasErrors ? 'Failed' : 'Passed'}\n\n${issues.length > 0 ? `šØ **Critical Issues:**\n${issues.join('\n')}\n\n` : ''}${warnings.length > 0 ? `ā ļø **Warnings:**\n${warnings.join('\n')}\n\n` : ''}${info.length > 0 ? `ā¹ļø **Information:**\n${info.join('\n')}\n\n` : ''}${preview ? `\nš **Flow Preview:**\n${preview}\n` : ''}${!hasErrors && args.test_mode ? '\nš§Ŗ **Test Mode:** Flow structure is valid for testing\n' : ''}${!hasErrors ? '\nā
Flow definition is valid and ready for deployment!' : '\nā Please fix the issues before deploying.'}`
|
|
2347
|
+
}
|
|
2348
|
+
]
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
catch (error) {
|
|
2352
|
+
throw new Error(`Flow validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Create Solution Package grouping multiple artifacts
|
|
2357
|
+
*/
|
|
2358
|
+
async createSolutionPackage(args) {
|
|
2359
|
+
try {
|
|
2360
|
+
// Check authentication
|
|
2361
|
+
const isAuth = await this.oauth.isAuthenticated();
|
|
2362
|
+
if (!isAuth) {
|
|
2363
|
+
return {
|
|
2364
|
+
content: [
|
|
2365
|
+
{
|
|
2366
|
+
type: 'text',
|
|
2367
|
+
text: 'ā Not authenticated with ServiceNow.\n\nPlease run: snow-flow auth login',
|
|
2368
|
+
},
|
|
2369
|
+
],
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
// Create new update set for the solution
|
|
2373
|
+
if (args.new_update_set) {
|
|
2374
|
+
const updateSetResult = await this.smartUpdateSet({
|
|
2375
|
+
detect_context: true,
|
|
2376
|
+
name_prefix: 'SOLUTION',
|
|
2377
|
+
description: args.description || `Solution Package: ${args.name}`,
|
|
2378
|
+
separate_by_task: false,
|
|
2379
|
+
close_previous: true
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
const deployedArtifacts = [];
|
|
2383
|
+
const failedArtifacts = [];
|
|
2384
|
+
// Deploy each artifact in the package
|
|
2385
|
+
for (const artifact of args.artifacts) {
|
|
2386
|
+
try {
|
|
2387
|
+
let result;
|
|
2388
|
+
switch (artifact.type) {
|
|
2389
|
+
case 'flow':
|
|
2390
|
+
result = await this.deployFlow(artifact.create);
|
|
2391
|
+
break;
|
|
2392
|
+
case 'widget':
|
|
2393
|
+
result = await this.deployWidget(artifact.create);
|
|
2394
|
+
break;
|
|
2395
|
+
case 'script_include':
|
|
2396
|
+
result = await this.deployScriptInclude(artifact.create);
|
|
2397
|
+
break;
|
|
2398
|
+
case 'business_rule':
|
|
2399
|
+
result = await this.deployBusinessRule(artifact.create);
|
|
2400
|
+
break;
|
|
2401
|
+
case 'table':
|
|
2402
|
+
result = await this.deployTable(artifact.create);
|
|
2403
|
+
break;
|
|
2404
|
+
default:
|
|
2405
|
+
throw new Error(`Unknown artifact type: ${artifact.type}`);
|
|
2406
|
+
}
|
|
2407
|
+
deployedArtifacts.push({
|
|
2408
|
+
type: artifact.type,
|
|
2409
|
+
name: artifact.create.name,
|
|
2410
|
+
result: 'Success'
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
failedArtifacts.push({
|
|
2415
|
+
type: artifact.type,
|
|
2416
|
+
name: artifact.create.name,
|
|
2417
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
const successCount = deployedArtifacts.length;
|
|
2422
|
+
const failureCount = failedArtifacts.length;
|
|
2423
|
+
const totalCount = successCount + failureCount;
|
|
2424
|
+
return {
|
|
2425
|
+
content: [
|
|
2426
|
+
{
|
|
2427
|
+
type: 'text',
|
|
2428
|
+
text: `š¦ **Solution Package Deployment Complete!**\n\nšÆ **Package Details:**\n- Name: ${args.name}\n- Description: ${args.description || 'N/A'}\n- Total Artifacts: ${totalCount}\n- Successful: ${successCount} ā
\n- Failed: ${failureCount} ${failureCount > 0 ? 'ā' : ''}\n\n${deployedArtifacts.length > 0 ? `ā
**Successfully Deployed:**\n${deployedArtifacts.map((a, i) => `${i + 1}. ${a.type}: ${a.name}`).join('\n')}\n` : ''}${failedArtifacts.length > 0 ? `\nā **Failed Deployments:**\n${failedArtifacts.map((a, i) => `${i + 1}. ${a.type}: ${a.name}\n Error: ${a.error}`).join('\n')}\n` : ''}\nš” **Solution Benefits:**\n- All artifacts grouped in one Update Set\n- Dependencies automatically resolved\n- Consistent deployment across artifacts\n- Easy rollback if needed\n\n${successCount === totalCount ? 'š All artifacts deployed successfully!' : 'ā ļø Some artifacts failed. Please review the errors above.'}`
|
|
2429
|
+
}
|
|
2430
|
+
]
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
catch (error) {
|
|
2434
|
+
throw new Error(`Solution package creation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Interactive Flow Creation Wizard
|
|
2439
|
+
*/
|
|
2440
|
+
async flowWizard(args) {
|
|
2441
|
+
try {
|
|
2442
|
+
const flowType = args.flow_type || 'flow';
|
|
2443
|
+
const steps = [];
|
|
2444
|
+
// Step 1: Basic Information
|
|
2445
|
+
steps.push({
|
|
2446
|
+
step: 1,
|
|
2447
|
+
name: 'Basic Information',
|
|
2448
|
+
status: 'ā
',
|
|
2449
|
+
details: `Name: ${args.name}\nType: ${flowType}\nDescription: Configure your flow step by step`
|
|
2450
|
+
});
|
|
2451
|
+
// Step 2: Trigger Configuration (for flows only)
|
|
2452
|
+
if (flowType === 'flow') {
|
|
2453
|
+
steps.push({
|
|
2454
|
+
step: 2,
|
|
2455
|
+
name: 'Trigger Configuration',
|
|
2456
|
+
status: 'š',
|
|
2457
|
+
details: 'Choose trigger type: record_created, record_updated, scheduled, or manual'
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
// Step 3: Activities
|
|
2461
|
+
steps.push({
|
|
2462
|
+
step: 3,
|
|
2463
|
+
name: 'Add Activities',
|
|
2464
|
+
status: 'š',
|
|
2465
|
+
details: 'Add activities: scripts, approvals, notifications, conditions'
|
|
2466
|
+
});
|
|
2467
|
+
// Step 4: Variables and Data
|
|
2468
|
+
steps.push({
|
|
2469
|
+
step: 4,
|
|
2470
|
+
name: 'Variables & Data',
|
|
2471
|
+
status: 'š',
|
|
2472
|
+
details: 'Define flow variables and data transformations'
|
|
2473
|
+
});
|
|
2474
|
+
// Step 5: Error Handling
|
|
2475
|
+
steps.push({
|
|
2476
|
+
step: 5,
|
|
2477
|
+
name: 'Error Handling',
|
|
2478
|
+
status: 'š',
|
|
2479
|
+
details: 'Configure error handlers and retry logic'
|
|
2480
|
+
});
|
|
2481
|
+
// Step 6: Testing
|
|
2482
|
+
steps.push({
|
|
2483
|
+
step: 6,
|
|
2484
|
+
name: 'Test Flow',
|
|
2485
|
+
status: 'š',
|
|
2486
|
+
details: 'Test with sample data before deployment'
|
|
2487
|
+
});
|
|
2488
|
+
// Generate wizard interface
|
|
2489
|
+
const wizardText = `š§āāļø **Flow Creation Wizard**\n\nš **Flow Details:**\n- Name: ${args.name}\n- Type: ${flowType}\n- Interactive: ${args.interactive ? 'ā
' : 'ā'}\n- Preview Each Step: ${args.preview_each_step ? 'ā
' : 'ā'}\n- Test As You Build: ${args.test_as_you_build ? 'ā
' : 'ā'}\n\nš **Wizard Steps:**\n${steps.map(s => `${s.step}. ${s.status} ${s.name}\n ${s.details}`).join('\n\n')}\n\nš” **Interactive Features:**\n- ā
Step-by-step guidance\n- ā
Preview after each step\n- ā
Validation at each stage\n- ā
Test before deployment\n- ā
Rollback capability\n\nšÆ **Next Actions:**\n1. Use snow_deploy_flow with your configuration\n2. Or continue building with individual artifact tools\n3. Test with snow_validate_flow_definition\n\nā” **Quick Start Example:**\n\`\`\`json\n{\n "name": "${args.name}",\n "flow_type": "${flowType}",\n "trigger_type": "record_created",\n "table": "incident",\n "flow_definition": {\n "activities": [\n {\n "name": "Check Priority",\n "type": "condition",\n "condition": "current.priority == 1"\n },\n {\n "name": "Send Alert",\n "type": "notification",\n "recipients": "incident.assigned_to"\n }\n ]\n }\n}\n\`\`\``;
|
|
2490
|
+
return {
|
|
2491
|
+
content: [
|
|
2492
|
+
{
|
|
2493
|
+
type: 'text',
|
|
2494
|
+
text: wizardText
|
|
2495
|
+
}
|
|
2496
|
+
]
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
catch (error) {
|
|
2500
|
+
throw new Error(`Flow wizard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Extract dependencies from flow definition
|
|
2505
|
+
*/
|
|
2506
|
+
extractDependencies(definition) {
|
|
2507
|
+
const dependencies = new Set();
|
|
2508
|
+
if (definition.activities) {
|
|
2509
|
+
definition.activities.forEach((activity) => {
|
|
2510
|
+
if (activity.type === 'rest' && activity.rest_message) {
|
|
2511
|
+
dependencies.add(`REST Message: ${activity.rest_message}`);
|
|
2512
|
+
}
|
|
2513
|
+
if (activity.type === 'script' && activity.script_include) {
|
|
2514
|
+
dependencies.add(`Script Include: ${activity.script_include}`);
|
|
2515
|
+
}
|
|
2516
|
+
if (activity.type === 'subflow' && activity.subflow_name) {
|
|
2517
|
+
dependencies.add(`Subflow: ${activity.subflow_name}`);
|
|
2518
|
+
}
|
|
2519
|
+
if (activity.artifact_reference) {
|
|
2520
|
+
dependencies.add(`${activity.artifact_reference.type}: ${activity.artifact_reference.name}`);
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return Array.from(dependencies);
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Generate visual preview of flow
|
|
2528
|
+
*/
|
|
2529
|
+
generateFlowPreview(definition, flowType) {
|
|
2530
|
+
let preview = `\n${flowType.toUpperCase()} STRUCTURE:\n`;
|
|
2531
|
+
preview += 'ā'.repeat(40) + '\n';
|
|
2532
|
+
if (flowType === 'flow' && definition.trigger) {
|
|
2533
|
+
preview += `\n[TRIGGER: ${definition.trigger.type || 'Unknown'}]\n ā\n`;
|
|
2534
|
+
}
|
|
2535
|
+
if (definition.activities) {
|
|
2536
|
+
definition.activities.forEach((activity, index) => {
|
|
2537
|
+
const isLast = index === definition.activities.length - 1;
|
|
2538
|
+
preview += `[${activity.type?.toUpperCase() || 'UNKNOWN'}: ${activity.name || `Activity ${index + 1}`}]\n`;
|
|
2539
|
+
if (!isLast) {
|
|
2540
|
+
preview += ' ā\n';
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
if (flowType !== 'flow' && definition.outputs) {
|
|
2545
|
+
preview += `\n[OUTPUTS: ${definition.outputs.length} defined]\n`;
|
|
2546
|
+
}
|
|
2547
|
+
return preview;
|
|
2548
|
+
}
|
|
1509
2549
|
async start() {
|
|
1510
2550
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
1511
2551
|
await this.server.connect(transport);
|