snow-flow 3.4.39 ā 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -2
- package/dist/mcp/enhancements/smart-query-enhancement.d.ts +49 -0
- package/dist/mcp/enhancements/smart-query-enhancement.js +233 -0
- package/dist/mcp/servicenow-local-development-mcp.d.ts +48 -0
- package/dist/mcp/servicenow-local-development-mcp.js +570 -0
- package/dist/queen/queen-knowledge-base.d.ts +151 -0
- package/dist/queen/queen-knowledge-base.js +283 -0
- package/dist/queen/servicenow-queen.d.ts +2 -0
- package/dist/queen/servicenow-queen.js +91 -0
- package/dist/utils/artifact-local-sync.d.ts +115 -0
- package/dist/utils/artifact-local-sync.js +705 -0
- package/dist/utils/artifact-sync/artifact-registry.d.ts +66 -0
- package/dist/utils/artifact-sync/artifact-registry.js +450 -0
- package/dist/utils/artifact-sync/index.d.ts +33 -0
- package/dist/utils/artifact-sync/index.js +52 -0
- package/dist/utils/mcp-tool-registry.js +109 -0
- package/dist/utils/smart-field-fetcher.d.ts +68 -0
- package/dist/utils/smart-field-fetcher.js +395 -0
- package/package.json +2 -2
- package/website/.dockerignore +64 -0
- package/website/Dockerfile +36 -0
- package/website/README.md +40 -1
- package/website/cloudbuild.yaml +69 -0
- package/website/css/style.css +119 -1
- package/website/index.html +385 -39
- package/website/js/main.js +26 -1
- package/website/nginx.conf +116 -0
- package/website/service.yaml +61 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Artifact Local Sync System
|
|
4
|
+
*
|
|
5
|
+
* Creates temporary local files from ServiceNow artifacts so Claude Code
|
|
6
|
+
* can use its native tools (search, edit, multi-file operations, etc.)
|
|
7
|
+
* Then syncs changes back to ServiceNow.
|
|
8
|
+
*
|
|
9
|
+
* THIS IS THE BRIDGE BETWEEN SERVICENOW AND CLAUDE CODE!
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.ArtifactLocalSync = void 0;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const smart_field_fetcher_1 = require("./smart-field-fetcher");
|
|
50
|
+
const artifact_registry_1 = require("./artifact-sync/artifact-registry");
|
|
51
|
+
class ArtifactLocalSync {
|
|
52
|
+
constructor(client) {
|
|
53
|
+
this.artifacts = new Map();
|
|
54
|
+
this.client = client;
|
|
55
|
+
this.smartFetcher = new smart_field_fetcher_1.SmartFieldFetcher(client);
|
|
56
|
+
// Create base directory for Snow-Flow artifacts
|
|
57
|
+
this.baseDir = path.join(os.tmpdir(), 'snow-flow-artifacts');
|
|
58
|
+
if (!fs.existsSync(this.baseDir)) {
|
|
59
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
console.log(`š Snow-Flow local sync directory: ${this.baseDir}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* DYNAMIC pull artifact from ServiceNow using artifact registry
|
|
65
|
+
* Works with ANY artifact type defined in the registry!
|
|
66
|
+
*/
|
|
67
|
+
async pullArtifact(tableName, sys_id) {
|
|
68
|
+
const config = (0, artifact_registry_1.getArtifactConfig)(tableName);
|
|
69
|
+
if (!config) {
|
|
70
|
+
throw new Error(`Unsupported artifact type: ${tableName}. Supported types: ${Object.keys(artifact_registry_1.ARTIFACT_REGISTRY).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
console.log(`\nš Pulling ${config.displayName} (${sys_id}) to local files...`);
|
|
73
|
+
// Use smart fetcher for known types, otherwise direct query
|
|
74
|
+
let artifactData;
|
|
75
|
+
if (tableName === 'sp_widget') {
|
|
76
|
+
artifactData = await this.smartFetcher.fetchWidget(sys_id);
|
|
77
|
+
}
|
|
78
|
+
else if (tableName === 'sys_hub_flow') {
|
|
79
|
+
artifactData = await this.smartFetcher.fetchFlow(sys_id);
|
|
80
|
+
}
|
|
81
|
+
else if (tableName === 'sys_script') {
|
|
82
|
+
artifactData = await this.smartFetcher.fetchBusinessRule(sys_id);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Generic fetch for other types
|
|
86
|
+
const allFields = config.fieldMappings.map(fm => fm.serviceNowField);
|
|
87
|
+
const response = await this.client.query(tableName, {
|
|
88
|
+
query: `sys_id=${sys_id}`,
|
|
89
|
+
fields: allFields.concat(['sys_id', 'sys_created_on', 'sys_updated_on']),
|
|
90
|
+
limit: 1
|
|
91
|
+
});
|
|
92
|
+
artifactData = response.result?.[0];
|
|
93
|
+
}
|
|
94
|
+
if (!artifactData) {
|
|
95
|
+
throw new Error(`Artifact not found: ${tableName}/${sys_id}`);
|
|
96
|
+
}
|
|
97
|
+
// Use identifier field from config
|
|
98
|
+
const identifier = artifactData[config.identifierField] || `${config.folderName}_${sys_id}`;
|
|
99
|
+
const sanitizedName = this.sanitizeFilename(identifier);
|
|
100
|
+
const artifactPath = path.join(this.baseDir, config.folderName, sanitizedName);
|
|
101
|
+
// Clean up existing files
|
|
102
|
+
if (fs.existsSync(artifactPath)) {
|
|
103
|
+
fs.rmSync(artifactPath, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
fs.mkdirSync(artifactPath, { recursive: true });
|
|
106
|
+
// Create local files based on field mappings
|
|
107
|
+
const files = [];
|
|
108
|
+
for (const mapping of config.fieldMappings) {
|
|
109
|
+
const fieldValue = artifactData[mapping.serviceNowField];
|
|
110
|
+
// Skip empty fields unless required
|
|
111
|
+
if (!fieldValue && !mapping.isRequired) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Apply preprocessor if defined
|
|
115
|
+
let processedContent = fieldValue || '';
|
|
116
|
+
if (mapping.preprocessor) {
|
|
117
|
+
processedContent = mapping.preprocessor(processedContent);
|
|
118
|
+
}
|
|
119
|
+
// Generate filename with placeholder replacement
|
|
120
|
+
const filename = mapping.localFileName
|
|
121
|
+
.replace('{name}', sanitizedName)
|
|
122
|
+
.replace('{api_name}', artifactData.api_name || sanitizedName)
|
|
123
|
+
.replace('{short_description}', artifactData.short_description || sanitizedName);
|
|
124
|
+
// Generate header/footer with replacements
|
|
125
|
+
const header = this.replacePlaceholders(mapping.wrapperHeader || '', artifactData);
|
|
126
|
+
const footer = this.replacePlaceholders(mapping.wrapperFooter || '', artifactData);
|
|
127
|
+
const file = this.createLocalFile(artifactPath, `${filename}.${mapping.fileExtension}`, processedContent, mapping.serviceNowField, mapping.fileExtension, header, footer);
|
|
128
|
+
file.fieldMapping = mapping;
|
|
129
|
+
files.push(file);
|
|
130
|
+
}
|
|
131
|
+
// Generate README with artifact-specific context
|
|
132
|
+
const readmeContent = this.generateArtifactReadme(config, artifactData, files);
|
|
133
|
+
const readmeFile = this.createLocalFile(artifactPath, 'README.md', readmeContent, 'documentation', 'md');
|
|
134
|
+
files.push(readmeFile);
|
|
135
|
+
// Create artifact record
|
|
136
|
+
const artifact = {
|
|
137
|
+
sys_id: artifactData.sys_id,
|
|
138
|
+
name: identifier,
|
|
139
|
+
type: config.displayName,
|
|
140
|
+
tableName: tableName,
|
|
141
|
+
localPath: artifactPath,
|
|
142
|
+
files: files,
|
|
143
|
+
metadata: artifactData,
|
|
144
|
+
syncStatus: 'synced',
|
|
145
|
+
createdAt: new Date(),
|
|
146
|
+
lastSyncedAt: new Date(),
|
|
147
|
+
artifactConfig: config
|
|
148
|
+
};
|
|
149
|
+
this.artifacts.set(sys_id, artifact);
|
|
150
|
+
console.log(`ā
${config.displayName} synced to local files:`);
|
|
151
|
+
console.log(`š Location: ${artifactPath}`);
|
|
152
|
+
console.log(`š Files created:`);
|
|
153
|
+
files.forEach(f => console.log(` - ${f.filename} (${f.type})`));
|
|
154
|
+
console.log(`\nš” Claude Code can now use its native tools on these files!`);
|
|
155
|
+
console.log(` Edit, search, refactor - then run 'pushArtifact' to sync back.`);
|
|
156
|
+
return artifact;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pull a widget from ServiceNow and create local files
|
|
160
|
+
* This is the magic that lets Claude Code use its native tools!
|
|
161
|
+
* (Wrapper for backward compatibility)
|
|
162
|
+
*/
|
|
163
|
+
async pullWidget(sys_id) {
|
|
164
|
+
return this.pullArtifact('sp_widget', sys_id);
|
|
165
|
+
console.log(`\nš Pulling widget ${sys_id} to local files...`);
|
|
166
|
+
// Fetch widget with smart chunking
|
|
167
|
+
const widget = await this.smartFetcher.fetchWidget(sys_id);
|
|
168
|
+
// Create local directory structure
|
|
169
|
+
const widgetName = this.sanitizeFilename(widget.name || `widget_${sys_id}`);
|
|
170
|
+
const widgetPath = path.join(this.baseDir, 'widgets', widgetName);
|
|
171
|
+
// Clean up any existing files
|
|
172
|
+
if (fs.existsSync(widgetPath)) {
|
|
173
|
+
fs.rmSync(widgetPath, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
fs.mkdirSync(widgetPath, { recursive: true });
|
|
176
|
+
// Create local files for each widget component
|
|
177
|
+
const files = [];
|
|
178
|
+
// 1. HTML Template
|
|
179
|
+
if (widget.template) {
|
|
180
|
+
const htmlFile = this.createLocalFile(widgetPath, `${widgetName}.html`, widget.template, 'template', 'html', '<!-- ServiceNow Widget Template -->\n<!-- Widget: ' + widget.name + ' -->\n<!-- Bindings: {{data.x}} from server, ng-click calls client methods -->\n\n');
|
|
181
|
+
files.push(htmlFile);
|
|
182
|
+
}
|
|
183
|
+
// 2. Server Script (ES5)
|
|
184
|
+
if (widget.script) {
|
|
185
|
+
const serverFile = this.createLocalFile(widgetPath, `${widgetName}.server.js`, widget.script, 'script', 'js', '/**\n * ServiceNow Widget Server Script (ES5 ONLY!)\n * Widget: ' + widget.name + '\n * \n * Available objects:\n * - data: Object to send to client\n * - input: Data from client\n * - options: Widget instance options\n * - gs: GlideSystem\n * - $sp: Service Portal API\n */\n\n(function() {\n', '\n})();');
|
|
186
|
+
files.push(serverFile);
|
|
187
|
+
}
|
|
188
|
+
// 3. Client Script (AngularJS)
|
|
189
|
+
if (widget.client_script) {
|
|
190
|
+
const clientFile = this.createLocalFile(widgetPath, `${widgetName}.client.js`, widget.client_script, 'client_script', 'js', '/**\n * ServiceNow Widget Client Controller (AngularJS)\n * Widget: ' + widget.name + '\n * \n * Available objects:\n * - c: Widget controller (this)\n * - c.data: Data from server\n * - c.server: Server communication\n * - $scope: Angular scope\n */\n\nfunction(');
|
|
191
|
+
files.push(clientFile);
|
|
192
|
+
}
|
|
193
|
+
// 4. CSS
|
|
194
|
+
if (widget.css) {
|
|
195
|
+
const cssFile = this.createLocalFile(widgetPath, `${widgetName}.css`, widget.css, 'css', 'css', '/* ServiceNow Widget Styles */\n/* Widget: ' + widget.name + ' */\n/* Prefix classes to avoid conflicts */\n\n');
|
|
196
|
+
files.push(cssFile);
|
|
197
|
+
}
|
|
198
|
+
// 5. Widget Configuration (JSON)
|
|
199
|
+
const config = {
|
|
200
|
+
sys_id: widget.sys_id,
|
|
201
|
+
name: widget.name,
|
|
202
|
+
title: widget.title,
|
|
203
|
+
option_schema: widget.option_schema,
|
|
204
|
+
data_table: widget.data_table,
|
|
205
|
+
roles: widget.roles,
|
|
206
|
+
public: widget.public,
|
|
207
|
+
_coherence_hints: widget._coherence_hints || []
|
|
208
|
+
};
|
|
209
|
+
const configFile = this.createLocalFile(widgetPath, `${widgetName}.config.json`, JSON.stringify(config, null, 2), 'metadata', 'json');
|
|
210
|
+
files.push(configFile);
|
|
211
|
+
// 6. README with context
|
|
212
|
+
const readmeContent = this.generateWidgetReadme(widget, files);
|
|
213
|
+
const readmeFile = this.createLocalFile(widgetPath, 'README.md', readmeContent, 'documentation', 'md');
|
|
214
|
+
files.push(readmeFile);
|
|
215
|
+
// Create artifact record
|
|
216
|
+
const artifact = {
|
|
217
|
+
sys_id: widget.sys_id,
|
|
218
|
+
name: widget.name,
|
|
219
|
+
type: 'widget',
|
|
220
|
+
tableName: 'sp_widget', // Added missing tableName
|
|
221
|
+
localPath: widgetPath,
|
|
222
|
+
files: files,
|
|
223
|
+
metadata: widget,
|
|
224
|
+
syncStatus: 'synced',
|
|
225
|
+
createdAt: new Date(),
|
|
226
|
+
lastSyncedAt: new Date()
|
|
227
|
+
};
|
|
228
|
+
this.artifacts.set(sys_id, artifact);
|
|
229
|
+
console.log(`ā
Widget synced to local files:`);
|
|
230
|
+
console.log(`š Location: ${widgetPath}`);
|
|
231
|
+
console.log(`š Files created:`);
|
|
232
|
+
files.forEach(f => console.log(` - ${f.filename} (${f.type})`));
|
|
233
|
+
console.log(`\nš” Claude Code can now use its native tools on these files!`);
|
|
234
|
+
console.log(` Edit, search, refactor - then run 'pushWidget' to sync back.`);
|
|
235
|
+
return artifact;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* DYNAMIC push local changes back to ServiceNow using artifact registry
|
|
239
|
+
*/
|
|
240
|
+
async pushArtifact(sys_id) {
|
|
241
|
+
const artifact = this.artifacts.get(sys_id);
|
|
242
|
+
if (!artifact) {
|
|
243
|
+
throw new Error(`No local artifact found for ${sys_id}. Run pullArtifact first.`);
|
|
244
|
+
}
|
|
245
|
+
const config = artifact.artifactConfig;
|
|
246
|
+
if (!config) {
|
|
247
|
+
throw new Error(`No configuration found for artifact ${sys_id}`);
|
|
248
|
+
}
|
|
249
|
+
console.log(`\nš Pushing local changes back to ServiceNow...`);
|
|
250
|
+
console.log(` Type: ${config.displayName}`);
|
|
251
|
+
console.log(` Table: ${config.tableName}`);
|
|
252
|
+
// Read current content from all files
|
|
253
|
+
const updates = {};
|
|
254
|
+
let hasChanges = false;
|
|
255
|
+
const validationResults = [];
|
|
256
|
+
for (const file of artifact.files) {
|
|
257
|
+
if (fs.existsSync(file.path) && file.fieldMapping) {
|
|
258
|
+
const currentContent = fs.readFileSync(file.path, 'utf8');
|
|
259
|
+
// Strip our added headers/footers for comparison
|
|
260
|
+
const cleanContent = this.stripAddedWrappers(currentContent, file.type, file.fieldMapping);
|
|
261
|
+
if (cleanContent !== file.originalContent) {
|
|
262
|
+
hasChanges = true;
|
|
263
|
+
file.currentContent = cleanContent;
|
|
264
|
+
file.isModified = true;
|
|
265
|
+
// Apply postprocessor if defined
|
|
266
|
+
let processedContent = cleanContent;
|
|
267
|
+
if (file.fieldMapping.postprocessor) {
|
|
268
|
+
processedContent = file.fieldMapping.postprocessor(processedContent);
|
|
269
|
+
}
|
|
270
|
+
// Map back to ServiceNow field
|
|
271
|
+
if (file.field && file.field !== 'documentation' && file.field !== 'metadata') {
|
|
272
|
+
updates[file.field] = processedContent;
|
|
273
|
+
console.log(` š Changed: ${file.filename} (${file.field})`);
|
|
274
|
+
// Validate ES5 if required
|
|
275
|
+
if (file.fieldMapping.validateES5) {
|
|
276
|
+
const es5Issues = this.validateES5(processedContent);
|
|
277
|
+
if (es5Issues.length > 0) {
|
|
278
|
+
validationResults.push({
|
|
279
|
+
valid: false,
|
|
280
|
+
errors: es5Issues.map(issue => `${file.field}: ${issue}`),
|
|
281
|
+
warnings: [],
|
|
282
|
+
hints: []
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!hasChanges) {
|
|
291
|
+
console.log(`ā
No changes detected. ${config.displayName} is up to date.`);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
// Run coherence validation if defined
|
|
295
|
+
if (config.coherenceRules) {
|
|
296
|
+
const fileContents = new Map();
|
|
297
|
+
for (const file of artifact.files) {
|
|
298
|
+
if (file.field && file.currentContent) {
|
|
299
|
+
fileContents.set(file.field, file.currentContent);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const rule of config.coherenceRules) {
|
|
303
|
+
const result = rule.validate(fileContents);
|
|
304
|
+
if (!result.valid) {
|
|
305
|
+
validationResults.push(result);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Show validation issues
|
|
310
|
+
if (validationResults.length > 0) {
|
|
311
|
+
console.log(`\nā ļø Validation Issues Found:`);
|
|
312
|
+
for (const result of validationResults) {
|
|
313
|
+
result.errors.forEach(err => console.log(` ā ${err}`));
|
|
314
|
+
result.warnings.forEach(warn => console.log(` ā ļø ${warn}`));
|
|
315
|
+
result.hints.forEach(hint => console.log(` š” ${hint}`));
|
|
316
|
+
}
|
|
317
|
+
console.log(`\nā Continue with deployment anyway? (Issues might cause runtime errors)`);
|
|
318
|
+
// In real implementation, prompt for confirmation
|
|
319
|
+
}
|
|
320
|
+
// Update in ServiceNow
|
|
321
|
+
try {
|
|
322
|
+
console.log(`\nš¤ Updating ${config.displayName} in ServiceNow...`);
|
|
323
|
+
await this.client.update(config.tableName, sys_id, updates);
|
|
324
|
+
artifact.syncStatus = 'synced';
|
|
325
|
+
artifact.lastSyncedAt = new Date();
|
|
326
|
+
console.log(`ā
${config.displayName} successfully updated in ServiceNow!`);
|
|
327
|
+
console.log(`š sys_id: ${sys_id}`);
|
|
328
|
+
console.log(`š All changes have been deployed!`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error(`ā Failed to update ${config.displayName}:`, error);
|
|
333
|
+
artifact.syncStatus = 'pending_upload';
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Push local changes back to ServiceNow
|
|
339
|
+
* (Wrapper for backward compatibility)
|
|
340
|
+
*/
|
|
341
|
+
async pushWidget(sys_id) {
|
|
342
|
+
return this.pushArtifact(sys_id);
|
|
343
|
+
const artifact = this.artifacts.get(sys_id);
|
|
344
|
+
if (!artifact) {
|
|
345
|
+
throw new Error(`No local artifact found for ${sys_id}. Run pullWidget first.`);
|
|
346
|
+
}
|
|
347
|
+
console.log(`\nš Pushing local changes back to ServiceNow...`);
|
|
348
|
+
// Read current content from all files
|
|
349
|
+
const updates = {};
|
|
350
|
+
let hasChanges = false;
|
|
351
|
+
for (const file of artifact.files) {
|
|
352
|
+
if (fs.existsSync(file.path)) {
|
|
353
|
+
const currentContent = fs.readFileSync(file.path, 'utf8');
|
|
354
|
+
// Strip our added headers/footers for comparison
|
|
355
|
+
const cleanContent = this.stripAddedWrappers(currentContent, file.type);
|
|
356
|
+
if (cleanContent !== file.originalContent) {
|
|
357
|
+
hasChanges = true;
|
|
358
|
+
file.currentContent = cleanContent;
|
|
359
|
+
file.isModified = true;
|
|
360
|
+
// Map back to ServiceNow field
|
|
361
|
+
if (file.field && file.field !== 'documentation' && file.field !== 'metadata') {
|
|
362
|
+
updates[file.field] = cleanContent;
|
|
363
|
+
console.log(` š Changed: ${file.filename} (${file.field})`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!hasChanges) {
|
|
369
|
+
console.log(`ā
No changes detected. Widget is up to date.`);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
// Validate ES5 compliance for server script
|
|
373
|
+
if (updates.script) {
|
|
374
|
+
const es5Issues = this.validateES5(updates.script);
|
|
375
|
+
if (es5Issues.length > 0) {
|
|
376
|
+
console.log(`\nā ļø ES5 Validation Issues in server script:`);
|
|
377
|
+
es5Issues.forEach(issue => console.log(` - ${issue}`));
|
|
378
|
+
console.log(`\nā Continue with deployment anyway? (ServiceNow might fail)`);
|
|
379
|
+
// In real implementation, prompt for confirmation
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Update in ServiceNow
|
|
383
|
+
try {
|
|
384
|
+
console.log(`\nš¤ Updating widget in ServiceNow...`);
|
|
385
|
+
await this.client.update('sp_widget', sys_id, updates);
|
|
386
|
+
artifact.syncStatus = 'synced';
|
|
387
|
+
artifact.lastSyncedAt = new Date();
|
|
388
|
+
console.log(`ā
Widget successfully updated in ServiceNow!`);
|
|
389
|
+
console.log(`š sys_id: ${sys_id}`);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error(`ā Failed to update widget:`, error);
|
|
394
|
+
artifact.syncStatus = 'pending_upload';
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Clean up local files after successful sync
|
|
400
|
+
*/
|
|
401
|
+
async cleanup(sys_id, force = false) {
|
|
402
|
+
const artifact = this.artifacts.get(sys_id);
|
|
403
|
+
if (!artifact)
|
|
404
|
+
return;
|
|
405
|
+
if (!force && artifact.syncStatus !== 'synced') {
|
|
406
|
+
console.log(`ā ļø Cannot cleanup - artifact has unsaved changes. Use force=true to override.`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.log(`\nš§¹ Cleaning up local files for ${artifact.name}...`);
|
|
410
|
+
if (fs.existsSync(artifact.localPath)) {
|
|
411
|
+
fs.rmSync(artifact.localPath, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
this.artifacts.delete(sys_id);
|
|
414
|
+
console.log(`ā
Local files removed.`);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Create a local file with appropriate headers
|
|
418
|
+
*/
|
|
419
|
+
createLocalFile(dirPath, filename, content, field, type, header = '', footer = '') {
|
|
420
|
+
const filePath = path.join(dirPath, filename);
|
|
421
|
+
const fullContent = header + content + footer;
|
|
422
|
+
fs.writeFileSync(filePath, fullContent, 'utf8');
|
|
423
|
+
return {
|
|
424
|
+
filename,
|
|
425
|
+
path: filePath,
|
|
426
|
+
field,
|
|
427
|
+
type: type,
|
|
428
|
+
originalContent: content,
|
|
429
|
+
isModified: false
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Generate README with artifact-specific context using registry
|
|
434
|
+
*/
|
|
435
|
+
generateArtifactReadme(config, artifact, files) {
|
|
436
|
+
const header = `# ServiceNow ${config.displayName}: ${artifact[config.identifierField] || 'Unnamed'}
|
|
437
|
+
|
|
438
|
+
## Overview
|
|
439
|
+
- **Type**: ${config.displayName}
|
|
440
|
+
- **Table**: ${config.tableName}
|
|
441
|
+
- **sys_id**: ${artifact.sys_id}
|
|
442
|
+
- **Created**: ${artifact.sys_created_on || 'Unknown'}
|
|
443
|
+
- **Updated**: ${artifact.sys_updated_on || 'Unknown'}
|
|
444
|
+
`;
|
|
445
|
+
const fileSection = `
|
|
446
|
+
## Files
|
|
447
|
+
${files.map(f => `- **${f.filename}** - ${f.fieldMapping?.description || f.field}`).join('\n')}
|
|
448
|
+
`;
|
|
449
|
+
// Add coherence rules if defined
|
|
450
|
+
let coherenceSection = '';
|
|
451
|
+
if (config.coherenceRules && config.coherenceRules.length > 0) {
|
|
452
|
+
coherenceSection = `
|
|
453
|
+
## Validation Rules
|
|
454
|
+
${config.coherenceRules.map(rule => `### ${rule.name}\n${rule.description}`).join('\n\n')}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
// Add artifact-specific documentation
|
|
458
|
+
const docSection = config.documentation || '';
|
|
459
|
+
// Add editing instructions
|
|
460
|
+
const instructions = `
|
|
461
|
+
## Editing Instructions
|
|
462
|
+
|
|
463
|
+
1. **Edit files** using Claude Code's native tools
|
|
464
|
+
2. **Maintain coherence** between related files
|
|
465
|
+
${config.fieldMappings.some(fm => fm.validateES5) ? '3. **Use ES5 only** in server-side scripts (no modern JavaScript)' : ''}
|
|
466
|
+
4. **Test locally** if possible
|
|
467
|
+
5. **Run pushArtifact** to sync changes back to ServiceNow
|
|
468
|
+
|
|
469
|
+
## Commands
|
|
470
|
+
|
|
471
|
+
\`\`\`bash
|
|
472
|
+
# Push changes back to ServiceNow
|
|
473
|
+
snow-flow sync push-artifact ${artifact.sys_id}
|
|
474
|
+
|
|
475
|
+
# Cleanup local files (after sync)
|
|
476
|
+
snow-flow sync cleanup ${artifact.sys_id}
|
|
477
|
+
|
|
478
|
+
# Check sync status
|
|
479
|
+
snow-flow sync status ${artifact.sys_id}
|
|
480
|
+
\`\`\`
|
|
481
|
+
`;
|
|
482
|
+
return header + fileSection + coherenceSection + docSection + instructions;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Replace placeholders in wrapper strings
|
|
486
|
+
*/
|
|
487
|
+
replacePlaceholders(template, data) {
|
|
488
|
+
if (!template)
|
|
489
|
+
return '';
|
|
490
|
+
return template
|
|
491
|
+
.replace(/\{name\}/g, data.name || '')
|
|
492
|
+
.replace(/\{api_name\}/g, data.api_name || '')
|
|
493
|
+
.replace(/\{title\}/g, data.title || '')
|
|
494
|
+
.replace(/\{collection\}/g, data.collection || data.table || '')
|
|
495
|
+
.replace(/\{when\}/g, data.when || '')
|
|
496
|
+
.replace(/\{order\}/g, data.order || '')
|
|
497
|
+
.replace(/\{type\}/g, data.type || '')
|
|
498
|
+
.replace(/\{client_callable\}/g, data.client_callable ? 'Client Callable' : 'Server Only')
|
|
499
|
+
.replace(/\{run_as\}/g, data.run_as || '')
|
|
500
|
+
.replace(/\{time_zone\}/g, data.time_zone || '')
|
|
501
|
+
.replace(/\{table\}/g, data.table || '')
|
|
502
|
+
.replace(/\{short_description\}/g, data.short_description || '');
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Generate README with widget context
|
|
506
|
+
* (Wrapper for backward compatibility)
|
|
507
|
+
*/
|
|
508
|
+
generateWidgetReadme(widget, files) {
|
|
509
|
+
const config = (0, artifact_registry_1.getArtifactConfig)('sp_widget');
|
|
510
|
+
if (!config) {
|
|
511
|
+
throw new Error('Widget configuration not found in registry');
|
|
512
|
+
}
|
|
513
|
+
return this.generateArtifactReadme(config, widget, files);
|
|
514
|
+
return `# ServiceNow Widget: ${widget.name}
|
|
515
|
+
|
|
516
|
+
## Overview
|
|
517
|
+
- **sys_id**: ${widget.sys_id}
|
|
518
|
+
- **Title**: ${widget.title || 'N/A'}
|
|
519
|
+
- **Created**: ${widget.sys_created_on}
|
|
520
|
+
- **Updated**: ${widget.sys_updated_on}
|
|
521
|
+
|
|
522
|
+
## Files
|
|
523
|
+
${files.map(f => `- **${f.filename}** - ${f.field}`).join('\n')}
|
|
524
|
+
|
|
525
|
+
## Widget Coherence Rules
|
|
526
|
+
|
|
527
|
+
### Template (HTML)
|
|
528
|
+
- References \`{{data.propertyName}}\` from server script
|
|
529
|
+
- Calls methods via \`ng-click="methodName()"\` from client script
|
|
530
|
+
- Uses CSS classes defined in the CSS file
|
|
531
|
+
|
|
532
|
+
### Server Script (ES5 ONLY!)
|
|
533
|
+
- Initializes all \`data.*\` properties referenced in template
|
|
534
|
+
- Handles all \`input.action\` requests from client
|
|
535
|
+
- Must use ES5 syntax (no const/let/arrow functions)
|
|
536
|
+
|
|
537
|
+
### Client Script (AngularJS)
|
|
538
|
+
- Implements all methods called by template ng-click
|
|
539
|
+
- Uses \`c.server.get({action: 'name'})\` to call server
|
|
540
|
+
- Updates \`c.data\` when server responds
|
|
541
|
+
|
|
542
|
+
### CSS
|
|
543
|
+
- Defines all classes used in template
|
|
544
|
+
- Should be prefixed to avoid conflicts
|
|
545
|
+
|
|
546
|
+
## Editing Instructions
|
|
547
|
+
|
|
548
|
+
1. **Edit files** using Claude Code's native tools
|
|
549
|
+
2. **Maintain coherence** between template, scripts, and CSS
|
|
550
|
+
3. **Use ES5 only** in server script (no modern JavaScript)
|
|
551
|
+
4. **Test locally** if possible
|
|
552
|
+
5. **Run pushWidget** to sync changes back to ServiceNow
|
|
553
|
+
|
|
554
|
+
## Coherence Hints
|
|
555
|
+
${widget._coherence_hints?.map((h) => `- ${h}`).join('\n') || 'No automatic hints detected'}
|
|
556
|
+
|
|
557
|
+
## Commands
|
|
558
|
+
|
|
559
|
+
\`\`\`bash
|
|
560
|
+
# Push changes back to ServiceNow
|
|
561
|
+
snow-flow sync push-widget ${widget.sys_id}
|
|
562
|
+
|
|
563
|
+
# Cleanup local files (after sync)
|
|
564
|
+
snow-flow sync cleanup ${widget.sys_id}
|
|
565
|
+
|
|
566
|
+
# Check sync status
|
|
567
|
+
snow-flow sync status ${widget.sys_id}
|
|
568
|
+
\`\`\`
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Validate ES5 compliance
|
|
573
|
+
*/
|
|
574
|
+
validateES5(script) {
|
|
575
|
+
const issues = [];
|
|
576
|
+
// Check for ES6+ syntax
|
|
577
|
+
if (/\bconst\s+/.test(script))
|
|
578
|
+
issues.push('Uses "const" - use "var" instead');
|
|
579
|
+
if (/\blet\s+/.test(script))
|
|
580
|
+
issues.push('Uses "let" - use "var" instead');
|
|
581
|
+
if (/=>\s*{/.test(script))
|
|
582
|
+
issues.push('Uses arrow functions - use function() instead');
|
|
583
|
+
if (/`[^`]*\$\{[^}]*\}[^`]*`/.test(script))
|
|
584
|
+
issues.push('Uses template literals - use string concatenation');
|
|
585
|
+
if (/\.\.\.\w+/.test(script))
|
|
586
|
+
issues.push('Uses spread operator - not supported in ES5');
|
|
587
|
+
if (/class\s+\w+/.test(script))
|
|
588
|
+
issues.push('Uses ES6 classes - use function constructors');
|
|
589
|
+
if (/async\s+function/.test(script))
|
|
590
|
+
issues.push('Uses async/await - use callbacks');
|
|
591
|
+
if (/\bfor\s*\(\s*(?:const|let)\s+\w+\s+of\s+/.test(script))
|
|
592
|
+
issues.push('Uses for...of - use traditional for loop');
|
|
593
|
+
return issues;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Strip headers/footers we added - now uses field mapping for accuracy
|
|
597
|
+
*/
|
|
598
|
+
stripAddedWrappers(content, type, fieldMapping) {
|
|
599
|
+
// Remove our added comments and wrappers
|
|
600
|
+
let cleaned = content;
|
|
601
|
+
if (type === 'js') {
|
|
602
|
+
// Remove wrapper function if we added it
|
|
603
|
+
cleaned = cleaned.replace(/^\(function\(\) \{\n/, '');
|
|
604
|
+
cleaned = cleaned.replace(/\n\}\)\(\);$/, '');
|
|
605
|
+
// Remove our header comments
|
|
606
|
+
cleaned = cleaned.replace(/^\/\*\*[\s\S]*?\*\/\n\n/, '');
|
|
607
|
+
cleaned = cleaned.replace(/^function\(/, 'function(');
|
|
608
|
+
}
|
|
609
|
+
else if (type === 'html') {
|
|
610
|
+
// Remove our HTML comments
|
|
611
|
+
cleaned = cleaned.replace(/^<!-- ServiceNow Widget Template -->[\s\S]*?-->\n\n/, '');
|
|
612
|
+
}
|
|
613
|
+
else if (type === 'css') {
|
|
614
|
+
// Remove our CSS comments
|
|
615
|
+
cleaned = cleaned.replace(/^\/\* ServiceNow Widget Styles \*\/[\s\S]*?\*\/\n\n/, '');
|
|
616
|
+
}
|
|
617
|
+
return cleaned.trim();
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Sanitize filename for filesystem
|
|
621
|
+
*/
|
|
622
|
+
sanitizeFilename(name) {
|
|
623
|
+
return name
|
|
624
|
+
.toLowerCase()
|
|
625
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
626
|
+
.replace(/_+/g, '_')
|
|
627
|
+
.substring(0, 50);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* List all local artifacts
|
|
631
|
+
*/
|
|
632
|
+
listLocalArtifacts() {
|
|
633
|
+
return Array.from(this.artifacts.values());
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Get sync status for an artifact
|
|
637
|
+
*/
|
|
638
|
+
getSyncStatus(sys_id) {
|
|
639
|
+
const artifact = this.artifacts.get(sys_id);
|
|
640
|
+
return artifact ? artifact.syncStatus : 'not_synced';
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Pull any supported artifact type by detecting table from sys_id
|
|
644
|
+
*/
|
|
645
|
+
async pullArtifactBySysId(sys_id) {
|
|
646
|
+
// Try to detect table by querying common tables
|
|
647
|
+
const tables = Object.keys(artifact_registry_1.ARTIFACT_REGISTRY);
|
|
648
|
+
for (const table of tables) {
|
|
649
|
+
try {
|
|
650
|
+
const response = await this.client.query(table, {
|
|
651
|
+
query: `sys_id=${sys_id}`,
|
|
652
|
+
fields: ['sys_id'],
|
|
653
|
+
limit: 1
|
|
654
|
+
});
|
|
655
|
+
if (response.result?.[0]) {
|
|
656
|
+
console.log(`š Found artifact in table: ${table}`);
|
|
657
|
+
return this.pullArtifact(table, sys_id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
// Table might not exist, continue searching
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
throw new Error(`Could not find artifact with sys_id ${sys_id} in any supported table`);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get supported artifact types
|
|
668
|
+
*/
|
|
669
|
+
getSupportedTypes() {
|
|
670
|
+
return Object.keys(artifact_registry_1.ARTIFACT_REGISTRY);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Validate coherence for an artifact
|
|
674
|
+
*/
|
|
675
|
+
async validateArtifactCoherence(sys_id) {
|
|
676
|
+
const artifact = this.artifacts.get(sys_id);
|
|
677
|
+
if (!artifact || !artifact.artifactConfig) {
|
|
678
|
+
throw new Error(`No local artifact found for ${sys_id}`);
|
|
679
|
+
}
|
|
680
|
+
const config = artifact.artifactConfig;
|
|
681
|
+
const results = [];
|
|
682
|
+
if (config.coherenceRules) {
|
|
683
|
+
const fileContents = new Map();
|
|
684
|
+
for (const file of artifact.files) {
|
|
685
|
+
if (file.field && fs.existsSync(file.path)) {
|
|
686
|
+
const content = fs.readFileSync(file.path, 'utf8');
|
|
687
|
+
const cleanContent = this.stripAddedWrappers(content, file.type, file.fieldMapping);
|
|
688
|
+
fileContents.set(file.field, cleanContent);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const rule of config.coherenceRules) {
|
|
692
|
+
const result = rule.validate(fileContents);
|
|
693
|
+
results.push(result);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Custom validation if defined
|
|
697
|
+
if (config.customValidation) {
|
|
698
|
+
const customResult = config.customValidation(artifact.metadata);
|
|
699
|
+
results.push(customResult);
|
|
700
|
+
}
|
|
701
|
+
return results;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
exports.ArtifactLocalSync = ArtifactLocalSync;
|
|
705
|
+
//# sourceMappingURL=artifact-local-sync.js.map
|