@opscompass/skills 0.1.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.
Files changed (38) hide show
  1. package/README.md +78 -0
  2. package/manifest.json +35 -0
  3. package/package.json +23 -0
  4. package/skills/opscompass-oracle-licensing/.opscompass-runtime/tools/cli-entrypoint.js +15 -0
  5. package/skills/opscompass-oracle-licensing/SKILL.md +211 -0
  6. package/skills/opscompass-oracle-licensing/api/entitlement-compliance/entitlement-compliance.json +189 -0
  7. package/skills/opscompass-oracle-licensing/api/entitlement-deployment/entitlement-deployment +26 -0
  8. package/skills/opscompass-oracle-licensing/api/entitlement-usage/entitlement-usage.json +114 -0
  9. package/skills/opscompass-oracle-licensing/assets/mock-entitlements/deployment-details.json +61 -0
  10. package/skills/opscompass-oracle-licensing/assets/mock-entitlements/license-usage-map.json +70 -0
  11. package/skills/opscompass-oracle-licensing/assets/mock-entitlements/overall-compliance.json +36 -0
  12. package/skills/opscompass-oracle-licensing/package.json +3 -0
  13. package/skills/opscompass-oracle-licensing/references/evidence-model.md +90 -0
  14. package/skills/opscompass-oracle-licensing/references/report-structure.md +54 -0
  15. package/skills/opscompass-oracle-licensing/scripts/fake-entitlements-api.tool.js +31 -0
  16. package/skills/opscompass-oracle-licensing/scripts/fake_entitlements_api.js +72 -0
  17. package/skills/opscompass-oracle-licensing/scripts/tools.js +2 -0
  18. package/skills/opscompass-resources/.opscompass-runtime/tools/cli-entrypoint.js +15 -0
  19. package/skills/opscompass-resources/.opscompass-runtime/tools/index.js +2 -0
  20. package/skills/opscompass-resources/.opscompass-runtime/tools/opsc-cli-context.js +24 -0
  21. package/skills/opscompass-resources/.opscompass-runtime/tools/opsc-cli-defaults.js +125 -0
  22. package/skills/opscompass-resources/.opscompass-runtime/tools/opsc-cli.js +90 -0
  23. package/skills/opscompass-resources/SKILL.md +335 -0
  24. package/skills/opscompass-resources/package.json +3 -0
  25. package/skills/opscompass-resources/references/resources-commands.md +178 -0
  26. package/skills/opscompass-resources/scripts/build-resources-command.tool.js +53 -0
  27. package/skills/opscompass-resources/scripts/build_resources_command.js +240 -0
  28. package/skills/opscompass-resources/scripts/list-and-fetch-latest-configs.tool.js +116 -0
  29. package/skills/opscompass-resources/scripts/list_and_fetch_latest_configs.js +714 -0
  30. package/skills/opscompass-resources/scripts/map-oracle-workloads-to-platform-cpus.tool.js +24 -0
  31. package/skills/opscompass-resources/scripts/map_oracle_workloads_to_platform_cpus.js +273 -0
  32. package/skills/opscompass-resources/scripts/opscompass-defaults.tool.js +80 -0
  33. package/skills/opscompass-resources/scripts/opscompass-resources.tool.js +51 -0
  34. package/skills/opscompass-resources/scripts/summarize-latest-configuration.tool.js +24 -0
  35. package/skills/opscompass-resources/scripts/summarize-resources-list.tool.js +24 -0
  36. package/skills/opscompass-resources/scripts/summarize_latest_configuration.js +254 -0
  37. package/skills/opscompass-resources/scripts/summarize_resources_list.js +126 -0
  38. package/skills/opscompass-resources/scripts/tools.js +16 -0
