@syndicalt/snow-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +691 -545
- package/dist/index.js +666 -203
- 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((
|
|
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;
|
|
@@ -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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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;
|
|
733
1245
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
spinner.
|
|
737
|
-
|
|
738
|
-
|
|
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"));
|
|
739
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
|
+
));
|
|
1299
|
+
}
|
|
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,9 +1317,9 @@ init_esm_shims();
|
|
|
746
1317
|
init_config();
|
|
747
1318
|
init_client();
|
|
748
1319
|
import { Command as Command4 } from "commander";
|
|
749
|
-
import { writeFileSync as
|
|
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
|
|
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";
|
|
@@ -797,12 +1368,12 @@ function scriptCommand() {
|
|
|
797
1368
|
if (opts.out) {
|
|
798
1369
|
filePath = opts.out;
|
|
799
1370
|
} else {
|
|
800
|
-
const dir =
|
|
801
|
-
if (!existsSync2(dir))
|
|
1371
|
+
const dir = join3(homedir2(), ".snow", "scripts");
|
|
1372
|
+
if (!existsSync2(dir)) mkdirSync3(dir, { recursive: true });
|
|
802
1373
|
const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
|
|
803
|
-
filePath =
|
|
1374
|
+
filePath = join3(dir, `${safeName}${extensionForField(field)}`);
|
|
804
1375
|
}
|
|
805
|
-
|
|
1376
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
806
1377
|
console.log(chalk4.green(`Saved to: ${filePath}`));
|
|
807
1378
|
if (opts.open === false) return;
|
|
808
1379
|
const editor = resolveEditor(opts.editor);
|
|
@@ -834,9 +1405,9 @@ function scriptCommand() {
|
|
|
834
1405
|
if (file) {
|
|
835
1406
|
filePath = file;
|
|
836
1407
|
} else {
|
|
837
|
-
const dir =
|
|
1408
|
+
const dir = join3(homedir2(), ".snow", "scripts");
|
|
838
1409
|
const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
|
|
839
|
-
filePath =
|
|
1410
|
+
filePath = join3(dir, `${safeName}${extensionForField(field)}`);
|
|
840
1411
|
}
|
|
841
1412
|
if (!existsSync2(filePath)) {
|
|
842
1413
|
console.error(chalk4.red(`File not found: ${filePath}`));
|
|
@@ -846,7 +1417,7 @@ function scriptCommand() {
|
|
|
846
1417
|
await pushScript(client, table, sysId, field, filePath);
|
|
847
1418
|
});
|
|
848
1419
|
cmd.command("list").alias("ls").description("List locally cached script files").action(() => {
|
|
849
|
-
const dir =
|
|
1420
|
+
const dir = join3(homedir2(), ".snow", "scripts");
|
|
850
1421
|
if (!existsSync2(dir)) {
|
|
851
1422
|
console.log(chalk4.dim("No scripts cached yet. Run `snow script pull` first."));
|
|
852
1423
|
return;
|
|
@@ -857,7 +1428,7 @@ function scriptCommand() {
|
|
|
857
1428
|
return;
|
|
858
1429
|
}
|
|
859
1430
|
for (const f of files) {
|
|
860
|
-
const stat = statSync(
|
|
1431
|
+
const stat = statSync(join3(dir, f));
|
|
861
1432
|
const modified = stat.mtime.toLocaleString();
|
|
862
1433
|
console.log(`${chalk4.cyan(f)} ${chalk4.dim(modified)}`);
|
|
863
1434
|
}
|
|
@@ -883,114 +1454,6 @@ init_config();
|
|
|
883
1454
|
import { Command as Command5 } from "commander";
|
|
884
1455
|
import chalk5 from "chalk";
|
|
885
1456
|
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
1457
|
var PROVIDER_NAMES = ["openai", "anthropic", "xai", "ollama"];
|
|
995
1458
|
var PROVIDER_DEFAULTS = {
|
|
996
1459
|
openai: { model: "gpt-4o" },
|
|
@@ -1142,8 +1605,8 @@ init_esm_shims();
|
|
|
1142
1605
|
init_config();
|
|
1143
1606
|
import { Command as Command6 } from "commander";
|
|
1144
1607
|
import * as readline from "readline";
|
|
1145
|
-
import { writeFileSync as
|
|
1146
|
-
import { join as
|
|
1608
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
|
|
1609
|
+
import { join as join4, dirname, basename } from "path";
|
|
1147
1610
|
import { tmpdir as tmpdir2 } from "os";
|
|
1148
1611
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1149
1612
|
import chalk6 from "chalk";
|
|
@@ -2498,12 +2961,12 @@ function printBuildSummary(build, previous = null) {
|
|
|
2498
2961
|
}
|
|
2499
2962
|
function saveBuild(build, customDir) {
|
|
2500
2963
|
const dir = customDir ?? slugify(build.name);
|
|
2501
|
-
|
|
2964
|
+
mkdirSync4(dir, { recursive: true });
|
|
2502
2965
|
const base = slugify(build.name);
|
|
2503
|
-
const xmlFile =
|
|
2504
|
-
const manifestFile =
|
|
2505
|
-
|
|
2506
|
-
|
|
2966
|
+
const xmlFile = join4(dir, `${base}.xml`);
|
|
2967
|
+
const manifestFile = join4(dir, `${base}.manifest.json`);
|
|
2968
|
+
writeFileSync4(xmlFile, generateUpdateSetXML(build), "utf-8");
|
|
2969
|
+
writeFileSync4(manifestFile, JSON.stringify(build, null, 2), "utf-8");
|
|
2507
2970
|
return dir;
|
|
2508
2971
|
}
|
|
2509
2972
|
function printSaveResult(dir, build) {
|
|
@@ -2596,7 +3059,7 @@ function resolveManifestPath(path2) {
|
|
|
2596
3059
|
}
|
|
2597
3060
|
if (existsSync3(path2) && !path2.includes(".")) {
|
|
2598
3061
|
const files = readdirSync2(path2).filter((f) => f.endsWith(".manifest.json"));
|
|
2599
|
-
if (files.length > 0) return
|
|
3062
|
+
if (files.length > 0) return join4(path2, files[0]);
|
|
2600
3063
|
}
|
|
2601
3064
|
console.error(chalk6.red(`Cannot resolve build manifest from: ${path2}`));
|
|
2602
3065
|
console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
|
|
@@ -2661,8 +3124,8 @@ async function runReview(build, buildDir) {
|
|
|
2661
3124
|
console.log();
|
|
2662
3125
|
const shouldEdit = await confirm2({ message: "Open in editor to edit?", default: false });
|
|
2663
3126
|
if (shouldEdit) {
|
|
2664
|
-
const tmpFile =
|
|
2665
|
-
|
|
3127
|
+
const tmpFile = join4(tmpdir2(), `snow-review-${Date.now()}${fieldDef.ext}`);
|
|
3128
|
+
writeFileSync4(tmpFile, currentCode, "utf-8");
|
|
2666
3129
|
const isWin = process.platform === "win32";
|
|
2667
3130
|
spawnSync2(editor, [tmpFile], { stdio: "inherit", shell: isWin });
|
|
2668
3131
|
const updatedCode = readFileSync3(tmpFile, "utf-8");
|
|
@@ -2670,10 +3133,10 @@ async function runReview(build, buildDir) {
|
|
|
2670
3133
|
build.artifacts[selectedIndex].fields[fieldDef.field] = updatedCode;
|
|
2671
3134
|
modified = true;
|
|
2672
3135
|
const base = slugify(build.name);
|
|
2673
|
-
const xmlFile =
|
|
2674
|
-
const manifestFile =
|
|
2675
|
-
|
|
2676
|
-
|
|
3136
|
+
const xmlFile = join4(buildDir, `${base}.xml`);
|
|
3137
|
+
const manifestFile = join4(buildDir, `${base}.manifest.json`);
|
|
3138
|
+
writeFileSync4(xmlFile, generateUpdateSetXML(build), "utf-8");
|
|
3139
|
+
writeFileSync4(manifestFile, JSON.stringify(build, null, 2), "utf-8");
|
|
2677
3140
|
console.log(chalk6.green(` \u2714 Saved changes to ${basename(manifestFile)}`));
|
|
2678
3141
|
} else {
|
|
2679
3142
|
console.log(chalk6.dim(" No changes made."));
|
|
@@ -2781,7 +3244,7 @@ function aiCommand() {
|
|
|
2781
3244
|
console.error(chalk6.red(`No .manifest.json found in directory: ${path2}`));
|
|
2782
3245
|
process.exit(1);
|
|
2783
3246
|
}
|
|
2784
|
-
manifestPath =
|
|
3247
|
+
manifestPath = join4(path2, files[0]);
|
|
2785
3248
|
} else {
|
|
2786
3249
|
console.error(chalk6.red(`Cannot resolve build from: ${path2}`));
|
|
2787
3250
|
console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
|
|
@@ -2963,13 +3426,13 @@ ${chalk6.dim("Slash commands:")}
|
|
|
2963
3426
|
// src/index.ts
|
|
2964
3427
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2965
3428
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2966
|
-
import { join as
|
|
3429
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
2967
3430
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2968
3431
|
var __dirname2 = dirname2(__filename2);
|
|
2969
3432
|
function getVersion() {
|
|
2970
3433
|
try {
|
|
2971
3434
|
const pkg = JSON.parse(
|
|
2972
|
-
readFileSync4(
|
|
3435
|
+
readFileSync4(join5(__dirname2, "..", "package.json"), "utf-8")
|
|
2973
3436
|
);
|
|
2974
3437
|
return pkg.version;
|
|
2975
3438
|
} catch {
|