vro-action-extractor 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mayank Goyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # vro-action-extractor
2
+
3
+ A Universal CLI tool to extract Scriptable Actions from vRealize Orchestrator (vRO) / VMware Aria Automation Orchestrator environments. It converts Workflows, Packages, and Folder structures into clean JavaScript files with automated JSDoc generation.
4
+
5
+ ## Features
6
+
7
+ - **Extract Actions**: Pulls code from "Scriptable task" items.
8
+ - **Auto JSDoc**: Generates inputs/outputs/description headers automatically.
9
+ - **Universal Import**: Supports multiple source formats:
10
+ - **.package Files**: Direct extraction from vRO exports (Zip).
11
+ - **Directory Structures**: Recursively scans folders (e.g., from a Git repo).
12
+ - **Flat XMLs**: Individual workflow files.
13
+ - **Smart Naming**: Uses display names for coherent file naming.
14
+ - **Conflict Resolution**: Handles duplicate action names automatically.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install -g vro-action-extractor
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The tool exposes the `vro-extract` command:
25
+
26
+ ### 1. Extract from a Package File
27
+
28
+ ```bash
29
+ vro-extract "path/to/my-package.package"
30
+ ```
31
+ *Creates an `Extracted_Actions` folder with organized subfolders.*
32
+
33
+ ### 2. Extract from a Directory (e.g., Git Repo)
34
+
35
+ ```bash
36
+ vro-extract "path/to/project_root"
37
+ ```
38
+ *Scans for all valid `data` and `.xml` files recursively.*
39
+
40
+ ### 3. Extract from Single XML
41
+
42
+ ```bash
43
+ vro-extract "path/to/workflow.xml" [output_directory]
44
+ ```
45
+
46
+ ## Example output
47
+
48
+ ```
49
+ Extracted_Actions/
50
+ ├── My_Workflow/
51
+ │ ├── Validate_Inputs.js
52
+ │ └── Call_API.js
53
+ └── Another_Workflow/
54
+ └── Send_Notification.js
55
+ ```
56
+
57
+ ## License
58
+
59
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "vro-action-extractor",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to extract Scriptable Actions from vRO Workflows (.xml), Packages (.package), and Folder structures (data).",
5
+ "main": "vro-action-extractor.js",
6
+ "bin": {
7
+ "vro-extract": "vro-action-extractor.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "vro",
14
+ "vrealize-orchestrator",
15
+ "workflow",
16
+ "converter",
17
+ "xml",
18
+ "javascript"
19
+ ],
20
+ "author": "Mayank Goyal <mayankgoyalmax@gmail.com>",
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "dependencies": {
24
+ "adm-zip": "^0.5.16",
25
+ "xml2js": "^0.6.2"
26
+ }
27
+ }
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from 'node:fs';
3
+ import { parseStringPromise } from 'xml2js';
4
+ import * as path from 'node:path';
5
+ import AdmZip from 'adm-zip';
6
+
7
+ // Helper to clean up file names
8
+ function sanitizeFilename(name) {
9
+ return name.replace(/[^a-zA-Z0-9\s-_]/g, '_').replace(/\s+/g, '_');
10
+ }
11
+
12
+ async function parseXmlBuffer(buffer) {
13
+ let xml = '';
14
+
15
+ // Simple BOM detection
16
+ if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
17
+ // UTF-16BE
18
+ const decoder = new TextDecoder('utf-16be');
19
+ xml = decoder.decode(buffer);
20
+ } else if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
21
+ // UTF-16LE
22
+ const decoder = new TextDecoder('utf-16le');
23
+ xml = decoder.decode(buffer);
24
+ } else {
25
+ // Default to UTF-8
26
+ xml = buffer.toString('utf8');
27
+ }
28
+
29
+ return parseStringPromise(xml, { explicitArray: false, mergeAttrs: true, explicitCharkey: true });
30
+ }
31
+
32
+ async function loadXml(file) {
33
+ const buffer = await fs.readFile(file);
34
+ return parseXmlBuffer(buffer);
35
+ }
36
+
37
+ function generateJSDoc(name, inputs, outputs, description) {
38
+ const lines = ['/**'];
39
+ lines.push(` * ${(description || name).trim()}`);
40
+ lines.push(' *');
41
+
42
+ if (inputs && inputs.length > 0) {
43
+ inputs.forEach(input => {
44
+ const type = input.type || 'Any';
45
+ const paramName = input.name || input['export-name'];
46
+ const desc = input.description ? ` - ${input.description}` : '';
47
+ lines.push(` * @param {${type}} ${paramName}${desc}`);
48
+ });
49
+ }
50
+
51
+ if (outputs && outputs.length > 0) {
52
+ outputs.forEach(output => {
53
+ const type = output.type || 'Any';
54
+ const paramName = output.name || output['export-name'];
55
+ const desc = output.description ? ` - ${output.description}` : '';
56
+ lines.push(` * @return {${type}} ${paramName}${desc}`);
57
+ });
58
+ }
59
+
60
+ lines.push(' */');
61
+ return lines.join('\n');
62
+ }
63
+
64
+ function extractScriptData(item) {
65
+ // Check if it is a task and has a script
66
+ if (item.type !== 'task' || !item.script) {
67
+ return null;
68
+ }
69
+
70
+ // Prepare inputs/outputs
71
+ const inBindings = item['in-binding']?.bind || [];
72
+ const inputs = Array.isArray(inBindings) ? inBindings : [inBindings];
73
+
74
+ const outBindings = item['out-binding']?.bind || [];
75
+ const outputs = Array.isArray(outBindings) ? outBindings : [outBindings];
76
+
77
+ // Script content often inside <script ...> ... </script>.
78
+ // In xml2js with explicitCharkey:true, text content is in ._
79
+ let code = item.script._ || '';
80
+
81
+ // Remove CDATA markers if present (xml2js usually handles this but good to be safe)
82
+ code = code.replace(/^<!\[CDATA\[|\]\]>$/g, '');
83
+
84
+ return {
85
+ name: item.name || item['display-name']?._ || 'unknown_task',
86
+ displayName: item['display-name']?._ || item.name,
87
+ description: item.description?._ || '',
88
+ code: code,
89
+ inputs: inputs.filter(i => i && i.name),
90
+ outputs: outputs.filter(i => i && i.name)
91
+ };
92
+ }
93
+
94
+ // Core logic to extract actions from a loaded Workflow Object
95
+ async function processWorkflowObj(workflowObj, outputDir) {
96
+ const createdFiles = new Set();
97
+
98
+ // Find workflow root
99
+ const rootKey = Object.keys(workflowObj)[0];
100
+ const root = workflowObj[rootKey];
101
+
102
+ // Items can be workflow-item or workflowItem depending on version/parser strictness?
103
+ // Usually workflow-item.
104
+ const rawItems = root['workflow-item'] ?? root['workflowItem'] ?? [];
105
+ const items = Array.isArray(rawItems) ? rawItems : [rawItems];
106
+
107
+ let count = 0;
108
+ let dirCreated = false;
109
+
110
+ for (const item of items) {
111
+ const scriptData = extractScriptData(item);
112
+ if (scriptData && scriptData.code.trim()) {
113
+ const jsDoc = generateJSDoc(scriptData.displayName, scriptData.inputs, scriptData.outputs, scriptData.description);
114
+ const fileContent = `${jsDoc}\n${scriptData.code}`;
115
+
116
+ const saneName = sanitizeFilename(scriptData.displayName || scriptData.name);
117
+ let outFileName = `${saneName}.js`;
118
+
119
+ // Handle collisions (local to this workflow processing)
120
+ let collisionCount = 1;
121
+ while (createdFiles.has(outFileName)) {
122
+ outFileName = `${saneName}_${collisionCount}.js`;
123
+ collisionCount++;
124
+ }
125
+ createdFiles.add(outFileName);
126
+
127
+ const outPath = path.join(outputDir, outFileName);
128
+
129
+ if (!dirCreated) {
130
+ try {
131
+ await fs.access(outputDir);
132
+ } catch {
133
+ await fs.mkdir(outputDir, { recursive: true });
134
+ }
135
+ dirCreated = true;
136
+ }
137
+
138
+ await fs.writeFile(outPath, fileContent, 'utf8');
139
+ console.log(`Extracted: ${outFileName}`);
140
+ count++;
141
+ }
142
+ }
143
+ return count;
144
+ }
145
+
146
+ // Convert a single XML file (standard mode)
147
+ async function convertWorkflow(filePath, outputDir) {
148
+ console.log(`Processing file: ${filePath}`);
149
+ try {
150
+ const workflowObj = await loadXml(filePath);
151
+ const count = await processWorkflowObj(workflowObj, outputDir);
152
+ console.log(`Done. Extracted ${count} scripts.`);
153
+ } catch (err) {
154
+ console.error("Error converting workflow:", err);
155
+ }
156
+ }
157
+
158
+ // Convert a .package (zip) file
159
+ async function convertPackage(filePath) {
160
+ console.log(`Processing Package: ${filePath} ...`);
161
+ try {
162
+ // Zip processing is synchronous in adm-zip usually, but we can treat buffers.
163
+ const zip = new AdmZip(filePath);
164
+ const zipEntries = zip.getEntries(); // an array of ZipEntry records
165
+
166
+ let totalScripts = 0;
167
+
168
+ for (const entry of zipEntries) {
169
+ // Looking for elements/[UUID]/data
170
+ // Pattern: elements/UUID/data
171
+ const parts = entry.entryName.split('/');
172
+ // Expecting: elements, UUID, data (3 parts) roughly, or just ending in /data and inside elements
173
+ if (parts.length >= 3 && parts[0] === 'elements' && parts[parts.length - 1] === 'data') {
174
+ try {
175
+ const buffer = entry.getData();
176
+ const workflowObj = await parseXmlBuffer(buffer);
177
+
178
+ // Check if it's a workflow
179
+ const rootKey = Object.keys(workflowObj)[0];
180
+ if (rootKey !== 'workflow') {
181
+ continue; // Not a workflow element (could be resource, etc)
182
+ }
183
+ const workflowRoot = workflowObj['workflow'];
184
+
185
+ // Extract Workflow Name
186
+ // usually <display-name>
187
+ let wfName = workflowRoot['display-name'] || workflowRoot['@']?.['name'] || 'Unnamed_Workflow';
188
+ // Handle CDATA object in xml2js ({ _: "Name" })
189
+ if (typeof wfName === 'object' && wfName._) {
190
+ wfName = wfName._;
191
+ }
192
+
193
+ const saneWfName = sanitizeFilename(wfName);
194
+
195
+ // Output: "a folder on top hierarchy with sub folder naming the workflow name"
196
+ // We'll interpret this as: [Package_Dir]/Extracted_Actions/[Workflow_Name]/
197
+ const packageDir = path.dirname(filePath);
198
+ const outputDir = path.join(packageDir, 'Extracted_Actions', saneWfName);
199
+
200
+ console.log(`Found Workflow in package: "${wfName}". Extracting to: ${outputDir}`);
201
+
202
+ const count = await processWorkflowObj(workflowObj, outputDir);
203
+ totalScripts += count;
204
+ } catch (err) {
205
+ console.error(`Failed to process entry ${entry.entryName}:`, err);
206
+ }
207
+ }
208
+ }
209
+ console.log(`Package processing complete. Total scripts extracted: ${totalScripts}`);
210
+
211
+ } catch (err) {
212
+ console.error("Error processing package:", err);
213
+ }
214
+ }
215
+
216
+ async function fileExists(path) {
217
+ try {
218
+ await fs.access(path);
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ // Recursively find files with a specific extension or specific name
226
+ async function findTargets(dir) {
227
+ let results = [];
228
+ const list = await fs.readdir(dir);
229
+ for (const file of list) {
230
+ const filePath = path.join(dir, file);
231
+ const stat = await fs.stat(filePath);
232
+ if (stat && stat.isDirectory()) {
233
+ results = results.concat(await findTargets(filePath));
234
+ } else {
235
+ // Check for .xml extension OR exact match 'data' OR .package
236
+ if (file.toLowerCase().endsWith('.xml') || file === 'data' || file.toLowerCase().endsWith('.package')) {
237
+ results.push(filePath);
238
+ }
239
+ }
240
+ }
241
+ return results;
242
+ }
243
+
244
+ // CLI Entry Point
245
+ async function main() {
246
+ const args = process.argv.slice(2);
247
+ if (args.length === 0) {
248
+ console.error("Usage: vro-extract <path_to_workflow_xml_or_directory_or_package>");
249
+ process.exit(1);
250
+ }
251
+
252
+ const inputPath = args[0];
253
+ const stat = await fs.stat(inputPath);
254
+
255
+ if (stat.isDirectory()) {
256
+ console.log(`Scanning directory: ${inputPath} ...`);
257
+ const targetFiles = await findTargets(inputPath);
258
+ console.log(`Found ${targetFiles.length} target files (.xml, .package or 'data').`);
259
+
260
+ for (const file of targetFiles) {
261
+ if (file.toLowerCase().endsWith('.package')) {
262
+ await convertPackage(file);
263
+ } else {
264
+ const fileName = path.basename(file);
265
+ let outputDir;
266
+
267
+ if (fileName === 'data') {
268
+ // For package structure: .../elements/[ID]/data
269
+ // We create an 'actions' folder next to the data file
270
+ outputDir = path.join(path.dirname(file), 'actions');
271
+ } else {
272
+ // Standard XML file: .../foo.xml -> .../foo/
273
+ const baseName = path.basename(file, path.extname(file));
274
+ outputDir = path.join(path.dirname(file), baseName);
275
+ }
276
+
277
+ await convertWorkflow(file, outputDir);
278
+ }
279
+ }
280
+ } else {
281
+ // Single file mode
282
+ if (inputPath.toLowerCase().endsWith('.package')) {
283
+ await convertPackage(inputPath);
284
+ } else {
285
+ let outputDir = args[1];
286
+ if (!outputDir) {
287
+ const fileName = path.basename(inputPath);
288
+ if (fileName === 'data') {
289
+ outputDir = path.join(path.dirname(inputPath), 'actions');
290
+ } else {
291
+ const baseName = path.basename(inputPath, path.extname(inputPath));
292
+ outputDir = path.join(path.dirname(inputPath), baseName);
293
+ }
294
+ }
295
+ await convertWorkflow(inputPath, outputDir);
296
+ }
297
+ }
298
+ }
299
+
300
+ main();