@@ -0,0 +1,714 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { isMainModule } from "../.opscompass-runtime/tools/cli-entrypoint.js";
5
+ import { resolveCliContext } from "../.opscompass-runtime/tools/opsc-cli-context.js";
6
+ import * as brc from "./build_resources_command.js";
7
+ import * as moc from "./map_oracle_workloads_to_platform_cpus.js";
8
+ import * as slc from "./summarize_latest_configuration.js";
9
+ function recordOf(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value)
11
+ ? value
12
+ : {};
13
+ }
14
+ function errorMessage(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function runJsonCommand(command, verbose, logger, execution) {
18
+ return new Promise((resolve, reject) => {
19
+ if (execution.signal?.aborted) {
20
+ reject(new Error("Opscompass command aborted."));
21
+ return;
22
+ }
23
+ if (verbose)
24
+ logger.error(`$ ${command.join(" ")}`);
25
+ let stdout = "", stderr = "";
26
+ const child = spawn(execution.context.cliEntryPoint, command.slice(1), {
27
+ shell: false,
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ cwd: execution.workspaceRoot,
30
+ env: execution.env,
31
+ signal: execution.signal,
32
+ detached: process.platform !== "win32",
33
+ });
34
+ let graceTimer;
35
+ const kill = () => {
36
+ try {
37
+ if (process.platform !== "win32" && child.pid)
38
+ process.kill(-child.pid, "SIGTERM");
39
+ else
40
+ child.kill("SIGTERM");
41
+ }
42
+ catch {
43
+ // The process may already have exited.
44
+ }
45
+ if (graceTimer === undefined) {
46
+ graceTimer = setTimeout(() => {
47
+ try {
48
+ if (process.platform !== "win32" && child.pid)
49
+ process.kill(-child.pid, "SIGKILL");
50
+ else
51
+ child.kill("SIGKILL");
52
+ }
53
+ catch {
54
+ // The process group may already have exited.
55
+ }
56
+ }, 5_000);
57
+ }
58
+ };
59
+ execution.signal?.addEventListener("abort", kill, { once: true });
60
+ child.stdout.on("data", (chunk) => {
61
+ stdout += chunk;
62
+ });
63
+ child.stderr.on("data", (chunk) => {
64
+ stderr += chunk;
65
+ });
66
+ child.once("error", (error) => {
67
+ // AbortError is emitted before close; wait until the child has stopped.
68
+ if (!execution.signal?.aborted)
69
+ reject(error);
70
+ });
71
+ child.once("close", (code) => {
72
+ clearTimeout(graceTimer);
73
+ execution.signal?.removeEventListener("abort", kill);
74
+ if (execution.signal?.aborted)
75
+ return reject(new Error("Opscompass command aborted."));
76
+ if (code !== 0)
77
+ return reject(new Error(`command failed (${code}): ${command.join(" ")}\n${stderr.trim()}`));
78
+ try {
79
+ resolve(JSON.parse(stdout));
80
+ }
81
+ catch (error) {
82
+ reject(new Error(`command did not return valid JSON: ${command.join(" ")}\n${errorMessage(error)}\nstdout:\n${stdout.slice(0, 2000)}`));
83
+ }
84
+ });
85
+ });
86
+ }
87
+ function saveJson(filePath, payload, workspaceRoot) {
88
+ if (!filePath)
89
+ return;
90
+ filePath = path.resolve(workspaceRoot, filePath);
91
+ mkdirSync(path.dirname(filePath), { recursive: true });
92
+ writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
93
+ }
94
+ function saveText(filePath, text, workspaceRoot) {
95
+ if (!filePath)
96
+ return;
97
+ filePath = path.resolve(workspaceRoot, filePath);
98
+ mkdirSync(path.dirname(filePath), { recursive: true });
99
+ writeFileSync(filePath, text, "utf8");
100
+ }
101
+ function parseArgs(argv, logger = console) {
102
+ const args = {
103
+ param: [],
104
+ summaryTop: 5,
105
+ ignoreDefaults: false,
106
+ noConfigs: false,
107
+ stopOnError: false,
108
+ verbose: false,
109
+ };
110
+ const values = new Map([
111
+ ["--company", "company"],
112
+ ["--cli", "cli"],
113
+ ["--param", "param"],
114
+ ["--max-resources", "maxResources"],
115
+ ["--page-size-hint", "pageSizeHint"],
116
+ ["--max-pages", "maxPages"],
117
+ ["--list-output-json", "listOutputJson"],
118
+ ["--configs-output-json", "configsOutputJson"],
119
+ ["--compliance-status", "complianceStatus"],
120
+ ["--compliance-output-json", "complianceOutputJson"],
121
+ ["--summary-output", "summaryOutput"],
122
+ ["--summary-top", "summaryTop"],
123
+ ]);
124
+ for (let i = 0; i < argv.length; i += 1) {
125
+ const flag = argv[i];
126
+ if (flag === "--no-configs")
127
+ args.noConfigs = true;
128
+ else if (flag === "--stop-on-error")
129
+ args.stopOnError = true;
130
+ else if (flag === "--verbose")
131
+ args.verbose = true;
132
+ else if (flag === "--ignore-defaults")
133
+ args.ignoreDefaults = true;
134
+ else if (values.has(flag)) {
135
+ if (++i >= argv.length)
136
+ throw new Error(`argument ${flag}: expected one argument`);
137
+ const key = values.get(flag);
138
+ if (key === "param")
139
+ args.param.push(argv[i]);
140
+ else if (key === "maxResources" ||
141
+ key === "pageSizeHint" ||
142
+ key === "maxPages" ||
143
+ key === "summaryTop") {
144
+ const value = Number(argv[i]);
145
+ if (!Number.isInteger(value))
146
+ throw new Error(`argument ${flag}: invalid int value: '${argv[i]}'`);
147
+ args[key] = value;
148
+ }
149
+ else if (key === "company" ||
150
+ key === "cli" ||
151
+ key === "listOutputJson" ||
152
+ key === "configsOutputJson" ||
153
+ key === "complianceStatus" ||
154
+ key === "complianceOutputJson" ||
155
+ key === "summaryOutput")
156
+ args[key] = argv[i];
157
+ }
158
+ else if (flag === "--help" || flag === "-h") {
159
+ logger.log("List resources, fetch latest configs by id, and summarize the results.");
160
+ args.help = true;
161
+ return args;
162
+ }
163
+ else
164
+ throw new Error(`unrecognized arguments: ${flag}`);
165
+ }
166
+ return args;
167
+ }
168
+ function builderList(company, params, context) {
169
+ const args = {
170
+ action: "list",
171
+ company,
172
+ param: params,
173
+ execute: false,
174
+ ignoreDefaults: false,
175
+ };
176
+ const map = brc.parseParamMap(params);
177
+ brc.validateArgs(args, map);
178
+ return brc.buildCommand(args, map, context);
179
+ }
180
+ function builderGet(company, id, context) {
181
+ const args = {
182
+ action: "get-latest-configuration",
183
+ company,
184
+ resourceNumericId: id,
185
+ param: [],
186
+ execute: false,
187
+ ignoreDefaults: false,
188
+ };
189
+ brc.validateArgs(args, new Map());
190
+ return brc.buildCommand(args, new Map(), context);
191
+ }
192
+ function builderCompliance(company, id, status, context) {
193
+ const args = {
194
+ action: "get-compliance",
195
+ company,
196
+ resourceNumericId: id,
197
+ param: [`status=${status}`],
198
+ execute: false,
199
+ ignoreDefaults: false,
200
+ };
201
+ const map = brc.parseParamMap(args.param);
202
+ brc.validateArgs(args, map);
203
+ return brc.buildCommand(args, map, context);
204
+ }
205
+ function addOrReplaceParam(params, key, value) {
206
+ return [
207
+ ...params.filter((item) => !item.startsWith(`${key}=`)),
208
+ `${key}=${value}`,
209
+ ];
210
+ }
211
+ function docOf(cfg) {
212
+ return recordOf(cfg.document);
213
+ }
214
+ function cfgName(cfg) {
215
+ return String(cfg.name || cfg._list_name || "(unnamed)");
216
+ }
217
+ export function summarizeComplianceBatches(batches, topN, status) {
218
+ const oracleOnly = batches.length > 0 &&
219
+ batches.every((batch) => String(batch.resource_type || "") === "databases/oracle");
220
+ let findingsTotal = 0;
221
+ const severity = new Map(), checks = new Map(), resources = [];
222
+ for (const batch of batches) {
223
+ if (!Array.isArray(batch.findings))
224
+ continue;
225
+ let count = 0;
226
+ for (const item of batch.findings) {
227
+ if (!item || typeof item !== "object" || Array.isArray(item))
228
+ continue;
229
+ findingsTotal += 1;
230
+ count += 1;
231
+ const sev = String(item.checkSeverity || "unknown"), check = String(item.checkName || "(unnamed check)");
232
+ severity.set(sev, (severity.get(sev) || 0) + 1);
233
+ checks.set(check, (checks.get(check) || 0) + 1);
234
+ }
235
+ if (count)
236
+ resources.push([String(batch.resource_name || batch.resource_id), count]);
237
+ }
238
+ const lines = [
239
+ oracleOnly
240
+ ? `Oracle license compliance summary (status=${status})`
241
+ : `Compliance summary (status=${status})`,
242
+ `Resources checked: ${batches.length}`,
243
+ `Total findings: ${findingsTotal}`,
244
+ ];
245
+ if (oracleOnly) {
246
+ const versions = new Map(), editions = new Map();
247
+ for (const batch of batches) {
248
+ const version = String(batch.oracle_version || "").trim(), edition = String(batch.oracle_edition || "").trim();
249
+ if (version)
250
+ versions.set(version, (versions.get(version) || 0) + 1);
251
+ if (edition)
252
+ editions.set(edition, (editions.get(edition) || 0) + 1);
253
+ }
254
+ lines.push("", "Oracle configuration context:");
255
+ const format = (map) => [...map]
256
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
257
+ .slice(0, topN)
258
+ .map(([name, count]) => `${name}: ${count}`)
259
+ .join(", ") || "none";
260
+ lines.push(`- versions: ${format(versions)}`, `- editions: ${format(editions)}`);
261
+ }
262
+ const formatCounts = (map) => [...map].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
263
+ lines.push("", "Findings by severity:");
264
+ for (const [name, count] of formatCounts(severity))
265
+ lines.push(`- ${name}: ${count}`);
266
+ if (!severity.size)
267
+ lines.push("- none");
268
+ lines.push("", `Top ${topN} checks by frequency:`);
269
+ for (const [name, count] of formatCounts(checks).slice(0, topN))
270
+ lines.push(`- ${name}: ${count}`);
271
+ if (!checks.size)
272
+ lines.push("- none");
273
+ lines.push("", `Top ${topN} resources by finding count:`);
274
+ for (const [name, count] of resources
275
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
276
+ .slice(0, topN))
277
+ lines.push(`- ${name}: ${count}`);
278
+ if (!resources.length)
279
+ lines.push("- none");
280
+ lines.push("", "Sample findings:");
281
+ let shown = 0;
282
+ for (const batch of batches) {
283
+ if (!Array.isArray(batch.findings))
284
+ continue;
285
+ for (const item of batch.findings) {
286
+ if (!item || typeof item !== "object" || Array.isArray(item))
287
+ continue;
288
+ const name = String(batch.resource_name || batch.resource_id), check = String(item.checkName || "(unnamed check)"), sev = String(item.checkSeverity || "unknown"), message = String(item.errorMessage || "");
289
+ let suffix = "";
290
+ if (oracleOnly) {
291
+ const context = [];
292
+ if (String(batch.oracle_version || "").trim())
293
+ context.push(`version=${String(batch.oracle_version).trim()}`);
294
+ if (String(batch.oracle_edition || "").trim())
295
+ context.push(`edition=${String(batch.oracle_edition).trim()}`);
296
+ if (context.length)
297
+ suffix = ` (${context.join(", ")})`;
298
+ }
299
+ lines.push(`- ${name}${suffix} [${sev}] ${check}`);
300
+ if (message)
301
+ lines.push(` error: ${message}`);
302
+ if (item.recommendedAction)
303
+ lines.push(` fix: ${item.recommendedAction}`);
304
+ shown += 1;
305
+ if (shown >= topN)
306
+ return lines.join("\n");
307
+ }
308
+ }
309
+ if (!shown)
310
+ lines.push("- none");
311
+ return lines.join("\n");
312
+ }
313
+ function summarizeVmware(configs, topN) {
314
+ const vm = configs.filter((cfg) => String(cfg.type || cfg._list_type || "") === "vcenter/virtualMachine"), hosts = configs.filter((cfg) => String(cfg.type || cfg._list_type || "") === "vcenter/host");
315
+ if (!vm.length && !hosts.length)
316
+ return "";
317
+ const hostById = new Map(), hostVmIds = new Map(), matches = [], unmatched = [];
318
+ for (const host of hosts) {
319
+ const doc = docOf(host), id = String(doc.id || "").trim();
320
+ if (id)
321
+ hostById.set(id, host);
322
+ const ids = Array.isArray(doc.virtualMachines)
323
+ ? doc.virtualMachines
324
+ .filter((item) => item && typeof item === "object")
325
+ .map((item) => String(recordOf(item).id || "").trim())
326
+ .filter(Boolean)
327
+ : [];
328
+ if (id && ids.length)
329
+ hostVmIds.set(id, ids);
330
+ }
331
+ for (const cfg of vm) {
332
+ const doc = docOf(cfg), hostRef = doc.host;
333
+ if (!hostRef || typeof hostRef !== "object" || Array.isArray(hostRef))
334
+ continue;
335
+ const id = String(doc.id || "").trim(), name = String(doc.name || cfgName(cfg)).trim(), hostId = String(recordOf(hostRef).id || "").trim(), host = hostById.get(hostId), hostDoc = host ? docOf(host) : {};
336
+ const cluster = String(recordOf(doc.cluster).name || "").trim(), datacenter = String(recordOf(doc.datacenter).name || "").trim(), matched = Boolean(host && id && (hostVmIds.get(hostId) || []).includes(id));
337
+ const entry = {
338
+ name,
339
+ id,
340
+ guest: String(doc.guestHostname || "").trim(),
341
+ hostId,
342
+ hostName: String(hostDoc.name || recordOf(hostRef).name || "").trim(),
343
+ cluster: String(recordOf(hostDoc.cluster).name || cluster).trim(),
344
+ datacenter: String(recordOf(hostDoc.datacenter).name || datacenter).trim(),
345
+ matched,
346
+ };
347
+ (host ? matches : unmatched).push(entry);
348
+ }
349
+ const lines = [
350
+ "VMware relationship summary",
351
+ `VM configurations collected: ${vm.length}`,
352
+ `Host configurations collected: ${hosts.length}`,
353
+ `VMs with host reference: ${matches.length + unmatched.length}`,
354
+ `VM-to-host matches confirmed from both payloads: ${matches.filter((item) => item.matched).length}`,
355
+ "",
356
+ `Top ${topN} VM-to-host relationships:`,
357
+ ];
358
+ if (matches.length)
359
+ for (const item of matches.slice(0, topN)) {
360
+ const context = [];
361
+ if (item.guest)
362
+ context.push(`guest=${item.guest}`);
363
+ if (item.cluster)
364
+ context.push(`cluster=${item.cluster}`);
365
+ if (item.datacenter)
366
+ context.push(`datacenter=${item.datacenter}`);
367
+ if (!item.matched)
368
+ context.push("host-side confirmation=missing");
369
+ lines.push(`- ${item.name || item.id || "(unnamed vm)"} -> ${item.hostName || item.hostId || "(unknown host)"}${context.length ? ` (${context.join(", ")})` : ""}`);
370
+ }
371
+ else
372
+ lines.push("- none");
373
+ if (unmatched.length) {
374
+ lines.push("", `Top ${topN} VM host references without matching host config:`);
375
+ for (const item of unmatched.slice(0, topN))
376
+ lines.push(`- ${item.name || item.id || "(unnamed vm)"} -> ${item.hostName || item.hostId || "(unknown host)"}`);
377
+ }
378
+ const hostSide = [];
379
+ for (const [id, ids] of hostVmIds)
380
+ if (!matches.some((item) => item.hostId === id))
381
+ hostSide.push(`${String(docOf(hostById.get(id) || {}).name || id).trim()}: ${ids.length} hosted VMs`);
382
+ if (hostSide.length) {
383
+ lines.push("", `Top ${topN} hosts with only host-side VM evidence:`);
384
+ for (const item of hostSide.slice(0, topN))
385
+ lines.push(`- ${item}`);
386
+ }
387
+ return lines.join("\n");
388
+ }
389
+ function summarizeNutanix(configs, topN) {
390
+ const vm = configs.filter((cfg) => String(cfg.type || cfg._list_type || "") === "Nutanix/Vm"), hosts = configs.filter((cfg) => String(cfg.type || cfg._list_type || "") === "Nutanix/Host"), clusters = configs.filter((cfg) => String(cfg.type || cfg._list_type || "") === "Nutanix/Cluster");
391
+ if (!vm.length && !hosts.length && !clusters.length)
392
+ return "";
393
+ const hostById = new Map(), clusterById = new Map(), nodes = new Map();
394
+ for (const cfg of hosts) {
395
+ const d = docOf(cfg);
396
+ if (d.extId)
397
+ hostById.set(String(d.extId).trim(), cfg);
398
+ }
399
+ for (const cfg of clusters) {
400
+ const d = docOf(cfg), id = String(d.extId || "").trim();
401
+ if (id)
402
+ clusterById.set(id, cfg);
403
+ const list = recordOf(d.nodes).nodeList;
404
+ if (id && Array.isArray(list))
405
+ nodes.set(id, new Set(list
406
+ .filter((item) => item && typeof item === "object")
407
+ .map((item) => String(recordOf(item).nodeUuid || "").trim())
408
+ .filter(Boolean)));
409
+ }
410
+ const rows = [];
411
+ for (const cfg of vm) {
412
+ const d = docOf(cfg), hostId = String(recordOf(d.host).extId || "").trim(), clusterId = String(recordOf(d.cluster).extId || "").trim(), host = hostById.get(hostId), cluster = clusterById.get(clusterId), hd = docOf(host || {}), cd = docOf(cluster || {}), nics = Array.isArray(d.nics) ? d.nics : [], ips = [], subnets = [];
413
+ for (const nic of nics) {
414
+ const info = recordOf(nic).networkInfo;
415
+ if (!info || typeof info !== "object" || Array.isArray(info))
416
+ continue;
417
+ const subnet = recordOf(recordOf(info).subnet);
418
+ if (subnet.extId)
419
+ subnets.push(String(subnet.extId).trim());
420
+ const addresses = recordOf(recordOf(info).ipv4Info).learnedIpAddresses;
421
+ for (const ip of Array.isArray(addresses) ? addresses : [])
422
+ if (recordOf(ip).value)
423
+ ips.push(String(recordOf(ip).value).trim());
424
+ }
425
+ const hostCluster = recordOf(hd.cluster);
426
+ rows.push({
427
+ name: String(d.name || cfgName(cfg)).trim(),
428
+ id: String(d.extId || "").trim(),
429
+ description: String(d.description || "").trim(),
430
+ power: String(d.powerState || "").trim(),
431
+ hostId,
432
+ hostName: String(hd.hostName || "").trim(),
433
+ clusterId,
434
+ clusterName: String(cd.name || hostCluster?.name || "").trim(),
435
+ ips: ips.slice(0, 3).join(", "),
436
+ subnets: subnets.slice(0, 3).join(", "),
437
+ hostMatch: Boolean(host),
438
+ clusterMatch: Boolean(cluster),
439
+ nodeMatch: Boolean(nodes.get(clusterId)?.has(hostId)),
440
+ });
441
+ }
442
+ const lines = [
443
+ "Nutanix relationship summary",
444
+ `VM configurations collected: ${vm.length}`,
445
+ `Host configurations collected: ${hosts.length}`,
446
+ `Cluster configurations collected: ${clusters.length}`,
447
+ `VMs with host reference: ${rows.filter((item) => item.hostId).length}`,
448
+ `VMs with cluster reference: ${rows.filter((item) => item.clusterId).length}`,
449
+ `VM-to-host matches confirmed from host configs: ${rows.filter((item) => item.hostMatch).length}`,
450
+ `VM-to-cluster matches confirmed from cluster configs: ${rows.filter((item) => item.clusterMatch).length}`,
451
+ "",
452
+ `Top ${topN} VM relationships:`,
453
+ ];
454
+ if (rows.length)
455
+ for (const item of rows.slice(0, topN)) {
456
+ const context = [];
457
+ if (item.ips)
458
+ context.push(`ips=${item.ips}`);
459
+ if (item.power)
460
+ context.push(`power=${item.power}`);
461
+ if (item.subnets)
462
+ context.push(`subnets=${item.subnets}`);
463
+ if (item.description)
464
+ context.push(`description=${item.description}`);
465
+ if (!item.hostMatch)
466
+ context.push("host-config=missing");
467
+ if (!item.clusterMatch)
468
+ context.push("cluster-config=missing");
469
+ else if (!item.nodeMatch)
470
+ context.push("cluster-node-confirmation=missing");
471
+ lines.push(`- ${item.name || item.id || "(unnamed vm)"} -> host=${item.hostName || item.hostId || "(unknown host)"}, cluster=${item.clusterName || item.clusterId || "(unknown cluster)"}${context.length ? ` (${context.join(", ")})` : ""}`);
472
+ }
473
+ else
474
+ lines.push("- none");
475
+ const migrations = rows.filter((item) => /migrat|move/i.test(item.description));
476
+ if (migrations.length) {
477
+ lines.push("", `Top ${topN} migration-related VM descriptions:`);
478
+ for (const item of migrations.slice(0, topN))
479
+ lines.push(`- ${item.name || item.id || "(unnamed vm)"}: ${item.description}`);
480
+ }
481
+ return lines.join("\n");
482
+ }
483
+ export async function main(argv = process.argv.slice(2), logger = console, execution) {
484
+ return runWorkflow(argv, logger, execution, execution?.signal);
485
+ }
486
+ async function runWorkflow(argv, logger, execution, signal) {
487
+ let args;
488
+ let company;
489
+ let selected;
490
+ try {
491
+ args = parseArgs(argv, logger);
492
+ if (execution &&
493
+ args.cli !== undefined &&
494
+ args.cli !== execution.context.cliCommand)
495
+ throw new Error("--cli cannot override the host-selected Opscompass CLI.");
496
+ const context = execution
497
+ ? { ...execution.context }
498
+ : resolveCliContext(args.cli);
499
+ selected = {
500
+ context,
501
+ workspaceRoot: execution
502
+ ? path.resolve(execution.workspaceRoot)
503
+ : process.cwd(),
504
+ signal,
505
+ env: {
506
+ ...process.env,
507
+ OPSCOMPASS_CLI_ENTRYPOINT: context.cliEntryPoint,
508
+ ...(execution
509
+ ? { ELECTRON_RUN_AS_NODE: "1", OPSCOMPASS_STUDIO: "1" }
510
+ : {}),
511
+ },
512
+ };
513
+ if (args.help)
514
+ return 0;
515
+ company = brc.resolveCompany(args.company, {
516
+ ignoreDefaults: args.ignoreDefaults,
517
+ context: selected.context,
518
+ });
519
+ }
520
+ catch (error) {
521
+ logger.error(`Error: ${errorMessage(error)}`);
522
+ return 2;
523
+ }
524
+ if (args.maxResources !== undefined && args.maxResources <= 0)
525
+ return (logger.error("Error: --max-resources must be greater than 0."), 2);
526
+ if (args.pageSizeHint !== undefined && args.pageSizeHint <= 0)
527
+ return (logger.error("Error: --page-size-hint must be greater than 0."), 2);
528
+ if (args.maxPages !== undefined && args.maxPages <= 0)
529
+ return (logger.error("Error: --max-pages must be greater than 0."), 2);
530
+ if (args.maxPages !== undefined && args.pageSizeHint === undefined)
531
+ return (logger.error("Error: --max-pages requires --page-size-hint."), 2);
532
+ if (args.pageSizeHint !== undefined &&
533
+ args.param.some((item) => item.startsWith("skip=")))
534
+ return (logger.error("Error: do not pass --param skip=... together with --page-size-hint/--max-pages."),
535
+ 2);
536
+ if (args.summaryTop <= 0)
537
+ return (logger.error("Error: --summary-top must be greater than 0."), 2);
538
+ if (args.noConfigs && !args.complianceStatus)
539
+ return (logger.error("Error: nothing to do. Remove --no-configs or provide --compliance-status."),
540
+ 2);
541
+ if (args.complianceStatus)
542
+ try {
543
+ builderCompliance(company, "1", args.complianceStatus, selected.context);
544
+ }
545
+ catch (error) {
546
+ logger.error(`Error: ${errorMessage(error)}`);
547
+ return 2;
548
+ }
549
+ let base;
550
+ try {
551
+ base = builderList(company, args.param, selected.context);
552
+ }
553
+ catch (error) {
554
+ logger.error(`Error: ${errorMessage(error)}`);
555
+ return 2;
556
+ }
557
+ let listPayload = [], listCommands = [], pages = 0;
558
+ try {
559
+ if (args.pageSizeHint !== undefined) {
560
+ const max = args.maxPages || 1;
561
+ for (let i = 0; i < max; i += 1) {
562
+ const command = builderList(company, addOrReplaceParam(args.param, "skip", String(i * args.pageSizeHint)), selected.context);
563
+ const payload = await runJsonCommand(command, args.verbose, logger, selected);
564
+ if (!Array.isArray(payload)) {
565
+ logger.error("Error: resources list did not return a JSON array.");
566
+ return 1;
567
+ }
568
+ listCommands.push(command);
569
+ pages += 1;
570
+ listPayload.push(...payload);
571
+ if (payload.length < args.pageSizeHint)
572
+ break;
573
+ }
574
+ }
575
+ else {
576
+ const payload = await runJsonCommand(base, args.verbose, logger, selected);
577
+ if (!Array.isArray(payload)) {
578
+ logger.error("Error: resources list did not return a JSON array.");
579
+ return 1;
580
+ }
581
+ listPayload = payload;
582
+ listCommands.push(base);
583
+ pages = 1;
584
+ }
585
+ }
586
+ catch (error) {
587
+ logger.error(`Error: ${errorMessage(error)}`);
588
+ return 1;
589
+ }
590
+ saveJson(args.listOutputJson, listPayload, selected.workspaceRoot);
591
+ const rows = listPayload.filter((item) => item && typeof item === "object" && !Array.isArray(item)), ids = rows
592
+ .filter((row) => row.id !== undefined && row.id !== null)
593
+ .map((row) => String(row.id))
594
+ .slice(0, args.maxResources);
595
+ if (!ids.length) {
596
+ logger.log("No resource ids found in list output.");
597
+ return 0;
598
+ }
599
+ const collected = [], configById = new Map(), batches = [], failures = [], rowById = new Map(rows.map((row) => [String(row.id), row]));
600
+ for (const id of ids) {
601
+ const source = rowById.get(id) || {}, needsOracle = Boolean(args.complianceStatus &&
602
+ String(source.type || "") === "databases/oracle"), shouldConfig = !args.noConfigs || needsOracle;
603
+ if (shouldConfig)
604
+ try {
605
+ const payload = await runJsonCommand(builderGet(company, id, selected.context), args.verbose, logger, selected);
606
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
607
+ const config = payload;
608
+ config._opscompass_resource_numeric_id ??= id;
609
+ if (source.name !== undefined)
610
+ config._list_name ??= source.name;
611
+ if (source.type !== undefined)
612
+ config._list_type ??= source.type;
613
+ configById.set(id, config);
614
+ if (!args.noConfigs)
615
+ collected.push(config);
616
+ }
617
+ else
618
+ throw new Error(`get-latest-configuration returned non-object JSON for id ${id}`);
619
+ }
620
+ catch (error) {
621
+ failures.push({ id, error: `config: ${errorMessage(error)}` });
622
+ if (args.stopOnError || selected.signal?.aborted) {
623
+ logger.error(`Error: ${errorMessage(error)}`);
624
+ return 1;
625
+ }
626
+ }
627
+ if (args.complianceStatus)
628
+ try {
629
+ const payload = await runJsonCommand(builderCompliance(company, id, args.complianceStatus, selected.context), args.verbose, logger, selected);
630
+ if (!Array.isArray(payload))
631
+ throw new Error(`get-compliance returned non-array JSON for id ${id}`);
632
+ const doc = docOf(configById.get(id) || {});
633
+ batches.push({
634
+ resource_id: id,
635
+ resource_name: source.name,
636
+ resource_type: source.type,
637
+ status: args.complianceStatus,
638
+ oracle_version: doc.version ?? null,
639
+ oracle_edition: doc.edition ?? null,
640
+ findings: payload,
641
+ });
642
+ }
643
+ catch (error) {
644
+ failures.push({ id, error: `compliance: ${errorMessage(error)}` });
645
+ if (args.stopOnError || selected.signal?.aborted) {
646
+ logger.error(`Error: ${errorMessage(error)}`);
647
+ return 1;
648
+ }
649
+ }
650
+ }
651
+ saveJson(args.configsOutputJson, collected, selected.workspaceRoot);
652
+ saveJson(args.complianceOutputJson, batches, selected.workspaceRoot);
653
+ const sections = [];
654
+ if (!args.noConfigs) {
655
+ sections.push(collected.length
656
+ ? slc.summarizeMany(collected, args.summaryTop)
657
+ : "No configuration objects collected.");
658
+ const vmware = summarizeVmware(collected, args.summaryTop), nutanix = summarizeNutanix(collected, args.summaryTop), cpu = moc.summarize(collected, args.summaryTop);
659
+ if (vmware)
660
+ sections.push(vmware);
661
+ if (nutanix)
662
+ sections.push(nutanix);
663
+ if (cpu)
664
+ sections.push(cpu);
665
+ }
666
+ if (args.complianceStatus)
667
+ sections.push(summarizeComplianceBatches(batches, args.summaryTop, args.complianceStatus));
668
+ let summary = sections.filter(Boolean).join("\n\n");
669
+ if (failures.length)
670
+ summary += `\n\nFailures: ${failures.length}\n${failures
671
+ .slice(0, args.summaryTop)
672
+ .map((item) => `- id=${item.id}: ${item.error}`)
673
+ .join("\n")}`;
674
+ saveText(args.summaryOutput, summary, selected.workspaceRoot);
675
+ if (listCommands.length === 1)
676
+ logger.log(`List command: ${listCommands[0].join(" ")}`);
677
+ else {
678
+ logger.log("List commands:");
679
+ for (const command of listCommands)
680
+ logger.log(`- ${command.join(" ")}`);
681
+ }
682
+ logger.log(`List pages fetched: ${pages}`);
683
+ logger.log(`Resources listed: ${rows.length}`);
684
+ logger.log(`Resource ids selected for follow-up fetches: ${ids.length}`);
685
+ if (!args.noConfigs)
686
+ logger.log(`Configurations collected: ${collected.length}`);
687
+ if (args.complianceStatus)
688
+ logger.log(`Compliance batches collected (status=${args.complianceStatus}): ${batches.length}`);
689
+ if (failures.length)
690
+ logger.log(`Follow-up fetch failures: ${failures.length}`);
691
+ logger.log("");
692
+ logger.log(summary);
693
+ return failures.length ? 1 : 0;
694
+ }
695
+ if (isMainModule(import.meta.url)) {
696
+ const controller = new AbortController();
697
+ let interrupted;
698
+ const interrupt = (signal) => {
699
+ interrupted ??= signal;
700
+ controller.abort();
701
+ };
702
+ process.on("SIGINT", interrupt);
703
+ process.on("SIGTERM", interrupt);
704
+ try {
705
+ process.exitCode = await runWorkflow(process.argv.slice(2), console, undefined, controller.signal);
706
+ }
707
+ finally {
708
+ process.removeListener("SIGINT", interrupt);
709
+ process.removeListener("SIGTERM", interrupt);
710
+ // Re-raise only after the active command has closed and listeners are gone.
711
+ if (interrupted)
712
+ process.kill(process.pid, interrupted);
713
+ }
714
+ }