@syndicalt/snow-cli 1.0.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 +1062 -545
  2. package/dist/index.js +2047 -237
  3. package/package.json +52 -52
package/dist/index.js CHANGED
@@ -202,7 +202,7 @@ var init_client = __esm({
202
202
  const retryAfterHeader = err.response?.headers["retry-after"];
203
203
  const baseDelay = 1e3 * Math.pow(2, config._retryCount - 1);
204
204
  const delayMs = retryAfterHeader ? Math.max(parseInt(String(retryAfterHeader), 10) * 1e3, baseDelay) : baseDelay;
205
- await new Promise((resolve) => setTimeout(resolve, delayMs));
205
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
206
206
  return this.http.request(config);
207
207
  }
208
208
  const detail2 = err.response?.data?.error?.message ?? err.message;
@@ -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
  });
@@ -663,82 +663,653 @@ init_client();
663
663
  import { Command as Command3 } from "commander";
664
664
  import chalk3 from "chalk";
665
665
  import ora2 from "ora";
666
- function schemaCommand() {
667
- return new Command3("schema").description("Retrieve the field schema for a ServiceNow table").argument("<table>", "Table name (e.g. incident, sys_user)").option("--format <fmt>", "Output format: table or json", "table").option("-f, --filter <text>", "Filter fields by name or label (case-insensitive)").action(
668
- async (table, opts) => {
669
- const instance = requireActiveInstance();
670
- const client = new ServiceNowClient(instance);
671
- const spinner = ora2(`Loading schema for ${table}...`).start();
672
- try {
673
- const res = await client.get(
674
- "/api/now/table/sys_dictionary",
675
- {
676
- params: {
677
- sysparm_query: `name=${table}^elementISNOTEMPTY`,
678
- sysparm_fields: "element,column_label,internal_type,max_length,mandatory,read_only,reference,default_value,comments",
679
- sysparm_display_value: "all",
680
- sysparm_limit: 500,
681
- sysparm_exclude_reference_link: true
682
- }
666
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
667
+ import { join as join2, resolve } from "path";
668
+
669
+ // src/lib/llm.ts
670
+ init_esm_shims();
671
+ import axios2 from "axios";
672
+ var OpenAIProvider = class {
673
+ constructor(apiKey, model, baseUrl = "https://api.openai.com/v1", name = "openai") {
674
+ this.apiKey = apiKey;
675
+ this.model = model;
676
+ this.baseUrl = baseUrl;
677
+ this.providerName = name;
678
+ }
679
+ providerName;
680
+ async complete(messages) {
681
+ const res = await axios2.post(
682
+ `${this.baseUrl.replace(/\/$/, "")}/chat/completions`,
683
+ { model: this.model, messages },
684
+ {
685
+ headers: {
686
+ Authorization: `Bearer ${this.apiKey}`,
687
+ "Content-Type": "application/json"
688
+ },
689
+ timeout: 12e4
690
+ }
691
+ );
692
+ const content = res.data.choices[0]?.message.content;
693
+ if (!content) throw new Error("LLM returned an empty response");
694
+ return content;
695
+ }
696
+ };
697
+ var AnthropicProvider = class {
698
+ constructor(apiKey, model) {
699
+ this.apiKey = apiKey;
700
+ this.model = model;
701
+ }
702
+ providerName = "anthropic";
703
+ async complete(messages) {
704
+ const system = messages.find((m) => m.role === "system")?.content;
705
+ const conversation = messages.filter((m) => m.role !== "system");
706
+ const res = await axios2.post(
707
+ "https://api.anthropic.com/v1/messages",
708
+ {
709
+ model: this.model,
710
+ max_tokens: 8192,
711
+ ...system ? { system } : {},
712
+ messages: conversation
713
+ },
714
+ {
715
+ headers: {
716
+ "x-api-key": this.apiKey,
717
+ "anthropic-version": "2023-06-01",
718
+ "Content-Type": "application/json"
719
+ },
720
+ timeout: 12e4
721
+ }
722
+ );
723
+ const text = res.data.content.find((c) => c.type === "text")?.text;
724
+ if (!text) throw new Error("Anthropic returned an empty response");
725
+ return text;
726
+ }
727
+ };
728
+ var OllamaProvider = class {
729
+ constructor(model, baseUrl = "http://localhost:11434") {
730
+ this.model = model;
731
+ this.baseUrl = baseUrl;
732
+ }
733
+ providerName = "ollama";
734
+ async complete(messages) {
735
+ const res = await axios2.post(
736
+ `${this.baseUrl.replace(/\/$/, "")}/api/chat`,
737
+ { model: this.model, messages, stream: false },
738
+ { headers: { "Content-Type": "application/json" }, timeout: 3e5 }
739
+ );
740
+ const content = res.data.message.content;
741
+ if (!content) throw new Error("Ollama returned an empty response");
742
+ return content;
743
+ }
744
+ };
745
+ function buildProvider(name, model, apiKey, baseUrl) {
746
+ switch (name) {
747
+ case "anthropic":
748
+ if (!apiKey) throw new Error("Anthropic provider requires an API key (https://platform.claude.com/)");
749
+ return new AnthropicProvider(apiKey, model);
750
+ case "xai":
751
+ if (!apiKey) throw new Error("xAI provider requires an API key (https://platform.x.ai/)");
752
+ return new OpenAIProvider(
753
+ apiKey,
754
+ model,
755
+ baseUrl ?? "https://api.x.ai/v1",
756
+ "xai"
757
+ );
758
+ case "ollama":
759
+ return new OllamaProvider(model, baseUrl);
760
+ case "openai":
761
+ default:
762
+ if (!apiKey) throw new Error("OpenAI provider requires an API key (https://platform.openai.com/)");
763
+ return new OpenAIProvider(apiKey, model, baseUrl, name);
764
+ }
765
+ }
766
+ function extractJSON(raw) {
767
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
768
+ if (fenced) return fenced[1].trim();
769
+ const start = raw.indexOf("{");
770
+ const end = raw.lastIndexOf("}");
771
+ if (start !== -1 && end !== -1) return raw.slice(start, end + 1);
772
+ return raw.trim();
773
+ }
774
+
775
+ // src/commands/schema.ts
776
+ async function fetchInboundReferences(client, tableName) {
777
+ const res = await client.get(
778
+ "/api/now/table/sys_dictionary",
779
+ {
780
+ params: {
781
+ sysparm_query: `internal_type=reference^reference=${tableName}^elementISNOTEMPTY`,
782
+ sysparm_fields: "name,element,column_label",
783
+ sysparm_display_value: "all",
784
+ sysparm_limit: 100,
785
+ sysparm_exclude_reference_link: true
786
+ }
787
+ }
788
+ );
789
+ return (res.result ?? []).map((e) => ({
790
+ refTable: e.name?.value ?? "",
791
+ fieldName: e.element?.value ?? "",
792
+ fieldLabel: e.column_label?.display_value ?? ""
793
+ })).filter((r) => r.refTable && r.fieldName);
794
+ }
795
+ async function fetchTableFields(client, tableName) {
796
+ const res = await client.get(
797
+ "/api/now/table/sys_dictionary",
798
+ {
799
+ params: {
800
+ sysparm_query: `name=${tableName}^elementISNOTEMPTY`,
801
+ sysparm_fields: "element,column_label,internal_type,max_length,mandatory,reference",
802
+ sysparm_display_value: "all",
803
+ sysparm_limit: 500,
804
+ sysparm_exclude_reference_link: true
805
+ }
806
+ }
807
+ );
808
+ return (res.result ?? []).map((e) => ({
809
+ name: e.element?.value ?? "",
810
+ label: e.column_label?.display_value ?? "",
811
+ type: e.internal_type?.value ?? "string",
812
+ maxLength: e.max_length?.value ? parseInt(e.max_length.value, 10) : void 0,
813
+ mandatory: e.mandatory?.value === "true",
814
+ reference: e.reference?.value || void 0
815
+ })).filter((f) => f.name);
816
+ }
817
+ async function fetchTableMeta(client, tableName) {
818
+ try {
819
+ const res = await client.get("/api/now/table/sys_db_object", {
820
+ params: {
821
+ sysparm_query: `name=${tableName}`,
822
+ sysparm_fields: "label,sys_scope",
823
+ sysparm_display_value: "all",
824
+ sysparm_limit: 1
825
+ }
826
+ });
827
+ if (res.result?.length > 0) {
828
+ return {
829
+ label: res.result[0].label?.display_value || tableName,
830
+ scope: res.result[0].sys_scope?.display_value || "Global"
831
+ };
832
+ }
833
+ } catch {
834
+ }
835
+ return { label: tableName, scope: "Global" };
836
+ }
837
+ async function crawlSchema(client, rootTable, maxDepth, showM2m, showInbound, onProgress) {
838
+ const tables = /* @__PURE__ */ new Map();
839
+ const edges = [];
840
+ const queue = [{ table: rootTable, depth: 0 }];
841
+ const visited = /* @__PURE__ */ new Set();
842
+ while (queue.length > 0) {
843
+ const { table, depth } = queue.shift();
844
+ if (visited.has(table)) continue;
845
+ visited.add(table);
846
+ onProgress(` [depth ${depth}] ${table}`);
847
+ const [fields, { label, scope }] = await Promise.all([
848
+ fetchTableFields(client, table),
849
+ fetchTableMeta(client, table)
850
+ ]);
851
+ tables.set(table, { name: table, label, scope, fields });
852
+ if (depth < maxDepth) {
853
+ for (const field of fields) {
854
+ const isRef = field.type === "reference" && field.reference;
855
+ const isM2m = showM2m && field.type === "glide_list" && field.reference;
856
+ if (isRef || isM2m) {
857
+ const target = field.reference;
858
+ const alreadyEdge = edges.some((e) => e.from === table && e.field === field.name);
859
+ if (!alreadyEdge) {
860
+ edges.push({
861
+ from: table,
862
+ field: field.name,
863
+ fieldLabel: field.label,
864
+ to: target,
865
+ type: isRef ? "reference" : "glide_list"
866
+ });
867
+ }
868
+ if (!visited.has(target)) {
869
+ queue.push({ table: target, depth: depth + 1 });
683
870
  }
684
- );
685
- spinner.stop();
686
- let entries = res.result ?? [];
687
- if (opts.filter) {
688
- const filterLower = opts.filter.toLowerCase();
689
- entries = entries.filter(
690
- (e) => e.element?.value?.toLowerCase().includes(filterLower) || e.column_label?.display_value?.toLowerCase().includes(filterLower)
691
- );
692
871
  }
693
- if (entries.length === 0) {
694
- console.log(chalk3.dim(`No fields found for table "${table}".`));
695
- return;
872
+ }
873
+ if (showInbound) {
874
+ const inbound = await fetchInboundReferences(client, table);
875
+ for (const { refTable, fieldName, fieldLabel } of inbound) {
876
+ const alreadyEdge = edges.some((e) => e.from === refTable && e.field === fieldName);
877
+ if (!alreadyEdge) {
878
+ edges.push({ from: refTable, field: fieldName, fieldLabel, to: table, type: "reference" });
879
+ }
880
+ if (!visited.has(refTable)) {
881
+ queue.push({ table: refTable, depth: depth + 1 });
882
+ }
696
883
  }
697
- if (opts.format === "json") {
698
- const mapped = entries.map((e) => ({
699
- name: e.element?.value,
700
- label: e.column_label?.display_value,
701
- type: e.internal_type?.value,
702
- maxLength: e.max_length?.value ? parseInt(e.max_length.value, 10) : void 0,
703
- mandatory: e.mandatory?.value === "true",
704
- readOnly: e.read_only?.value === "true",
705
- reference: e.reference?.value || void 0,
706
- defaultValue: e.default_value?.value || void 0,
707
- comments: e.comments?.value || void 0
708
- }));
709
- console.log(JSON.stringify(mapped, null, 2));
710
- return;
884
+ }
885
+ }
886
+ }
887
+ return { tables, edges, enums: /* @__PURE__ */ new Map() };
888
+ }
889
+ async function fetchChoiceValues(client, tableName, fieldName) {
890
+ try {
891
+ const res = await client.get("/api/now/table/sys_choice", {
892
+ params: {
893
+ sysparm_query: `name=${tableName}^element=${fieldName}^language=en^inactive=false`,
894
+ sysparm_fields: "value,label",
895
+ sysparm_display_value: "all",
896
+ sysparm_limit: 100,
897
+ sysparm_orderby: "sequence"
898
+ }
899
+ });
900
+ return (res.result ?? []).map((e) => ({ value: e.value?.value ?? "", label: e.label?.display_value ?? "" })).filter((e) => e.value !== "");
901
+ } catch {
902
+ return [];
903
+ }
904
+ }
905
+ async function fetchAllEnums(client, tables, onProgress) {
906
+ const pairs = [];
907
+ for (const [, node] of tables) {
908
+ for (const f of node.fields) {
909
+ if (f.type === "choice") pairs.push({ table: node.name, field: f.name });
910
+ }
911
+ }
912
+ onProgress(` Fetching choices for ${pairs.length} choice field(s)\u2026`);
913
+ const enums = /* @__PURE__ */ new Map();
914
+ const CHUNK = 5;
915
+ for (let i = 0; i < pairs.length; i += CHUNK) {
916
+ const chunk = pairs.slice(i, i + CHUNK);
917
+ const results = await Promise.all(
918
+ chunk.map(({ table, field }) => fetchChoiceValues(client, table, field))
919
+ );
920
+ for (let j = 0; j < chunk.length; j++) {
921
+ if (results[j].length > 0) {
922
+ enums.set(`${chunk[j].table}__${chunk[j].field}`, results[j]);
923
+ }
924
+ }
925
+ }
926
+ return enums;
927
+ }
928
+ var MAX_EXPLAIN_CHARS = 12e3;
929
+ async function explainSchema(graph, schemaContent, rootTable, fmt) {
930
+ const activeProvider = getActiveProvider();
931
+ if (!activeProvider) {
932
+ throw new Error("No AI provider configured. Run `snow provider set` to configure one.");
933
+ }
934
+ const llm = buildProvider(
935
+ activeProvider.name,
936
+ activeProvider.config.model,
937
+ activeProvider.config.apiKey,
938
+ activeProvider.config.baseUrl
939
+ );
940
+ const scopes = collectScopes(graph);
941
+ const scopeList = [...scopes.keys()].join(", ");
942
+ const truncated = schemaContent.length > MAX_EXPLAIN_CHARS;
943
+ const schemaSnippet = truncated ? schemaContent.slice(0, MAX_EXPLAIN_CHARS) + "\n... (truncated)" : schemaContent;
944
+ const messages = [
945
+ {
946
+ role: "system",
947
+ content: `You are a ServiceNow architect and data modeler. When given a schema map, provide a clear, practical explanation covering:
948
+ 1. The business domain this data model represents
949
+ 2. Key tables and their purpose (focus on the most important ones)
950
+ 3. Notable relationships \u2014 references, M2M links, self-referencing tables, hierarchies
951
+ 4. Any cross-scope dependencies that developers should be aware of
952
+ 5. A one-line summary suitable for documentation
953
+
954
+ Be concise and practical. Assume the reader is a developer working with this schema. Format your response in Markdown.`
955
+ },
956
+ {
957
+ role: "user",
958
+ content: `Explain this ServiceNow schema map for table \`${rootTable}\` (${graph.tables.size} tables, scopes: ${scopeList}). Format: ${fmt.toUpperCase()}${truncated ? " [schema truncated]" : ""}
959
+
960
+ \`\`\`
961
+ ${schemaSnippet}
962
+ \`\`\``
963
+ }
964
+ ];
965
+ return llm.complete(messages);
966
+ }
967
+ var MERMAID_TYPES = {
968
+ integer: "int",
969
+ long: "bigint",
970
+ float: "float",
971
+ decimal: "float",
972
+ currency: "float",
973
+ boolean: "boolean",
974
+ date: "date",
975
+ glide_date: "date",
976
+ glide_date_time: "datetime",
977
+ reference: "string",
978
+ glide_list: "string"
979
+ };
980
+ var DBML_TYPES = {
981
+ integer: "int",
982
+ long: "bigint",
983
+ float: "float",
984
+ decimal: "decimal(15,4)",
985
+ currency: "decimal(15,2)",
986
+ boolean: "boolean",
987
+ date: "date",
988
+ glide_date: "date",
989
+ glide_date_time: "datetime",
990
+ reference: "varchar(32)",
991
+ glide_list: "text",
992
+ choice: "varchar(40)",
993
+ script: "text",
994
+ html: "text",
995
+ url: "varchar(1024)",
996
+ email: "varchar(255)",
997
+ phone_number: "varchar(40)"
998
+ };
999
+ function toMermaidType(t) {
1000
+ return MERMAID_TYPES[t] ?? "string";
1001
+ }
1002
+ function toDBMLType(t) {
1003
+ return DBML_TYPES[t] ?? "varchar(255)";
1004
+ }
1005
+ function collectScopes(graph) {
1006
+ const scopes = /* @__PURE__ */ new Map();
1007
+ for (const [, node] of graph.tables) {
1008
+ const s = node.scope || "Global";
1009
+ if (!scopes.has(s)) scopes.set(s, []);
1010
+ scopes.get(s).push(node.name);
1011
+ }
1012
+ return scopes;
1013
+ }
1014
+ function collectStubTables(graph) {
1015
+ const stubs = /* @__PURE__ */ new Set();
1016
+ for (const [, node] of graph.tables) {
1017
+ for (const f of node.fields) {
1018
+ if (f.reference && !graph.tables.has(f.reference)) {
1019
+ stubs.add(f.reference);
1020
+ }
1021
+ }
1022
+ }
1023
+ return stubs;
1024
+ }
1025
+ function renderMermaid(graph, rootTable) {
1026
+ const scopes = collectScopes(graph);
1027
+ const scopeSummary = [...scopes.entries()].map(([s, tables]) => `${s} (${tables.length})`).join(", ");
1028
+ const lines = [
1029
+ `---`,
1030
+ `title: Schema map \u2014 ${rootTable}`,
1031
+ `---`,
1032
+ `erDiagram`,
1033
+ ``,
1034
+ ` %% Scopes: ${scopeSummary}`,
1035
+ ...scopes.size > 1 ? [` %% Warning: tables from ${scopes.size} scopes \u2014 cross-scope references present`] : []
1036
+ ];
1037
+ for (const [, node] of graph.tables) {
1038
+ lines.push(` ${node.name} {`);
1039
+ for (const f of node.fields) {
1040
+ if (f.type === "glide_list") continue;
1041
+ const mType = toMermaidType(f.type);
1042
+ const note = f.mandatory ? ' "M"' : "";
1043
+ lines.push(` ${mType} ${f.name}${note}`);
1044
+ }
1045
+ lines.push(` }`);
1046
+ }
1047
+ lines.push("");
1048
+ for (const edge of graph.edges) {
1049
+ if (!graph.tables.has(edge.to)) continue;
1050
+ const rel = edge.type === "glide_list" ? `}o--o{` : `}o--||`;
1051
+ const safeLabel = edge.fieldLabel.replace(/"/g, "'");
1052
+ lines.push(` ${edge.from} ${rel} ${edge.to} : "${safeLabel}"`);
1053
+ }
1054
+ const stubs = collectStubTables(graph);
1055
+ if (stubs.size > 0) {
1056
+ lines.push("");
1057
+ lines.push(` %% Stub tables \u2014 referenced but not crawled (increase --depth to explore)`);
1058
+ for (const stubName of [...stubs].sort()) {
1059
+ lines.push(` ${stubName} {`);
1060
+ lines.push(` string sys_id`);
1061
+ lines.push(` }`);
1062
+ }
1063
+ for (const [, node] of graph.tables) {
1064
+ for (const f of node.fields) {
1065
+ if (f.reference && stubs.has(f.reference)) {
1066
+ const rel = f.type === "glide_list" ? `}o--o{` : `}o--||`;
1067
+ const safeLabel = f.label.replace(/"/g, "'");
1068
+ lines.push(` ${node.name} ${rel} ${f.reference} : "${safeLabel}"`);
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ if (graph.enums.size > 0) {
1074
+ lines.push("");
1075
+ lines.push(` %% Choice field enums`);
1076
+ for (const [key, values] of graph.enums) {
1077
+ const valStr = values.map((v) => `${v.value}=${v.label}`).join(", ");
1078
+ lines.push(` %% ${key}: ${valStr}`);
1079
+ }
1080
+ }
1081
+ return lines.join("\n");
1082
+ }
1083
+ function renderDBML(graph, rootTable) {
1084
+ const scopes = collectScopes(graph);
1085
+ const scopeSummary = [...scopes.entries()].map(([s, tables]) => `${s} (${tables.length})`).join(", ");
1086
+ const lines = [
1087
+ `// Schema map \u2014 ${rootTable}`,
1088
+ `// Generated by @syndicalt/snow-cli`,
1089
+ `// Scopes: ${scopeSummary}`,
1090
+ ...scopes.size > 1 ? [`// Warning: tables from ${scopes.size} scopes \u2014 cross-scope references present`] : [],
1091
+ ""
1092
+ ];
1093
+ for (const [, node] of graph.tables) {
1094
+ const safeLabel = node.label.replace(/'/g, "\\'");
1095
+ const safeScope = node.scope.replace(/'/g, "\\'");
1096
+ const safeNote = node.scope && node.scope !== "Global" ? `${safeLabel} | scope: ${safeScope}` : safeLabel;
1097
+ lines.push(`Table ${node.name} [note: '${safeNote}'] {`);
1098
+ for (const f of node.fields) {
1099
+ const enumKey = `${node.name}__${f.name}`;
1100
+ const dbType = f.type === "choice" && graph.enums.has(enumKey) ? enumKey : toDBMLType(f.type);
1101
+ const annots = [];
1102
+ if (f.name === "sys_id") annots.push("pk");
1103
+ if (f.mandatory) annots.push("not null");
1104
+ if (f.type === "reference" && f.reference) annots.push(`ref: > ${f.reference}.sys_id`);
1105
+ const annotStr = annots.length ? ` [${annots.join(", ")}]` : "";
1106
+ lines.push(` ${f.name} ${dbType}${annotStr}`);
1107
+ }
1108
+ lines.push(`}`);
1109
+ lines.push("");
1110
+ }
1111
+ for (const edge of graph.edges) {
1112
+ if (edge.type === "glide_list" && graph.tables.has(edge.to)) {
1113
+ lines.push(`Ref: ${edge.from}.${edge.field} <> ${edge.to}.sys_id // ${edge.fieldLabel}`);
1114
+ }
1115
+ }
1116
+ const stubs = collectStubTables(graph);
1117
+ if (stubs.size > 0) {
1118
+ lines.push("");
1119
+ lines.push(`// Placeholder tables \u2014 referenced but not crawled (increase --depth to explore)`);
1120
+ for (const stubName of [...stubs].sort()) {
1121
+ lines.push(`Table ${stubName} [note: 'not crawled'] {`);
1122
+ lines.push(` sys_id varchar(32) [pk]`);
1123
+ lines.push(`}`);
1124
+ lines.push("");
1125
+ }
1126
+ }
1127
+ if (graph.enums.size > 0) {
1128
+ lines.push("");
1129
+ lines.push(`// Choice field enums`);
1130
+ for (const [key, values] of graph.enums) {
1131
+ lines.push(`Enum ${key} {`);
1132
+ for (const v of values) {
1133
+ const safeNote = v.label.replace(/'/g, "\\'");
1134
+ lines.push(` "${v.value}" [note: '${safeNote}']`);
1135
+ }
1136
+ lines.push(`}`);
1137
+ lines.push("");
1138
+ }
1139
+ }
1140
+ return lines.join("\n");
1141
+ }
1142
+ function schemaShowCommand() {
1143
+ return new Command3("show").description("Show field schema for a ServiceNow table").argument("<table>", "Table name (e.g. incident, sys_user)").option("--format <fmt>", "Output format: table or json", "table").option("-f, --filter <text>", "Filter fields by name or label (case-insensitive)").action(async (table, opts) => {
1144
+ const instance = requireActiveInstance();
1145
+ const client = new ServiceNowClient(instance);
1146
+ const spinner = ora2(`Loading schema for ${table}...`).start();
1147
+ try {
1148
+ const res = await client.get(
1149
+ "/api/now/table/sys_dictionary",
1150
+ {
1151
+ params: {
1152
+ sysparm_query: `name=${table}^elementISNOTEMPTY`,
1153
+ sysparm_fields: "element,column_label,internal_type,max_length,mandatory,read_only,reference,default_value,comments",
1154
+ sysparm_display_value: "all",
1155
+ sysparm_limit: 500,
1156
+ sysparm_exclude_reference_link: true
1157
+ }
711
1158
  }
712
- console.log(chalk3.bold(`
1159
+ );
1160
+ spinner.stop();
1161
+ let entries = res.result ?? [];
1162
+ if (opts.filter) {
1163
+ const filterLower = opts.filter.toLowerCase();
1164
+ entries = entries.filter(
1165
+ (e) => e.element?.value?.toLowerCase().includes(filterLower) || e.column_label?.display_value?.toLowerCase().includes(filterLower)
1166
+ );
1167
+ }
1168
+ if (entries.length === 0) {
1169
+ console.log(chalk3.dim(`No fields found for table "${table}".`));
1170
+ return;
1171
+ }
1172
+ if (opts.format === "json") {
1173
+ const mapped = entries.map((e) => ({
1174
+ name: e.element?.value,
1175
+ label: e.column_label?.display_value,
1176
+ type: e.internal_type?.value,
1177
+ maxLength: e.max_length?.value ? parseInt(e.max_length.value, 10) : void 0,
1178
+ mandatory: e.mandatory?.value === "true",
1179
+ readOnly: e.read_only?.value === "true",
1180
+ reference: e.reference?.value || void 0,
1181
+ defaultValue: e.default_value?.value || void 0,
1182
+ comments: e.comments?.value || void 0
1183
+ }));
1184
+ console.log(JSON.stringify(mapped, null, 2));
1185
+ return;
1186
+ }
1187
+ console.log(chalk3.bold(`
713
1188
  Schema for ${chalk3.cyan(table)} (${entries.length} fields)
714
1189
  `));
715
- const colWidths = { name: 30, label: 30, type: 20, extra: 20 };
716
- const header = [
717
- "Field Name".padEnd(colWidths.name),
718
- "Label".padEnd(colWidths.label),
719
- "Type".padEnd(colWidths.type),
720
- "Flags"
721
- ].join(" ");
722
- console.log(chalk3.bold(header));
723
- console.log(chalk3.dim("-".repeat(header.length)));
724
- for (const e of entries) {
725
- const name = (e.element?.value ?? "").slice(0, colWidths.name).padEnd(colWidths.name);
726
- const label = (e.column_label?.display_value ?? "").slice(0, colWidths.label).padEnd(colWidths.label);
727
- const type = (e.internal_type?.value ?? "").slice(0, colWidths.type).padEnd(colWidths.type);
728
- const flags = [];
729
- if (e.mandatory?.value === "true") flags.push(chalk3.red("M"));
730
- if (e.read_only?.value === "true") flags.push(chalk3.yellow("R"));
731
- if (e.reference?.value) flags.push(chalk3.blue(`ref:${e.reference.value}`));
732
- console.log(`${name} ${label} ${type} ${flags.join(" ")}`);
1190
+ const colWidths = { name: 30, label: 30, type: 20 };
1191
+ const header = [
1192
+ "Field Name".padEnd(colWidths.name),
1193
+ "Label".padEnd(colWidths.label),
1194
+ "Type".padEnd(colWidths.type),
1195
+ "Flags"
1196
+ ].join(" ");
1197
+ console.log(chalk3.bold(header));
1198
+ console.log(chalk3.dim("-".repeat(header.length)));
1199
+ for (const e of entries) {
1200
+ const name = (e.element?.value ?? "").slice(0, colWidths.name).padEnd(colWidths.name);
1201
+ const label = (e.column_label?.display_value ?? "").slice(0, colWidths.label).padEnd(colWidths.label);
1202
+ const type = (e.internal_type?.value ?? "").slice(0, colWidths.type).padEnd(colWidths.type);
1203
+ const flags = [];
1204
+ if (e.mandatory?.value === "true") flags.push(chalk3.red("M"));
1205
+ if (e.read_only?.value === "true") flags.push(chalk3.yellow("R"));
1206
+ if (e.reference?.value) flags.push(chalk3.blue(`ref:${e.reference.value}`));
1207
+ console.log(`${name} ${label} ${type} ${flags.join(" ")}`);
1208
+ }
1209
+ console.log(chalk3.dim("\nFlags: M=mandatory R=read-only ref=reference table"));
1210
+ } catch (err) {
1211
+ spinner.fail();
1212
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1213
+ process.exit(1);
1214
+ }
1215
+ });
1216
+ }
1217
+ function schemaMapCommand() {
1218
+ return new Command3("map").description("Crawl table references and generate a Mermaid or DBML schema map").argument("<table>", "Root table to start from (e.g. incident, x_myapp_request)").option("-d, --depth <n>", "Levels of references to follow", "2").option("--show-m2m", "Include glide_list fields as M2M relationships", false).option("--format <fmt>", "Output format: mermaid or dbml", "mermaid").option("--out <dir>", "Directory to write the output file", ".").option("--name <name>", "Base filename for the output file (default: <table>-schema)").option("--inbound", "Also crawl tables that reference this table (inbound references)", false).option("--enums", "Fetch choice field values and emit Enum blocks (DBML) or comments (Mermaid)", false).option("--explain", "Use the active AI provider to explain the schema in plain English", false).action(async (table, opts) => {
1219
+ const instance = requireActiveInstance();
1220
+ const client = new ServiceNowClient(instance);
1221
+ const depth = Math.max(0, parseInt(opts.depth, 10) || 2);
1222
+ const fmt = opts.format === "dbml" ? "dbml" : "mermaid";
1223
+ console.log(
1224
+ chalk3.bold(`
1225
+ Building schema map for ${chalk3.cyan(table)}`),
1226
+ chalk3.dim(`(depth: ${depth}, m2m: ${opts.showM2m}, inbound: ${opts.inbound}, format: ${fmt})
1227
+ `)
1228
+ );
1229
+ if (opts.inbound && depth > 1) {
1230
+ console.log(chalk3.yellow(
1231
+ ` Advisory: --inbound with depth ${depth} can produce very large graphs for highly-referenced tables. Consider --depth 1 if the result is too large.
1232
+ `
1233
+ ));
1234
+ }
1235
+ const spinner = ora2("Crawling\u2026").start();
1236
+ try {
1237
+ const graph = await crawlSchema(
1238
+ client,
1239
+ table,
1240
+ depth,
1241
+ opts.showM2m,
1242
+ opts.inbound,
1243
+ (msg) => {
1244
+ spinner.text = msg;
1245
+ }
1246
+ );
1247
+ if (opts.enums) {
1248
+ spinner.start("Fetching choice field values\u2026");
1249
+ graph.enums = await fetchAllEnums(client, graph.tables, (msg) => {
1250
+ spinner.text = msg;
1251
+ });
1252
+ spinner.stop();
1253
+ } else {
1254
+ spinner.stop();
1255
+ }
1256
+ const output = fmt === "dbml" ? renderDBML(graph, table) : renderMermaid(graph, table);
1257
+ const ext = fmt === "dbml" ? ".dbml" : ".mmd";
1258
+ const baseName = opts.name ?? `${table}-schema`;
1259
+ const outDir = resolve(opts.out);
1260
+ mkdirSync2(outDir, { recursive: true });
1261
+ const outFile = join2(outDir, `${baseName}${ext}`);
1262
+ writeFileSync2(outFile, output, "utf8");
1263
+ const tableCount = graph.tables.size;
1264
+ if (tableCount > 50) {
1265
+ console.log(chalk3.yellow(
1266
+ `
1267
+ Warning: large graph (${tableCount} tables). Diagrams may be hard to read. Consider reducing --depth or scoping to a smaller root table.`
1268
+ ));
1269
+ }
1270
+ const edgeCount = graph.edges.filter((e) => graph.tables.has(e.to)).length;
1271
+ const m2mCount = graph.edges.filter((e) => e.type === "glide_list" && graph.tables.has(e.to)).length;
1272
+ console.log(chalk3.green(`
1273
+ Schema map written to ${outFile}`));
1274
+ const enumCount = graph.enums.size;
1275
+ console.log(chalk3.dim(
1276
+ `${tableCount} table${tableCount !== 1 ? "s" : ""}, ${edgeCount - m2mCount} reference${edgeCount - m2mCount !== 1 ? "s" : ""}` + (m2mCount ? `, ${m2mCount} M2M link${m2mCount !== 1 ? "s" : ""}` : "") + (enumCount ? `, ${enumCount} enum${enumCount !== 1 ? "s" : ""}` : "")
1277
+ ));
1278
+ if (fmt === "mermaid") {
1279
+ console.log(chalk3.dim("\nOpen the .mmd file in any Mermaid viewer, or paste into https://mermaid.live"));
1280
+ } else {
1281
+ console.log(chalk3.dim("\nOpen the .dbml file in https://dbdiagram.io or any DBML-compatible tool"));
1282
+ }
1283
+ if (opts.explain) {
1284
+ const explainSpinner = ora2("Generating AI explanation\u2026").start();
1285
+ try {
1286
+ const explanation = await explainSchema(graph, output, table, fmt);
1287
+ explainSpinner.stop();
1288
+ const explainFile = join2(outDir, `${baseName}.explanation.md`);
1289
+ writeFileSync2(explainFile, explanation, "utf8");
1290
+ console.log(chalk3.green(`
1291
+ Explanation saved to ${explainFile}`));
1292
+ console.log("\n" + explanation);
1293
+ } catch (err) {
1294
+ explainSpinner.stop();
1295
+ console.log(chalk3.yellow(
1296
+ `
1297
+ Note: AI explanation failed \u2014 ${err instanceof Error ? err.message : String(err)}`
1298
+ ));
733
1299
  }
734
- console.log(chalk3.dim("\nFlags: M=mandatory R=read-only ref=reference table"));
735
- } catch (err) {
736
- spinner.fail();
737
- console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
738
- process.exit(1);
739
1300
  }
1301
+ } catch (err) {
1302
+ spinner.fail();
1303
+ console.error(chalk3.red(err instanceof Error ? err.message : String(err)));
1304
+ process.exit(1);
740
1305
  }
741
- );
1306
+ });
1307
+ }
1308
+ function schemaCommand() {
1309
+ const cmd = new Command3("schema").description("Schema inspection and mapping");
1310
+ cmd.addCommand(schemaShowCommand(), { isDefault: true });
1311
+ cmd.addCommand(schemaMapCommand());
1312
+ return cmd;
742
1313
  }
743
1314
 
744
1315
  // src/commands/script.ts
@@ -746,12 +1317,23 @@ init_esm_shims();
746
1317
  init_config();
747
1318
  init_client();
748
1319
  import { Command as Command4 } from "commander";
749
- import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, statSync } from "fs";
1320
+ import { writeFileSync as writeFileSync3, readFileSync as readFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync2, readdirSync, statSync } from "fs";
750
1321
  import { homedir as homedir2 } from "os";
751
- import { join as join2 } from "path";
1322
+ import { join as join3 } from "path";
752
1323
  import { spawnSync } from "child_process";
753
1324
  import chalk4 from "chalk";
754
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
+ ];
755
1337
  function extensionForField(fieldName, fieldType) {
756
1338
  if (fieldType === "html" || fieldName.endsWith("_html")) return ".html";
757
1339
  if (fieldType === "css" || fieldName === "css") return ".css";
@@ -797,12 +1379,12 @@ function scriptCommand() {
797
1379
  if (opts.out) {
798
1380
  filePath = opts.out;
799
1381
  } else {
800
- const dir = join2(homedir2(), ".snow", "scripts");
801
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
1382
+ const dir = join3(homedir2(), ".snow", "scripts");
1383
+ if (!existsSync2(dir)) mkdirSync3(dir, { recursive: true });
802
1384
  const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
803
- filePath = join2(dir, `${safeName}${extensionForField(field)}`);
1385
+ filePath = join3(dir, `${safeName}${extensionForField(field)}`);
804
1386
  }
805
- writeFileSync2(filePath, content, "utf-8");
1387
+ writeFileSync3(filePath, content, "utf-8");
806
1388
  console.log(chalk4.green(`Saved to: ${filePath}`));
807
1389
  if (opts.open === false) return;
808
1390
  const editor = resolveEditor(opts.editor);
@@ -815,8 +1397,8 @@ function scriptCommand() {
815
1397
  console.error(chalk4.red(`Editor exited with code ${result.status ?? "?"}`));
816
1398
  process.exit(result.status ?? 1);
817
1399
  }
818
- const { confirm: confirm2 } = await import("@inquirer/prompts");
819
- const shouldPush = await confirm2({
1400
+ const { confirm: confirm6 } = await import("@inquirer/prompts");
1401
+ const shouldPush = await confirm6({
820
1402
  message: `Push changes to ${instance.alias}?`,
821
1403
  default: true
822
1404
  });
@@ -834,9 +1416,9 @@ function scriptCommand() {
834
1416
  if (file) {
835
1417
  filePath = file;
836
1418
  } else {
837
- const dir = join2(homedir2(), ".snow", "scripts");
1419
+ const dir = join3(homedir2(), ".snow", "scripts");
838
1420
  const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
839
- filePath = join2(dir, `${safeName}${extensionForField(field)}`);
1421
+ filePath = join3(dir, `${safeName}${extensionForField(field)}`);
840
1422
  }
841
1423
  if (!existsSync2(filePath)) {
842
1424
  console.error(chalk4.red(`File not found: ${filePath}`));
@@ -846,7 +1428,7 @@ function scriptCommand() {
846
1428
  await pushScript(client, table, sysId, field, filePath);
847
1429
  });
848
1430
  cmd.command("list").alias("ls").description("List locally cached script files").action(() => {
849
- const dir = join2(homedir2(), ".snow", "scripts");
1431
+ const dir = join3(homedir2(), ".snow", "scripts");
850
1432
  if (!existsSync2(dir)) {
851
1433
  console.log(chalk4.dim("No scripts cached yet. Run `snow script pull` first."));
852
1434
  return;
@@ -857,18 +1439,160 @@ function scriptCommand() {
857
1439
  return;
858
1440
  }
859
1441
  for (const f of files) {
860
- const stat = statSync(join2(dir, f));
1442
+ const stat = statSync(join3(dir, f));
861
1443
  const modified = stat.mtime.toLocaleString();
862
1444
  console.log(`${chalk4.cyan(f)} ${chalk4.dim(modified)}`);
863
1445
  }
864
1446
  });
865
- return cmd;
866
- }
867
- async function pushScript(client, table, sysId, field, filePath) {
868
- const content = readFileSync2(filePath, "utf-8");
869
- const spinner = ora3(`Pushing to ${table}/${sysId} field "${field}"...`).start();
870
- try {
871
- await client.updateRecord(table, sysId, { [field]: content });
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
+ });
1589
+ return cmd;
1590
+ }
1591
+ async function pushScript(client, table, sysId, field, filePath) {
1592
+ const content = readFileSync2(filePath, "utf-8");
1593
+ const spinner = ora3(`Pushing to ${table}/${sysId} field "${field}"...`).start();
1594
+ try {
1595
+ await client.updateRecord(table, sysId, { [field]: content });
872
1596
  spinner.succeed(chalk4.green(`Pushed successfully to ${table}/${sysId}.`));
873
1597
  } catch (err) {
874
1598
  spinner.fail();
@@ -883,114 +1607,6 @@ init_config();
883
1607
  import { Command as Command5 } from "commander";
884
1608
  import chalk5 from "chalk";
885
1609
  import ora4 from "ora";
886
-
887
- // src/lib/llm.ts
888
- init_esm_shims();
889
- import axios2 from "axios";
890
- var OpenAIProvider = class {
891
- constructor(apiKey, model, baseUrl = "https://api.openai.com/v1", name = "openai") {
892
- this.apiKey = apiKey;
893
- this.model = model;
894
- this.baseUrl = baseUrl;
895
- this.providerName = name;
896
- }
897
- providerName;
898
- async complete(messages) {
899
- const res = await axios2.post(
900
- `${this.baseUrl.replace(/\/$/, "")}/chat/completions`,
901
- { model: this.model, messages },
902
- {
903
- headers: {
904
- Authorization: `Bearer ${this.apiKey}`,
905
- "Content-Type": "application/json"
906
- },
907
- timeout: 12e4
908
- }
909
- );
910
- const content = res.data.choices[0]?.message.content;
911
- if (!content) throw new Error("LLM returned an empty response");
912
- return content;
913
- }
914
- };
915
- var AnthropicProvider = class {
916
- constructor(apiKey, model) {
917
- this.apiKey = apiKey;
918
- this.model = model;
919
- }
920
- providerName = "anthropic";
921
- async complete(messages) {
922
- const system = messages.find((m) => m.role === "system")?.content;
923
- const conversation = messages.filter((m) => m.role !== "system");
924
- const res = await axios2.post(
925
- "https://api.anthropic.com/v1/messages",
926
- {
927
- model: this.model,
928
- max_tokens: 8192,
929
- ...system ? { system } : {},
930
- messages: conversation
931
- },
932
- {
933
- headers: {
934
- "x-api-key": this.apiKey,
935
- "anthropic-version": "2023-06-01",
936
- "Content-Type": "application/json"
937
- },
938
- timeout: 12e4
939
- }
940
- );
941
- const text = res.data.content.find((c) => c.type === "text")?.text;
942
- if (!text) throw new Error("Anthropic returned an empty response");
943
- return text;
944
- }
945
- };
946
- var OllamaProvider = class {
947
- constructor(model, baseUrl = "http://localhost:11434") {
948
- this.model = model;
949
- this.baseUrl = baseUrl;
950
- }
951
- providerName = "ollama";
952
- async complete(messages) {
953
- const res = await axios2.post(
954
- `${this.baseUrl.replace(/\/$/, "")}/api/chat`,
955
- { model: this.model, messages, stream: false },
956
- { headers: { "Content-Type": "application/json" }, timeout: 3e5 }
957
- );
958
- const content = res.data.message.content;
959
- if (!content) throw new Error("Ollama returned an empty response");
960
- return content;
961
- }
962
- };
963
- function buildProvider(name, model, apiKey, baseUrl) {
964
- switch (name) {
965
- case "anthropic":
966
- if (!apiKey) throw new Error("Anthropic provider requires an API key (https://platform.claude.com/)");
967
- return new AnthropicProvider(apiKey, model);
968
- case "xai":
969
- if (!apiKey) throw new Error("xAI provider requires an API key (https://platform.x.ai/)");
970
- return new OpenAIProvider(
971
- apiKey,
972
- model,
973
- baseUrl ?? "https://api.x.ai/v1",
974
- "xai"
975
- );
976
- case "ollama":
977
- return new OllamaProvider(model, baseUrl);
978
- case "openai":
979
- default:
980
- if (!apiKey) throw new Error("OpenAI provider requires an API key (https://platform.openai.com/)");
981
- return new OpenAIProvider(apiKey, model, baseUrl, name);
982
- }
983
- }
984
- function extractJSON(raw) {
985
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
986
- if (fenced) return fenced[1].trim();
987
- const start = raw.indexOf("{");
988
- const end = raw.lastIndexOf("}");
989
- if (start !== -1 && end !== -1) return raw.slice(start, end + 1);
990
- return raw.trim();
991
- }
992
-
993
- // src/commands/provider.ts
994
1610
  var PROVIDER_NAMES = ["openai", "anthropic", "xai", "ollama"];
995
1611
  var PROVIDER_DEFAULTS = {
996
1612
  openai: { model: "gpt-4o" },
@@ -1142,8 +1758,8 @@ init_esm_shims();
1142
1758
  init_config();
1143
1759
  import { Command as Command6 } from "commander";
1144
1760
  import * as readline from "readline";
1145
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
1146
- import { join as join3, dirname, basename } from "path";
1761
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
1762
+ import { join as join4, dirname, basename } from "path";
1147
1763
  import { tmpdir as tmpdir2 } from "os";
1148
1764
  import { spawnSync as spawnSync2 } from "child_process";
1149
1765
  import chalk6 from "chalk";
@@ -2498,12 +3114,12 @@ function printBuildSummary(build, previous = null) {
2498
3114
  }
2499
3115
  function saveBuild(build, customDir) {
2500
3116
  const dir = customDir ?? slugify(build.name);
2501
- mkdirSync3(dir, { recursive: true });
3117
+ mkdirSync4(dir, { recursive: true });
2502
3118
  const base = slugify(build.name);
2503
- const xmlFile = join3(dir, `${base}.xml`);
2504
- const manifestFile = join3(dir, `${base}.manifest.json`);
2505
- writeFileSync3(xmlFile, generateUpdateSetXML(build), "utf-8");
2506
- writeFileSync3(manifestFile, JSON.stringify(build, null, 2), "utf-8");
3119
+ const xmlFile = join4(dir, `${base}.xml`);
3120
+ const manifestFile = join4(dir, `${base}.manifest.json`);
3121
+ writeFileSync4(xmlFile, generateUpdateSetXML(build), "utf-8");
3122
+ writeFileSync4(manifestFile, JSON.stringify(build, null, 2), "utf-8");
2507
3123
  return dir;
2508
3124
  }
2509
3125
  function printSaveResult(dir, build) {
@@ -2547,8 +3163,8 @@ async function confirmPush(build, dir, autoPush) {
2547
3163
  let shouldPush = autoPush ?? false;
2548
3164
  if (!shouldPush) {
2549
3165
  console.log();
2550
- const { confirm: confirm2 } = await import("@inquirer/prompts");
2551
- shouldPush = await confirm2({
3166
+ const { confirm: confirm6 } = await import("@inquirer/prompts");
3167
+ shouldPush = await confirm6({
2552
3168
  message: `Push ${build.artifacts.length} artifact(s) to ${chalk6.cyan(instance.alias)} (${instance.url})?`,
2553
3169
  default: false
2554
3170
  });
@@ -2596,7 +3212,7 @@ function resolveManifestPath(path2) {
2596
3212
  }
2597
3213
  if (existsSync3(path2) && !path2.includes(".")) {
2598
3214
  const files = readdirSync2(path2).filter((f) => f.endsWith(".manifest.json"));
2599
- if (files.length > 0) return join3(path2, files[0]);
3215
+ if (files.length > 0) return join4(path2, files[0]);
2600
3216
  }
2601
3217
  console.error(chalk6.red(`Cannot resolve build manifest from: ${path2}`));
2602
3218
  console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
@@ -2613,7 +3229,7 @@ function printCodeBlock(code, label) {
2613
3229
  console.log(chalk6.dim("\u2500".repeat(width)));
2614
3230
  }
2615
3231
  async function runReview(build, buildDir) {
2616
- const { select: select2, confirm: confirm2 } = await import("@inquirer/prompts");
3232
+ const { select: select2, confirm: confirm6 } = await import("@inquirer/prompts");
2617
3233
  const editor = resolveEditor2();
2618
3234
  let modified = false;
2619
3235
  console.log();
@@ -2659,10 +3275,10 @@ async function runReview(build, buildDir) {
2659
3275
  console.log();
2660
3276
  printCodeBlock(currentCode, `${artifactName} \u2014 ${fieldDef.label}`);
2661
3277
  console.log();
2662
- 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 });
2663
3279
  if (shouldEdit) {
2664
- const tmpFile = join3(tmpdir2(), `snow-review-${Date.now()}${fieldDef.ext}`);
2665
- writeFileSync3(tmpFile, currentCode, "utf-8");
3280
+ const tmpFile = join4(tmpdir2(), `snow-review-${Date.now()}${fieldDef.ext}`);
3281
+ writeFileSync4(tmpFile, currentCode, "utf-8");
2666
3282
  const isWin = process.platform === "win32";
2667
3283
  spawnSync2(editor, [tmpFile], { stdio: "inherit", shell: isWin });
2668
3284
  const updatedCode = readFileSync3(tmpFile, "utf-8");
@@ -2670,10 +3286,10 @@ async function runReview(build, buildDir) {
2670
3286
  build.artifacts[selectedIndex].fields[fieldDef.field] = updatedCode;
2671
3287
  modified = true;
2672
3288
  const base = slugify(build.name);
2673
- const xmlFile = join3(buildDir, `${base}.xml`);
2674
- const manifestFile = join3(buildDir, `${base}.manifest.json`);
2675
- writeFileSync3(xmlFile, generateUpdateSetXML(build), "utf-8");
2676
- writeFileSync3(manifestFile, JSON.stringify(build, null, 2), "utf-8");
3289
+ const xmlFile = join4(buildDir, `${base}.xml`);
3290
+ const manifestFile = join4(buildDir, `${base}.manifest.json`);
3291
+ writeFileSync4(xmlFile, generateUpdateSetXML(build), "utf-8");
3292
+ writeFileSync4(manifestFile, JSON.stringify(build, null, 2), "utf-8");
2677
3293
  console.log(chalk6.green(` \u2714 Saved changes to ${basename(manifestFile)}`));
2678
3294
  } else {
2679
3295
  console.log(chalk6.dim(" No changes made."));
@@ -2781,7 +3397,7 @@ function aiCommand() {
2781
3397
  console.error(chalk6.red(`No .manifest.json found in directory: ${path2}`));
2782
3398
  process.exit(1);
2783
3399
  }
2784
- manifestPath = join3(path2, files[0]);
3400
+ manifestPath = join4(path2, files[0]);
2785
3401
  } else {
2786
3402
  console.error(chalk6.red(`Cannot resolve build from: ${path2}`));
2787
3403
  console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
@@ -2960,52 +3576,1241 @@ ${chalk6.dim("Slash commands:")}
2960
3576
  return cmd;
2961
3577
  }
2962
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
+
2963
4768
  // src/index.ts
2964
- import { readFileSync as readFileSync4 } from "fs";
4769
+ import { readFileSync as readFileSync6 } from "fs";
2965
4770
  import { fileURLToPath as fileURLToPath2 } from "url";
2966
- import { join as join4, dirname as dirname2 } from "path";
4771
+ import { join as join7, dirname as dirname2 } from "path";
2967
4772
  var __filename2 = fileURLToPath2(import.meta.url);
2968
4773
  var __dirname2 = dirname2(__filename2);
2969
4774
  function getVersion() {
2970
4775
  try {
2971
4776
  const pkg = JSON.parse(
2972
- readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8")
4777
+ readFileSync6(join7(__dirname2, "..", "package.json"), "utf-8")
2973
4778
  );
2974
4779
  return pkg.version;
2975
4780
  } catch {
2976
4781
  return "0.0.0";
2977
4782
  }
2978
4783
  }
2979
- var program = new Command7();
2980
- 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(
2981
4786
  "after",
2982
4787
  `
2983
- ${chalk7.bold("Examples:")}
2984
- ${chalk7.dim("# Add a ServiceNow instance")}
4788
+ ${chalk12.bold("Examples:")}
4789
+ ${chalk12.dim("# Add a ServiceNow instance")}
2985
4790
  snow instance add
2986
4791
 
2987
- ${chalk7.dim("# Query records from a table")}
4792
+ ${chalk12.dim("# Query records from a table")}
2988
4793
  snow table get incident -q "active=true" -l 10
2989
4794
 
2990
- ${chalk7.dim("# View the schema for a table")}
4795
+ ${chalk12.dim("# View the schema for a table")}
2991
4796
  snow schema incident
2992
4797
 
2993
- ${chalk7.dim("# Pull a script field, edit it, and push back")}
4798
+ ${chalk12.dim("# Pull a script field, edit it, and push back")}
2994
4799
  snow script pull sys_script_include <sys_id> script
2995
4800
 
2996
- ${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)")}
2997
4802
  snow provider set openai
2998
4803
  snow provider set anthropic
2999
4804
  snow provider set xai
3000
4805
  snow provider set ollama --model llama3
3001
4806
 
3002
- ${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")}
3003
4808
  snow ai build "Create a script include that auto-routes incidents by category"
3004
4809
 
3005
- ${chalk7.dim("# Generate and immediately push artifacts to the active instance")}
4810
+ ${chalk12.dim("# Generate and immediately push artifacts to the active instance")}
3006
4811
  snow ai build "Create a business rule that sets priority on incident insert" --push
3007
4812
 
3008
- ${chalk7.dim("# Interactive multi-turn app builder")}
4813
+ ${chalk12.dim("# Interactive multi-turn app builder")}
3009
4814
  snow ai chat
3010
4815
  `
3011
4816
  );
@@ -3013,6 +4818,11 @@ program.addCommand(instanceCommand());
3013
4818
  program.addCommand(tableCommand());
3014
4819
  program.addCommand(schemaCommand());
3015
4820
  program.addCommand(scriptCommand());
4821
+ program.addCommand(bulkCommand());
4822
+ program.addCommand(userCommand());
4823
+ program.addCommand(attachmentCommand());
4824
+ program.addCommand(updatesetCommand());
4825
+ program.addCommand(statusCommand());
3016
4826
  program.addCommand(providerCommand());
3017
4827
  program.addCommand(aiCommand());
3018
4828
  program.command("instances", { hidden: true }).description("Alias for `snow instance list`").action(() => {
@@ -3020,6 +4830,6 @@ program.command("instances", { hidden: true }).description("Alias for `snow inst
3020
4830
  sub.parse(["node", "snow", "list"]);
3021
4831
  });
3022
4832
  program.parseAsync(process.argv).catch((err) => {
3023
- console.error(chalk7.red(err instanceof Error ? err.message : String(err)));
4833
+ console.error(chalk12.red(err instanceof Error ? err.message : String(err)));
3024
4834
  process.exit(1);
3025
4835
  });