@syndicalt/snow-cli 1.1.0 → 1.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 +380 -9
- package/dist/index.js +1376 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -321,8 +321,8 @@ Hint: PDI instances have lower transaction quotas. Wait a moment then retry, or
|
|
|
321
321
|
|
|
322
322
|
// src/index.ts
|
|
323
323
|
init_esm_shims();
|
|
324
|
-
import { Command as
|
|
325
|
-
import
|
|
324
|
+
import { Command as Command12 } from "commander";
|
|
325
|
+
import chalk12 from "chalk";
|
|
326
326
|
|
|
327
327
|
// src/commands/instance.ts
|
|
328
328
|
init_esm_shims();
|
|
@@ -517,8 +517,8 @@ function printRecords(records, format) {
|
|
|
517
517
|
return;
|
|
518
518
|
}
|
|
519
519
|
const naturalWidths = keys.map((k) => {
|
|
520
|
-
const maxVal = rows.reduce((max,
|
|
521
|
-
return Math.max(max, flattenValue(
|
|
520
|
+
const maxVal = rows.reduce((max, row2) => {
|
|
521
|
+
return Math.max(max, flattenValue(row2[k]).length);
|
|
522
522
|
}, k.length);
|
|
523
523
|
return Math.min(maxVal, 60);
|
|
524
524
|
});
|
|
@@ -528,9 +528,9 @@ function printRecords(records, format) {
|
|
|
528
528
|
const divider = chalk2.dim(colWidths.map((w) => "\u2500".repeat(w)).join(" "));
|
|
529
529
|
console.log(header);
|
|
530
530
|
console.log(divider);
|
|
531
|
-
for (const
|
|
531
|
+
for (const row2 of rows) {
|
|
532
532
|
const line = keys.map((k, i) => {
|
|
533
|
-
const val = flattenValue(
|
|
533
|
+
const val = flattenValue(row2[k]);
|
|
534
534
|
return val.length > colWidths[i] ? val.slice(0, colWidths[i] - 1) + "\u2026" : val.padEnd(colWidths[i]);
|
|
535
535
|
}).join(" ");
|
|
536
536
|
console.log(line);
|
|
@@ -631,8 +631,8 @@ function tableCommand() {
|
|
|
631
631
|
);
|
|
632
632
|
cmd.command("delete <table> <sys_id>").description("Delete a record").option("-y, --yes", "Skip confirmation prompt").action(async (table, sysId, opts) => {
|
|
633
633
|
if (!opts.yes) {
|
|
634
|
-
const { confirm:
|
|
635
|
-
const ok = await
|
|
634
|
+
const { confirm: confirm6 } = await import("@inquirer/prompts");
|
|
635
|
+
const ok = await confirm6({
|
|
636
636
|
message: `Delete ${table}/${sysId}?`,
|
|
637
637
|
default: false
|
|
638
638
|
});
|
|
@@ -1323,6 +1323,17 @@ import { join as join3 } from "path";
|
|
|
1323
1323
|
import { spawnSync } from "child_process";
|
|
1324
1324
|
import chalk4 from "chalk";
|
|
1325
1325
|
import ora3 from "ora";
|
|
1326
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1327
|
+
var SCRIPT_TABLES = [
|
|
1328
|
+
{ table: "sys_script_include", field: "script", label: "Script Include" },
|
|
1329
|
+
{ table: "sys_script", field: "script", label: "Business Rule" },
|
|
1330
|
+
{ table: "sys_script_client", field: "script", label: "Client Script" },
|
|
1331
|
+
{ table: "sys_ui_action", field: "script", label: "UI Action" },
|
|
1332
|
+
{ table: "sys_ui_page", field: "html", label: "UI Page (HTML)" },
|
|
1333
|
+
{ table: "sys_ui_page", field: "client_script", label: "UI Page (Client)" },
|
|
1334
|
+
{ table: "sys_ui_page", field: "processing_script", label: "UI Page (Server)" },
|
|
1335
|
+
{ table: "sysauto_script", field: "script", label: "Scheduled Job" }
|
|
1336
|
+
];
|
|
1326
1337
|
function extensionForField(fieldName, fieldType) {
|
|
1327
1338
|
if (fieldType === "html" || fieldName.endsWith("_html")) return ".html";
|
|
1328
1339
|
if (fieldType === "css" || fieldName === "css") return ".css";
|
|
@@ -1386,8 +1397,8 @@ function scriptCommand() {
|
|
|
1386
1397
|
console.error(chalk4.red(`Editor exited with code ${result.status ?? "?"}`));
|
|
1387
1398
|
process.exit(result.status ?? 1);
|
|
1388
1399
|
}
|
|
1389
|
-
const { confirm:
|
|
1390
|
-
const shouldPush = await
|
|
1400
|
+
const { confirm: confirm6 } = await import("@inquirer/prompts");
|
|
1401
|
+
const shouldPush = await confirm6({
|
|
1391
1402
|
message: `Push changes to ${instance.alias}?`,
|
|
1392
1403
|
default: true
|
|
1393
1404
|
});
|
|
@@ -1433,6 +1444,148 @@ function scriptCommand() {
|
|
|
1433
1444
|
console.log(`${chalk4.cyan(f)} ${chalk4.dim(modified)}`);
|
|
1434
1445
|
}
|
|
1435
1446
|
});
|
|
1447
|
+
cmd.command("search <scope>").description("Search for a pattern across script fields in an app scope").requiredOption("-c, --contains <pattern>", "Text or regex pattern to search for").option("-t, --tables <tables>", "Comma-separated list of tables to search (default: all script tables)").option("--regex", "Treat --contains as a JavaScript regex").option("-l, --limit <n>", "Max records to fetch per table (default: 500)", "500").action(async (scope, opts) => {
|
|
1448
|
+
const instance = requireActiveInstance();
|
|
1449
|
+
const client = new ServiceNowClient(instance);
|
|
1450
|
+
const limit = parseInt(opts.limit, 10) || 500;
|
|
1451
|
+
const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()).flatMap(
|
|
1452
|
+
(t) => SCRIPT_TABLES.filter((st) => st.table === t)
|
|
1453
|
+
) : SCRIPT_TABLES;
|
|
1454
|
+
let matcher;
|
|
1455
|
+
if (opts.regex) {
|
|
1456
|
+
const re = new RegExp(opts.contains, "g");
|
|
1457
|
+
matcher = (s) => re.test(s);
|
|
1458
|
+
} else {
|
|
1459
|
+
matcher = (s) => s.includes(opts.contains);
|
|
1460
|
+
}
|
|
1461
|
+
let totalMatches = 0;
|
|
1462
|
+
for (const { table, field, label } of tables) {
|
|
1463
|
+
const spinner = ora3(`Searching ${label} (${table}.${field})...`).start();
|
|
1464
|
+
let records;
|
|
1465
|
+
try {
|
|
1466
|
+
records = await client.queryTable(table, {
|
|
1467
|
+
sysparmQuery: `sys_scope.scope=${scope}^${field}ISNOTEMPTY`,
|
|
1468
|
+
sysparmFields: `sys_id,name,${field}`,
|
|
1469
|
+
sysparmLimit: limit
|
|
1470
|
+
});
|
|
1471
|
+
spinner.stop();
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
spinner.fail(chalk4.red(`${label}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
const matches = records.filter((r) => matcher(r[field] ?? ""));
|
|
1477
|
+
if (matches.length === 0) continue;
|
|
1478
|
+
console.log(chalk4.bold(`
|
|
1479
|
+
${label} \u2014 ${matches.length} match(es):`));
|
|
1480
|
+
for (const r of matches) {
|
|
1481
|
+
const content = r[field] ?? "";
|
|
1482
|
+
const lines = content.split("\n");
|
|
1483
|
+
const matchingLines = lines.map((line, i) => ({ line, num: i + 1 })).filter(({ line }) => matcher(line));
|
|
1484
|
+
console.log(` ${chalk4.cyan(r["name"] ?? r["sys_id"])} ${chalk4.dim(r["sys_id"])}`);
|
|
1485
|
+
for (const { line, num } of matchingLines.slice(0, 5)) {
|
|
1486
|
+
const trimmed = line.trim().slice(0, 120);
|
|
1487
|
+
console.log(` ${chalk4.dim(`L${num}:`)} ${trimmed}`);
|
|
1488
|
+
}
|
|
1489
|
+
if (matchingLines.length > 5) {
|
|
1490
|
+
console.log(chalk4.dim(` ... and ${matchingLines.length - 5} more line(s)`));
|
|
1491
|
+
}
|
|
1492
|
+
totalMatches++;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
console.log();
|
|
1496
|
+
if (totalMatches === 0) {
|
|
1497
|
+
console.log(chalk4.yellow(`No matches found for "${opts.contains}" in scope "${scope}".`));
|
|
1498
|
+
} else {
|
|
1499
|
+
console.log(chalk4.green(`Found matches in ${totalMatches} record(s).`));
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
cmd.command("replace <scope>").description("Find and replace text across script fields in an app scope").requiredOption("-f, --find <pattern>", "Text to find").requiredOption("-r, --replace <text>", "Replacement text").option("-t, --tables <tables>", "Comma-separated list of tables to target (default: all script tables)").option("--regex", "Treat --find as a JavaScript regex").option("-l, --limit <n>", "Max records to fetch per table (default: 500)", "500").option("--dry-run", "Show what would change without writing to the instance").option("--yes", "Skip confirmation prompt").action(async (scope, opts) => {
|
|
1503
|
+
const instance = requireActiveInstance();
|
|
1504
|
+
const client = new ServiceNowClient(instance);
|
|
1505
|
+
const limit = parseInt(opts.limit, 10) || 500;
|
|
1506
|
+
const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()).flatMap(
|
|
1507
|
+
(t) => SCRIPT_TABLES.filter((st) => st.table === t)
|
|
1508
|
+
) : SCRIPT_TABLES;
|
|
1509
|
+
const pattern = opts.regex ? new RegExp(opts.find, "g") : opts.find;
|
|
1510
|
+
const doReplace = (s) => typeof pattern === "string" ? s.split(pattern).join(opts.replace) : s.replace(pattern, opts.replace);
|
|
1511
|
+
const hasMatch = (s) => typeof pattern === "string" ? s.includes(pattern) : pattern.test(s);
|
|
1512
|
+
const candidates = [];
|
|
1513
|
+
for (const { table, field, label } of tables) {
|
|
1514
|
+
const spinner = ora3(`Scanning ${label} (${table}.${field})...`).start();
|
|
1515
|
+
let records;
|
|
1516
|
+
try {
|
|
1517
|
+
records = await client.queryTable(table, {
|
|
1518
|
+
sysparmQuery: `sys_scope.scope=${scope}^${field}ISNOTEMPTY`,
|
|
1519
|
+
sysparmFields: `sys_id,name,${field}`,
|
|
1520
|
+
sysparmLimit: limit
|
|
1521
|
+
});
|
|
1522
|
+
spinner.stop();
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
spinner.fail(chalk4.red(`${label}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
for (const r of records) {
|
|
1528
|
+
if (pattern instanceof RegExp) pattern.lastIndex = 0;
|
|
1529
|
+
const original = r[field] ?? "";
|
|
1530
|
+
if (!hasMatch(original)) continue;
|
|
1531
|
+
if (pattern instanceof RegExp) pattern.lastIndex = 0;
|
|
1532
|
+
const updated = doReplace(original);
|
|
1533
|
+
candidates.push({
|
|
1534
|
+
table,
|
|
1535
|
+
field,
|
|
1536
|
+
label,
|
|
1537
|
+
sysId: r["sys_id"],
|
|
1538
|
+
name: r["name"] ?? r["sys_id"],
|
|
1539
|
+
original,
|
|
1540
|
+
updated
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (candidates.length === 0) {
|
|
1545
|
+
console.log(chalk4.yellow(`No matches found for "${opts.find}" in scope "${scope}".`));
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
console.log(chalk4.bold(`
|
|
1549
|
+
${candidates.length} record(s) will be modified:
|
|
1550
|
+
`));
|
|
1551
|
+
for (const c of candidates) {
|
|
1552
|
+
console.log(` ${chalk4.cyan(c.name)} ${chalk4.dim(`${c.label} \u2014 ${c.table}/${c.sysId}`)}`);
|
|
1553
|
+
}
|
|
1554
|
+
console.log();
|
|
1555
|
+
if (opts.dryRun) {
|
|
1556
|
+
console.log(chalk4.yellow("Dry run \u2014 no changes made."));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (!opts.yes) {
|
|
1560
|
+
const ok = await confirm2({
|
|
1561
|
+
message: `Replace "${opts.find}" \u2192 "${opts.replace}" in ${candidates.length} record(s) on ${instance.alias}?`,
|
|
1562
|
+
default: false
|
|
1563
|
+
});
|
|
1564
|
+
if (!ok) {
|
|
1565
|
+
console.log(chalk4.dim("Aborted."));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
let successCount = 0;
|
|
1570
|
+
let failCount = 0;
|
|
1571
|
+
for (const c of candidates) {
|
|
1572
|
+
const spinner = ora3(`Updating ${c.name}...`).start();
|
|
1573
|
+
try {
|
|
1574
|
+
await client.updateRecord(c.table, c.sysId, { [c.field]: c.updated });
|
|
1575
|
+
spinner.succeed(chalk4.green(`Updated: ${c.name}`));
|
|
1576
|
+
successCount++;
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
spinner.fail(chalk4.red(`Failed ${c.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1579
|
+
failCount++;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
console.log();
|
|
1583
|
+
if (failCount === 0) {
|
|
1584
|
+
console.log(chalk4.green(`Replaced in ${successCount} record(s) successfully.`));
|
|
1585
|
+
} else {
|
|
1586
|
+
console.log(chalk4.yellow(`Replaced in ${successCount} record(s). ${failCount} failed.`));
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1436
1589
|
return cmd;
|
|
1437
1590
|
}
|
|
1438
1591
|
async function pushScript(client, table, sysId, field, filePath) {
|
|
@@ -3010,8 +3163,8 @@ async function confirmPush(build, dir, autoPush) {
|
|
|
3010
3163
|
let shouldPush = autoPush ?? false;
|
|
3011
3164
|
if (!shouldPush) {
|
|
3012
3165
|
console.log();
|
|
3013
|
-
const { confirm:
|
|
3014
|
-
shouldPush = await
|
|
3166
|
+
const { confirm: confirm6 } = await import("@inquirer/prompts");
|
|
3167
|
+
shouldPush = await confirm6({
|
|
3015
3168
|
message: `Push ${build.artifacts.length} artifact(s) to ${chalk6.cyan(instance.alias)} (${instance.url})?`,
|
|
3016
3169
|
default: false
|
|
3017
3170
|
});
|
|
@@ -3076,7 +3229,7 @@ function printCodeBlock(code, label) {
|
|
|
3076
3229
|
console.log(chalk6.dim("\u2500".repeat(width)));
|
|
3077
3230
|
}
|
|
3078
3231
|
async function runReview(build, buildDir) {
|
|
3079
|
-
const { select: select2, confirm:
|
|
3232
|
+
const { select: select2, confirm: confirm6 } = await import("@inquirer/prompts");
|
|
3080
3233
|
const editor = resolveEditor2();
|
|
3081
3234
|
let modified = false;
|
|
3082
3235
|
console.log();
|
|
@@ -3122,7 +3275,7 @@ async function runReview(build, buildDir) {
|
|
|
3122
3275
|
console.log();
|
|
3123
3276
|
printCodeBlock(currentCode, `${artifactName} \u2014 ${fieldDef.label}`);
|
|
3124
3277
|
console.log();
|
|
3125
|
-
const shouldEdit = await
|
|
3278
|
+
const shouldEdit = await confirm6({ message: "Open in editor to edit?", default: false });
|
|
3126
3279
|
if (shouldEdit) {
|
|
3127
3280
|
const tmpFile = join4(tmpdir2(), `snow-review-${Date.now()}${fieldDef.ext}`);
|
|
3128
3281
|
writeFileSync4(tmpFile, currentCode, "utf-8");
|
|
@@ -3423,52 +3576,1241 @@ ${chalk6.dim("Slash commands:")}
|
|
|
3423
3576
|
return cmd;
|
|
3424
3577
|
}
|
|
3425
3578
|
|
|
3579
|
+
// src/commands/bulk.ts
|
|
3580
|
+
init_esm_shims();
|
|
3581
|
+
init_config();
|
|
3582
|
+
init_client();
|
|
3583
|
+
import { Command as Command7 } from "commander";
|
|
3584
|
+
import chalk7 from "chalk";
|
|
3585
|
+
import ora6 from "ora";
|
|
3586
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3587
|
+
function parseSetArgs(setArgs) {
|
|
3588
|
+
const fields = {};
|
|
3589
|
+
for (const arg of setArgs) {
|
|
3590
|
+
const eq = arg.indexOf("=");
|
|
3591
|
+
if (eq === -1) {
|
|
3592
|
+
console.error(chalk7.red(`Invalid --set value "${arg}": expected field=value`));
|
|
3593
|
+
process.exit(1);
|
|
3594
|
+
}
|
|
3595
|
+
fields[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
3596
|
+
}
|
|
3597
|
+
return fields;
|
|
3598
|
+
}
|
|
3599
|
+
function renderPreviewTable(records, fields) {
|
|
3600
|
+
const fieldNames = Object.keys(fields);
|
|
3601
|
+
const headers = ["sys_id", "display_name", ...fieldNames.map((f) => `${f} (new)`)];
|
|
3602
|
+
const rows = records.map((r) => {
|
|
3603
|
+
const sysId = String(r["sys_id"] ?? "");
|
|
3604
|
+
const displayName = String(r["name"] ?? r["short_description"] ?? r["number"] ?? r["sys_name"] ?? "");
|
|
3605
|
+
return [sysId, displayName, ...fieldNames.map((f) => fields[f])];
|
|
3606
|
+
});
|
|
3607
|
+
const colWidths = headers.map(
|
|
3608
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0))
|
|
3609
|
+
);
|
|
3610
|
+
const divider = colWidths.map((w) => "-".repeat(w + 2)).join("+");
|
|
3611
|
+
const fmt = (row2) => row2.map((cell, i) => ` ${cell.padEnd(colWidths[i])} `).join("|");
|
|
3612
|
+
console.log(chalk7.bold(fmt(headers)));
|
|
3613
|
+
console.log(divider);
|
|
3614
|
+
for (const row2 of rows) {
|
|
3615
|
+
console.log(fmt(row2));
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
function bulkCommand() {
|
|
3619
|
+
const cmd = new Command7("bulk").description("Bulk operations on ServiceNow records");
|
|
3620
|
+
cmd.command("update <table>").description("Update multiple records matching a query").requiredOption("-q, --query <query>", "ServiceNow encoded query to select records").option("-s, --set <field=value>", "Field to set (repeat for multiple fields)", (val, acc) => {
|
|
3621
|
+
acc.push(val);
|
|
3622
|
+
return acc;
|
|
3623
|
+
}, []).option("-l, --limit <n>", "Max records to update (default: 200)", "200").option("--dry-run", "Preview which records would be updated without making changes").option("--yes", "Skip confirmation prompt").action(async (table, opts) => {
|
|
3624
|
+
if (opts.set.length === 0) {
|
|
3625
|
+
console.error(chalk7.red("At least one --set field=value is required."));
|
|
3626
|
+
process.exit(1);
|
|
3627
|
+
}
|
|
3628
|
+
const fields = parseSetArgs(opts.set);
|
|
3629
|
+
const limit = parseInt(opts.limit, 10);
|
|
3630
|
+
if (isNaN(limit) || limit < 1) {
|
|
3631
|
+
console.error(chalk7.red("--limit must be a positive integer"));
|
|
3632
|
+
process.exit(1);
|
|
3633
|
+
}
|
|
3634
|
+
const instance = requireActiveInstance();
|
|
3635
|
+
const client = new ServiceNowClient(instance);
|
|
3636
|
+
const fetchSpinner = ora6(`Fetching records from ${table}...`).start();
|
|
3637
|
+
let records;
|
|
3638
|
+
try {
|
|
3639
|
+
records = await client.queryTable(table, {
|
|
3640
|
+
sysparmQuery: opts.query,
|
|
3641
|
+
sysparmFields: `sys_id,name,short_description,number,sys_name`,
|
|
3642
|
+
sysparmLimit: limit
|
|
3643
|
+
});
|
|
3644
|
+
fetchSpinner.stop();
|
|
3645
|
+
} catch (err) {
|
|
3646
|
+
fetchSpinner.fail();
|
|
3647
|
+
console.error(chalk7.red(err instanceof Error ? err.message : String(err)));
|
|
3648
|
+
process.exit(1);
|
|
3649
|
+
}
|
|
3650
|
+
if (records.length === 0) {
|
|
3651
|
+
console.log(chalk7.yellow("No records matched the query."));
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
console.log(chalk7.bold(`
|
|
3655
|
+
${records.length} record(s) will be updated on ${chalk7.cyan(instance.alias)}:`));
|
|
3656
|
+
renderPreviewTable(records, fields);
|
|
3657
|
+
console.log();
|
|
3658
|
+
if (opts.dryRun) {
|
|
3659
|
+
console.log(chalk7.yellow("Dry run \u2014 no changes made."));
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
if (!opts.yes) {
|
|
3663
|
+
const ok = await confirm3({
|
|
3664
|
+
message: `Update ${records.length} record(s) in ${table}?`,
|
|
3665
|
+
default: false
|
|
3666
|
+
});
|
|
3667
|
+
if (!ok) {
|
|
3668
|
+
console.log(chalk7.dim("Aborted."));
|
|
3669
|
+
return;
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
let successCount = 0;
|
|
3673
|
+
let failCount = 0;
|
|
3674
|
+
const updateSpinner = ora6(`Updating 0/${records.length}...`).start();
|
|
3675
|
+
for (let i = 0; i < records.length; i++) {
|
|
3676
|
+
const sysId = String(records[i]["sys_id"]);
|
|
3677
|
+
updateSpinner.text = `Updating ${i + 1}/${records.length}...`;
|
|
3678
|
+
try {
|
|
3679
|
+
await client.updateRecord(table, sysId, fields);
|
|
3680
|
+
successCount++;
|
|
3681
|
+
} catch (err) {
|
|
3682
|
+
failCount++;
|
|
3683
|
+
updateSpinner.stop();
|
|
3684
|
+
console.error(chalk7.red(` Failed ${sysId}: ${err instanceof Error ? err.message : String(err)}`));
|
|
3685
|
+
updateSpinner.start(`Updating ${i + 1}/${records.length}...`);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
updateSpinner.stop();
|
|
3689
|
+
if (failCount === 0) {
|
|
3690
|
+
console.log(chalk7.green(`
|
|
3691
|
+
Updated ${successCount} record(s) successfully.`));
|
|
3692
|
+
} else {
|
|
3693
|
+
console.log(chalk7.yellow(`
|
|
3694
|
+
Updated ${successCount} record(s). ${failCount} failed.`));
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
return cmd;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// src/commands/user.ts
|
|
3701
|
+
init_esm_shims();
|
|
3702
|
+
init_config();
|
|
3703
|
+
init_client();
|
|
3704
|
+
import { Command as Command8 } from "commander";
|
|
3705
|
+
import chalk8 from "chalk";
|
|
3706
|
+
import ora7 from "ora";
|
|
3707
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
3708
|
+
async function resolveUser(client, query) {
|
|
3709
|
+
const isSysId = /^[0-9a-f]{32}$/i.test(query);
|
|
3710
|
+
const snQuery = isSysId ? `sys_id=${query}` : `user_name=${query}^ORemail=${query}^ORname=${query}`;
|
|
3711
|
+
const res = await client.queryTable("sys_user", {
|
|
3712
|
+
sysparmQuery: snQuery,
|
|
3713
|
+
sysparmFields: "sys_id,name,user_name,email",
|
|
3714
|
+
sysparmLimit: 2
|
|
3715
|
+
});
|
|
3716
|
+
if (res.length === 0) throw new Error(`User not found: ${query}`);
|
|
3717
|
+
if (res.length > 1) throw new Error(`Ambiguous user query "${query}" \u2014 matched multiple users. Use sys_id or user_name.`);
|
|
3718
|
+
return { sysId: res[0].sys_id, name: `${res[0].name} (${res[0].user_name})` };
|
|
3719
|
+
}
|
|
3720
|
+
async function resolveGroup(client, query) {
|
|
3721
|
+
const isSysId = /^[0-9a-f]{32}$/i.test(query);
|
|
3722
|
+
const snQuery = isSysId ? `sys_id=${query}` : `name=${query}`;
|
|
3723
|
+
const res = await client.queryTable("sys_user_group", {
|
|
3724
|
+
sysparmQuery: snQuery,
|
|
3725
|
+
sysparmFields: "sys_id,name",
|
|
3726
|
+
sysparmLimit: 2
|
|
3727
|
+
});
|
|
3728
|
+
if (res.length === 0) throw new Error(`Group not found: ${query}`);
|
|
3729
|
+
if (res.length > 1) throw new Error(`Ambiguous group query "${query}" \u2014 matched multiple groups. Use sys_id or exact name.`);
|
|
3730
|
+
return { sysId: res[0].sys_id, name: res[0].name };
|
|
3731
|
+
}
|
|
3732
|
+
async function resolveRole(client, query) {
|
|
3733
|
+
const isSysId = /^[0-9a-f]{32}$/i.test(query);
|
|
3734
|
+
const snQuery = isSysId ? `sys_id=${query}` : `name=${query}`;
|
|
3735
|
+
const res = await client.queryTable("sys_user_role", {
|
|
3736
|
+
sysparmQuery: snQuery,
|
|
3737
|
+
sysparmFields: "sys_id,name",
|
|
3738
|
+
sysparmLimit: 2
|
|
3739
|
+
});
|
|
3740
|
+
if (res.length === 0) throw new Error(`Role not found: ${query}`);
|
|
3741
|
+
if (res.length > 1) throw new Error(`Ambiguous role "${query}" \u2014 use sys_id or exact name.`);
|
|
3742
|
+
return { sysId: res[0].sys_id, name: res[0].name };
|
|
3743
|
+
}
|
|
3744
|
+
function userCommand() {
|
|
3745
|
+
const cmd = new Command8("user").description("Manage ServiceNow users, groups, and roles");
|
|
3746
|
+
cmd.command("add-to-group <user> <group>").description("Add a user to a group").option("--yes", "Skip confirmation").action(async (userQuery, groupQuery, opts) => {
|
|
3747
|
+
const instance = requireActiveInstance();
|
|
3748
|
+
const client = new ServiceNowClient(instance);
|
|
3749
|
+
const spinner = ora7("Resolving user and group...").start();
|
|
3750
|
+
let user;
|
|
3751
|
+
let group;
|
|
3752
|
+
try {
|
|
3753
|
+
[user, group] = await Promise.all([
|
|
3754
|
+
resolveUser(client, userQuery),
|
|
3755
|
+
resolveGroup(client, groupQuery)
|
|
3756
|
+
]);
|
|
3757
|
+
spinner.stop();
|
|
3758
|
+
} catch (err) {
|
|
3759
|
+
spinner.fail();
|
|
3760
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3761
|
+
process.exit(1);
|
|
3762
|
+
}
|
|
3763
|
+
const existing = await client.queryTable("sys_user_grmember", {
|
|
3764
|
+
sysparmQuery: `user=${user.sysId}^group=${group.sysId}`,
|
|
3765
|
+
sysparmFields: "sys_id",
|
|
3766
|
+
sysparmLimit: 1
|
|
3767
|
+
});
|
|
3768
|
+
if (existing.length > 0) {
|
|
3769
|
+
console.log(chalk8.yellow(`${user.name} is already a member of ${group.name}.`));
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
console.log(`Add ${chalk8.cyan(user.name)} to group ${chalk8.cyan(group.name)} on ${chalk8.bold(instance.alias)}?`);
|
|
3773
|
+
if (!opts.yes) {
|
|
3774
|
+
const ok = await confirm4({ message: "Proceed?", default: true });
|
|
3775
|
+
if (!ok) {
|
|
3776
|
+
console.log(chalk8.dim("Aborted."));
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
const addSpinner = ora7("Adding to group...").start();
|
|
3781
|
+
try {
|
|
3782
|
+
await client.createRecord("sys_user_grmember", { user: user.sysId, group: group.sysId });
|
|
3783
|
+
addSpinner.succeed(chalk8.green(`Added ${user.name} to ${group.name}.`));
|
|
3784
|
+
} catch (err) {
|
|
3785
|
+
addSpinner.fail();
|
|
3786
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3787
|
+
process.exit(1);
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
cmd.command("remove-from-group <user> <group>").description("Remove a user from a group").option("--yes", "Skip confirmation").action(async (userQuery, groupQuery, opts) => {
|
|
3791
|
+
const instance = requireActiveInstance();
|
|
3792
|
+
const client = new ServiceNowClient(instance);
|
|
3793
|
+
const spinner = ora7("Resolving user and group...").start();
|
|
3794
|
+
let user;
|
|
3795
|
+
let group;
|
|
3796
|
+
try {
|
|
3797
|
+
[user, group] = await Promise.all([
|
|
3798
|
+
resolveUser(client, userQuery),
|
|
3799
|
+
resolveGroup(client, groupQuery)
|
|
3800
|
+
]);
|
|
3801
|
+
spinner.stop();
|
|
3802
|
+
} catch (err) {
|
|
3803
|
+
spinner.fail();
|
|
3804
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3805
|
+
process.exit(1);
|
|
3806
|
+
}
|
|
3807
|
+
const members = await client.queryTable("sys_user_grmember", {
|
|
3808
|
+
sysparmQuery: `user=${user.sysId}^group=${group.sysId}`,
|
|
3809
|
+
sysparmFields: "sys_id",
|
|
3810
|
+
sysparmLimit: 1
|
|
3811
|
+
});
|
|
3812
|
+
if (members.length === 0) {
|
|
3813
|
+
console.log(chalk8.yellow(`${user.name} is not a member of ${group.name}.`));
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
console.log(`Remove ${chalk8.cyan(user.name)} from group ${chalk8.cyan(group.name)} on ${chalk8.bold(instance.alias)}?`);
|
|
3817
|
+
if (!opts.yes) {
|
|
3818
|
+
const ok = await confirm4({ message: "Proceed?", default: false });
|
|
3819
|
+
if (!ok) {
|
|
3820
|
+
console.log(chalk8.dim("Aborted."));
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
const removeSpinner = ora7("Removing from group...").start();
|
|
3825
|
+
try {
|
|
3826
|
+
await client.deleteRecord("sys_user_grmember", members[0].sys_id);
|
|
3827
|
+
removeSpinner.succeed(chalk8.green(`Removed ${user.name} from ${group.name}.`));
|
|
3828
|
+
} catch (err) {
|
|
3829
|
+
removeSpinner.fail();
|
|
3830
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3831
|
+
process.exit(1);
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
cmd.command("assign-role <user> <role>").description("Assign a role to a user").option("--yes", "Skip confirmation").action(async (userQuery, roleQuery, opts) => {
|
|
3835
|
+
const instance = requireActiveInstance();
|
|
3836
|
+
const client = new ServiceNowClient(instance);
|
|
3837
|
+
const spinner = ora7("Resolving user and role...").start();
|
|
3838
|
+
let user;
|
|
3839
|
+
let role;
|
|
3840
|
+
try {
|
|
3841
|
+
[user, role] = await Promise.all([
|
|
3842
|
+
resolveUser(client, userQuery),
|
|
3843
|
+
resolveRole(client, roleQuery)
|
|
3844
|
+
]);
|
|
3845
|
+
spinner.stop();
|
|
3846
|
+
} catch (err) {
|
|
3847
|
+
spinner.fail();
|
|
3848
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3849
|
+
process.exit(1);
|
|
3850
|
+
}
|
|
3851
|
+
const existing = await client.queryTable("sys_user_has_role", {
|
|
3852
|
+
sysparmQuery: `user=${user.sysId}^role=${role.sysId}`,
|
|
3853
|
+
sysparmFields: "sys_id",
|
|
3854
|
+
sysparmLimit: 1
|
|
3855
|
+
});
|
|
3856
|
+
if (existing.length > 0) {
|
|
3857
|
+
console.log(chalk8.yellow(`${user.name} already has role ${role.name}.`));
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
console.log(`Assign role ${chalk8.cyan(role.name)} to ${chalk8.cyan(user.name)} on ${chalk8.bold(instance.alias)}?`);
|
|
3861
|
+
if (!opts.yes) {
|
|
3862
|
+
const ok = await confirm4({ message: "Proceed?", default: true });
|
|
3863
|
+
if (!ok) {
|
|
3864
|
+
console.log(chalk8.dim("Aborted."));
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
const assignSpinner = ora7("Assigning role...").start();
|
|
3869
|
+
try {
|
|
3870
|
+
await client.createRecord("sys_user_has_role", { user: user.sysId, role: role.sysId });
|
|
3871
|
+
assignSpinner.succeed(chalk8.green(`Assigned role ${role.name} to ${user.name}.`));
|
|
3872
|
+
} catch (err) {
|
|
3873
|
+
assignSpinner.fail();
|
|
3874
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3875
|
+
process.exit(1);
|
|
3876
|
+
}
|
|
3877
|
+
});
|
|
3878
|
+
cmd.command("remove-role <user> <role>").description("Remove a role from a user").option("--yes", "Skip confirmation").action(async (userQuery, roleQuery, opts) => {
|
|
3879
|
+
const instance = requireActiveInstance();
|
|
3880
|
+
const client = new ServiceNowClient(instance);
|
|
3881
|
+
const spinner = ora7("Resolving user and role...").start();
|
|
3882
|
+
let user;
|
|
3883
|
+
let role;
|
|
3884
|
+
try {
|
|
3885
|
+
[user, role] = await Promise.all([
|
|
3886
|
+
resolveUser(client, userQuery),
|
|
3887
|
+
resolveRole(client, roleQuery)
|
|
3888
|
+
]);
|
|
3889
|
+
spinner.stop();
|
|
3890
|
+
} catch (err) {
|
|
3891
|
+
spinner.fail();
|
|
3892
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3893
|
+
process.exit(1);
|
|
3894
|
+
}
|
|
3895
|
+
const existing = await client.queryTable("sys_user_has_role", {
|
|
3896
|
+
sysparmQuery: `user=${user.sysId}^role=${role.sysId}`,
|
|
3897
|
+
sysparmFields: "sys_id",
|
|
3898
|
+
sysparmLimit: 1
|
|
3899
|
+
});
|
|
3900
|
+
if (existing.length === 0) {
|
|
3901
|
+
console.log(chalk8.yellow(`${user.name} does not have role ${role.name}.`));
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
console.log(`Remove role ${chalk8.cyan(role.name)} from ${chalk8.cyan(user.name)} on ${chalk8.bold(instance.alias)}?`);
|
|
3905
|
+
if (!opts.yes) {
|
|
3906
|
+
const ok = await confirm4({ message: "Proceed?", default: false });
|
|
3907
|
+
if (!ok) {
|
|
3908
|
+
console.log(chalk8.dim("Aborted."));
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
const removeSpinner = ora7("Removing role...").start();
|
|
3913
|
+
try {
|
|
3914
|
+
await client.deleteRecord("sys_user_has_role", existing[0].sys_id);
|
|
3915
|
+
removeSpinner.succeed(chalk8.green(`Removed role ${role.name} from ${user.name}.`));
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
removeSpinner.fail();
|
|
3918
|
+
console.error(chalk8.red(err instanceof Error ? err.message : String(err)));
|
|
3919
|
+
process.exit(1);
|
|
3920
|
+
}
|
|
3921
|
+
});
|
|
3922
|
+
return cmd;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// src/commands/attachment.ts
|
|
3926
|
+
init_esm_shims();
|
|
3927
|
+
init_config();
|
|
3928
|
+
init_client();
|
|
3929
|
+
import { Command as Command9 } from "commander";
|
|
3930
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
|
|
3931
|
+
import { join as join5, basename as basename2 } from "path";
|
|
3932
|
+
import { createReadStream, statSync as statSync2 } from "fs";
|
|
3933
|
+
import chalk9 from "chalk";
|
|
3934
|
+
import ora8 from "ora";
|
|
3935
|
+
function humanSize(bytes) {
|
|
3936
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3937
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3938
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3939
|
+
}
|
|
3940
|
+
async function listAttachments(client, table, sysId) {
|
|
3941
|
+
const res = await client.get("/api/now/attachment", {
|
|
3942
|
+
params: {
|
|
3943
|
+
sysparm_query: `table_name=${table}^table_sys_id=${sysId}`,
|
|
3944
|
+
sysparm_fields: "sys_id,file_name,content_type,size_bytes,table_name,table_sys_id",
|
|
3945
|
+
sysparm_limit: 500
|
|
3946
|
+
}
|
|
3947
|
+
});
|
|
3948
|
+
return res.result ?? [];
|
|
3949
|
+
}
|
|
3950
|
+
async function downloadAttachment(client, attSysId) {
|
|
3951
|
+
const res = await client.getAxiosInstance().get(
|
|
3952
|
+
`/api/now/attachment/${attSysId}/file`,
|
|
3953
|
+
{ responseType: "arraybuffer" }
|
|
3954
|
+
);
|
|
3955
|
+
return Buffer.from(res.data);
|
|
3956
|
+
}
|
|
3957
|
+
function attachmentCommand() {
|
|
3958
|
+
const cmd = new Command9("attachment").alias("att").description("Manage ServiceNow record attachments");
|
|
3959
|
+
cmd.command("list <table> <sys_id>").alias("ls").description("List attachments on a record").action(async (table, sysId) => {
|
|
3960
|
+
const instance = requireActiveInstance();
|
|
3961
|
+
const client = new ServiceNowClient(instance);
|
|
3962
|
+
const spinner = ora8("Fetching attachments...").start();
|
|
3963
|
+
let attachments;
|
|
3964
|
+
try {
|
|
3965
|
+
attachments = await listAttachments(client, table, sysId);
|
|
3966
|
+
spinner.stop();
|
|
3967
|
+
} catch (err) {
|
|
3968
|
+
spinner.fail();
|
|
3969
|
+
console.error(chalk9.red(err instanceof Error ? err.message : String(err)));
|
|
3970
|
+
process.exit(1);
|
|
3971
|
+
}
|
|
3972
|
+
if (attachments.length === 0) {
|
|
3973
|
+
console.log(chalk9.dim("No attachments found."));
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
console.log(chalk9.bold(`
|
|
3977
|
+
${attachments.length} attachment(s) on ${table}/${sysId}:
|
|
3978
|
+
`));
|
|
3979
|
+
const nameWidth = Math.max(9, ...attachments.map((a) => a.file_name.length));
|
|
3980
|
+
const typeWidth = Math.max(12, ...attachments.map((a) => a.content_type.length));
|
|
3981
|
+
console.log(
|
|
3982
|
+
chalk9.bold(
|
|
3983
|
+
`${"File name".padEnd(nameWidth)} ${"Content-Type".padEnd(typeWidth)} Size sys_id`
|
|
3984
|
+
)
|
|
3985
|
+
);
|
|
3986
|
+
console.log(`${"-".repeat(nameWidth)} ${"-".repeat(typeWidth)} --------- ${"-".repeat(32)}`);
|
|
3987
|
+
for (const att of attachments) {
|
|
3988
|
+
const size = humanSize(parseInt(att.size_bytes, 10) || 0);
|
|
3989
|
+
console.log(
|
|
3990
|
+
`${chalk9.cyan(att.file_name.padEnd(nameWidth))} ${att.content_type.padEnd(typeWidth)} ${size.padStart(9)} ${chalk9.dim(att.sys_id)}`
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
});
|
|
3994
|
+
cmd.command("pull <table> <sys_id>").description("Download attachment(s) from a record").option("-a, --all", "Download all attachments").option("-n, --name <file_name>", "Download a specific attachment by file name").option("-o, --out <dir>", "Output directory (default: current directory)").action(async (table, sysId, opts) => {
|
|
3995
|
+
if (!opts.all && !opts.name) {
|
|
3996
|
+
console.error(chalk9.red("Specify --all to download all attachments, or --name <file_name> for one."));
|
|
3997
|
+
process.exit(1);
|
|
3998
|
+
}
|
|
3999
|
+
const instance = requireActiveInstance();
|
|
4000
|
+
const client = new ServiceNowClient(instance);
|
|
4001
|
+
const outDir = opts.out ?? ".";
|
|
4002
|
+
if (!existsSync4(outDir)) mkdirSync5(outDir, { recursive: true });
|
|
4003
|
+
const spinner = ora8("Fetching attachment list...").start();
|
|
4004
|
+
let attachments;
|
|
4005
|
+
try {
|
|
4006
|
+
attachments = await listAttachments(client, table, sysId);
|
|
4007
|
+
spinner.stop();
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
spinner.fail();
|
|
4010
|
+
console.error(chalk9.red(err instanceof Error ? err.message : String(err)));
|
|
4011
|
+
process.exit(1);
|
|
4012
|
+
}
|
|
4013
|
+
if (attachments.length === 0) {
|
|
4014
|
+
console.log(chalk9.dim("No attachments found."));
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
let targets = attachments;
|
|
4018
|
+
if (opts.name) {
|
|
4019
|
+
targets = attachments.filter((a) => a.file_name === opts.name);
|
|
4020
|
+
if (targets.length === 0) {
|
|
4021
|
+
console.error(chalk9.red(`No attachment named "${opts.name}" found.`));
|
|
4022
|
+
process.exit(1);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
let downloaded = 0;
|
|
4026
|
+
for (const att of targets) {
|
|
4027
|
+
const dlSpinner = ora8(`Downloading ${att.file_name}...`).start();
|
|
4028
|
+
try {
|
|
4029
|
+
const buf = await downloadAttachment(client, att.sys_id);
|
|
4030
|
+
const dest = join5(outDir, att.file_name);
|
|
4031
|
+
writeFileSync5(dest, buf);
|
|
4032
|
+
dlSpinner.succeed(chalk9.green(`Saved: ${dest} (${humanSize(buf.length)})`));
|
|
4033
|
+
downloaded++;
|
|
4034
|
+
} catch (err) {
|
|
4035
|
+
dlSpinner.fail(chalk9.red(`Failed ${att.file_name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
if (targets.length > 1) {
|
|
4039
|
+
console.log(chalk9.bold(`
|
|
4040
|
+
${downloaded}/${targets.length} downloaded to ${outDir}`));
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
cmd.command("push <table> <sys_id> <file>").description("Upload a file as an attachment to a record").option("-t, --type <content-type>", "Override Content-Type (auto-detected by default)").action(async (table, sysId, file, opts) => {
|
|
4044
|
+
if (!existsSync4(file)) {
|
|
4045
|
+
console.error(chalk9.red(`File not found: ${file}`));
|
|
4046
|
+
process.exit(1);
|
|
4047
|
+
}
|
|
4048
|
+
const instance = requireActiveInstance();
|
|
4049
|
+
const client = new ServiceNowClient(instance);
|
|
4050
|
+
const fileName = basename2(file);
|
|
4051
|
+
const stat = statSync2(file);
|
|
4052
|
+
const contentType = opts.type ?? guessContentType(fileName);
|
|
4053
|
+
const spinner = ora8(`Uploading ${fileName} (${humanSize(stat.size)})...`).start();
|
|
4054
|
+
try {
|
|
4055
|
+
const stream = createReadStream(file);
|
|
4056
|
+
const res = await client.getAxiosInstance().post(
|
|
4057
|
+
"/api/now/attachment/file",
|
|
4058
|
+
stream,
|
|
4059
|
+
{
|
|
4060
|
+
params: {
|
|
4061
|
+
table_name: table,
|
|
4062
|
+
table_sys_id: sysId,
|
|
4063
|
+
file_name: fileName
|
|
4064
|
+
},
|
|
4065
|
+
headers: {
|
|
4066
|
+
"Content-Type": contentType,
|
|
4067
|
+
"Content-Length": stat.size
|
|
4068
|
+
},
|
|
4069
|
+
maxBodyLength: Infinity,
|
|
4070
|
+
maxContentLength: Infinity
|
|
4071
|
+
}
|
|
4072
|
+
);
|
|
4073
|
+
const created = res.data.result;
|
|
4074
|
+
spinner.succeed(
|
|
4075
|
+
chalk9.green(`Uploaded: ${created.file_name} (${humanSize(parseInt(created.size_bytes, 10))}) \u2014 sys_id: ${created.sys_id}`)
|
|
4076
|
+
);
|
|
4077
|
+
} catch (err) {
|
|
4078
|
+
spinner.fail();
|
|
4079
|
+
console.error(chalk9.red(err instanceof Error ? err.message : String(err)));
|
|
4080
|
+
process.exit(1);
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
return cmd;
|
|
4084
|
+
}
|
|
4085
|
+
function guessContentType(fileName) {
|
|
4086
|
+
const ext = fileName.slice(fileName.lastIndexOf(".")).toLowerCase();
|
|
4087
|
+
const map = {
|
|
4088
|
+
".pdf": "application/pdf",
|
|
4089
|
+
".png": "image/png",
|
|
4090
|
+
".jpg": "image/jpeg",
|
|
4091
|
+
".jpeg": "image/jpeg",
|
|
4092
|
+
".gif": "image/gif",
|
|
4093
|
+
".svg": "image/svg+xml",
|
|
4094
|
+
".txt": "text/plain",
|
|
4095
|
+
".csv": "text/csv",
|
|
4096
|
+
".xml": "application/xml",
|
|
4097
|
+
".json": "application/json",
|
|
4098
|
+
".zip": "application/zip",
|
|
4099
|
+
".js": "application/javascript",
|
|
4100
|
+
".html": "text/html",
|
|
4101
|
+
".css": "text/css",
|
|
4102
|
+
".md": "text/markdown",
|
|
4103
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
4104
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
4105
|
+
};
|
|
4106
|
+
return map[ext] ?? "application/octet-stream";
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// src/commands/updateset.ts
|
|
4110
|
+
init_esm_shims();
|
|
4111
|
+
init_config();
|
|
4112
|
+
init_client();
|
|
4113
|
+
import { Command as Command10 } from "commander";
|
|
4114
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync5 } from "fs";
|
|
4115
|
+
import { join as join6 } from "path";
|
|
4116
|
+
import chalk10 from "chalk";
|
|
4117
|
+
import ora9 from "ora";
|
|
4118
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
4119
|
+
async function resolveUpdateSet(client, query) {
|
|
4120
|
+
const isSysId = /^[0-9a-f]{32}$/i.test(query);
|
|
4121
|
+
const snQuery = isSysId ? `sys_id=${query}` : `nameCONTAINS${query}`;
|
|
4122
|
+
const res = await client.queryTable("sys_update_set", {
|
|
4123
|
+
sysparmQuery: snQuery,
|
|
4124
|
+
sysparmFields: "sys_id,name,description,state,application,is_default,sys_created_by,sys_created_on",
|
|
4125
|
+
sysparmDisplayValue: "true",
|
|
4126
|
+
sysparmLimit: isSysId ? 1 : 5
|
|
4127
|
+
});
|
|
4128
|
+
if (res.length === 0) throw new Error(`Update set not found: "${query}"`);
|
|
4129
|
+
if (res.length > 1) {
|
|
4130
|
+
const names = res.map((s) => ` ${chalk10.cyan(s.name)} (${s.sys_id})`).join("\n");
|
|
4131
|
+
throw new Error(`Ambiguous update set name "${query}" \u2014 matched ${res.length}:
|
|
4132
|
+
${names}
|
|
4133
|
+
Use sys_id or a more specific name.`);
|
|
4134
|
+
}
|
|
4135
|
+
return res[0];
|
|
4136
|
+
}
|
|
4137
|
+
async function getCurrentUpdateSet(client) {
|
|
4138
|
+
const prefs = await client.queryTable("sys_user_preference", {
|
|
4139
|
+
sysparmQuery: "name=sys_update_set^userISEMPTY^ORuser.user_name=javascript:gs.getUserName()",
|
|
4140
|
+
sysparmFields: "value",
|
|
4141
|
+
sysparmLimit: 1
|
|
4142
|
+
});
|
|
4143
|
+
const prefSysId = prefs[0]?.value;
|
|
4144
|
+
if (prefSysId) {
|
|
4145
|
+
const sets = await client.queryTable("sys_update_set", {
|
|
4146
|
+
sysparmQuery: `sys_id=${prefSysId}`,
|
|
4147
|
+
sysparmFields: "sys_id,name,description,state,application,is_default,sys_created_by,sys_created_on",
|
|
4148
|
+
sysparmDisplayValue: "true",
|
|
4149
|
+
sysparmLimit: 1
|
|
4150
|
+
});
|
|
4151
|
+
if (sets.length > 0) return sets[0];
|
|
4152
|
+
}
|
|
4153
|
+
const defaults = await client.queryTable("sys_update_set", {
|
|
4154
|
+
sysparmQuery: "state=in progress^is_default=true",
|
|
4155
|
+
sysparmFields: "sys_id,name,description,state,application,is_default,sys_created_by,sys_created_on",
|
|
4156
|
+
sysparmDisplayValue: "true",
|
|
4157
|
+
sysparmLimit: 1
|
|
4158
|
+
});
|
|
4159
|
+
return defaults[0] ?? null;
|
|
4160
|
+
}
|
|
4161
|
+
async function setCurrentUpdateSet(client, updateSetSysId) {
|
|
4162
|
+
const existing = await client.queryTable("sys_user_preference", {
|
|
4163
|
+
sysparmQuery: "name=sys_update_set^userISEMPTY",
|
|
4164
|
+
sysparmFields: "sys_id",
|
|
4165
|
+
sysparmLimit: 1
|
|
4166
|
+
});
|
|
4167
|
+
if (existing.length > 0) {
|
|
4168
|
+
await client.updateRecord("sys_user_preference", existing[0].sys_id, { value: updateSetSysId });
|
|
4169
|
+
} else {
|
|
4170
|
+
await client.createRecord("sys_user_preference", { name: "sys_update_set", value: updateSetSysId });
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
function displaySet(set) {
|
|
4174
|
+
const app = typeof set.application === "object" ? set.application.display_value : set.application;
|
|
4175
|
+
const isDefault = set.is_default === "true" || set.is_default === "1";
|
|
4176
|
+
const stateColor = set.state === "in progress" ? chalk10.green : set.state === "complete" ? chalk10.blue : chalk10.dim;
|
|
4177
|
+
console.log(`${chalk10.bold(set.name)}${isDefault ? chalk10.yellow(" \u2605 active") : ""}`);
|
|
4178
|
+
console.log(` sys_id: ${chalk10.dim(set.sys_id)}`);
|
|
4179
|
+
console.log(` state: ${stateColor(set.state)}`);
|
|
4180
|
+
console.log(` app: ${app || chalk10.dim("Global")}`);
|
|
4181
|
+
console.log(` created: ${set.sys_created_on} by ${set.sys_created_by}`);
|
|
4182
|
+
if (set.description) console.log(` desc: ${set.description}`);
|
|
4183
|
+
}
|
|
4184
|
+
function updatesetCommand() {
|
|
4185
|
+
const cmd = new Command10("updateset").alias("us").description("Manage ServiceNow update sets");
|
|
4186
|
+
cmd.command("list").alias("ls").description("List update sets on the instance").option("-s, --state <state>", 'Filter by state: "in progress", "complete", "ignore" (default: all)').option("-l, --limit <n>", "Max results (default: 50)", "50").action(async (opts) => {
|
|
4187
|
+
const instance = requireActiveInstance();
|
|
4188
|
+
const client = new ServiceNowClient(instance);
|
|
4189
|
+
const limit = parseInt(opts.limit, 10) || 50;
|
|
4190
|
+
const query = opts.state ? `state=${opts.state}` : "state!=ignore";
|
|
4191
|
+
const spinner = ora9("Fetching update sets...").start();
|
|
4192
|
+
let sets;
|
|
4193
|
+
try {
|
|
4194
|
+
sets = await client.queryTable("sys_update_set", {
|
|
4195
|
+
sysparmQuery: `${query}^ORDERBYDESCsys_created_on`,
|
|
4196
|
+
sysparmFields: "sys_id,name,state,application,is_default,sys_created_by,sys_created_on",
|
|
4197
|
+
sysparmDisplayValue: "true",
|
|
4198
|
+
sysparmLimit: limit
|
|
4199
|
+
});
|
|
4200
|
+
spinner.stop();
|
|
4201
|
+
} catch (err) {
|
|
4202
|
+
spinner.fail();
|
|
4203
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4204
|
+
process.exit(1);
|
|
4205
|
+
}
|
|
4206
|
+
if (sets.length === 0) {
|
|
4207
|
+
console.log(chalk10.dim("No update sets found."));
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const appLabel = (s) => (typeof s.application === "object" ? s.application.display_value : s.application) || "Global";
|
|
4211
|
+
const nameWidth = Math.max(4, ...sets.map((s) => s.name.length));
|
|
4212
|
+
const stateWidth = Math.max(5, ...sets.map((s) => s.state.length));
|
|
4213
|
+
const appWidth = Math.max(11, ...sets.map((s) => appLabel(s).length));
|
|
4214
|
+
console.log(chalk10.bold(
|
|
4215
|
+
`${"Name".padEnd(nameWidth)} ${"State".padEnd(stateWidth)} ${"Application".padEnd(appWidth)} ${"Created by".padEnd(16)} Created on`
|
|
4216
|
+
));
|
|
4217
|
+
console.log(`${"-".repeat(nameWidth)} ${"-".repeat(stateWidth)} ${"-".repeat(appWidth)} ${"-".repeat(16)} ${"-".repeat(19)}`);
|
|
4218
|
+
for (const s of sets) {
|
|
4219
|
+
const isDefault = s.is_default === "true" || s.is_default === "1";
|
|
4220
|
+
const stateStr = s.state === "in progress" ? chalk10.green(s.state.padEnd(stateWidth)) : chalk10.dim(s.state.padEnd(stateWidth));
|
|
4221
|
+
const marker = isDefault ? chalk10.yellow(" \u2605") : " ";
|
|
4222
|
+
const app = appLabel(s);
|
|
4223
|
+
console.log(`${chalk10.cyan(s.name.padEnd(nameWidth))}${marker} ${stateStr} ${app.padEnd(appWidth)} ${s.sys_created_by.padEnd(16)} ${s.sys_created_on}`);
|
|
4224
|
+
}
|
|
4225
|
+
});
|
|
4226
|
+
cmd.command("current").description("Show the currently active update set").action(async () => {
|
|
4227
|
+
const instance = requireActiveInstance();
|
|
4228
|
+
const client = new ServiceNowClient(instance);
|
|
4229
|
+
const spinner = ora9("Fetching current update set...").start();
|
|
4230
|
+
let current;
|
|
4231
|
+
try {
|
|
4232
|
+
current = await getCurrentUpdateSet(client);
|
|
4233
|
+
spinner.stop();
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
spinner.fail();
|
|
4236
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4237
|
+
process.exit(1);
|
|
4238
|
+
}
|
|
4239
|
+
if (!current) {
|
|
4240
|
+
console.log(chalk10.yellow("No active update set found. Use `snow updateset set <name>` to activate one."));
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
displaySet(current);
|
|
4244
|
+
});
|
|
4245
|
+
cmd.command("set <name>").description("Set the active update set (stored in sys_user_preference)").action(async (nameOrId) => {
|
|
4246
|
+
const instance = requireActiveInstance();
|
|
4247
|
+
const client = new ServiceNowClient(instance);
|
|
4248
|
+
const spinner = ora9("Resolving update set...").start();
|
|
4249
|
+
let set;
|
|
4250
|
+
try {
|
|
4251
|
+
set = await resolveUpdateSet(client, nameOrId);
|
|
4252
|
+
spinner.stop();
|
|
4253
|
+
} catch (err) {
|
|
4254
|
+
spinner.fail();
|
|
4255
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4256
|
+
process.exit(1);
|
|
4257
|
+
}
|
|
4258
|
+
const setSpinner = ora9(`Activating "${set.name}"...`).start();
|
|
4259
|
+
try {
|
|
4260
|
+
await setCurrentUpdateSet(client, set.sys_id);
|
|
4261
|
+
setSpinner.succeed(chalk10.green(`Active update set: ${set.name}`));
|
|
4262
|
+
} catch (err) {
|
|
4263
|
+
setSpinner.fail();
|
|
4264
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4265
|
+
process.exit(1);
|
|
4266
|
+
}
|
|
4267
|
+
});
|
|
4268
|
+
cmd.command("show <name>").description("Show details and captured items for an update set").option("-l, --limit <n>", "Max captured items to show (default: 100)", "100").action(async (nameOrId, opts) => {
|
|
4269
|
+
const instance = requireActiveInstance();
|
|
4270
|
+
const client = new ServiceNowClient(instance);
|
|
4271
|
+
const limit = parseInt(opts.limit, 10) || 100;
|
|
4272
|
+
const spinner = ora9("Fetching update set...").start();
|
|
4273
|
+
let set;
|
|
4274
|
+
try {
|
|
4275
|
+
set = await resolveUpdateSet(client, nameOrId);
|
|
4276
|
+
spinner.stop();
|
|
4277
|
+
} catch (err) {
|
|
4278
|
+
spinner.fail();
|
|
4279
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4280
|
+
process.exit(1);
|
|
4281
|
+
}
|
|
4282
|
+
displaySet(set);
|
|
4283
|
+
const itemSpinner = ora9("Fetching captured items...").start();
|
|
4284
|
+
let items;
|
|
4285
|
+
try {
|
|
4286
|
+
items = await client.queryTable("sys_update_xml", {
|
|
4287
|
+
sysparmQuery: `update_set=${set.sys_id}^ORDERBYtype`,
|
|
4288
|
+
sysparmFields: "sys_id,name,type,target_name,action,sys_created_on",
|
|
4289
|
+
sysparmLimit: limit
|
|
4290
|
+
});
|
|
4291
|
+
itemSpinner.stop();
|
|
4292
|
+
} catch (err) {
|
|
4293
|
+
itemSpinner.fail();
|
|
4294
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4295
|
+
process.exit(1);
|
|
4296
|
+
}
|
|
4297
|
+
if (items.length === 0) {
|
|
4298
|
+
console.log(chalk10.dim("\n No captured items."));
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
console.log(chalk10.bold(`
|
|
4302
|
+
${items.length} captured item(s):
|
|
4303
|
+
`));
|
|
4304
|
+
const typeWidth = Math.max(4, ...items.map((i) => i.type.length));
|
|
4305
|
+
const actionWidth = Math.max(6, ...items.map((i) => i.action.length));
|
|
4306
|
+
console.log(chalk10.bold(` ${"Type".padEnd(typeWidth)} ${"Action".padEnd(actionWidth)} Target`));
|
|
4307
|
+
console.log(` ${"-".repeat(typeWidth)} ${"-".repeat(actionWidth)} ${"-".repeat(40)}`);
|
|
4308
|
+
for (const item of items) {
|
|
4309
|
+
const actionColor = item.action === "INSERT_OR_UPDATE" ? chalk10.green : item.action === "DELETE" ? chalk10.red : chalk10.dim;
|
|
4310
|
+
console.log(` ${item.type.padEnd(typeWidth)} ${actionColor(item.action.padEnd(actionWidth))} ${item.target_name}`);
|
|
4311
|
+
}
|
|
4312
|
+
if (items.length === limit) {
|
|
4313
|
+
console.log(chalk10.dim(`
|
|
4314
|
+
(showing first ${limit} items \u2014 use --limit to increase)`));
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
cmd.command("capture <name>").description("Capture specific records into an update set by temporarily making it active").requiredOption("-a, --add <table:sys_id>", "Record to capture as table:sys_id (repeat for multiple)", (val, acc) => {
|
|
4318
|
+
acc.push(val);
|
|
4319
|
+
return acc;
|
|
4320
|
+
}, []).option("--yes", "Skip confirmation").action(async (nameOrId, opts) => {
|
|
4321
|
+
const instance = requireActiveInstance();
|
|
4322
|
+
const client = new ServiceNowClient(instance);
|
|
4323
|
+
const records = [];
|
|
4324
|
+
for (const entry of opts.add) {
|
|
4325
|
+
const colon = entry.indexOf(":");
|
|
4326
|
+
if (colon === -1) {
|
|
4327
|
+
console.error(chalk10.red(`Invalid format "${entry}" \u2014 expected table:sys_id`));
|
|
4328
|
+
process.exit(1);
|
|
4329
|
+
}
|
|
4330
|
+
records.push({ table: entry.slice(0, colon), sysId: entry.slice(colon + 1) });
|
|
4331
|
+
}
|
|
4332
|
+
const spinner = ora9("Resolving update set...").start();
|
|
4333
|
+
let set;
|
|
4334
|
+
let previousSet;
|
|
4335
|
+
try {
|
|
4336
|
+
[set, previousSet] = await Promise.all([
|
|
4337
|
+
resolveUpdateSet(client, nameOrId),
|
|
4338
|
+
getCurrentUpdateSet(client)
|
|
4339
|
+
]);
|
|
4340
|
+
spinner.stop();
|
|
4341
|
+
} catch (err) {
|
|
4342
|
+
spinner.fail();
|
|
4343
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4344
|
+
process.exit(1);
|
|
4345
|
+
}
|
|
4346
|
+
console.log(chalk10.bold(`Capture into update set: ${set.name}`));
|
|
4347
|
+
for (const r of records) console.log(` ${chalk10.cyan(r.table)} ${chalk10.dim(r.sysId)}`);
|
|
4348
|
+
console.log(chalk10.dim("\n Method: temporarily activates the update set, patches each record (no-op), then restores."));
|
|
4349
|
+
if (!opts.yes) {
|
|
4350
|
+
const ok = await confirm5({ message: "Proceed?", default: true });
|
|
4351
|
+
if (!ok) {
|
|
4352
|
+
console.log(chalk10.dim("Aborted."));
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
const activateSpinner = ora9(`Activating "${set.name}"...`).start();
|
|
4357
|
+
try {
|
|
4358
|
+
await setCurrentUpdateSet(client, set.sys_id);
|
|
4359
|
+
activateSpinner.succeed();
|
|
4360
|
+
} catch (err) {
|
|
4361
|
+
activateSpinner.fail();
|
|
4362
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
let captureFailures = 0;
|
|
4366
|
+
for (const { table, sysId } of records) {
|
|
4367
|
+
const captureSpinner = ora9(`Capturing ${table}/${sysId}...`).start();
|
|
4368
|
+
try {
|
|
4369
|
+
const record = await client.getRecord(table, sysId, { sysparmFields: "sys_id" });
|
|
4370
|
+
await client.updateRecord(table, record.sys_id, { sys_mod_count: record["sys_mod_count"] });
|
|
4371
|
+
captureSpinner.succeed(chalk10.green(`Captured: ${table}/${sysId}`));
|
|
4372
|
+
} catch (err) {
|
|
4373
|
+
captureSpinner.fail(chalk10.red(`Failed ${table}/${sysId}: ${err instanceof Error ? err.message : String(err)}`));
|
|
4374
|
+
captureFailures++;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
if (previousSet) {
|
|
4378
|
+
const restoreSpinner = ora9(`Restoring active update set to "${previousSet.name}"...`).start();
|
|
4379
|
+
try {
|
|
4380
|
+
await setCurrentUpdateSet(client, previousSet.sys_id);
|
|
4381
|
+
restoreSpinner.succeed();
|
|
4382
|
+
} catch {
|
|
4383
|
+
restoreSpinner.fail(chalk10.yellow("Could not restore previous update set \u2014 run `snow updateset set` manually."));
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
if (captureFailures === 0) {
|
|
4387
|
+
console.log(chalk10.green(`
|
|
4388
|
+
Captured ${records.length} record(s) into "${set.name}".`));
|
|
4389
|
+
} else {
|
|
4390
|
+
console.log(chalk10.yellow(`
|
|
4391
|
+
Captured ${records.length - captureFailures}/${records.length} record(s).`));
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
cmd.command("export <name>").description("Export an update set as an XML file").option("-o, --out <dir>", "Output directory (default: current directory)", ".").action(async (nameOrId, opts) => {
|
|
4395
|
+
const instance = requireActiveInstance();
|
|
4396
|
+
const client = new ServiceNowClient(instance);
|
|
4397
|
+
const resolveSpinner = ora9("Resolving update set...").start();
|
|
4398
|
+
let set;
|
|
4399
|
+
try {
|
|
4400
|
+
set = await resolveUpdateSet(client, nameOrId);
|
|
4401
|
+
resolveSpinner.stop();
|
|
4402
|
+
} catch (err) {
|
|
4403
|
+
resolveSpinner.fail();
|
|
4404
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4405
|
+
process.exit(1);
|
|
4406
|
+
}
|
|
4407
|
+
const exportSpinner = ora9(`Exporting "${set.name}"...`).start();
|
|
4408
|
+
let xmlContent;
|
|
4409
|
+
try {
|
|
4410
|
+
const res = await client.getAxiosInstance().get(
|
|
4411
|
+
"/export_update_set.do",
|
|
4412
|
+
{
|
|
4413
|
+
params: { type: "XML", sys_id: set.sys_id },
|
|
4414
|
+
responseType: "text"
|
|
4415
|
+
}
|
|
4416
|
+
);
|
|
4417
|
+
xmlContent = res.data;
|
|
4418
|
+
exportSpinner.stop();
|
|
4419
|
+
} catch (err) {
|
|
4420
|
+
exportSpinner.fail();
|
|
4421
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4422
|
+
process.exit(1);
|
|
4423
|
+
}
|
|
4424
|
+
if (!existsSync5(opts.out)) mkdirSync6(opts.out, { recursive: true });
|
|
4425
|
+
const safeName = set.name.replace(/[^a-z0-9_-]/gi, "_");
|
|
4426
|
+
const outPath = join6(opts.out, `${safeName}.xml`);
|
|
4427
|
+
writeFileSync6(outPath, xmlContent, "utf-8");
|
|
4428
|
+
console.log(chalk10.green(`Exported to: ${outPath}`));
|
|
4429
|
+
});
|
|
4430
|
+
cmd.command("apply <xml-file>").description("Import an update set XML into an instance (creates a Retrieved Update Set record)").option("-t, --target <alias>", "Target instance alias (default: active instance)").option("--yes", "Skip confirmation").action(async (xmlFile, opts) => {
|
|
4431
|
+
if (!existsSync5(xmlFile)) {
|
|
4432
|
+
console.error(chalk10.red(`File not found: ${xmlFile}`));
|
|
4433
|
+
process.exit(1);
|
|
4434
|
+
}
|
|
4435
|
+
const xmlContent = readFileSync5(xmlFile, "utf-8");
|
|
4436
|
+
const nameMatch = xmlContent.match(/<update_set[^>]*>[\s\S]*?<name>([^<]+)<\/name>/);
|
|
4437
|
+
const xmlName = nameMatch ? nameMatch[1] : xmlFile;
|
|
4438
|
+
let instance;
|
|
4439
|
+
if (opts.target) {
|
|
4440
|
+
const config = loadConfig();
|
|
4441
|
+
instance = config.instances[opts.target];
|
|
4442
|
+
if (!instance) {
|
|
4443
|
+
console.error(chalk10.red(`Instance alias not found: ${opts.target}`));
|
|
4444
|
+
process.exit(1);
|
|
4445
|
+
}
|
|
4446
|
+
} else {
|
|
4447
|
+
instance = requireActiveInstance();
|
|
4448
|
+
}
|
|
4449
|
+
console.log(`Import ${chalk10.cyan(xmlName)} \u2192 ${chalk10.bold(instance.alias)} (${instance.url})`);
|
|
4450
|
+
if (!opts.yes) {
|
|
4451
|
+
const ok = await confirm5({ message: "Proceed?", default: true });
|
|
4452
|
+
if (!ok) {
|
|
4453
|
+
console.log(chalk10.dim("Aborted."));
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
const client = new ServiceNowClient(instance);
|
|
4458
|
+
const importSpinner = ora9("Uploading update set XML...").start();
|
|
4459
|
+
let remoteSetSysId;
|
|
4460
|
+
try {
|
|
4461
|
+
const res = await client.createRecord("sys_remote_update_set", {
|
|
4462
|
+
name: xmlName,
|
|
4463
|
+
payload: xmlContent,
|
|
4464
|
+
state: "loaded"
|
|
4465
|
+
});
|
|
4466
|
+
remoteSetSysId = res.sys_id;
|
|
4467
|
+
importSpinner.succeed(chalk10.green(`Created retrieved update set: ${remoteSetSysId}`));
|
|
4468
|
+
} catch (err) {
|
|
4469
|
+
importSpinner.fail();
|
|
4470
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4471
|
+
process.exit(1);
|
|
4472
|
+
}
|
|
4473
|
+
console.log();
|
|
4474
|
+
console.log(chalk10.bold("Next steps in ServiceNow:"));
|
|
4475
|
+
console.log(` 1. Navigate to ${chalk10.cyan("System Update Sets \u2192 Retrieved Update Sets")}`);
|
|
4476
|
+
console.log(` 2. Open ${chalk10.cyan(xmlName)}`);
|
|
4477
|
+
console.log(` 3. Click ${chalk10.cyan("Preview Update Set")} \u2192 resolve any conflicts`);
|
|
4478
|
+
console.log(` 4. Click ${chalk10.cyan("Commit Update Set")}`);
|
|
4479
|
+
console.log();
|
|
4480
|
+
console.log(chalk10.dim(` Direct link: ${instance.url}/sys_remote_update_set.do?sys_id=${remoteSetSysId}`));
|
|
4481
|
+
});
|
|
4482
|
+
cmd.command("diff <set1> <set2>").description("Compare captured items between two update sets").option("-l, --limit <n>", "Max captured items per set (default: 500)", "500").action(async (nameOrId1, nameOrId2, opts) => {
|
|
4483
|
+
const instance = requireActiveInstance();
|
|
4484
|
+
const client = new ServiceNowClient(instance);
|
|
4485
|
+
const limit = parseInt(opts.limit, 10) || 500;
|
|
4486
|
+
const spinner = ora9("Fetching both update sets...").start();
|
|
4487
|
+
let set1, set2;
|
|
4488
|
+
let items1, items2;
|
|
4489
|
+
try {
|
|
4490
|
+
[set1, set2] = await Promise.all([
|
|
4491
|
+
resolveUpdateSet(client, nameOrId1),
|
|
4492
|
+
resolveUpdateSet(client, nameOrId2)
|
|
4493
|
+
]);
|
|
4494
|
+
const fetchItems = (sysId) => client.queryTable("sys_update_xml", {
|
|
4495
|
+
sysparmQuery: `update_set=${sysId}`,
|
|
4496
|
+
sysparmFields: "sys_id,name,type,target_name,action,sys_created_on",
|
|
4497
|
+
sysparmLimit: limit
|
|
4498
|
+
});
|
|
4499
|
+
[items1, items2] = await Promise.all([fetchItems(set1.sys_id), fetchItems(set2.sys_id)]);
|
|
4500
|
+
spinner.stop();
|
|
4501
|
+
} catch (err) {
|
|
4502
|
+
spinner.fail();
|
|
4503
|
+
console.error(chalk10.red(err instanceof Error ? err.message : String(err)));
|
|
4504
|
+
process.exit(1);
|
|
4505
|
+
}
|
|
4506
|
+
const map1 = new Map(items1.map((i) => [i.target_name, i]));
|
|
4507
|
+
const map2 = new Map(items2.map((i) => [i.target_name, i]));
|
|
4508
|
+
const onlyIn1 = [];
|
|
4509
|
+
const onlyIn2 = [];
|
|
4510
|
+
const inBoth = [];
|
|
4511
|
+
for (const [key, item] of map1) {
|
|
4512
|
+
if (map2.has(key)) inBoth.push({ item1: item, item2: map2.get(key) });
|
|
4513
|
+
else onlyIn1.push(item);
|
|
4514
|
+
}
|
|
4515
|
+
for (const [key, item] of map2) {
|
|
4516
|
+
if (!map1.has(key)) onlyIn2.push(item);
|
|
4517
|
+
}
|
|
4518
|
+
console.log(chalk10.bold(`
|
|
4519
|
+
Diff: ${chalk10.cyan(set1.name)} \u2190 \u2192 ${chalk10.cyan(set2.name)}
|
|
4520
|
+
`));
|
|
4521
|
+
console.log(` Items in ${chalk10.cyan(set1.name)}: ${items1.length}`);
|
|
4522
|
+
console.log(` Items in ${chalk10.cyan(set2.name)}: ${items2.length}`);
|
|
4523
|
+
console.log();
|
|
4524
|
+
if (onlyIn1.length > 0) {
|
|
4525
|
+
console.log(chalk10.red(` Only in "${set1.name}" (${onlyIn1.length}):`));
|
|
4526
|
+
for (const i of onlyIn1) {
|
|
4527
|
+
console.log(` ${chalk10.red("\u2212")} ${i.type.padEnd(30)} ${i.target_name}`);
|
|
4528
|
+
}
|
|
4529
|
+
console.log();
|
|
4530
|
+
}
|
|
4531
|
+
if (onlyIn2.length > 0) {
|
|
4532
|
+
console.log(chalk10.green(` Only in "${set2.name}" (${onlyIn2.length}):`));
|
|
4533
|
+
for (const i of onlyIn2) {
|
|
4534
|
+
console.log(` ${chalk10.green("+")} ${i.type.padEnd(30)} ${i.target_name}`);
|
|
4535
|
+
}
|
|
4536
|
+
console.log();
|
|
4537
|
+
}
|
|
4538
|
+
if (inBoth.length > 0) {
|
|
4539
|
+
console.log(chalk10.dim(` In both (${inBoth.length}):`));
|
|
4540
|
+
for (const { item1, item2 } of inBoth) {
|
|
4541
|
+
const actionChanged = item1.action !== item2.action;
|
|
4542
|
+
const marker = actionChanged ? chalk10.yellow("~") : chalk10.dim("=");
|
|
4543
|
+
const detail = actionChanged ? chalk10.yellow(` [action: ${item1.action} \u2192 ${item2.action}]`) : "";
|
|
4544
|
+
console.log(` ${marker} ${item1.type.padEnd(30)} ${item1.target_name}${detail}`);
|
|
4545
|
+
}
|
|
4546
|
+
console.log();
|
|
4547
|
+
}
|
|
4548
|
+
const summary = [
|
|
4549
|
+
onlyIn1.length > 0 ? chalk10.red(`${onlyIn1.length} removed`) : "",
|
|
4550
|
+
onlyIn2.length > 0 ? chalk10.green(`${onlyIn2.length} added`) : "",
|
|
4551
|
+
inBoth.filter(({ item1, item2 }) => item1.action !== item2.action).length > 0 ? chalk10.yellow(`${inBoth.filter(({ item1, item2 }) => item1.action !== item2.action).length} changed`) : "",
|
|
4552
|
+
chalk10.dim(`${inBoth.filter(({ item1, item2 }) => item1.action === item2.action).length} unchanged`)
|
|
4553
|
+
].filter(Boolean).join(" ");
|
|
4554
|
+
console.log(` ${summary}`);
|
|
4555
|
+
});
|
|
4556
|
+
return cmd;
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
// src/commands/status.ts
|
|
4560
|
+
init_esm_shims();
|
|
4561
|
+
init_config();
|
|
4562
|
+
init_client();
|
|
4563
|
+
import { Command as Command11 } from "commander";
|
|
4564
|
+
import chalk11 from "chalk";
|
|
4565
|
+
import ora10 from "ora";
|
|
4566
|
+
async function countRecords(client, table, query) {
|
|
4567
|
+
const res = await client.get(
|
|
4568
|
+
`/api/now/stats/${table}`,
|
|
4569
|
+
{ params: { sysparm_count: true, sysparm_query: query } }
|
|
4570
|
+
);
|
|
4571
|
+
return parseInt(res.result?.stats?.count ?? "0", 10);
|
|
4572
|
+
}
|
|
4573
|
+
var DIVIDER = chalk11.dim("\u2500".repeat(52));
|
|
4574
|
+
var NA = chalk11.dim("N/A");
|
|
4575
|
+
function section(title) {
|
|
4576
|
+
console.log();
|
|
4577
|
+
console.log(chalk11.bold.cyan(` ${title}`));
|
|
4578
|
+
console.log(chalk11.dim(` ${"\u2500".repeat(title.length)}`));
|
|
4579
|
+
}
|
|
4580
|
+
function row(label, value, width = 22) {
|
|
4581
|
+
console.log(` ${label.padEnd(width)}${value}`);
|
|
4582
|
+
}
|
|
4583
|
+
async function fetchVersion(client, debug = false) {
|
|
4584
|
+
const props = await client.queryTable("sys_properties", {
|
|
4585
|
+
sysparmQuery: "nameSTARTSWITHglide.build",
|
|
4586
|
+
sysparmFields: "name,value",
|
|
4587
|
+
sysparmLimit: 50
|
|
4588
|
+
});
|
|
4589
|
+
if (debug) {
|
|
4590
|
+
console.log(chalk11.dim(` [debug] glide.build.* properties returned (${props.length}):`));
|
|
4591
|
+
for (const p of props) {
|
|
4592
|
+
console.log(chalk11.dim(` ${p.name} = ${JSON.stringify(p.value)}`));
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
if (props.length === 0) throw new Error("No glide.build.* properties accessible");
|
|
4596
|
+
const val = (key) => {
|
|
4597
|
+
const p = props.find((r) => r.name === key);
|
|
4598
|
+
const raw = p?.value;
|
|
4599
|
+
if (!raw) return "";
|
|
4600
|
+
if (typeof raw === "object" && raw !== null) {
|
|
4601
|
+
return raw["value"] ?? raw["display_value"] ?? "";
|
|
4602
|
+
}
|
|
4603
|
+
return String(raw).trim();
|
|
4604
|
+
};
|
|
4605
|
+
const buildtag = val("glide.buildtag.last");
|
|
4606
|
+
const date = val("glide.build.date");
|
|
4607
|
+
if (!buildtag) throw new Error("glide.buildtag.last is empty or not accessible");
|
|
4608
|
+
const dateShort = date.match(/^(\d{2})-(\d{2})-(\d{4})/) ? `${date.slice(6, 10)}-${date.slice(0, 2)}-${date.slice(3, 5)}` : date.replace(/_\d+$/, "");
|
|
4609
|
+
return { version: buildtag, date: dateShort };
|
|
4610
|
+
}
|
|
4611
|
+
async function fetchNodes(client) {
|
|
4612
|
+
const nodes = await client.queryTable("sys_cluster_state", {
|
|
4613
|
+
sysparmFields: "node_name,status",
|
|
4614
|
+
sysparmLimit: 50
|
|
4615
|
+
});
|
|
4616
|
+
const active = nodes.filter((n) => n.status === "Online" || n.status === "online").length;
|
|
4617
|
+
return { active: active || nodes.length, total: nodes.length };
|
|
4618
|
+
}
|
|
4619
|
+
async function fetchActiveUsers(client) {
|
|
4620
|
+
return countRecords(client, "sys_user", "active=true");
|
|
4621
|
+
}
|
|
4622
|
+
async function fetchCustomApps(client) {
|
|
4623
|
+
return countRecords(client, "sys_app", "active=true^scopeSTARTSWITHx_");
|
|
4624
|
+
}
|
|
4625
|
+
async function fetchCustomTables(client) {
|
|
4626
|
+
return countRecords(client, "sys_db_object", "nameSTARTSWITHx_");
|
|
4627
|
+
}
|
|
4628
|
+
async function fetchInProgressUpdateSets(client) {
|
|
4629
|
+
return client.queryTable("sys_update_set", {
|
|
4630
|
+
sysparmQuery: "state=in progress^ORDERBYDESCsys_created_on",
|
|
4631
|
+
sysparmFields: "name,sys_created_by",
|
|
4632
|
+
sysparmLimit: 5
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
async function fetchSyslogErrors(client) {
|
|
4636
|
+
const [count, recent] = await Promise.all([
|
|
4637
|
+
countRecords(client, "syslog", "level=error^sys_created_on>javascript:gs.hoursAgo(1)"),
|
|
4638
|
+
client.queryTable("syslog", {
|
|
4639
|
+
sysparmQuery: "level=error^sys_created_on>javascript:gs.hoursAgo(1)^ORDERBYDESCsys_created_on",
|
|
4640
|
+
sysparmFields: "sys_created_on,message",
|
|
4641
|
+
sysparmLimit: 3
|
|
4642
|
+
})
|
|
4643
|
+
]);
|
|
4644
|
+
return { count, recent };
|
|
4645
|
+
}
|
|
4646
|
+
async function fetchSchedulerErrors(client) {
|
|
4647
|
+
const [count, recent] = await Promise.all([
|
|
4648
|
+
countRecords(client, "syslog", "level=error^sys_created_on>javascript:gs.hoursAgo(24)^source=SCHEDULER"),
|
|
4649
|
+
client.queryTable("syslog", {
|
|
4650
|
+
sysparmQuery: "level=error^sys_created_on>javascript:gs.hoursAgo(24)^source=SCHEDULER^ORDERBYDESCsys_created_on",
|
|
4651
|
+
sysparmFields: "message",
|
|
4652
|
+
sysparmLimit: 3
|
|
4653
|
+
})
|
|
4654
|
+
]);
|
|
4655
|
+
return { count, recent };
|
|
4656
|
+
}
|
|
4657
|
+
function statusCommand() {
|
|
4658
|
+
const cmd = new Command11("status").description("Show a health and stats overview of the active instance").option("--no-errors", "Skip the syslog error sections (faster for restricted users)").option("--debug", "Print raw property values to help diagnose N/A sections").action(async (opts) => {
|
|
4659
|
+
const instance = requireActiveInstance();
|
|
4660
|
+
const client = new ServiceNowClient(instance);
|
|
4661
|
+
const spinner = ora10("Fetching instance stats...").start();
|
|
4662
|
+
const [
|
|
4663
|
+
versionResult,
|
|
4664
|
+
nodesResult,
|
|
4665
|
+
activeUsersResult,
|
|
4666
|
+
customAppsResult,
|
|
4667
|
+
customTablesResult,
|
|
4668
|
+
updateSetsResult,
|
|
4669
|
+
syslogResult,
|
|
4670
|
+
schedulerResult
|
|
4671
|
+
] = await Promise.allSettled([
|
|
4672
|
+
fetchVersion(client, opts.debug),
|
|
4673
|
+
fetchNodes(client),
|
|
4674
|
+
fetchActiveUsers(client),
|
|
4675
|
+
fetchCustomApps(client),
|
|
4676
|
+
fetchCustomTables(client),
|
|
4677
|
+
fetchInProgressUpdateSets(client),
|
|
4678
|
+
opts.errors ? fetchSyslogErrors(client) : Promise.reject(new Error("skipped")),
|
|
4679
|
+
opts.errors ? fetchSchedulerErrors(client) : Promise.reject(new Error("skipped"))
|
|
4680
|
+
]);
|
|
4681
|
+
spinner.stop();
|
|
4682
|
+
console.log();
|
|
4683
|
+
console.log(DIVIDER);
|
|
4684
|
+
console.log(` ${chalk11.bold("snow-cli")} \xB7 ${chalk11.cyan(instance.alias)} ${chalk11.dim(instance.url)}`);
|
|
4685
|
+
console.log(DIVIDER);
|
|
4686
|
+
section("Instance");
|
|
4687
|
+
if (versionResult.status === "fulfilled") {
|
|
4688
|
+
row("Version", chalk11.white(versionResult.value.version));
|
|
4689
|
+
if (versionResult.value.date) {
|
|
4690
|
+
row("Last updated", chalk11.dim(versionResult.value.date));
|
|
4691
|
+
}
|
|
4692
|
+
} else {
|
|
4693
|
+
const reason = opts.debug ? NA + chalk11.dim(` (${versionResult.reason?.message ?? "unknown error"})`) : NA;
|
|
4694
|
+
row("Version", reason);
|
|
4695
|
+
}
|
|
4696
|
+
if (nodesResult.status === "fulfilled") {
|
|
4697
|
+
const { active, total } = nodesResult.value;
|
|
4698
|
+
const nodeStr = total === 0 ? NA : total === 1 ? chalk11.white("1 (single-node)") : `${chalk11.white(String(active))} active / ${total} total`;
|
|
4699
|
+
row("Cluster nodes", nodeStr);
|
|
4700
|
+
} else {
|
|
4701
|
+
row("Cluster nodes", NA);
|
|
4702
|
+
}
|
|
4703
|
+
section("Users");
|
|
4704
|
+
if (activeUsersResult.status === "fulfilled") {
|
|
4705
|
+
row("Active users", chalk11.white(activeUsersResult.value.toLocaleString()));
|
|
4706
|
+
} else {
|
|
4707
|
+
row("Active users", NA);
|
|
4708
|
+
}
|
|
4709
|
+
section("Development");
|
|
4710
|
+
if (customAppsResult.status === "fulfilled") {
|
|
4711
|
+
row("Custom apps", chalk11.white(String(customAppsResult.value)));
|
|
4712
|
+
} else {
|
|
4713
|
+
row("Custom apps", NA);
|
|
4714
|
+
}
|
|
4715
|
+
if (customTablesResult.status === "fulfilled") {
|
|
4716
|
+
row("Custom tables", chalk11.white(String(customTablesResult.value)));
|
|
4717
|
+
} else {
|
|
4718
|
+
row("Custom tables", NA);
|
|
4719
|
+
}
|
|
4720
|
+
if (updateSetsResult.status === "fulfilled") {
|
|
4721
|
+
const sets = updateSetsResult.value;
|
|
4722
|
+
const countStr = sets.length === 0 ? chalk11.dim("none") : sets.length === 5 ? chalk11.yellow(`${sets.length}+ in progress`) : chalk11.white(`${sets.length} in progress`);
|
|
4723
|
+
row("Update sets", countStr);
|
|
4724
|
+
for (const s of sets) {
|
|
4725
|
+
const truncName = s.name.length > 32 ? s.name.slice(0, 31) + "\u2026" : s.name;
|
|
4726
|
+
console.log(` ${"".padEnd(22)}${chalk11.dim("\u2022 ")}${truncName} ${chalk11.dim(s.sys_created_by)}`);
|
|
4727
|
+
}
|
|
4728
|
+
} else {
|
|
4729
|
+
row("Update sets", NA);
|
|
4730
|
+
}
|
|
4731
|
+
if (!opts.errors) {
|
|
4732
|
+
console.log();
|
|
4733
|
+
console.log(chalk11.dim(" (error sections skipped \u2014 pass --errors to include)"));
|
|
4734
|
+
} else {
|
|
4735
|
+
section("Syslog errors (last hour)");
|
|
4736
|
+
if (syslogResult.status === "fulfilled") {
|
|
4737
|
+
const { count, recent } = syslogResult.value;
|
|
4738
|
+
const countColor = count === 0 ? chalk11.green : count < 10 ? chalk11.yellow : chalk11.red;
|
|
4739
|
+
row("Error count", countColor(String(count)));
|
|
4740
|
+
for (const e of recent) {
|
|
4741
|
+
const time = e.sys_created_on.slice(11, 19);
|
|
4742
|
+
const msg = e.message.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
4743
|
+
console.log(` ${"".padEnd(22)}${chalk11.dim(`[${time}]`)} ${msg}`);
|
|
4744
|
+
}
|
|
4745
|
+
} else {
|
|
4746
|
+
row("Error count", NA + chalk11.dim(" (no access to syslog)"));
|
|
4747
|
+
}
|
|
4748
|
+
section("Scheduler errors (last 24h)");
|
|
4749
|
+
if (schedulerResult.status === "fulfilled") {
|
|
4750
|
+
const { count, recent } = schedulerResult.value;
|
|
4751
|
+
const countColor = count === 0 ? chalk11.green : count < 5 ? chalk11.yellow : chalk11.red;
|
|
4752
|
+
row("Failed jobs", countColor(String(count)));
|
|
4753
|
+
for (const e of recent) {
|
|
4754
|
+
const msg = e.message.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
4755
|
+
console.log(` ${"".padEnd(22)}${chalk11.dim("\u2022 ")}${msg}`);
|
|
4756
|
+
}
|
|
4757
|
+
} else {
|
|
4758
|
+
row("Failed jobs", NA + chalk11.dim(" (no access to syslog)"));
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
console.log();
|
|
4762
|
+
console.log(DIVIDER);
|
|
4763
|
+
console.log();
|
|
4764
|
+
});
|
|
4765
|
+
return cmd;
|
|
4766
|
+
}
|
|
4767
|
+
|
|
3426
4768
|
// src/index.ts
|
|
3427
|
-
import { readFileSync as
|
|
4769
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3428
4770
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3429
|
-
import { join as
|
|
4771
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
3430
4772
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
3431
4773
|
var __dirname2 = dirname2(__filename2);
|
|
3432
4774
|
function getVersion() {
|
|
3433
4775
|
try {
|
|
3434
4776
|
const pkg = JSON.parse(
|
|
3435
|
-
|
|
4777
|
+
readFileSync6(join7(__dirname2, "..", "package.json"), "utf-8")
|
|
3436
4778
|
);
|
|
3437
4779
|
return pkg.version;
|
|
3438
4780
|
} catch {
|
|
3439
4781
|
return "0.0.0";
|
|
3440
4782
|
}
|
|
3441
4783
|
}
|
|
3442
|
-
var program = new
|
|
3443
|
-
program.name("snow").description(
|
|
4784
|
+
var program = new Command12();
|
|
4785
|
+
program.name("snow").description(chalk12.bold("snow") + " \u2014 ServiceNow CLI: query tables, edit scripts, and generate apps with AI").version(getVersion(), "-v, --version", "Output the current version").addHelpText(
|
|
3444
4786
|
"after",
|
|
3445
4787
|
`
|
|
3446
|
-
${
|
|
3447
|
-
${
|
|
4788
|
+
${chalk12.bold("Examples:")}
|
|
4789
|
+
${chalk12.dim("# Add a ServiceNow instance")}
|
|
3448
4790
|
snow instance add
|
|
3449
4791
|
|
|
3450
|
-
${
|
|
4792
|
+
${chalk12.dim("# Query records from a table")}
|
|
3451
4793
|
snow table get incident -q "active=true" -l 10
|
|
3452
4794
|
|
|
3453
|
-
${
|
|
4795
|
+
${chalk12.dim("# View the schema for a table")}
|
|
3454
4796
|
snow schema incident
|
|
3455
4797
|
|
|
3456
|
-
${
|
|
4798
|
+
${chalk12.dim("# Pull a script field, edit it, and push back")}
|
|
3457
4799
|
snow script pull sys_script_include <sys_id> script
|
|
3458
4800
|
|
|
3459
|
-
${
|
|
4801
|
+
${chalk12.dim("# Configure an LLM provider (OpenAI, Anthropic, xAI/Grok, or Ollama)")}
|
|
3460
4802
|
snow provider set openai
|
|
3461
4803
|
snow provider set anthropic
|
|
3462
4804
|
snow provider set xai
|
|
3463
4805
|
snow provider set ollama --model llama3
|
|
3464
4806
|
|
|
3465
|
-
${
|
|
4807
|
+
${chalk12.dim("# Generate a ServiceNow app and export as an update set XML")}
|
|
3466
4808
|
snow ai build "Create a script include that auto-routes incidents by category"
|
|
3467
4809
|
|
|
3468
|
-
${
|
|
4810
|
+
${chalk12.dim("# Generate and immediately push artifacts to the active instance")}
|
|
3469
4811
|
snow ai build "Create a business rule that sets priority on incident insert" --push
|
|
3470
4812
|
|
|
3471
|
-
${
|
|
4813
|
+
${chalk12.dim("# Interactive multi-turn app builder")}
|
|
3472
4814
|
snow ai chat
|
|
3473
4815
|
`
|
|
3474
4816
|
);
|
|
@@ -3476,6 +4818,11 @@ program.addCommand(instanceCommand());
|
|
|
3476
4818
|
program.addCommand(tableCommand());
|
|
3477
4819
|
program.addCommand(schemaCommand());
|
|
3478
4820
|
program.addCommand(scriptCommand());
|
|
4821
|
+
program.addCommand(bulkCommand());
|
|
4822
|
+
program.addCommand(userCommand());
|
|
4823
|
+
program.addCommand(attachmentCommand());
|
|
4824
|
+
program.addCommand(updatesetCommand());
|
|
4825
|
+
program.addCommand(statusCommand());
|
|
3479
4826
|
program.addCommand(providerCommand());
|
|
3480
4827
|
program.addCommand(aiCommand());
|
|
3481
4828
|
program.command("instances", { hidden: true }).description("Alias for `snow instance list`").action(() => {
|
|
@@ -3483,6 +4830,6 @@ program.command("instances", { hidden: true }).description("Alias for `snow inst
|
|
|
3483
4830
|
sub.parse(["node", "snow", "list"]);
|
|
3484
4831
|
});
|
|
3485
4832
|
program.parseAsync(process.argv).catch((err) => {
|
|
3486
|
-
console.error(
|
|
4833
|
+
console.error(chalk12.red(err instanceof Error ? err.message : String(err)));
|
|
3487
4834
|
process.exit(1);
|
|
3488
4835
|
});
|