@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.
Files changed (3) hide show
  1. package/README.md +380 -9
  2. package/dist/index.js +1376 -29
  3. 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 Command7 } from "commander";
325
- import chalk7 from "chalk";
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, row) => {
521
- return Math.max(max, flattenValue(row[k]).length);
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 row of rows) {
531
+ for (const row2 of rows) {
532
532
  const line = keys.map((k, i) => {
533
- const val = flattenValue(row[k]);
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: confirm2 } = await import("@inquirer/prompts");
635
- const ok = await confirm2({
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: confirm2 } = await import("@inquirer/prompts");
1390
- const shouldPush = await confirm2({
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: confirm2 } = await import("@inquirer/prompts");
3014
- shouldPush = await confirm2({
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: confirm2 } = await import("@inquirer/prompts");
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 confirm2({ message: "Open in editor to edit?", default: false });
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 readFileSync4 } from "fs";
4769
+ import { readFileSync as readFileSync6 } from "fs";
3428
4770
  import { fileURLToPath as fileURLToPath2 } from "url";
3429
- import { join as join5, dirname as dirname2 } from "path";
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
- readFileSync4(join5(__dirname2, "..", "package.json"), "utf-8")
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 Command7();
3443
- program.name("snow").description(chalk7.bold("snow") + " \u2014 ServiceNow CLI: query tables, edit scripts, and generate apps with AI").version(getVersion(), "-v, --version", "Output the current version").addHelpText(
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
- ${chalk7.bold("Examples:")}
3447
- ${chalk7.dim("# Add a ServiceNow instance")}
4788
+ ${chalk12.bold("Examples:")}
4789
+ ${chalk12.dim("# Add a ServiceNow instance")}
3448
4790
  snow instance add
3449
4791
 
3450
- ${chalk7.dim("# Query records from a table")}
4792
+ ${chalk12.dim("# Query records from a table")}
3451
4793
  snow table get incident -q "active=true" -l 10
3452
4794
 
3453
- ${chalk7.dim("# View the schema for a table")}
4795
+ ${chalk12.dim("# View the schema for a table")}
3454
4796
  snow schema incident
3455
4797
 
3456
- ${chalk7.dim("# Pull a script field, edit it, and push back")}
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
- ${chalk7.dim("# Configure an LLM provider (OpenAI, Anthropic, xAI/Grok, or Ollama)")}
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
- ${chalk7.dim("# Generate a ServiceNow app and export as an update set XML")}
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
- ${chalk7.dim("# Generate and immediately push artifacts to the active instance")}
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
- ${chalk7.dim("# Interactive multi-turn app builder")}
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(chalk7.red(err instanceof Error ? err.message : String(err)));
4833
+ console.error(chalk12.red(err instanceof Error ? err.message : String(err)));
3487
4834
  process.exit(1);
3488
4835
  });