@syndicalt/snow-cli 1.0.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 +545 -0
- package/dist/index.js +3025 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3025 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/lib/config.ts
|
|
22
|
+
var config_exports = {};
|
|
23
|
+
__export(config_exports, {
|
|
24
|
+
addInstance: () => addInstance,
|
|
25
|
+
getAIConfig: () => getAIConfig,
|
|
26
|
+
getActiveInstance: () => getActiveInstance,
|
|
27
|
+
getActiveProvider: () => getActiveProvider,
|
|
28
|
+
listInstances: () => listInstances,
|
|
29
|
+
loadConfig: () => loadConfig,
|
|
30
|
+
removeInstance: () => removeInstance,
|
|
31
|
+
removeProviderConfig: () => removeProviderConfig,
|
|
32
|
+
requireActiveInstance: () => requireActiveInstance,
|
|
33
|
+
saveConfig: () => saveConfig,
|
|
34
|
+
setActiveInstance: () => setActiveInstance,
|
|
35
|
+
setActiveProvider: () => setActiveProvider,
|
|
36
|
+
setProviderConfig: () => setProviderConfig
|
|
37
|
+
});
|
|
38
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
39
|
+
import { homedir } from "os";
|
|
40
|
+
import { join } from "path";
|
|
41
|
+
function ensureConfigDir() {
|
|
42
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
43
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
ensureConfigDir();
|
|
48
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
49
|
+
return { instances: {} };
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return { instances: {} };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function saveConfig(config) {
|
|
59
|
+
ensureConfigDir();
|
|
60
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
61
|
+
}
|
|
62
|
+
function getActiveInstance() {
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
if (!config.activeInstance) return null;
|
|
65
|
+
return config.instances[config.activeInstance] ?? null;
|
|
66
|
+
}
|
|
67
|
+
function requireActiveInstance() {
|
|
68
|
+
const instance = getActiveInstance();
|
|
69
|
+
if (!instance) {
|
|
70
|
+
console.error(
|
|
71
|
+
"No active instance configured. Run `snow instance add` to add one."
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return instance;
|
|
76
|
+
}
|
|
77
|
+
function addInstance(instance) {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
config.instances[instance.alias] = instance;
|
|
80
|
+
if (!config.activeInstance) {
|
|
81
|
+
config.activeInstance = instance.alias;
|
|
82
|
+
}
|
|
83
|
+
saveConfig(config);
|
|
84
|
+
}
|
|
85
|
+
function removeInstance(alias) {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
if (!config.instances[alias]) return false;
|
|
88
|
+
delete config.instances[alias];
|
|
89
|
+
if (config.activeInstance === alias) {
|
|
90
|
+
const remaining = Object.keys(config.instances);
|
|
91
|
+
config.activeInstance = remaining[0];
|
|
92
|
+
}
|
|
93
|
+
saveConfig(config);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function setActiveInstance(alias) {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
if (!config.instances[alias]) return false;
|
|
99
|
+
config.activeInstance = alias;
|
|
100
|
+
saveConfig(config);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function listInstances() {
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
return Object.values(config.instances).map((instance) => ({
|
|
106
|
+
instance,
|
|
107
|
+
active: config.activeInstance === instance.alias
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function getAIConfig() {
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
return config.ai ?? { providers: {} };
|
|
113
|
+
}
|
|
114
|
+
function setProviderConfig(name, providerConfig) {
|
|
115
|
+
const config = loadConfig();
|
|
116
|
+
config.ai ??= { providers: {} };
|
|
117
|
+
config.ai.providers[name] = providerConfig;
|
|
118
|
+
if (!config.ai.activeProvider) config.ai.activeProvider = name;
|
|
119
|
+
saveConfig(config);
|
|
120
|
+
}
|
|
121
|
+
function removeProviderConfig(name) {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
if (!config.ai?.providers[name]) return false;
|
|
124
|
+
delete config.ai.providers[name];
|
|
125
|
+
if (config.ai.activeProvider === name) {
|
|
126
|
+
const remaining = Object.keys(config.ai.providers);
|
|
127
|
+
config.ai.activeProvider = remaining[0];
|
|
128
|
+
}
|
|
129
|
+
saveConfig(config);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
function setActiveProvider(name) {
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
if (!config.ai?.providers[name]) return false;
|
|
135
|
+
config.ai.activeProvider = name;
|
|
136
|
+
saveConfig(config);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function getActiveProvider() {
|
|
140
|
+
const ai = getAIConfig();
|
|
141
|
+
if (!ai.activeProvider) return null;
|
|
142
|
+
const providerConfig = ai.providers[ai.activeProvider];
|
|
143
|
+
if (!providerConfig) return null;
|
|
144
|
+
return { name: ai.activeProvider, config: providerConfig };
|
|
145
|
+
}
|
|
146
|
+
var CONFIG_DIR, CONFIG_FILE;
|
|
147
|
+
var init_config = __esm({
|
|
148
|
+
"src/lib/config.ts"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
init_esm_shims();
|
|
151
|
+
CONFIG_DIR = join(homedir(), ".snow");
|
|
152
|
+
CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// src/lib/client.ts
|
|
157
|
+
var client_exports = {};
|
|
158
|
+
__export(client_exports, {
|
|
159
|
+
ServiceNowClient: () => ServiceNowClient
|
|
160
|
+
});
|
|
161
|
+
import axios from "axios";
|
|
162
|
+
var ServiceNowClient;
|
|
163
|
+
var init_client = __esm({
|
|
164
|
+
"src/lib/client.ts"() {
|
|
165
|
+
"use strict";
|
|
166
|
+
init_esm_shims();
|
|
167
|
+
init_config();
|
|
168
|
+
ServiceNowClient = class {
|
|
169
|
+
http;
|
|
170
|
+
instance;
|
|
171
|
+
constructor(instance) {
|
|
172
|
+
this.instance = instance;
|
|
173
|
+
this.http = axios.create({
|
|
174
|
+
baseURL: instance.url.replace(/\/$/, ""),
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
Accept: "application/json"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
this.http.interceptors.request.use(async (config) => {
|
|
181
|
+
config.headers = config.headers ?? {};
|
|
182
|
+
if (this.instance.auth.type === "basic") {
|
|
183
|
+
const { username, password: password2 } = this.instance.auth;
|
|
184
|
+
const encoded = Buffer.from(`${username}:${password2}`).toString("base64");
|
|
185
|
+
config.headers["Authorization"] = `Basic ${encoded}`;
|
|
186
|
+
} else {
|
|
187
|
+
const token = await this.getOAuthToken();
|
|
188
|
+
config.headers["Authorization"] = `Bearer ${token}`;
|
|
189
|
+
}
|
|
190
|
+
return config;
|
|
191
|
+
});
|
|
192
|
+
this.http.interceptors.response.use(
|
|
193
|
+
(res) => res,
|
|
194
|
+
async (err) => {
|
|
195
|
+
if (axios.isAxiosError(err)) {
|
|
196
|
+
const status = err.response?.status;
|
|
197
|
+
const retryable = status === 429 || status === 503 || status === 502;
|
|
198
|
+
const config = err.config;
|
|
199
|
+
if (retryable && config) {
|
|
200
|
+
config._retryCount = (config._retryCount ?? 0) + 1;
|
|
201
|
+
if (config._retryCount <= 3) {
|
|
202
|
+
const retryAfterHeader = err.response?.headers["retry-after"];
|
|
203
|
+
const baseDelay = 1e3 * Math.pow(2, config._retryCount - 1);
|
|
204
|
+
const delayMs = retryAfterHeader ? Math.max(parseInt(String(retryAfterHeader), 10) * 1e3, baseDelay) : baseDelay;
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
206
|
+
return this.http.request(config);
|
|
207
|
+
}
|
|
208
|
+
const detail2 = err.response?.data?.error?.message ?? err.message;
|
|
209
|
+
return Promise.reject(
|
|
210
|
+
new Error(
|
|
211
|
+
`ServiceNow API error (${status}): ${detail2}
|
|
212
|
+
Hint: PDI instances have lower transaction quotas. Wait a moment then retry, or reduce the number of artifacts per build.`
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const detail = err.response?.data?.error?.message ?? err.message;
|
|
217
|
+
return Promise.reject(new Error(`ServiceNow API error (${status}): ${detail}`));
|
|
218
|
+
}
|
|
219
|
+
return Promise.reject(err);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
async getOAuthToken() {
|
|
224
|
+
const auth = this.instance.auth;
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
if (auth.accessToken && auth.tokenExpiry && auth.tokenExpiry > now + 3e4) {
|
|
227
|
+
return auth.accessToken;
|
|
228
|
+
}
|
|
229
|
+
const params = new URLSearchParams({
|
|
230
|
+
grant_type: "password",
|
|
231
|
+
client_id: auth.clientId,
|
|
232
|
+
client_secret: auth.clientSecret
|
|
233
|
+
});
|
|
234
|
+
const res = await axios.post(
|
|
235
|
+
`${this.instance.url.replace(/\/$/, "")}/oauth_token.do`,
|
|
236
|
+
params.toString(),
|
|
237
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
|
|
238
|
+
);
|
|
239
|
+
auth.accessToken = res.data.access_token;
|
|
240
|
+
auth.tokenExpiry = now + res.data.expires_in * 1e3;
|
|
241
|
+
const config = loadConfig();
|
|
242
|
+
config.instances[this.instance.alias].auth = auth;
|
|
243
|
+
saveConfig(config);
|
|
244
|
+
return auth.accessToken;
|
|
245
|
+
}
|
|
246
|
+
async get(path2, config) {
|
|
247
|
+
const res = await this.http.get(path2, config);
|
|
248
|
+
return res.data;
|
|
249
|
+
}
|
|
250
|
+
async post(path2, data, config) {
|
|
251
|
+
const res = await this.http.post(path2, data, config);
|
|
252
|
+
return res.data;
|
|
253
|
+
}
|
|
254
|
+
async put(path2, data, config) {
|
|
255
|
+
const res = await this.http.put(path2, data, config);
|
|
256
|
+
return res.data;
|
|
257
|
+
}
|
|
258
|
+
async patch(path2, data, config) {
|
|
259
|
+
const res = await this.http.patch(path2, data, config);
|
|
260
|
+
return res.data;
|
|
261
|
+
}
|
|
262
|
+
async delete(path2, config) {
|
|
263
|
+
const res = await this.http.delete(path2, config);
|
|
264
|
+
return res.data;
|
|
265
|
+
}
|
|
266
|
+
// Table API helpers
|
|
267
|
+
async queryTable(table, options = {}) {
|
|
268
|
+
const params = {};
|
|
269
|
+
if (options.sysparmQuery) params["sysparm_query"] = options.sysparmQuery;
|
|
270
|
+
if (options.sysparmFields) params["sysparm_fields"] = options.sysparmFields;
|
|
271
|
+
if (options.sysparmLimit !== void 0) params["sysparm_limit"] = options.sysparmLimit;
|
|
272
|
+
if (options.sysparmOffset !== void 0) params["sysparm_offset"] = options.sysparmOffset;
|
|
273
|
+
if (options.sysparmDisplayValue !== void 0)
|
|
274
|
+
params["sysparm_display_value"] = String(options.sysparmDisplayValue);
|
|
275
|
+
if (options.sysparmExcludeReferenceLink !== void 0)
|
|
276
|
+
params["sysparm_exclude_reference_link"] = options.sysparmExcludeReferenceLink;
|
|
277
|
+
const res = await this.get(
|
|
278
|
+
`/api/now/table/${table}`,
|
|
279
|
+
{ params }
|
|
280
|
+
);
|
|
281
|
+
return res.result;
|
|
282
|
+
}
|
|
283
|
+
async getRecord(table, sysId, options = {}) {
|
|
284
|
+
const params = {};
|
|
285
|
+
if (options.sysparmFields) params["sysparm_fields"] = options.sysparmFields;
|
|
286
|
+
if (options.sysparmDisplayValue !== void 0)
|
|
287
|
+
params["sysparm_display_value"] = String(options.sysparmDisplayValue);
|
|
288
|
+
const res = await this.get(
|
|
289
|
+
`/api/now/table/${table}/${sysId}`,
|
|
290
|
+
{ params }
|
|
291
|
+
);
|
|
292
|
+
return res.result;
|
|
293
|
+
}
|
|
294
|
+
async createRecord(table, data) {
|
|
295
|
+
const res = await this.post(`/api/now/table/${table}`, data);
|
|
296
|
+
return res.result;
|
|
297
|
+
}
|
|
298
|
+
async updateRecord(table, sysId, data) {
|
|
299
|
+
const res = await this.patch(
|
|
300
|
+
`/api/now/table/${table}/${sysId}`,
|
|
301
|
+
data
|
|
302
|
+
);
|
|
303
|
+
return res.result;
|
|
304
|
+
}
|
|
305
|
+
async deleteRecord(table, sysId) {
|
|
306
|
+
await this.delete(`/api/now/table/${table}/${sysId}`);
|
|
307
|
+
}
|
|
308
|
+
async getTableSchema(table) {
|
|
309
|
+
const res = await this.get(`/api/now/table/${table}?sysparm_limit=0`);
|
|
310
|
+
return res;
|
|
311
|
+
}
|
|
312
|
+
getAxiosInstance() {
|
|
313
|
+
return this.http;
|
|
314
|
+
}
|
|
315
|
+
getInstanceUrl() {
|
|
316
|
+
return this.instance.url.replace(/\/$/, "");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// src/index.ts
|
|
323
|
+
init_esm_shims();
|
|
324
|
+
import { Command as Command7 } from "commander";
|
|
325
|
+
import chalk7 from "chalk";
|
|
326
|
+
|
|
327
|
+
// src/commands/instance.ts
|
|
328
|
+
init_esm_shims();
|
|
329
|
+
init_config();
|
|
330
|
+
init_client();
|
|
331
|
+
import { Command } from "commander";
|
|
332
|
+
import chalk from "chalk";
|
|
333
|
+
import { input, password, select, confirm } from "@inquirer/prompts";
|
|
334
|
+
function instanceCommand() {
|
|
335
|
+
const cmd = new Command("instance").description(
|
|
336
|
+
"Manage ServiceNow instance connections"
|
|
337
|
+
);
|
|
338
|
+
cmd.command("add").description("Add a new ServiceNow instance").option("-a, --alias <alias>", "Alias for the instance").option("-u, --url <url>", "Instance URL (e.g. https://dev12345.service-now.com)").option("--auth <type>", "Auth type: basic or oauth").action(async (opts) => {
|
|
339
|
+
const alias = opts.alias ?? await input({
|
|
340
|
+
message: "Alias for this instance:",
|
|
341
|
+
validate: (v) => v.trim() ? true : "Alias is required"
|
|
342
|
+
});
|
|
343
|
+
const config = loadConfig();
|
|
344
|
+
if (config.instances[alias]) {
|
|
345
|
+
const overwrite = await confirm({
|
|
346
|
+
message: `Instance "${alias}" already exists. Overwrite?`,
|
|
347
|
+
default: false
|
|
348
|
+
});
|
|
349
|
+
if (!overwrite) {
|
|
350
|
+
console.log("Aborted.");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const url = opts.url ?? await input({
|
|
355
|
+
message: "Instance URL:",
|
|
356
|
+
validate: (v) => {
|
|
357
|
+
try {
|
|
358
|
+
new URL(v);
|
|
359
|
+
return true;
|
|
360
|
+
} catch {
|
|
361
|
+
return "Enter a valid URL (e.g. https://dev12345.service-now.com)";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const authType = opts.auth ?? await select({
|
|
366
|
+
message: "Authentication type:",
|
|
367
|
+
choices: [
|
|
368
|
+
{ name: "Basic (username/password)", value: "basic" },
|
|
369
|
+
{ name: "OAuth (client credentials)", value: "oauth" }
|
|
370
|
+
]
|
|
371
|
+
});
|
|
372
|
+
let auth;
|
|
373
|
+
if (authType === "basic") {
|
|
374
|
+
const username = await input({
|
|
375
|
+
message: "Username:",
|
|
376
|
+
validate: (v) => v.trim() ? true : "Username is required"
|
|
377
|
+
});
|
|
378
|
+
const pwd = await password({
|
|
379
|
+
message: "Password:",
|
|
380
|
+
mask: "*"
|
|
381
|
+
});
|
|
382
|
+
auth = { type: "basic", username, password: pwd };
|
|
383
|
+
} else {
|
|
384
|
+
const clientId = await input({
|
|
385
|
+
message: "OAuth Client ID:",
|
|
386
|
+
validate: (v) => v.trim() ? true : "Client ID is required"
|
|
387
|
+
});
|
|
388
|
+
const clientSecret = await password({
|
|
389
|
+
message: "OAuth Client Secret:",
|
|
390
|
+
mask: "*"
|
|
391
|
+
});
|
|
392
|
+
auth = { type: "oauth", clientId, clientSecret };
|
|
393
|
+
}
|
|
394
|
+
const instance = { alias, url: url.replace(/\/$/, ""), auth };
|
|
395
|
+
console.log(chalk.dim("Testing connection..."));
|
|
396
|
+
try {
|
|
397
|
+
const client = new ServiceNowClient(instance);
|
|
398
|
+
await client.get("/api/now/table/sys_user?sysparm_limit=1");
|
|
399
|
+
console.log(chalk.green("Connection successful."));
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.log(chalk.yellow(`Warning: Could not verify connection: ${msg}`));
|
|
403
|
+
const proceed = await confirm({
|
|
404
|
+
message: "Save instance anyway?",
|
|
405
|
+
default: false
|
|
406
|
+
});
|
|
407
|
+
if (!proceed) return;
|
|
408
|
+
}
|
|
409
|
+
addInstance(instance);
|
|
410
|
+
console.log(chalk.green(`Instance "${alias}" saved.`));
|
|
411
|
+
const config2 = loadConfig();
|
|
412
|
+
if (config2.activeInstance === alias) {
|
|
413
|
+
console.log(chalk.dim(`"${alias}" is now the active instance.`));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
cmd.command("remove <alias>").description("Remove a configured instance").action(async (alias) => {
|
|
417
|
+
const ok = await confirm({
|
|
418
|
+
message: `Remove instance "${alias}"?`,
|
|
419
|
+
default: false
|
|
420
|
+
});
|
|
421
|
+
if (!ok) return;
|
|
422
|
+
if (removeInstance(alias)) {
|
|
423
|
+
console.log(chalk.green(`Instance "${alias}" removed.`));
|
|
424
|
+
} else {
|
|
425
|
+
console.error(chalk.red(`Instance "${alias}" not found.`));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
cmd.command("list").alias("ls").description("List configured instances").action(() => {
|
|
430
|
+
const instances = listInstances();
|
|
431
|
+
if (instances.length === 0) {
|
|
432
|
+
console.log("No instances configured. Run `snow instance add` to add one.");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
for (const { instance, active } of instances) {
|
|
436
|
+
const marker = active ? chalk.green("*") : " ";
|
|
437
|
+
const authLabel = instance.auth.type === "basic" ? chalk.dim(`basic (${instance.auth.username})`) : chalk.dim("oauth");
|
|
438
|
+
console.log(`${marker} ${chalk.bold(instance.alias)} ${instance.url} ${authLabel}`);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
cmd.command("use <alias>").description("Switch the active instance").action((alias) => {
|
|
442
|
+
if (setActiveInstance(alias)) {
|
|
443
|
+
console.log(chalk.green(`Active instance set to "${alias}".`));
|
|
444
|
+
} else {
|
|
445
|
+
console.error(chalk.red(`Instance "${alias}" not found.`));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
cmd.command("test").description("Test connection to the active instance").action(async () => {
|
|
450
|
+
const { requireActiveInstance: requireActiveInstance2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
451
|
+
const instance = requireActiveInstance2();
|
|
452
|
+
const client = new ServiceNowClient(instance);
|
|
453
|
+
try {
|
|
454
|
+
await client.get("/api/now/table/sys_user?sysparm_limit=1");
|
|
455
|
+
console.log(
|
|
456
|
+
chalk.green(`Connection to "${instance.alias}" (${instance.url}) is OK.`)
|
|
457
|
+
);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
460
|
+
console.error(chalk.red(`Connection failed: ${msg}`));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return cmd;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/commands/table.ts
|
|
468
|
+
init_esm_shims();
|
|
469
|
+
init_config();
|
|
470
|
+
init_client();
|
|
471
|
+
import { Command as Command2 } from "commander";
|
|
472
|
+
import chalk2 from "chalk";
|
|
473
|
+
import ora from "ora";
|
|
474
|
+
function flattenValue(val) {
|
|
475
|
+
if (val === null || val === void 0) return "";
|
|
476
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
477
|
+
const obj = val;
|
|
478
|
+
if ("display_value" in obj && obj["display_value"] !== "") {
|
|
479
|
+
return String(obj["display_value"]);
|
|
480
|
+
}
|
|
481
|
+
if ("value" in obj) return String(obj["value"]);
|
|
482
|
+
return JSON.stringify(val);
|
|
483
|
+
}
|
|
484
|
+
return String(val);
|
|
485
|
+
}
|
|
486
|
+
function printRecords(records, format) {
|
|
487
|
+
if (format === "json") {
|
|
488
|
+
console.log(JSON.stringify(records, null, 2));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (records.length === 0) {
|
|
492
|
+
console.log(chalk2.dim("No records found."));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const rows = records;
|
|
496
|
+
const allKeys = Object.keys(rows[0]);
|
|
497
|
+
const keys = allKeys.filter((k) => rows.some((r) => flattenValue(r[k]) !== ""));
|
|
498
|
+
const termWidth = process.stdout.columns ?? 120;
|
|
499
|
+
const COL_GAP = 2;
|
|
500
|
+
const MIN_COL_WIDTH = 10;
|
|
501
|
+
const fitsHorizontally = keys.length * MIN_COL_WIDTH + COL_GAP * (keys.length - 1) <= termWidth;
|
|
502
|
+
if (!fitsHorizontally) {
|
|
503
|
+
const keyWidth = Math.max(...keys.map((k) => k.length));
|
|
504
|
+
const valWidth = Math.max(20, termWidth - keyWidth - 3);
|
|
505
|
+
const divider2 = chalk2.dim("\u2500".repeat(Math.min(termWidth, 80)));
|
|
506
|
+
for (let i = 0; i < rows.length; i++) {
|
|
507
|
+
console.log(divider2 + chalk2.dim(` Record ${i + 1}`));
|
|
508
|
+
for (const k of keys) {
|
|
509
|
+
const val = flattenValue(rows[i][k]);
|
|
510
|
+
if (val === "") continue;
|
|
511
|
+
const truncated = val.length > valWidth ? val.slice(0, valWidth - 1) + "\u2026" : val;
|
|
512
|
+
console.log(`${chalk2.bold(k.padEnd(keyWidth))} ${truncated}`);
|
|
513
|
+
}
|
|
514
|
+
console.log();
|
|
515
|
+
}
|
|
516
|
+
console.log(chalk2.dim(`${rows.length} record(s) \u2014 use -f/--fields to select specific fields for tabular output`));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const naturalWidths = keys.map((k) => {
|
|
520
|
+
const maxVal = rows.reduce((max, row) => {
|
|
521
|
+
return Math.max(max, flattenValue(row[k]).length);
|
|
522
|
+
}, k.length);
|
|
523
|
+
return Math.min(maxVal, 60);
|
|
524
|
+
});
|
|
525
|
+
const totalNatural = naturalWidths.reduce((s, w) => s + w, 0) + COL_GAP * (keys.length - 1);
|
|
526
|
+
const colWidths = totalNatural > termWidth ? naturalWidths.map((w) => Math.max(4, Math.floor(w * termWidth / totalNatural))) : naturalWidths;
|
|
527
|
+
const header = keys.map((k, i) => chalk2.bold(k.padEnd(colWidths[i]))).join(" ");
|
|
528
|
+
const divider = chalk2.dim(colWidths.map((w) => "\u2500".repeat(w)).join(" "));
|
|
529
|
+
console.log(header);
|
|
530
|
+
console.log(divider);
|
|
531
|
+
for (const row of rows) {
|
|
532
|
+
const line = keys.map((k, i) => {
|
|
533
|
+
const val = flattenValue(row[k]);
|
|
534
|
+
return val.length > colWidths[i] ? val.slice(0, colWidths[i] - 1) + "\u2026" : val.padEnd(colWidths[i]);
|
|
535
|
+
}).join(" ");
|
|
536
|
+
console.log(line);
|
|
537
|
+
}
|
|
538
|
+
console.log(chalk2.dim(`
|
|
539
|
+
${rows.length} record(s)`));
|
|
540
|
+
}
|
|
541
|
+
function tableCommand() {
|
|
542
|
+
const cmd = new Command2("table").description("Perform Table API operations");
|
|
543
|
+
cmd.command("get <table>").description("Query records from a table").option("-q, --query <sysparm_query>", "Encoded query string").option("-f, --fields <fields>", "Comma-separated list of fields to return").option("-l, --limit <n>", "Max number of records", "20").option("-o, --offset <n>", "Offset for pagination", "0").option("--display-value", "Return display values instead of raw values").option("--format <fmt>", "Output format: table or json", "table").option("--json", "Shorthand for --format json").action(
|
|
544
|
+
async (table, opts) => {
|
|
545
|
+
const instance = requireActiveInstance();
|
|
546
|
+
const client = new ServiceNowClient(instance);
|
|
547
|
+
const options = {
|
|
548
|
+
sysparmQuery: opts.query,
|
|
549
|
+
sysparmFields: opts.fields,
|
|
550
|
+
sysparmLimit: parseInt(opts.limit ?? "20", 10),
|
|
551
|
+
sysparmOffset: parseInt(opts.offset ?? "0", 10),
|
|
552
|
+
sysparmDisplayValue: opts.displayValue,
|
|
553
|
+
sysparmExcludeReferenceLink: true
|
|
554
|
+
};
|
|
555
|
+
const spinner = ora(`Querying ${table}...`).start();
|
|
556
|
+
try {
|
|
557
|
+
const records = await client.queryTable(table, options);
|
|
558
|
+
spinner.stop();
|
|
559
|
+
printRecords(records, opts.json ? "json" : opts.format);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
spinner.fail();
|
|
562
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
cmd.command("fetch <table> <sys_id>").description("Fetch a single record by sys_id").option("-f, --fields <fields>", "Comma-separated list of fields to return").option("--display-value", "Return display values").option("--format <fmt>", "Output format: table or json", "table").option("--json", "Shorthand for --format json").action(
|
|
568
|
+
async (table, sysId, opts) => {
|
|
569
|
+
const instance = requireActiveInstance();
|
|
570
|
+
const client = new ServiceNowClient(instance);
|
|
571
|
+
const spinner = ora(`Fetching ${table}/${sysId}...`).start();
|
|
572
|
+
try {
|
|
573
|
+
const record = await client.getRecord(table, sysId, {
|
|
574
|
+
sysparmFields: opts.fields,
|
|
575
|
+
sysparmDisplayValue: opts.displayValue
|
|
576
|
+
});
|
|
577
|
+
spinner.stop();
|
|
578
|
+
printRecords([record], opts.json ? "json" : opts.format);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
spinner.fail();
|
|
581
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
cmd.command("create <table>").description("Create a new record").requiredOption("-d, --data <json>", "JSON object of field values").option("--format <fmt>", "Output format: table or json", "json").action(
|
|
587
|
+
async (table, opts) => {
|
|
588
|
+
let data;
|
|
589
|
+
try {
|
|
590
|
+
data = JSON.parse(opts.data);
|
|
591
|
+
} catch {
|
|
592
|
+
console.error(chalk2.red("Invalid JSON provided to --data"));
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
const instance = requireActiveInstance();
|
|
596
|
+
const client = new ServiceNowClient(instance);
|
|
597
|
+
const spinner = ora(`Creating record in ${table}...`).start();
|
|
598
|
+
try {
|
|
599
|
+
const record = await client.createRecord(table, data);
|
|
600
|
+
spinner.succeed(`Created sys_id: ${chalk2.green(record.sys_id)}`);
|
|
601
|
+
printRecords([record], opts.format);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
spinner.fail();
|
|
604
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
cmd.command("update <table> <sys_id>").description("Update an existing record").requiredOption("-d, --data <json>", "JSON object of field values to update").option("--format <fmt>", "Output format: table or json", "json").action(
|
|
610
|
+
async (table, sysId, opts) => {
|
|
611
|
+
let data;
|
|
612
|
+
try {
|
|
613
|
+
data = JSON.parse(opts.data);
|
|
614
|
+
} catch {
|
|
615
|
+
console.error(chalk2.red("Invalid JSON provided to --data"));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
const instance = requireActiveInstance();
|
|
619
|
+
const client = new ServiceNowClient(instance);
|
|
620
|
+
const spinner = ora(`Updating ${table}/${sysId}...`).start();
|
|
621
|
+
try {
|
|
622
|
+
const record = await client.updateRecord(table, sysId, data);
|
|
623
|
+
spinner.succeed("Record updated.");
|
|
624
|
+
printRecords([record], opts.format);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
spinner.fail();
|
|
627
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
cmd.command("delete <table> <sys_id>").description("Delete a record").option("-y, --yes", "Skip confirmation prompt").action(async (table, sysId, opts) => {
|
|
633
|
+
if (!opts.yes) {
|
|
634
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
635
|
+
const ok = await confirm2({
|
|
636
|
+
message: `Delete ${table}/${sysId}?`,
|
|
637
|
+
default: false
|
|
638
|
+
});
|
|
639
|
+
if (!ok) {
|
|
640
|
+
console.log("Aborted.");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const instance = requireActiveInstance();
|
|
645
|
+
const client = new ServiceNowClient(instance);
|
|
646
|
+
const spinner = ora(`Deleting ${table}/${sysId}...`).start();
|
|
647
|
+
try {
|
|
648
|
+
await client.deleteRecord(table, sysId);
|
|
649
|
+
spinner.succeed(chalk2.green(`Deleted ${table}/${sysId}.`));
|
|
650
|
+
} catch (err) {
|
|
651
|
+
spinner.fail();
|
|
652
|
+
console.error(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return cmd;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/commands/schema.ts
|
|
660
|
+
init_esm_shims();
|
|
661
|
+
init_config();
|
|
662
|
+
init_client();
|
|
663
|
+
import { Command as Command3 } from "commander";
|
|
664
|
+
import chalk3 from "chalk";
|
|
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
|
+
}
|
|
683
|
+
}
|
|
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
|
+
}
|
|
693
|
+
if (entries.length === 0) {
|
|
694
|
+
console.log(chalk3.dim(`No fields found for table "${table}".`));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
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;
|
|
711
|
+
}
|
|
712
|
+
console.log(chalk3.bold(`
|
|
713
|
+
Schema for ${chalk3.cyan(table)} (${entries.length} fields)
|
|
714
|
+
`));
|
|
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(" ")}`);
|
|
733
|
+
}
|
|
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
|
+
}
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/commands/script.ts
|
|
745
|
+
init_esm_shims();
|
|
746
|
+
init_config();
|
|
747
|
+
init_client();
|
|
748
|
+
import { Command as Command4 } from "commander";
|
|
749
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
750
|
+
import { homedir as homedir2 } from "os";
|
|
751
|
+
import { join as join2 } from "path";
|
|
752
|
+
import { spawnSync } from "child_process";
|
|
753
|
+
import chalk4 from "chalk";
|
|
754
|
+
import ora3 from "ora";
|
|
755
|
+
function extensionForField(fieldName, fieldType) {
|
|
756
|
+
if (fieldType === "html" || fieldName.endsWith("_html")) return ".html";
|
|
757
|
+
if (fieldType === "css" || fieldName === "css") return ".css";
|
|
758
|
+
if (fieldType === "xml") return ".xml";
|
|
759
|
+
if (fieldType === "json") return ".json";
|
|
760
|
+
return ".js";
|
|
761
|
+
}
|
|
762
|
+
function resolveEditor(flag) {
|
|
763
|
+
if (flag) return flag;
|
|
764
|
+
if (process.env["VISUAL"]) return process.env["VISUAL"];
|
|
765
|
+
if (process.env["EDITOR"]) return process.env["EDITOR"];
|
|
766
|
+
const isWindows = process.platform === "win32";
|
|
767
|
+
const lookup = isWindows ? "where" : "which";
|
|
768
|
+
const candidates = isWindows ? ["code", "notepad++", "notepad"] : ["code", "nvim", "vim", "nano", "vi"];
|
|
769
|
+
for (const editor of candidates) {
|
|
770
|
+
const result = spawnSync(lookup, [editor], { encoding: "utf-8", shell: isWindows });
|
|
771
|
+
if (result.status === 0) return editor;
|
|
772
|
+
}
|
|
773
|
+
return isWindows ? "notepad" : "vi";
|
|
774
|
+
}
|
|
775
|
+
function scriptCommand() {
|
|
776
|
+
const cmd = new Command4("script").description(
|
|
777
|
+
"Download, edit, and push script fields to/from a ServiceNow instance"
|
|
778
|
+
);
|
|
779
|
+
cmd.command("pull <table> <sys_id> <field>").description("Download a script field to a local file and open it in an editor").option("-e, --editor <editor>", "Editor to open (default: $VISUAL / $EDITOR)").option("-o, --out <file>", "Save to a specific file path instead of a temp file").option("--no-open", "Download without opening the editor").action(
|
|
780
|
+
async (table, sysId, field, opts) => {
|
|
781
|
+
const instance = requireActiveInstance();
|
|
782
|
+
const client = new ServiceNowClient(instance);
|
|
783
|
+
const spinner = ora3(`Fetching ${table}/${sysId} field "${field}"...`).start();
|
|
784
|
+
let record;
|
|
785
|
+
try {
|
|
786
|
+
record = await client.getRecord(table, sysId, {
|
|
787
|
+
sysparmFields: `${field},sys_id,name,sys_name`
|
|
788
|
+
});
|
|
789
|
+
spinner.stop();
|
|
790
|
+
} catch (err) {
|
|
791
|
+
spinner.fail();
|
|
792
|
+
console.error(chalk4.red(err instanceof Error ? err.message : String(err)));
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
const content = String(record[field] ?? "");
|
|
796
|
+
let filePath;
|
|
797
|
+
if (opts.out) {
|
|
798
|
+
filePath = opts.out;
|
|
799
|
+
} else {
|
|
800
|
+
const dir = join2(homedir2(), ".snow", "scripts");
|
|
801
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
802
|
+
const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
|
|
803
|
+
filePath = join2(dir, `${safeName}${extensionForField(field)}`);
|
|
804
|
+
}
|
|
805
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
806
|
+
console.log(chalk4.green(`Saved to: ${filePath}`));
|
|
807
|
+
if (opts.open === false) return;
|
|
808
|
+
const editor = resolveEditor(opts.editor);
|
|
809
|
+
console.log(chalk4.dim(`Opening with: ${editor}`));
|
|
810
|
+
const result = spawnSync(editor, [filePath], {
|
|
811
|
+
stdio: "inherit",
|
|
812
|
+
shell: process.platform === "win32"
|
|
813
|
+
});
|
|
814
|
+
if (result.status !== 0) {
|
|
815
|
+
console.error(chalk4.red(`Editor exited with code ${result.status ?? "?"}`));
|
|
816
|
+
process.exit(result.status ?? 1);
|
|
817
|
+
}
|
|
818
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
819
|
+
const shouldPush = await confirm2({
|
|
820
|
+
message: `Push changes to ${instance.alias}?`,
|
|
821
|
+
default: true
|
|
822
|
+
});
|
|
823
|
+
if (!shouldPush) {
|
|
824
|
+
console.log(chalk4.dim(`File kept at: ${filePath}`));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await pushScript(client, table, sysId, field, filePath);
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
cmd.command("push <table> <sys_id> <field> [file]").description("Push a local file to a script field on the instance").action(async (table, sysId, field, file) => {
|
|
831
|
+
const instance = requireActiveInstance();
|
|
832
|
+
const client = new ServiceNowClient(instance);
|
|
833
|
+
let filePath;
|
|
834
|
+
if (file) {
|
|
835
|
+
filePath = file;
|
|
836
|
+
} else {
|
|
837
|
+
const dir = join2(homedir2(), ".snow", "scripts");
|
|
838
|
+
const safeName = `${table}_${sysId}_${field}`.replace(/[^a-z0-9_-]/gi, "_");
|
|
839
|
+
filePath = join2(dir, `${safeName}${extensionForField(field)}`);
|
|
840
|
+
}
|
|
841
|
+
if (!existsSync2(filePath)) {
|
|
842
|
+
console.error(chalk4.red(`File not found: ${filePath}`));
|
|
843
|
+
console.error(chalk4.dim("Run `snow script pull` first, or provide a file path."));
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
await pushScript(client, table, sysId, field, filePath);
|
|
847
|
+
});
|
|
848
|
+
cmd.command("list").alias("ls").description("List locally cached script files").action(() => {
|
|
849
|
+
const dir = join2(homedir2(), ".snow", "scripts");
|
|
850
|
+
if (!existsSync2(dir)) {
|
|
851
|
+
console.log(chalk4.dim("No scripts cached yet. Run `snow script pull` first."));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const files = readdirSync(dir);
|
|
855
|
+
if (files.length === 0) {
|
|
856
|
+
console.log(chalk4.dim("No scripts cached."));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
for (const f of files) {
|
|
860
|
+
const stat = statSync(join2(dir, f));
|
|
861
|
+
const modified = stat.mtime.toLocaleString();
|
|
862
|
+
console.log(`${chalk4.cyan(f)} ${chalk4.dim(modified)}`);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
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 });
|
|
872
|
+
spinner.succeed(chalk4.green(`Pushed successfully to ${table}/${sysId}.`));
|
|
873
|
+
} catch (err) {
|
|
874
|
+
spinner.fail();
|
|
875
|
+
console.error(chalk4.red(err instanceof Error ? err.message : String(err)));
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/commands/provider.ts
|
|
881
|
+
init_esm_shims();
|
|
882
|
+
init_config();
|
|
883
|
+
import { Command as Command5 } from "commander";
|
|
884
|
+
import chalk5 from "chalk";
|
|
885
|
+
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
|
+
var PROVIDER_NAMES = ["openai", "anthropic", "xai", "ollama"];
|
|
995
|
+
var PROVIDER_DEFAULTS = {
|
|
996
|
+
openai: { model: "gpt-4o" },
|
|
997
|
+
anthropic: { model: "claude-opus-4-6" },
|
|
998
|
+
xai: { model: "grok-3", baseUrl: "https://api.x.ai/v1" },
|
|
999
|
+
ollama: { model: "llama3", baseUrl: "http://localhost:11434" }
|
|
1000
|
+
};
|
|
1001
|
+
function providerCommand() {
|
|
1002
|
+
const cmd = new Command5("provider").description(
|
|
1003
|
+
"Configure LLM providers for AI-powered ServiceNow app generation"
|
|
1004
|
+
);
|
|
1005
|
+
cmd.command("list").alias("ls").description("List configured LLM providers").action(() => {
|
|
1006
|
+
const ai = getAIConfig();
|
|
1007
|
+
const configured = Object.entries(ai.providers);
|
|
1008
|
+
if (configured.length === 0) {
|
|
1009
|
+
console.log(chalk5.dim("No providers configured. Run `snow provider set <name>` to add one."));
|
|
1010
|
+
console.log();
|
|
1011
|
+
console.log(chalk5.bold("Available providers:"));
|
|
1012
|
+
for (const name of PROVIDER_NAMES) {
|
|
1013
|
+
const d = PROVIDER_DEFAULTS[name];
|
|
1014
|
+
console.log(` ${chalk5.cyan(name.padEnd(12))} default model: ${chalk5.dim(d.model)}`);
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
console.log(chalk5.bold("Configured providers:"));
|
|
1019
|
+
for (const [name, config] of configured) {
|
|
1020
|
+
const isActive = ai.activeProvider === name;
|
|
1021
|
+
const marker = isActive ? chalk5.green("\u2714 ") : " ";
|
|
1022
|
+
const keyDisplay = config.apiKey ? chalk5.dim("key: " + config.apiKey.slice(0, 8) + "\u2026") : chalk5.dim("(no key \u2014 local)");
|
|
1023
|
+
const urlDisplay = config.baseUrl ? chalk5.dim(` @ ${config.baseUrl}`) : "";
|
|
1024
|
+
console.log(`${marker}${chalk5.cyan(name.padEnd(12))} ${config.model} ${keyDisplay}${urlDisplay}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (ai.activeProvider) {
|
|
1027
|
+
console.log();
|
|
1028
|
+
console.log(chalk5.dim(`Active: ${ai.activeProvider}`));
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
cmd.command("set <name>").description("Add or update an LLM provider configuration").option("-k, --key <apiKey>", "API key (not required for ollama)").option("-m, --model <model>", "Model name to use").option("-u, --url <baseUrl>", "Custom base URL (for ollama or self-hosted endpoints)").action(
|
|
1032
|
+
async (name, opts) => {
|
|
1033
|
+
if (!PROVIDER_NAMES.includes(name)) {
|
|
1034
|
+
console.error(
|
|
1035
|
+
chalk5.red(`Unknown provider: ${name}. Choose from: ${PROVIDER_NAMES.join(", ")}`)
|
|
1036
|
+
);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
const providerName = name;
|
|
1040
|
+
const defaults = PROVIDER_DEFAULTS[providerName];
|
|
1041
|
+
let apiKey = opts.key;
|
|
1042
|
+
let model = opts.model ?? defaults.model;
|
|
1043
|
+
let baseUrl = opts.url ?? defaults.baseUrl;
|
|
1044
|
+
if (providerName !== "ollama" && !apiKey) {
|
|
1045
|
+
const { input: input2, password: password2 } = await import("@inquirer/prompts");
|
|
1046
|
+
apiKey = await password2({ message: `${name} API key:` });
|
|
1047
|
+
if (!opts.model) {
|
|
1048
|
+
model = await input2({
|
|
1049
|
+
message: `Model (default: ${defaults.model}):`,
|
|
1050
|
+
default: defaults.model
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
setProviderConfig(providerName, {
|
|
1055
|
+
model,
|
|
1056
|
+
...apiKey ? { apiKey } : {},
|
|
1057
|
+
...baseUrl ? { baseUrl } : {}
|
|
1058
|
+
});
|
|
1059
|
+
console.log(chalk5.green(`\u2714 Provider "${name}" configured (model: ${model})`));
|
|
1060
|
+
const ai = getAIConfig();
|
|
1061
|
+
if (ai.activeProvider === providerName) {
|
|
1062
|
+
console.log(chalk5.dim("This is now the active provider."));
|
|
1063
|
+
} else {
|
|
1064
|
+
console.log(chalk5.dim(`Run \`snow provider use ${name}\` to activate it.`));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
cmd.command("use <name>").description("Set the active LLM provider").action((name) => {
|
|
1069
|
+
if (!PROVIDER_NAMES.includes(name)) {
|
|
1070
|
+
console.error(chalk5.red(`Unknown provider: ${name}`));
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
const ok = setActiveProvider(name);
|
|
1074
|
+
if (!ok) {
|
|
1075
|
+
console.error(chalk5.red(`Provider "${name}" is not configured. Run \`snow provider set ${name}\` first.`));
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
console.log(chalk5.green(`\u2714 Active provider set to: ${name}`));
|
|
1079
|
+
});
|
|
1080
|
+
cmd.command("remove <name>").alias("rm").description("Remove a provider configuration").action((name) => {
|
|
1081
|
+
const ok = removeProviderConfig(name);
|
|
1082
|
+
if (!ok) {
|
|
1083
|
+
console.error(chalk5.yellow(`Provider "${name}" was not configured.`));
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
console.log(chalk5.green(`\u2714 Removed provider: ${name}`));
|
|
1087
|
+
});
|
|
1088
|
+
cmd.command("test [name]").description("Send a test message to verify provider connectivity").action(async (name) => {
|
|
1089
|
+
let providerName;
|
|
1090
|
+
let config;
|
|
1091
|
+
if (name) {
|
|
1092
|
+
const ai = getAIConfig();
|
|
1093
|
+
const stored = ai.providers[name];
|
|
1094
|
+
if (!stored) {
|
|
1095
|
+
console.error(chalk5.red(`Provider "${name}" is not configured.`));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
providerName = name;
|
|
1099
|
+
config = stored;
|
|
1100
|
+
} else {
|
|
1101
|
+
const active = getActiveProvider();
|
|
1102
|
+
if (!active) {
|
|
1103
|
+
console.error(chalk5.red("No active provider configured. Run `snow provider set <name>`."));
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
providerName = active.name;
|
|
1107
|
+
config = active.config;
|
|
1108
|
+
}
|
|
1109
|
+
const spinner = ora4(`Testing ${providerName} (${config.model})\u2026`).start();
|
|
1110
|
+
try {
|
|
1111
|
+
const provider = buildProvider(providerName, config.model, config.apiKey, config.baseUrl);
|
|
1112
|
+
const response = await provider.complete([
|
|
1113
|
+
{ role: "user", content: 'Reply with exactly: "snow-cli connection OK"' }
|
|
1114
|
+
]);
|
|
1115
|
+
spinner.succeed(chalk5.green(`${providerName} responded: ${response.trim()}`));
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
spinner.fail(chalk5.red(`Connection failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
cmd.command("show").description("Show the active provider configuration").action(() => {
|
|
1122
|
+
const active = getActiveProvider();
|
|
1123
|
+
if (!active) {
|
|
1124
|
+
console.log(chalk5.dim("No active provider. Run `snow provider set <name>`."));
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
console.log(chalk5.bold("Active provider:"));
|
|
1128
|
+
console.log(` Name: ${chalk5.cyan(active.name)}`);
|
|
1129
|
+
console.log(` Model: ${active.config.model}`);
|
|
1130
|
+
if (active.config.baseUrl) {
|
|
1131
|
+
console.log(` URL: ${chalk5.dim(active.config.baseUrl)}`);
|
|
1132
|
+
}
|
|
1133
|
+
if (active.config.apiKey) {
|
|
1134
|
+
console.log(` API Key: ${chalk5.dim(active.config.apiKey.slice(0, 8) + "\u2026")}`);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
return cmd;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/commands/ai.ts
|
|
1141
|
+
init_esm_shims();
|
|
1142
|
+
init_config();
|
|
1143
|
+
import { Command as Command6 } from "commander";
|
|
1144
|
+
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";
|
|
1147
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1148
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1149
|
+
import chalk6 from "chalk";
|
|
1150
|
+
import ora5 from "ora";
|
|
1151
|
+
|
|
1152
|
+
// src/lib/sn-context.ts
|
|
1153
|
+
init_esm_shims();
|
|
1154
|
+
var SN_SYSTEM_PROMPT = `
|
|
1155
|
+
You are an expert ServiceNow developer. Your task is to generate ServiceNow application artifacts based on user requirements.
|
|
1156
|
+
|
|
1157
|
+
## Response Format
|
|
1158
|
+
|
|
1159
|
+
You MUST respond with a single JSON object (optionally wrapped in a \`\`\`json code fence). Do not include any explanation outside the JSON.
|
|
1160
|
+
|
|
1161
|
+
Schema:
|
|
1162
|
+
\`\`\`json
|
|
1163
|
+
{
|
|
1164
|
+
"name": "Human-readable update set name",
|
|
1165
|
+
"description": "What this application/feature does",
|
|
1166
|
+
"scope": { ... },
|
|
1167
|
+
"artifacts": [
|
|
1168
|
+
{
|
|
1169
|
+
"type": "<artifact_type>",
|
|
1170
|
+
"fields": { ... type-specific fields ... }
|
|
1171
|
+
}
|
|
1172
|
+
]
|
|
1173
|
+
}
|
|
1174
|
+
\`\`\`
|
|
1175
|
+
|
|
1176
|
+
The "scope" key is optional \u2014 see the Scoped Applications section below.
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Scoped Applications
|
|
1181
|
+
|
|
1182
|
+
Add a "scope" object to the root of your JSON response when the request:
|
|
1183
|
+
- Creates 2 or more custom tables
|
|
1184
|
+
- Is described as an "application", "module", or "package"
|
|
1185
|
+
- Needs a distinct namespace to avoid naming conflicts with OOTB or other customisations
|
|
1186
|
+
|
|
1187
|
+
Do NOT add a scope for:
|
|
1188
|
+
- Business rules, client scripts, or script includes that customise existing OOTB tables
|
|
1189
|
+
- Simple standalone features that don't require custom tables
|
|
1190
|
+
|
|
1191
|
+
Scope object format:
|
|
1192
|
+
\`\`\`json
|
|
1193
|
+
"scope": {
|
|
1194
|
+
"prefix": "x_myco_myapp",
|
|
1195
|
+
"name": "My Application Name",
|
|
1196
|
+
"version": "1.0.0",
|
|
1197
|
+
"vendor": "My Company"
|
|
1198
|
+
}
|
|
1199
|
+
\`\`\`
|
|
1200
|
+
|
|
1201
|
+
Prefix rules: must match \`x_<vendor_abbrev>_<app_abbrev>\`, all lowercase, underscores only.
|
|
1202
|
+
|
|
1203
|
+
When scope is set:
|
|
1204
|
+
- All custom **table names** MUST be prefixed: \`{prefix}_tablename\`
|
|
1205
|
+
- Script include **api_name** MUST be prefixed: \`{prefix}.ClassName\`
|
|
1206
|
+
- Business rules, client scripts, and other artifacts reference the prefixed table name in their "table" field
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
## Artifact Types
|
|
1211
|
+
|
|
1212
|
+
### script_include
|
|
1213
|
+
Server-side reusable JavaScript class. Referenced by other server-side scripts.
|
|
1214
|
+
|
|
1215
|
+
Fields:
|
|
1216
|
+
- name (string, required) \u2014 PascalCase class name, e.g. "IncidentUtils"
|
|
1217
|
+
- api_name (string, required) \u2014 same as name; if scoped: "{prefix}.ClassName"
|
|
1218
|
+
- description (string)
|
|
1219
|
+
- script (string, required) \u2014 full script body (see patterns below)
|
|
1220
|
+
- client_callable (boolean) \u2014 true only if called via GlideAjax from a client script
|
|
1221
|
+
- active (boolean, default: true)
|
|
1222
|
+
|
|
1223
|
+
Script pattern (standard):
|
|
1224
|
+
\`\`\`javascript
|
|
1225
|
+
var ClassName = Class.create();
|
|
1226
|
+
ClassName.prototype = {
|
|
1227
|
+
initialize: function(param) {
|
|
1228
|
+
this.param = param;
|
|
1229
|
+
},
|
|
1230
|
+
methodName: function() {
|
|
1231
|
+
var gr = new GlideRecord('table_name');
|
|
1232
|
+
gr.addQuery('field', 'value');
|
|
1233
|
+
gr.query();
|
|
1234
|
+
while (gr.next()) {
|
|
1235
|
+
// process records
|
|
1236
|
+
}
|
|
1237
|
+
return result;
|
|
1238
|
+
},
|
|
1239
|
+
type: 'ClassName'
|
|
1240
|
+
};
|
|
1241
|
+
\`\`\`
|
|
1242
|
+
|
|
1243
|
+
Client-callable (GlideAjax) pattern:
|
|
1244
|
+
\`\`\`javascript
|
|
1245
|
+
var ClassName = Class.create();
|
|
1246
|
+
ClassName.prototype = Object.extendsObject(AbstractAjaxProcessor, {
|
|
1247
|
+
methodName: function() {
|
|
1248
|
+
var param = this.getParameter('sysparm_param');
|
|
1249
|
+
return result;
|
|
1250
|
+
},
|
|
1251
|
+
type: 'ClassName'
|
|
1252
|
+
});
|
|
1253
|
+
\`\`\`
|
|
1254
|
+
|
|
1255
|
+
---
|
|
1256
|
+
|
|
1257
|
+
### business_rule
|
|
1258
|
+
Server-side script that executes automatically on database operations.
|
|
1259
|
+
|
|
1260
|
+
Fields:
|
|
1261
|
+
- name (string, required)
|
|
1262
|
+
- description (string)
|
|
1263
|
+
- table (string, required) \u2014 table name, e.g. "incident", "x_myco_myapp_request"
|
|
1264
|
+
- when (string, required) \u2014 "before" | "after" | "async" | "display"
|
|
1265
|
+
- order (number, default: 100)
|
|
1266
|
+
- active (boolean, default: true)
|
|
1267
|
+
- action_insert (boolean) \u2014 fire on INSERT
|
|
1268
|
+
- action_update (boolean) \u2014 fire on UPDATE
|
|
1269
|
+
- action_delete (boolean) \u2014 fire on DELETE
|
|
1270
|
+
- action_query (boolean) \u2014 fire on SELECT (display rules only)
|
|
1271
|
+
- condition (string) \u2014 GlideRecord condition expression
|
|
1272
|
+
- script (string, required)
|
|
1273
|
+
|
|
1274
|
+
Script pattern:
|
|
1275
|
+
\`\`\`javascript
|
|
1276
|
+
(function executeRule(current, previous /*null when async*/) {
|
|
1277
|
+
gs.log('Rule fired: ' + current.getDisplayValue(), 'source');
|
|
1278
|
+
current.setValue('field_name', 'new_value');
|
|
1279
|
+
// For before rules do NOT call current.update()
|
|
1280
|
+
})(current, previous);
|
|
1281
|
+
\`\`\`
|
|
1282
|
+
|
|
1283
|
+
---
|
|
1284
|
+
|
|
1285
|
+
### client_script
|
|
1286
|
+
JavaScript that runs in the user's browser on ServiceNow forms.
|
|
1287
|
+
|
|
1288
|
+
Fields:
|
|
1289
|
+
- name (string, required)
|
|
1290
|
+
- description (string)
|
|
1291
|
+
- table (string, required)
|
|
1292
|
+
- type (string, required) \u2014 "onLoad" | "onChange" | "onSubmit" | "onCellEdit"
|
|
1293
|
+
- field_name (string) \u2014 required when type is "onChange" or "onCellEdit"
|
|
1294
|
+
- active (boolean, default: true)
|
|
1295
|
+
- script (string, required)
|
|
1296
|
+
|
|
1297
|
+
Script patterns:
|
|
1298
|
+
\`\`\`javascript
|
|
1299
|
+
// onLoad
|
|
1300
|
+
function onLoad() {
|
|
1301
|
+
g_form.setMandatory('field_name', true);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// onChange
|
|
1305
|
+
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
|
|
1306
|
+
if (isLoading || newValue === '') return;
|
|
1307
|
+
g_form.setMandatory('other_field', newValue === 'specific_value');
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// onSubmit
|
|
1311
|
+
function onSubmit() {
|
|
1312
|
+
if (!g_form.getValue('field_name')) {
|
|
1313
|
+
g_form.addErrorMessage('Field is required.');
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
\`\`\`
|
|
1319
|
+
|
|
1320
|
+
Available client globals: g_form, g_user, g_list, g_menu, NOW
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
### ui_action
|
|
1325
|
+
A button, context menu item, or link on a form or list.
|
|
1326
|
+
|
|
1327
|
+
Fields:
|
|
1328
|
+
- name (string, required)
|
|
1329
|
+
- description (string)
|
|
1330
|
+
- table (string, required)
|
|
1331
|
+
- action_name (string, required) \u2014 unique identifier, snake_case
|
|
1332
|
+
- active (boolean, default: true)
|
|
1333
|
+
- client (boolean) \u2014 true if onclick runs client-side
|
|
1334
|
+
- form_button (boolean)
|
|
1335
|
+
- form_context_menu (boolean)
|
|
1336
|
+
- list_action (boolean)
|
|
1337
|
+
- condition (string)
|
|
1338
|
+
- onclick (string) \u2014 client-side JS (if client: true)
|
|
1339
|
+
- script (string) \u2014 server-side script (if client: false)
|
|
1340
|
+
|
|
1341
|
+
Server-side script pattern:
|
|
1342
|
+
\`\`\`javascript
|
|
1343
|
+
(function() {
|
|
1344
|
+
current.setValue('state', '3');
|
|
1345
|
+
current.update();
|
|
1346
|
+
action.setRedirectURL(current);
|
|
1347
|
+
})();
|
|
1348
|
+
\`\`\`
|
|
1349
|
+
|
|
1350
|
+
---
|
|
1351
|
+
|
|
1352
|
+
### ui_page
|
|
1353
|
+
A full HTML page served by ServiceNow.
|
|
1354
|
+
|
|
1355
|
+
Fields:
|
|
1356
|
+
- name (string, required) \u2014 URL-safe name, path: /{name}.do
|
|
1357
|
+
- description (string)
|
|
1358
|
+
- html (string, required) \u2014 full HTML with Jelly templating
|
|
1359
|
+
- client_script (string) \u2014 client-side JavaScript (runs in the browser)
|
|
1360
|
+
- processing_script (string) \u2014 server-side JavaScript (runs before the page renders)
|
|
1361
|
+
- direct (boolean, default: false) \u2014 allow direct URL access without authentication
|
|
1362
|
+
- category (string, default: "general")
|
|
1363
|
+
|
|
1364
|
+
HTML template example:
|
|
1365
|
+
\`\`\`html
|
|
1366
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
|
1367
|
+
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
|
|
1368
|
+
<g:evaluate var="jvar_result" expression="new MyScriptInclude().getData();" />
|
|
1369
|
+
<div id="main">
|
|
1370
|
+
<h2>Page Title</h2>
|
|
1371
|
+
<p>\${jvar_result}</p>
|
|
1372
|
+
</div>
|
|
1373
|
+
</j:jelly>
|
|
1374
|
+
\`\`\`
|
|
1375
|
+
|
|
1376
|
+
---
|
|
1377
|
+
|
|
1378
|
+
### scheduled_job
|
|
1379
|
+
A script that runs on a schedule.
|
|
1380
|
+
|
|
1381
|
+
Fields:
|
|
1382
|
+
- name (string, required)
|
|
1383
|
+
- description (string)
|
|
1384
|
+
- active (boolean, default: true)
|
|
1385
|
+
- run_type (string, required) \u2014 "daily" | "weekly" | "monthly" | "periodically" | "once"
|
|
1386
|
+
- run_time (string) \u2014 time of day, e.g. "03:00:00"
|
|
1387
|
+
- run_period (string) \u2014 interval for periodically, e.g. "00:30:00"
|
|
1388
|
+
- run_dayofweek (string) \u2014 for weekly: "1"=Sun \u2026 "7"=Sat
|
|
1389
|
+
- run_dayofmonth (string) \u2014 for monthly: "1"\u2013"31"
|
|
1390
|
+
- script (string, required)
|
|
1391
|
+
|
|
1392
|
+
Script pattern:
|
|
1393
|
+
\`\`\`javascript
|
|
1394
|
+
(function runJob() {
|
|
1395
|
+
var gr = new GlideRecord('table_name');
|
|
1396
|
+
gr.addQuery('state', 'open');
|
|
1397
|
+
gr.query();
|
|
1398
|
+
while (gr.next()) {
|
|
1399
|
+
gs.log('Processed: ' + gr.sys_id, 'ScheduledJob');
|
|
1400
|
+
}
|
|
1401
|
+
})();
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
|
|
1404
|
+
---
|
|
1405
|
+
|
|
1406
|
+
### table
|
|
1407
|
+
A custom database table with fields. Generates sys_db_object + sys_dictionary entries.
|
|
1408
|
+
|
|
1409
|
+
Fields:
|
|
1410
|
+
- name (string, required) \u2014 table name; MUST be prefixed if scoped: "{prefix}_tablename"
|
|
1411
|
+
- label (string, required) \u2014 singular display label, e.g. "Asset Request"
|
|
1412
|
+
- plural (string) \u2014 plural label, e.g. "Asset Requests"
|
|
1413
|
+
- extends (string) \u2014 parent table to extend, e.g. "task" for task-based tables
|
|
1414
|
+
- is_extendable (boolean, default: false)
|
|
1415
|
+
- columns (array, required) \u2014 list of column definitions (see below)
|
|
1416
|
+
|
|
1417
|
+
Column definition:
|
|
1418
|
+
\`\`\`json
|
|
1419
|
+
{
|
|
1420
|
+
"element": "u_field_name",
|
|
1421
|
+
"label": "Field Label",
|
|
1422
|
+
"internal_type": "string",
|
|
1423
|
+
"max_length": 255,
|
|
1424
|
+
"mandatory": false,
|
|
1425
|
+
"default_value": "",
|
|
1426
|
+
"reference": "sys_user"
|
|
1427
|
+
}
|
|
1428
|
+
\`\`\`
|
|
1429
|
+
|
|
1430
|
+
Valid internal_type values: string, integer, boolean, reference, glide_date_time, glide_date,
|
|
1431
|
+
float, decimal, html, url, email, phone_number, choice, sys_class_name
|
|
1432
|
+
|
|
1433
|
+
For choice fields add a "choices" array:
|
|
1434
|
+
\`\`\`json
|
|
1435
|
+
{
|
|
1436
|
+
"element": "u_status",
|
|
1437
|
+
"label": "Status",
|
|
1438
|
+
"internal_type": "choice",
|
|
1439
|
+
"choices": [
|
|
1440
|
+
{ "value": "draft", "label": "Draft" },
|
|
1441
|
+
{ "value": "pending", "label": "Pending Approval" },
|
|
1442
|
+
{ "value": "approved", "label": "Approved" },
|
|
1443
|
+
{ "value": "rejected", "label": "Rejected" }
|
|
1444
|
+
]
|
|
1445
|
+
}
|
|
1446
|
+
\`\`\`
|
|
1447
|
+
|
|
1448
|
+
Note: if the table extends "task", standard fields (number, state, priority, assigned_to,
|
|
1449
|
+
short_description, etc.) are inherited \u2014 do not re-declare them as columns.
|
|
1450
|
+
|
|
1451
|
+
---
|
|
1452
|
+
|
|
1453
|
+
### decision_table
|
|
1454
|
+
A Decision Table that maps sets of input conditions to an output result.
|
|
1455
|
+
Generates sys_decision + sys_decision_question + sys_decision_case + sys_decision_case_question records.
|
|
1456
|
+
|
|
1457
|
+
Fields:
|
|
1458
|
+
- name (string, required) \u2014 unique snake_case identifier
|
|
1459
|
+
- label (string, required) \u2014 human-readable table name
|
|
1460
|
+
- description (string)
|
|
1461
|
+
- inputs (array, required) \u2014 input column definitions
|
|
1462
|
+
- output_label (string, required) \u2014 label for the result column, e.g. "Priority"
|
|
1463
|
+
- output_type (string) \u2014 type of the output value: "string" (default) | "integer" | "reference"
|
|
1464
|
+
- rules (array, required) \u2014 ordered list of rules (evaluated top-down, first match wins)
|
|
1465
|
+
|
|
1466
|
+
Input definition:
|
|
1467
|
+
\`\`\`json
|
|
1468
|
+
{ "name": "urgency", "label": "Urgency", "type": "string" }
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
|
|
1471
|
+
Rule definition:
|
|
1472
|
+
\`\`\`json
|
|
1473
|
+
{
|
|
1474
|
+
"label": "Critical \u2014 P1",
|
|
1475
|
+
"conditions": [
|
|
1476
|
+
{ "input": "urgency", "operator": "=", "value": "1" },
|
|
1477
|
+
{ "input": "impact", "operator": "=", "value": "1" }
|
|
1478
|
+
],
|
|
1479
|
+
"result": "1"
|
|
1480
|
+
}
|
|
1481
|
+
\`\`\`
|
|
1482
|
+
|
|
1483
|
+
Valid operators: = | != | > | >= | < | <= | starts_with | contains | is_empty | is_not_empty
|
|
1484
|
+
|
|
1485
|
+
Full example:
|
|
1486
|
+
\`\`\`json
|
|
1487
|
+
{
|
|
1488
|
+
"type": "decision_table",
|
|
1489
|
+
"fields": {
|
|
1490
|
+
"name": "incident_priority_matrix",
|
|
1491
|
+
"label": "Incident Priority Matrix",
|
|
1492
|
+
"inputs": [
|
|
1493
|
+
{ "name": "urgency", "label": "Urgency", "type": "string" },
|
|
1494
|
+
{ "name": "impact", "label": "Impact", "type": "string" }
|
|
1495
|
+
],
|
|
1496
|
+
"output_label": "Priority",
|
|
1497
|
+
"output_type": "string",
|
|
1498
|
+
"rules": [
|
|
1499
|
+
{ "label": "P1 Critical", "conditions": [{"input":"urgency","operator":"=","value":"1"},{"input":"impact","operator":"=","value":"1"}], "result": "1" },
|
|
1500
|
+
{ "label": "P2 High", "conditions": [{"input":"urgency","operator":"=","value":"1"},{"input":"impact","operator":"=","value":"2"}], "result": "2" },
|
|
1501
|
+
{ "label": "P3 Moderate", "conditions": [{"input":"urgency","operator":"=","value":"2"},{"input":"impact","operator":"=","value":"2"}], "result": "3" }
|
|
1502
|
+
]
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
\`\`\`
|
|
1506
|
+
|
|
1507
|
+
---
|
|
1508
|
+
|
|
1509
|
+
### flow_action
|
|
1510
|
+
A reusable Custom Action for Flow Designer.
|
|
1511
|
+
Generates sys_hub_action_type_definition + sys_hub_action_input + sys_hub_action_output records.
|
|
1512
|
+
|
|
1513
|
+
Fields:
|
|
1514
|
+
- name (string, required) \u2014 unique snake_case identifier
|
|
1515
|
+
- label (string, required) \u2014 display name shown in Flow Designer action picker
|
|
1516
|
+
- description (string)
|
|
1517
|
+
- category (string) \u2014 category grouping in the picker, e.g. "Incident Management"
|
|
1518
|
+
- script (string, required) \u2014 action body (see pattern below)
|
|
1519
|
+
- active (boolean, default: true)
|
|
1520
|
+
- inputs (array) \u2014 input variable definitions
|
|
1521
|
+
- outputs (array) \u2014 output variable definitions
|
|
1522
|
+
|
|
1523
|
+
Input/output definition:
|
|
1524
|
+
\`\`\`json
|
|
1525
|
+
{ "name": "incident_sys_id", "label": "Incident Sys ID", "type": "string", "mandatory": true }
|
|
1526
|
+
\`\`\`
|
|
1527
|
+
|
|
1528
|
+
Valid types for inputs/outputs: string, integer, boolean, reference, glide_date_time, script
|
|
1529
|
+
|
|
1530
|
+
Script pattern \u2014 inputs and outputs are available as plain objects:
|
|
1531
|
+
\`\`\`javascript
|
|
1532
|
+
(function execute(inputs, outputs) {
|
|
1533
|
+
var gr = new GlideRecord('incident');
|
|
1534
|
+
gr.initialize();
|
|
1535
|
+
gr.setValue('short_description', inputs.short_description);
|
|
1536
|
+
gr.setValue('category', inputs.category || 'inquiry');
|
|
1537
|
+
var sysId = gr.insert();
|
|
1538
|
+
outputs.incident_sys_id = sysId;
|
|
1539
|
+
outputs.success = (sysId !== null && sysId !== '');
|
|
1540
|
+
})(inputs, outputs);
|
|
1541
|
+
\`\`\`
|
|
1542
|
+
|
|
1543
|
+
Full example:
|
|
1544
|
+
\`\`\`json
|
|
1545
|
+
{
|
|
1546
|
+
"type": "flow_action",
|
|
1547
|
+
"fields": {
|
|
1548
|
+
"name": "create_incident_action",
|
|
1549
|
+
"label": "Create Incident",
|
|
1550
|
+
"description": "Creates an incident record and returns its sys_id",
|
|
1551
|
+
"category": "Incident Management",
|
|
1552
|
+
"inputs": [
|
|
1553
|
+
{ "name": "short_description", "label": "Short Description", "type": "string", "mandatory": true },
|
|
1554
|
+
{ "name": "category", "label": "Category", "type": "string" },
|
|
1555
|
+
{ "name": "urgency", "label": "Urgency", "type": "string" }
|
|
1556
|
+
],
|
|
1557
|
+
"outputs": [
|
|
1558
|
+
{ "name": "incident_sys_id", "label": "Incident Sys ID", "type": "string" },
|
|
1559
|
+
{ "name": "success", "label": "Success", "type": "boolean" }
|
|
1560
|
+
],
|
|
1561
|
+
"script": "(function execute(inputs, outputs) { ... })(inputs, outputs);"
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
\`\`\`
|
|
1565
|
+
|
|
1566
|
+
---
|
|
1567
|
+
|
|
1568
|
+
## ServiceNow Server-Side API Reference
|
|
1569
|
+
|
|
1570
|
+
### GlideRecord
|
|
1571
|
+
\`\`\`javascript
|
|
1572
|
+
var gr = new GlideRecord('incident');
|
|
1573
|
+
gr.addQuery('active', true);
|
|
1574
|
+
gr.addEncodedQuery('state=1^urgency=2');
|
|
1575
|
+
gr.orderBy('sys_created_on');
|
|
1576
|
+
gr.setLimit(100);
|
|
1577
|
+
gr.query();
|
|
1578
|
+
while (gr.next()) {
|
|
1579
|
+
var id = gr.sys_id.toString();
|
|
1580
|
+
var val = gr.getValue('short_description');
|
|
1581
|
+
var disp = gr.getDisplayValue('assigned_to');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
var newGr = new GlideRecord('incident');
|
|
1585
|
+
newGr.initialize();
|
|
1586
|
+
newGr.setValue('short_description', 'New incident');
|
|
1587
|
+
var sysId = newGr.insert();
|
|
1588
|
+
|
|
1589
|
+
gr.setValue('state', '2');
|
|
1590
|
+
gr.update();
|
|
1591
|
+
|
|
1592
|
+
gr.deleteRecord();
|
|
1593
|
+
|
|
1594
|
+
var rec = new GlideRecord('incident');
|
|
1595
|
+
if (rec.get(sysId)) { /* found */ }
|
|
1596
|
+
\`\`\`
|
|
1597
|
+
|
|
1598
|
+
### GlideSystem (gs)
|
|
1599
|
+
\`\`\`javascript
|
|
1600
|
+
gs.log('message', 'source');
|
|
1601
|
+
gs.info('message');
|
|
1602
|
+
gs.warn('message');
|
|
1603
|
+
gs.error('message');
|
|
1604
|
+
gs.getUserID();
|
|
1605
|
+
gs.getUserName();
|
|
1606
|
+
gs.getUser().getFullName();
|
|
1607
|
+
gs.hasRole('admin');
|
|
1608
|
+
gs.now(); // YYYY-MM-DD
|
|
1609
|
+
gs.nowDateTime(); // YYYY-MM-DD HH:MM:SS
|
|
1610
|
+
gs.setProperty('prop.name', 'value');
|
|
1611
|
+
gs.getProperty('prop.name', 'default');
|
|
1612
|
+
\`\`\`
|
|
1613
|
+
|
|
1614
|
+
### GlideDateTime
|
|
1615
|
+
\`\`\`javascript
|
|
1616
|
+
var gdt = new GlideDateTime();
|
|
1617
|
+
gdt.addDays(5);
|
|
1618
|
+
gdt.addSeconds(3600);
|
|
1619
|
+
var iso = gdt.getValue();
|
|
1620
|
+
var disp = gdt.getDisplayValue();
|
|
1621
|
+
\`\`\`
|
|
1622
|
+
|
|
1623
|
+
---
|
|
1624
|
+
|
|
1625
|
+
## Important Rules
|
|
1626
|
+
|
|
1627
|
+
1. All scripts must be valid ES5 JavaScript \u2014 no arrow functions, no const/let in server scripts, use var.
|
|
1628
|
+
2. Business rules: use "before" for validation/field manipulation; "async" for integrations and heavy operations.
|
|
1629
|
+
3. Never use synchronous XMLHttpRequest in client scripts \u2014 use GlideAjax or REST Message.
|
|
1630
|
+
4. Script includes should be stateless where possible; store state in the constructor only.
|
|
1631
|
+
5. Always handle the case where GlideRecord queries return no results.
|
|
1632
|
+
6. Generate realistic, working code \u2014 not stubs or placeholders.
|
|
1633
|
+
7. sys_id values in artifacts will be auto-generated; do not include them.
|
|
1634
|
+
8. For decision tables, generate a rule for every meaningful input combination; put the most specific rules first.
|
|
1635
|
+
9. For flow actions, the script MUST use the (function execute(inputs, outputs) { ... })(inputs, outputs) wrapper \u2014 do not use GlideScriptedExtensionPoint patterns.
|
|
1636
|
+
|
|
1637
|
+
---
|
|
1638
|
+
|
|
1639
|
+
## Interaction Mode
|
|
1640
|
+
|
|
1641
|
+
### Conversational mode (plain text)
|
|
1642
|
+
Use this when you need clarification before generating. Ask targeted questions about:
|
|
1643
|
+
- Which table(s) the feature operates on
|
|
1644
|
+
- Whether a scoped application is needed (if ambiguous)
|
|
1645
|
+
- Specific field names, reference fields, or lookup values
|
|
1646
|
+
- Business logic conditions or edge cases
|
|
1647
|
+
- Whether the operation should be client-side, server-side, or both
|
|
1648
|
+
|
|
1649
|
+
Respond in plain conversational text. Do NOT wrap in JSON.
|
|
1650
|
+
|
|
1651
|
+
### Build mode (JSON only)
|
|
1652
|
+
Once you have enough information, respond with ONLY the JSON object wrapped in a \`\`\`json code fence \u2014 no prose before or after it.
|
|
1653
|
+
|
|
1654
|
+
**On refinement requests**: Always output the COMPLETE updated artifact list, not just the changed artifact. The CLI replaces the entire build on each JSON response.
|
|
1655
|
+
|
|
1656
|
+
**Decide based on clarity**:
|
|
1657
|
+
- Clear, specific request \u2192 go straight to Build mode
|
|
1658
|
+
- Vague or ambiguous request \u2192 ask 1\u20133 focused questions first, then build
|
|
1659
|
+
`.trim();
|
|
1660
|
+
var ARTIFACT_REQUIRED_FIELDS = {
|
|
1661
|
+
script_include: ["name", "api_name", "script"],
|
|
1662
|
+
business_rule: ["name", "table", "when", "script"],
|
|
1663
|
+
client_script: ["name", "table", "type", "script"],
|
|
1664
|
+
ui_action: ["name", "table", "action_name"],
|
|
1665
|
+
ui_page: ["name", "html"],
|
|
1666
|
+
scheduled_job: ["name", "run_type", "script"],
|
|
1667
|
+
table: ["name", "label", "columns"],
|
|
1668
|
+
decision_table: ["name", "label", "inputs", "output_label", "rules"],
|
|
1669
|
+
flow_action: ["name", "label", "script"]
|
|
1670
|
+
};
|
|
1671
|
+
var ARTIFACT_TABLE = {
|
|
1672
|
+
script_include: "sys_script_include",
|
|
1673
|
+
business_rule: "sys_script",
|
|
1674
|
+
client_script: "sys_client_script",
|
|
1675
|
+
ui_action: "sys_ui_action",
|
|
1676
|
+
ui_page: "sys_ui_page",
|
|
1677
|
+
scheduled_job: "sys_trigger"
|
|
1678
|
+
};
|
|
1679
|
+
var ARTIFACT_LABEL = {
|
|
1680
|
+
script_include: "Script Include",
|
|
1681
|
+
business_rule: "Business Rule",
|
|
1682
|
+
client_script: "Client Script",
|
|
1683
|
+
ui_action: "UI Action",
|
|
1684
|
+
ui_page: "UI Page",
|
|
1685
|
+
scheduled_job: "Scheduled Script Execution",
|
|
1686
|
+
table: "Table",
|
|
1687
|
+
decision_table: "Decision Table",
|
|
1688
|
+
flow_action: "Flow Action"
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
// src/lib/update-set.ts
|
|
1692
|
+
init_esm_shims();
|
|
1693
|
+
import { randomUUID } from "crypto";
|
|
1694
|
+
function validateBuild(build) {
|
|
1695
|
+
const errors = [];
|
|
1696
|
+
for (let i = 0; i < build.artifacts.length; i++) {
|
|
1697
|
+
const artifact = build.artifacts[i];
|
|
1698
|
+
const required = ARTIFACT_REQUIRED_FIELDS[artifact.type] ?? [];
|
|
1699
|
+
const missing = required.filter(
|
|
1700
|
+
(f) => artifact.fields[f] === void 0 || artifact.fields[f] === "" || artifact.fields[f] === null
|
|
1701
|
+
);
|
|
1702
|
+
if (missing.length > 0) {
|
|
1703
|
+
errors.push({
|
|
1704
|
+
artifactIndex: i,
|
|
1705
|
+
type: artifact.type,
|
|
1706
|
+
name: String(artifact.fields["name"] ?? "(unnamed)"),
|
|
1707
|
+
missing
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return errors;
|
|
1712
|
+
}
|
|
1713
|
+
function expandSimple(artifact, scopeSysId) {
|
|
1714
|
+
const table = ARTIFACT_TABLE[artifact.type];
|
|
1715
|
+
if (!table) return [];
|
|
1716
|
+
const sysId = randomUUID().replace(/-/g, "");
|
|
1717
|
+
const fields = {
|
|
1718
|
+
...artifact.fields,
|
|
1719
|
+
active: artifact.fields["active"] !== false ? "true" : "false"
|
|
1720
|
+
};
|
|
1721
|
+
if (scopeSysId) {
|
|
1722
|
+
fields["sys_scope"] = scopeSysId;
|
|
1723
|
+
fields["sys_package"] = scopeSysId;
|
|
1724
|
+
}
|
|
1725
|
+
return [{ table, sysId, label: ARTIFACT_LABEL[artifact.type] ?? artifact.type, fields }];
|
|
1726
|
+
}
|
|
1727
|
+
function expandTable(artifact, scopeSysId) {
|
|
1728
|
+
const records = [];
|
|
1729
|
+
const tableSysId = randomUUID().replace(/-/g, "");
|
|
1730
|
+
const tableName = String(artifact.fields["name"] ?? "");
|
|
1731
|
+
const dbObjFields = {
|
|
1732
|
+
name: tableName,
|
|
1733
|
+
label: String(artifact.fields["label"] ?? tableName),
|
|
1734
|
+
plural: String(artifact.fields["plural"] ?? String(artifact.fields["label"] ?? tableName) + "s"),
|
|
1735
|
+
is_extendable: artifact.fields["is_extendable"] ? "true" : "false",
|
|
1736
|
+
create_access: "true",
|
|
1737
|
+
read_access: "true",
|
|
1738
|
+
write_access: "true",
|
|
1739
|
+
delete_access: "true"
|
|
1740
|
+
};
|
|
1741
|
+
if (artifact.fields["extends"]) dbObjFields["super_class"] = String(artifact.fields["extends"]);
|
|
1742
|
+
if (scopeSysId) {
|
|
1743
|
+
dbObjFields["sys_scope"] = scopeSysId;
|
|
1744
|
+
dbObjFields["sys_package"] = scopeSysId;
|
|
1745
|
+
}
|
|
1746
|
+
records.push({ table: "sys_db_object", sysId: tableSysId, label: "Table", fields: dbObjFields });
|
|
1747
|
+
const columns = Array.isArray(artifact.fields["columns"]) ? artifact.fields["columns"] : [];
|
|
1748
|
+
for (const col of columns) {
|
|
1749
|
+
const dictSysId = randomUUID().replace(/-/g, "");
|
|
1750
|
+
const isChoice = String(col["internal_type"] ?? "") === "choice";
|
|
1751
|
+
const dictFields = {
|
|
1752
|
+
name: tableName,
|
|
1753
|
+
element: String(col["element"] ?? ""),
|
|
1754
|
+
column_label: String(col["label"] ?? col["element"] ?? ""),
|
|
1755
|
+
internal_type: isChoice ? "string" : String(col["internal_type"] ?? "string"),
|
|
1756
|
+
max_length: col["max_length"] ?? (isChoice || String(col["internal_type"]) === "string" ? 255 : ""),
|
|
1757
|
+
mandatory: col["mandatory"] ? "true" : "false",
|
|
1758
|
+
active: "true"
|
|
1759
|
+
};
|
|
1760
|
+
if (col["default_value"] !== void 0) dictFields["default_value"] = String(col["default_value"]);
|
|
1761
|
+
if (col["reference"]) dictFields["reference"] = String(col["reference"]);
|
|
1762
|
+
if (isChoice) dictFields["choice"] = "1";
|
|
1763
|
+
if (scopeSysId) {
|
|
1764
|
+
dictFields["sys_scope"] = scopeSysId;
|
|
1765
|
+
dictFields["sys_package"] = scopeSysId;
|
|
1766
|
+
}
|
|
1767
|
+
records.push({ table: "sys_dictionary", sysId: dictSysId, label: "Field", fields: dictFields });
|
|
1768
|
+
if (isChoice && Array.isArray(col["choices"])) {
|
|
1769
|
+
const choices = col["choices"];
|
|
1770
|
+
for (let ci = 0; ci < choices.length; ci++) {
|
|
1771
|
+
const ch = choices[ci];
|
|
1772
|
+
const choiceVal = typeof ch === "object" ? String(ch["value"] ?? "") : String(ch).toLowerCase().replace(/\s+/g, "_");
|
|
1773
|
+
const choiceLabel = typeof ch === "object" ? String(ch["label"] ?? ch["value"] ?? "") : String(ch);
|
|
1774
|
+
const choiceSysId = randomUUID().replace(/-/g, "");
|
|
1775
|
+
const choiceFields = {
|
|
1776
|
+
name: tableName,
|
|
1777
|
+
element: String(col["element"] ?? ""),
|
|
1778
|
+
value: choiceVal,
|
|
1779
|
+
label: choiceLabel,
|
|
1780
|
+
sequence: ci * 100,
|
|
1781
|
+
inactive: "false"
|
|
1782
|
+
};
|
|
1783
|
+
if (scopeSysId) {
|
|
1784
|
+
choiceFields["sys_scope"] = scopeSysId;
|
|
1785
|
+
choiceFields["sys_package"] = scopeSysId;
|
|
1786
|
+
}
|
|
1787
|
+
records.push({ table: "sys_choice", sysId: choiceSysId, label: "Choice", fields: choiceFields });
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return records;
|
|
1792
|
+
}
|
|
1793
|
+
function expandDecisionTable(artifact, scopeSysId) {
|
|
1794
|
+
const records = [];
|
|
1795
|
+
const decisionSysId = randomUUID().replace(/-/g, "");
|
|
1796
|
+
const baseFields = {
|
|
1797
|
+
name: String(artifact.fields["name"] ?? ""),
|
|
1798
|
+
label: String(artifact.fields["label"] ?? artifact.fields["name"] ?? ""),
|
|
1799
|
+
description: String(artifact.fields["description"] ?? ""),
|
|
1800
|
+
active: "true"
|
|
1801
|
+
};
|
|
1802
|
+
if (scopeSysId) {
|
|
1803
|
+
baseFields["sys_scope"] = scopeSysId;
|
|
1804
|
+
baseFields["sys_package"] = scopeSysId;
|
|
1805
|
+
}
|
|
1806
|
+
records.push({ table: "sys_decision", sysId: decisionSysId, label: "Decision Table", fields: baseFields });
|
|
1807
|
+
const inputs = Array.isArray(artifact.fields["inputs"]) ? artifact.fields["inputs"] : [];
|
|
1808
|
+
const inputSysIds = {};
|
|
1809
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
1810
|
+
const inp = inputs[i];
|
|
1811
|
+
const inpSysId = randomUUID().replace(/-/g, "");
|
|
1812
|
+
const inpName = String(inp["name"] ?? inp["field"] ?? `input_${i}`);
|
|
1813
|
+
inputSysIds[inpName] = inpSysId;
|
|
1814
|
+
const qFields = {
|
|
1815
|
+
decision: decisionSysId,
|
|
1816
|
+
label: String(inp["label"] ?? inpName),
|
|
1817
|
+
order: i * 100,
|
|
1818
|
+
question_type: String(inp["type"] ?? "string")
|
|
1819
|
+
};
|
|
1820
|
+
if (inp["reference"]) qFields["reference_table"] = String(inp["reference"]);
|
|
1821
|
+
if (scopeSysId) {
|
|
1822
|
+
qFields["sys_scope"] = scopeSysId;
|
|
1823
|
+
qFields["sys_package"] = scopeSysId;
|
|
1824
|
+
}
|
|
1825
|
+
records.push({ table: "sys_decision_question", sysId: inpSysId, label: "Decision Input", fields: qFields });
|
|
1826
|
+
}
|
|
1827
|
+
const rules = Array.isArray(artifact.fields["rules"]) ? artifact.fields["rules"] : [];
|
|
1828
|
+
for (let ri = 0; ri < rules.length; ri++) {
|
|
1829
|
+
const rule = rules[ri];
|
|
1830
|
+
const caseSysId = randomUUID().replace(/-/g, "");
|
|
1831
|
+
const caseFields = {
|
|
1832
|
+
decision: decisionSysId,
|
|
1833
|
+
label: String(rule["label"] ?? `Rule ${ri + 1}`),
|
|
1834
|
+
order: ri * 100,
|
|
1835
|
+
result: String(rule["result"] ?? "")
|
|
1836
|
+
};
|
|
1837
|
+
if (scopeSysId) {
|
|
1838
|
+
caseFields["sys_scope"] = scopeSysId;
|
|
1839
|
+
caseFields["sys_package"] = scopeSysId;
|
|
1840
|
+
}
|
|
1841
|
+
records.push({ table: "sys_decision_case", sysId: caseSysId, label: "Decision Rule", fields: caseFields });
|
|
1842
|
+
const conditions = Array.isArray(rule["conditions"]) ? rule["conditions"] : [];
|
|
1843
|
+
for (const cond of conditions) {
|
|
1844
|
+
const condSysId = randomUUID().replace(/-/g, "");
|
|
1845
|
+
const inputName = String(cond["input"] ?? "");
|
|
1846
|
+
const cqFields = {
|
|
1847
|
+
case: caseSysId,
|
|
1848
|
+
question: inputSysIds[inputName] ?? "",
|
|
1849
|
+
operator: String(cond["operator"] ?? "="),
|
|
1850
|
+
value: String(cond["value"] ?? "")
|
|
1851
|
+
};
|
|
1852
|
+
if (scopeSysId) {
|
|
1853
|
+
cqFields["sys_scope"] = scopeSysId;
|
|
1854
|
+
cqFields["sys_package"] = scopeSysId;
|
|
1855
|
+
}
|
|
1856
|
+
records.push({ table: "sys_decision_case_question", sysId: condSysId, label: "Decision Condition", fields: cqFields });
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return records;
|
|
1860
|
+
}
|
|
1861
|
+
function expandFlowAction(artifact, scopeSysId) {
|
|
1862
|
+
const records = [];
|
|
1863
|
+
const actionSysId = randomUUID().replace(/-/g, "");
|
|
1864
|
+
const actionFields = {
|
|
1865
|
+
name: String(artifact.fields["name"] ?? ""),
|
|
1866
|
+
label: String(artifact.fields["label"] ?? artifact.fields["name"] ?? ""),
|
|
1867
|
+
description: String(artifact.fields["description"] ?? ""),
|
|
1868
|
+
category: String(artifact.fields["category"] ?? "Custom"),
|
|
1869
|
+
script: String(artifact.fields["script"] ?? ""),
|
|
1870
|
+
active: artifact.fields["active"] !== false ? "true" : "false"
|
|
1871
|
+
};
|
|
1872
|
+
if (scopeSysId) {
|
|
1873
|
+
actionFields["sys_scope"] = scopeSysId;
|
|
1874
|
+
actionFields["sys_package"] = scopeSysId;
|
|
1875
|
+
}
|
|
1876
|
+
records.push({ table: "sys_hub_action_type_definition", sysId: actionSysId, label: "Flow Action", fields: actionFields });
|
|
1877
|
+
const inputs = Array.isArray(artifact.fields["inputs"]) ? artifact.fields["inputs"] : [];
|
|
1878
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
1879
|
+
const inp = inputs[i];
|
|
1880
|
+
const inpSysId = randomUUID().replace(/-/g, "");
|
|
1881
|
+
const inpFields = {
|
|
1882
|
+
action_type: actionSysId,
|
|
1883
|
+
name: String(inp["name"] ?? `input_${i}`),
|
|
1884
|
+
label: String(inp["label"] ?? inp["name"] ?? `Input ${i + 1}`),
|
|
1885
|
+
type: String(inp["type"] ?? "string"),
|
|
1886
|
+
mandatory: inp["mandatory"] ? "true" : "false",
|
|
1887
|
+
order: i * 100
|
|
1888
|
+
};
|
|
1889
|
+
if (scopeSysId) {
|
|
1890
|
+
inpFields["sys_scope"] = scopeSysId;
|
|
1891
|
+
inpFields["sys_package"] = scopeSysId;
|
|
1892
|
+
}
|
|
1893
|
+
records.push({ table: "sys_hub_action_input", sysId: inpSysId, label: "Action Input", fields: inpFields });
|
|
1894
|
+
}
|
|
1895
|
+
const outputs = Array.isArray(artifact.fields["outputs"]) ? artifact.fields["outputs"] : [];
|
|
1896
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
1897
|
+
const out = outputs[i];
|
|
1898
|
+
const outSysId = randomUUID().replace(/-/g, "");
|
|
1899
|
+
const outFields = {
|
|
1900
|
+
action_type: actionSysId,
|
|
1901
|
+
name: String(out["name"] ?? `output_${i}`),
|
|
1902
|
+
label: String(out["label"] ?? out["name"] ?? `Output ${i + 1}`),
|
|
1903
|
+
type: String(out["type"] ?? "string"),
|
|
1904
|
+
order: i * 100
|
|
1905
|
+
};
|
|
1906
|
+
if (scopeSysId) {
|
|
1907
|
+
outFields["sys_scope"] = scopeSysId;
|
|
1908
|
+
outFields["sys_package"] = scopeSysId;
|
|
1909
|
+
}
|
|
1910
|
+
records.push({ table: "sys_hub_action_output", sysId: outSysId, label: "Action Output", fields: outFields });
|
|
1911
|
+
}
|
|
1912
|
+
return records;
|
|
1913
|
+
}
|
|
1914
|
+
function expandArtifact(artifact, scopeSysId) {
|
|
1915
|
+
switch (artifact.type) {
|
|
1916
|
+
case "table":
|
|
1917
|
+
return expandTable(artifact, scopeSysId);
|
|
1918
|
+
case "decision_table":
|
|
1919
|
+
return expandDecisionTable(artifact, scopeSysId);
|
|
1920
|
+
case "flow_action":
|
|
1921
|
+
return expandFlowAction(artifact, scopeSysId);
|
|
1922
|
+
default:
|
|
1923
|
+
return expandSimple(artifact, scopeSysId);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function buildScopeRecord(scope, scopeSysId) {
|
|
1927
|
+
return {
|
|
1928
|
+
table: "sys_app",
|
|
1929
|
+
sysId: scopeSysId,
|
|
1930
|
+
label: "Application",
|
|
1931
|
+
fields: {
|
|
1932
|
+
name: scope.name,
|
|
1933
|
+
scope: scope.prefix,
|
|
1934
|
+
short_description: scope.name,
|
|
1935
|
+
version: scope.version,
|
|
1936
|
+
vendor: scope.vendor ?? "",
|
|
1937
|
+
active: "true",
|
|
1938
|
+
licensable: "false",
|
|
1939
|
+
sys_class_name: "sys_app"
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function escapeXML(str) {
|
|
1944
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1945
|
+
}
|
|
1946
|
+
function wrapCDATA(str) {
|
|
1947
|
+
return `<![CDATA[${str.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
|
|
1948
|
+
}
|
|
1949
|
+
function buildFlatRecord(rec, packageRef, scopeRef) {
|
|
1950
|
+
const skip = /* @__PURE__ */ new Set(["sys_id", "sys_scope", "sys_package", "sys_class_name", "sys_update_name"]);
|
|
1951
|
+
const lines = [];
|
|
1952
|
+
for (const [k, v] of Object.entries(rec.fields)) {
|
|
1953
|
+
if (skip.has(k) || v === void 0 || v === null || Array.isArray(v) || typeof v === "object") continue;
|
|
1954
|
+
const strVal = String(v);
|
|
1955
|
+
const content = strVal.includes("<") || strVal.includes("&") || strVal.includes("\n") ? wrapCDATA(strVal) : escapeXML(strVal);
|
|
1956
|
+
lines.push(` <${k}>${content}</${k}>`);
|
|
1957
|
+
}
|
|
1958
|
+
lines.push(` <sys_class_name>${rec.table}</sys_class_name>`);
|
|
1959
|
+
lines.push(` <sys_id>${rec.sysId}</sys_id>`);
|
|
1960
|
+
lines.push(` <sys_package>${packageRef}</sys_package>`);
|
|
1961
|
+
lines.push(` <sys_scope>${scopeRef}</sys_scope>`);
|
|
1962
|
+
lines.push(` <sys_update_name>${rec.table}_${rec.sysId}</sys_update_name>`);
|
|
1963
|
+
return `<${rec.table} action="INSERT_OR_UPDATE">
|
|
1964
|
+
${lines.join("\n")}
|
|
1965
|
+
</${rec.table}>`;
|
|
1966
|
+
}
|
|
1967
|
+
function generateUpdateSetXML(build) {
|
|
1968
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
1969
|
+
const updateSetId = randomUUID().replace(/-/g, "");
|
|
1970
|
+
const scopeSysId = build.scope ? randomUUID().replace(/-/g, "") : void 0;
|
|
1971
|
+
const packageRef = scopeSysId ?? "global";
|
|
1972
|
+
const scopeRef = scopeSysId ?? "global";
|
|
1973
|
+
const appLabel = build.scope?.name ?? "Global";
|
|
1974
|
+
const blocks = [];
|
|
1975
|
+
blocks.push(
|
|
1976
|
+
`<sys_update_set action="INSERT_OR_UPDATE">
|
|
1977
|
+
<application display_value="${escapeXML(appLabel)}">${packageRef}</application>
|
|
1978
|
+
<description>${escapeXML(build.description)}</description>
|
|
1979
|
+
<is_default>false</is_default>
|
|
1980
|
+
<name>${escapeXML(build.name)}</name>
|
|
1981
|
+
<state>complete</state>
|
|
1982
|
+
<sys_id>${updateSetId}</sys_id>
|
|
1983
|
+
</sys_update_set>`
|
|
1984
|
+
);
|
|
1985
|
+
if (build.scope && scopeSysId) {
|
|
1986
|
+
blocks.push(buildFlatRecord(buildScopeRecord(build.scope, scopeSysId), packageRef, scopeRef));
|
|
1987
|
+
}
|
|
1988
|
+
for (const artifact of build.artifacts) {
|
|
1989
|
+
for (const rec of expandArtifact(artifact, scopeSysId)) {
|
|
1990
|
+
blocks.push(buildFlatRecord(rec, packageRef, scopeRef));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1994
|
+
<unload unload_date="${now}">
|
|
1995
|
+
${blocks.join("\n")}
|
|
1996
|
+
</unload>`;
|
|
1997
|
+
}
|
|
1998
|
+
function scopeFilter(scopeSysId) {
|
|
1999
|
+
return `^sys_scope=${scopeSysId ?? "global"}`;
|
|
2000
|
+
}
|
|
2001
|
+
async function checkCrossScope(client, artifacts, targetScopeSysId, onWarning) {
|
|
2002
|
+
const targetScope = targetScopeSysId ?? "global";
|
|
2003
|
+
for (const artifact of artifacts) {
|
|
2004
|
+
const table = ARTIFACT_TABLE[artifact.type];
|
|
2005
|
+
if (!table) continue;
|
|
2006
|
+
const name = String(artifact.fields["name"] ?? "");
|
|
2007
|
+
if (!name) continue;
|
|
2008
|
+
try {
|
|
2009
|
+
const conflicts = await client.queryTable(table, {
|
|
2010
|
+
sysparmQuery: `name=${name}^sys_scope!=${targetScope}`,
|
|
2011
|
+
sysparmLimit: 1,
|
|
2012
|
+
sysparmFields: "sys_id,sys_scope"
|
|
2013
|
+
});
|
|
2014
|
+
if (conflicts.length > 0) {
|
|
2015
|
+
const conflictScope = String(conflicts[0]["sys_scope"] ?? "unknown");
|
|
2016
|
+
onWarning(` Warning: "${name}" (${artifact.type}) exists in scope "${conflictScope}" \u2014 your push targets "${targetScope}" so that record will not be touched`);
|
|
2017
|
+
}
|
|
2018
|
+
} catch {
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
async function resolveOrCreateScope(client, scope) {
|
|
2023
|
+
const existing = await client.queryTable("sys_scope", {
|
|
2024
|
+
sysparmQuery: `scope=${scope.prefix}`,
|
|
2025
|
+
sysparmLimit: 1,
|
|
2026
|
+
sysparmFields: "sys_id"
|
|
2027
|
+
});
|
|
2028
|
+
if (existing.length > 0) return String(existing[0].sys_id);
|
|
2029
|
+
const created = await client.createRecord("sys_app", {
|
|
2030
|
+
name: scope.name,
|
|
2031
|
+
scope: scope.prefix,
|
|
2032
|
+
short_description: scope.name,
|
|
2033
|
+
version: scope.version,
|
|
2034
|
+
vendor: scope.vendor ?? "",
|
|
2035
|
+
active: true,
|
|
2036
|
+
licensable: false,
|
|
2037
|
+
sys_class_name: "sys_app"
|
|
2038
|
+
});
|
|
2039
|
+
return String(created.sys_id);
|
|
2040
|
+
}
|
|
2041
|
+
async function pushArtifacts(client, build, onProgress) {
|
|
2042
|
+
const results = [];
|
|
2043
|
+
const errors = [];
|
|
2044
|
+
let scopeSysId;
|
|
2045
|
+
if (build.scope) {
|
|
2046
|
+
try {
|
|
2047
|
+
onProgress?.(` Resolving scope: ${build.scope.prefix}`);
|
|
2048
|
+
scopeSysId = await resolveOrCreateScope(client, build.scope);
|
|
2049
|
+
onProgress?.(` Scope sys_id: ${scopeSysId}`);
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
errors.push({
|
|
2052
|
+
type: "scope",
|
|
2053
|
+
name: build.scope.prefix,
|
|
2054
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
await checkCrossScope(client, build.artifacts, scopeSysId, (msg) => onProgress?.(msg));
|
|
2059
|
+
for (const artifact of build.artifacts) {
|
|
2060
|
+
const artifactName = String(artifact.fields["name"] ?? artifact.type);
|
|
2061
|
+
onProgress?.(` Pushing ${artifact.type}: ${artifactName}`);
|
|
2062
|
+
try {
|
|
2063
|
+
if (artifact.type === "table") {
|
|
2064
|
+
await pushTableArtifact(client, artifact, scopeSysId, results, errors, onProgress);
|
|
2065
|
+
} else if (artifact.type === "decision_table") {
|
|
2066
|
+
await pushDecisionTableArtifact(client, artifact, scopeSysId, results, errors, onProgress);
|
|
2067
|
+
} else if (artifact.type === "flow_action") {
|
|
2068
|
+
await pushFlowActionArtifact(client, artifact, scopeSysId, results, errors, onProgress);
|
|
2069
|
+
} else {
|
|
2070
|
+
const table = ARTIFACT_TABLE[artifact.type];
|
|
2071
|
+
if (!table) {
|
|
2072
|
+
errors.push({ type: artifact.type, name: artifactName, error: `Unknown artifact type: ${artifact.type}` });
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
const payload = { ...artifact.fields };
|
|
2076
|
+
if (payload["active"] === void 0) payload["active"] = true;
|
|
2077
|
+
if (scopeSysId) {
|
|
2078
|
+
payload["sys_scope"] = scopeSysId;
|
|
2079
|
+
payload["sys_package"] = scopeSysId;
|
|
2080
|
+
}
|
|
2081
|
+
const existing = await client.queryTable(table, {
|
|
2082
|
+
sysparmQuery: `name=${artifactName}${scopeFilter(scopeSysId)}`,
|
|
2083
|
+
sysparmLimit: 1,
|
|
2084
|
+
sysparmFields: "sys_id"
|
|
2085
|
+
});
|
|
2086
|
+
if (existing.length > 0) {
|
|
2087
|
+
const sysId = String(existing[0].sys_id);
|
|
2088
|
+
await client.updateRecord(table, sysId, payload);
|
|
2089
|
+
results.push({ type: artifact.type, name: artifactName, sysId, action: "updated" });
|
|
2090
|
+
} else {
|
|
2091
|
+
const created = await client.createRecord(table, payload);
|
|
2092
|
+
results.push({ type: artifact.type, name: artifactName, sysId: String(created.sys_id), action: "created" });
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
errors.push({
|
|
2097
|
+
type: artifact.type,
|
|
2098
|
+
name: artifactName,
|
|
2099
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return { results, errors };
|
|
2104
|
+
}
|
|
2105
|
+
async function pushTableArtifact(client, artifact, scopeSysId, results, errors, onProgress) {
|
|
2106
|
+
const tableName = String(artifact.fields["name"] ?? "");
|
|
2107
|
+
const dbObjPayload = {
|
|
2108
|
+
name: tableName,
|
|
2109
|
+
label: String(artifact.fields["label"] ?? tableName),
|
|
2110
|
+
plural: String(artifact.fields["plural"] ?? String(artifact.fields["label"] ?? tableName) + "s"),
|
|
2111
|
+
is_extendable: artifact.fields["is_extendable"] ? true : false
|
|
2112
|
+
};
|
|
2113
|
+
if (artifact.fields["extends"]) dbObjPayload["super_class"] = String(artifact.fields["extends"]);
|
|
2114
|
+
if (scopeSysId) {
|
|
2115
|
+
dbObjPayload["sys_scope"] = scopeSysId;
|
|
2116
|
+
dbObjPayload["sys_package"] = scopeSysId;
|
|
2117
|
+
}
|
|
2118
|
+
const existingTable = await client.queryTable("sys_db_object", {
|
|
2119
|
+
sysparmQuery: `name=${tableName}${scopeFilter(scopeSysId)}`,
|
|
2120
|
+
sysparmLimit: 1,
|
|
2121
|
+
sysparmFields: "sys_id"
|
|
2122
|
+
});
|
|
2123
|
+
let tableSysId;
|
|
2124
|
+
if (existingTable.length > 0) {
|
|
2125
|
+
tableSysId = String(existingTable[0].sys_id);
|
|
2126
|
+
await client.updateRecord("sys_db_object", tableSysId, dbObjPayload);
|
|
2127
|
+
results.push({ type: "Table", name: tableName, sysId: tableSysId, action: "updated" });
|
|
2128
|
+
} else {
|
|
2129
|
+
const created = await client.createRecord("sys_db_object", dbObjPayload);
|
|
2130
|
+
tableSysId = String(created.sys_id);
|
|
2131
|
+
results.push({ type: "Table", name: tableName, sysId: tableSysId, action: "created" });
|
|
2132
|
+
}
|
|
2133
|
+
const columns = Array.isArray(artifact.fields["columns"]) ? artifact.fields["columns"] : [];
|
|
2134
|
+
for (const col of columns) {
|
|
2135
|
+
const element = String(col["element"] ?? "");
|
|
2136
|
+
const isChoice = String(col["internal_type"] ?? "") === "choice";
|
|
2137
|
+
onProgress?.(` column: ${tableName}.${element}`);
|
|
2138
|
+
const dictPayload = {
|
|
2139
|
+
name: tableName,
|
|
2140
|
+
element,
|
|
2141
|
+
column_label: String(col["label"] ?? element),
|
|
2142
|
+
internal_type: isChoice ? "string" : String(col["internal_type"] ?? "string"),
|
|
2143
|
+
max_length: col["max_length"] ?? (isChoice || String(col["internal_type"]) === "string" ? 255 : ""),
|
|
2144
|
+
mandatory: col["mandatory"] ? true : false,
|
|
2145
|
+
active: true
|
|
2146
|
+
};
|
|
2147
|
+
if (col["default_value"] !== void 0) dictPayload["default_value"] = String(col["default_value"]);
|
|
2148
|
+
if (col["reference"]) dictPayload["reference"] = String(col["reference"]);
|
|
2149
|
+
if (isChoice) dictPayload["choice"] = 1;
|
|
2150
|
+
if (scopeSysId) {
|
|
2151
|
+
dictPayload["sys_scope"] = scopeSysId;
|
|
2152
|
+
dictPayload["sys_package"] = scopeSysId;
|
|
2153
|
+
}
|
|
2154
|
+
const existingDict = await client.queryTable("sys_dictionary", {
|
|
2155
|
+
sysparmQuery: `name=${tableName}^element=${element}${scopeFilter(scopeSysId)}`,
|
|
2156
|
+
sysparmLimit: 1,
|
|
2157
|
+
sysparmFields: "sys_id"
|
|
2158
|
+
});
|
|
2159
|
+
if (existingDict.length > 0) {
|
|
2160
|
+
await client.updateRecord("sys_dictionary", String(existingDict[0].sys_id), dictPayload);
|
|
2161
|
+
} else {
|
|
2162
|
+
await client.createRecord("sys_dictionary", dictPayload);
|
|
2163
|
+
}
|
|
2164
|
+
if (isChoice && Array.isArray(col["choices"])) {
|
|
2165
|
+
const choices = col["choices"];
|
|
2166
|
+
for (let ci = 0; ci < choices.length; ci++) {
|
|
2167
|
+
const ch = choices[ci];
|
|
2168
|
+
const choiceVal = typeof ch === "object" ? String(ch["value"] ?? "") : String(ch).toLowerCase().replace(/\s+/g, "_");
|
|
2169
|
+
const choiceLabel = typeof ch === "object" ? String(ch["label"] ?? ch["value"] ?? "") : String(ch);
|
|
2170
|
+
const choicePayload = {
|
|
2171
|
+
name: tableName,
|
|
2172
|
+
element,
|
|
2173
|
+
value: choiceVal,
|
|
2174
|
+
label: choiceLabel,
|
|
2175
|
+
sequence: ci * 100,
|
|
2176
|
+
inactive: false
|
|
2177
|
+
};
|
|
2178
|
+
if (scopeSysId) {
|
|
2179
|
+
choicePayload["sys_scope"] = scopeSysId;
|
|
2180
|
+
choicePayload["sys_package"] = scopeSysId;
|
|
2181
|
+
}
|
|
2182
|
+
const existingChoice = await client.queryTable("sys_choice", {
|
|
2183
|
+
sysparmQuery: `name=${tableName}^element=${element}^value=${choiceVal}${scopeFilter(scopeSysId)}`,
|
|
2184
|
+
sysparmLimit: 1,
|
|
2185
|
+
sysparmFields: "sys_id"
|
|
2186
|
+
});
|
|
2187
|
+
if (existingChoice.length > 0) {
|
|
2188
|
+
await client.updateRecord("sys_choice", String(existingChoice[0].sys_id), choicePayload);
|
|
2189
|
+
} else {
|
|
2190
|
+
await client.createRecord("sys_choice", choicePayload);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
async function pushDecisionTableArtifact(client, artifact, scopeSysId, results, errors, onProgress) {
|
|
2197
|
+
const dtName = String(artifact.fields["name"] ?? "");
|
|
2198
|
+
const dtPayload = {
|
|
2199
|
+
name: dtName,
|
|
2200
|
+
label: String(artifact.fields["label"] ?? dtName),
|
|
2201
|
+
description: String(artifact.fields["description"] ?? ""),
|
|
2202
|
+
active: true
|
|
2203
|
+
};
|
|
2204
|
+
if (scopeSysId) {
|
|
2205
|
+
dtPayload["sys_scope"] = scopeSysId;
|
|
2206
|
+
dtPayload["sys_package"] = scopeSysId;
|
|
2207
|
+
}
|
|
2208
|
+
const existing = await client.queryTable("sys_decision", {
|
|
2209
|
+
sysparmQuery: `name=${dtName}${scopeFilter(scopeSysId)}`,
|
|
2210
|
+
sysparmLimit: 1,
|
|
2211
|
+
sysparmFields: "sys_id"
|
|
2212
|
+
});
|
|
2213
|
+
let decisionSysId;
|
|
2214
|
+
if (existing.length > 0) {
|
|
2215
|
+
decisionSysId = String(existing[0].sys_id);
|
|
2216
|
+
await client.updateRecord("sys_decision", decisionSysId, dtPayload);
|
|
2217
|
+
results.push({ type: "Decision Table", name: dtName, sysId: decisionSysId, action: "updated" });
|
|
2218
|
+
onProgress?.(` Removing existing rules for: ${dtName}`);
|
|
2219
|
+
for (const childTable of ["sys_decision_question", "sys_decision_case"]) {
|
|
2220
|
+
const children = await client.queryTable(childTable, {
|
|
2221
|
+
sysparmQuery: `decision=${decisionSysId}`,
|
|
2222
|
+
sysparmFields: "sys_id",
|
|
2223
|
+
sysparmLimit: 500
|
|
2224
|
+
});
|
|
2225
|
+
for (const child of children) {
|
|
2226
|
+
await client.deleteRecord(childTable, String(child.sys_id));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
} else {
|
|
2230
|
+
const created = await client.createRecord("sys_decision", dtPayload);
|
|
2231
|
+
decisionSysId = String(created.sys_id);
|
|
2232
|
+
results.push({ type: "Decision Table", name: dtName, sysId: decisionSysId, action: "created" });
|
|
2233
|
+
}
|
|
2234
|
+
const inputs = Array.isArray(artifact.fields["inputs"]) ? artifact.fields["inputs"] : [];
|
|
2235
|
+
const inputSysIds = {};
|
|
2236
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
2237
|
+
const inp = inputs[i];
|
|
2238
|
+
const inpName = String(inp["name"] ?? inp["field"] ?? `input_${i}`);
|
|
2239
|
+
const qPayload = {
|
|
2240
|
+
decision: decisionSysId,
|
|
2241
|
+
label: String(inp["label"] ?? inpName),
|
|
2242
|
+
order: i * 100,
|
|
2243
|
+
question_type: String(inp["type"] ?? "string")
|
|
2244
|
+
};
|
|
2245
|
+
if (inp["reference"]) qPayload["reference_table"] = String(inp["reference"]);
|
|
2246
|
+
if (scopeSysId) {
|
|
2247
|
+
qPayload["sys_scope"] = scopeSysId;
|
|
2248
|
+
qPayload["sys_package"] = scopeSysId;
|
|
2249
|
+
}
|
|
2250
|
+
const created = await client.createRecord("sys_decision_question", qPayload);
|
|
2251
|
+
inputSysIds[inpName] = String(created.sys_id);
|
|
2252
|
+
}
|
|
2253
|
+
const rules = Array.isArray(artifact.fields["rules"]) ? artifact.fields["rules"] : [];
|
|
2254
|
+
for (let ri = 0; ri < rules.length; ri++) {
|
|
2255
|
+
const rule = rules[ri];
|
|
2256
|
+
const casePayload = {
|
|
2257
|
+
decision: decisionSysId,
|
|
2258
|
+
label: String(rule["label"] ?? `Rule ${ri + 1}`),
|
|
2259
|
+
order: ri * 100,
|
|
2260
|
+
result: String(rule["result"] ?? "")
|
|
2261
|
+
};
|
|
2262
|
+
if (scopeSysId) {
|
|
2263
|
+
casePayload["sys_scope"] = scopeSysId;
|
|
2264
|
+
casePayload["sys_package"] = scopeSysId;
|
|
2265
|
+
}
|
|
2266
|
+
const createdCase = await client.createRecord("sys_decision_case", casePayload);
|
|
2267
|
+
const caseSysId = String(createdCase.sys_id);
|
|
2268
|
+
const conditions = Array.isArray(rule["conditions"]) ? rule["conditions"] : [];
|
|
2269
|
+
for (const cond of conditions) {
|
|
2270
|
+
const inputName = String(cond["input"] ?? "");
|
|
2271
|
+
const cqPayload = {
|
|
2272
|
+
case: caseSysId,
|
|
2273
|
+
question: inputSysIds[inputName] ?? "",
|
|
2274
|
+
operator: String(cond["operator"] ?? "="),
|
|
2275
|
+
value: String(cond["value"] ?? "")
|
|
2276
|
+
};
|
|
2277
|
+
if (scopeSysId) {
|
|
2278
|
+
cqPayload["sys_scope"] = scopeSysId;
|
|
2279
|
+
cqPayload["sys_package"] = scopeSysId;
|
|
2280
|
+
}
|
|
2281
|
+
await client.createRecord("sys_decision_case_question", cqPayload);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
async function pushFlowActionArtifact(client, artifact, scopeSysId, results, errors, onProgress) {
|
|
2286
|
+
const actionName = String(artifact.fields["name"] ?? "");
|
|
2287
|
+
const actionPayload = {
|
|
2288
|
+
name: actionName,
|
|
2289
|
+
label: String(artifact.fields["label"] ?? actionName),
|
|
2290
|
+
description: String(artifact.fields["description"] ?? ""),
|
|
2291
|
+
category: String(artifact.fields["category"] ?? "Custom"),
|
|
2292
|
+
script: String(artifact.fields["script"] ?? ""),
|
|
2293
|
+
active: true
|
|
2294
|
+
};
|
|
2295
|
+
if (scopeSysId) {
|
|
2296
|
+
actionPayload["sys_scope"] = scopeSysId;
|
|
2297
|
+
actionPayload["sys_package"] = scopeSysId;
|
|
2298
|
+
}
|
|
2299
|
+
const existing = await client.queryTable("sys_hub_action_type_definition", {
|
|
2300
|
+
sysparmQuery: `name=${actionName}${scopeFilter(scopeSysId)}`,
|
|
2301
|
+
sysparmLimit: 1,
|
|
2302
|
+
sysparmFields: "sys_id"
|
|
2303
|
+
});
|
|
2304
|
+
let actionSysId;
|
|
2305
|
+
if (existing.length > 0) {
|
|
2306
|
+
actionSysId = String(existing[0].sys_id);
|
|
2307
|
+
await client.updateRecord("sys_hub_action_type_definition", actionSysId, actionPayload);
|
|
2308
|
+
results.push({ type: "Flow Action", name: actionName, sysId: actionSysId, action: "updated" });
|
|
2309
|
+
onProgress?.(` Removing existing variables for: ${actionName}`);
|
|
2310
|
+
for (const childTable of ["sys_hub_action_input", "sys_hub_action_output"]) {
|
|
2311
|
+
const children = await client.queryTable(childTable, {
|
|
2312
|
+
sysparmQuery: `action_type=${actionSysId}`,
|
|
2313
|
+
sysparmFields: "sys_id",
|
|
2314
|
+
sysparmLimit: 100
|
|
2315
|
+
});
|
|
2316
|
+
for (const child of children) {
|
|
2317
|
+
await client.deleteRecord(childTable, String(child.sys_id));
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
} else {
|
|
2321
|
+
const created = await client.createRecord("sys_hub_action_type_definition", actionPayload);
|
|
2322
|
+
actionSysId = String(created.sys_id);
|
|
2323
|
+
results.push({ type: "Flow Action", name: actionName, sysId: actionSysId, action: "created" });
|
|
2324
|
+
}
|
|
2325
|
+
const inputs = Array.isArray(artifact.fields["inputs"]) ? artifact.fields["inputs"] : [];
|
|
2326
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
2327
|
+
const inp = inputs[i];
|
|
2328
|
+
const inpPayload = {
|
|
2329
|
+
action_type: actionSysId,
|
|
2330
|
+
name: String(inp["name"] ?? `input_${i}`),
|
|
2331
|
+
label: String(inp["label"] ?? inp["name"] ?? `Input ${i + 1}`),
|
|
2332
|
+
type: String(inp["type"] ?? "string"),
|
|
2333
|
+
mandatory: inp["mandatory"] ? true : false,
|
|
2334
|
+
order: i * 100
|
|
2335
|
+
};
|
|
2336
|
+
if (scopeSysId) {
|
|
2337
|
+
inpPayload["sys_scope"] = scopeSysId;
|
|
2338
|
+
inpPayload["sys_package"] = scopeSysId;
|
|
2339
|
+
}
|
|
2340
|
+
await client.createRecord("sys_hub_action_input", inpPayload);
|
|
2341
|
+
}
|
|
2342
|
+
const outputs = Array.isArray(artifact.fields["outputs"]) ? artifact.fields["outputs"] : [];
|
|
2343
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
2344
|
+
const out = outputs[i];
|
|
2345
|
+
const outPayload = {
|
|
2346
|
+
action_type: actionSysId,
|
|
2347
|
+
name: String(out["name"] ?? `output_${i}`),
|
|
2348
|
+
label: String(out["label"] ?? out["name"] ?? `Output ${i + 1}`),
|
|
2349
|
+
type: String(out["type"] ?? "string"),
|
|
2350
|
+
order: i * 100
|
|
2351
|
+
};
|
|
2352
|
+
if (scopeSysId) {
|
|
2353
|
+
outPayload["sys_scope"] = scopeSysId;
|
|
2354
|
+
outPayload["sys_package"] = scopeSysId;
|
|
2355
|
+
}
|
|
2356
|
+
await client.createRecord("sys_hub_action_output", outPayload);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/commands/ai.ts
|
|
2361
|
+
var debugMode = false;
|
|
2362
|
+
function dbg(label, value) {
|
|
2363
|
+
if (!debugMode) return;
|
|
2364
|
+
console.error(chalk6.dim(`[debug] ${label}`));
|
|
2365
|
+
if (typeof value === "string") {
|
|
2366
|
+
console.error(chalk6.dim(value));
|
|
2367
|
+
} else {
|
|
2368
|
+
console.error(chalk6.dim(JSON.stringify(value, null, 2)));
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function resolveProvider() {
|
|
2372
|
+
const active = getActiveProvider();
|
|
2373
|
+
if (!active) {
|
|
2374
|
+
console.error(
|
|
2375
|
+
chalk6.red("No LLM provider configured. Run `snow provider set <name>` first.")
|
|
2376
|
+
);
|
|
2377
|
+
process.exit(1);
|
|
2378
|
+
}
|
|
2379
|
+
dbg("active provider", { name: active.name, model: active.config.model });
|
|
2380
|
+
return buildProvider(
|
|
2381
|
+
active.name,
|
|
2382
|
+
active.config.model,
|
|
2383
|
+
active.config.apiKey,
|
|
2384
|
+
active.config.baseUrl
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
function normaliseBuildResponse(parsed) {
|
|
2388
|
+
const name = String(parsed["name"] ?? "");
|
|
2389
|
+
const description = String(parsed["description"] ?? "");
|
|
2390
|
+
if (!name) throw new Error('Response missing "name" field');
|
|
2391
|
+
const rawArtifacts = parsed["artifacts"];
|
|
2392
|
+
if (!Array.isArray(rawArtifacts)) {
|
|
2393
|
+
throw new Error('Response missing "artifacts" array');
|
|
2394
|
+
}
|
|
2395
|
+
const artifacts = rawArtifacts.map((a, i) => {
|
|
2396
|
+
if (typeof a !== "object" || a === null) {
|
|
2397
|
+
throw new Error(`Artifact[${i}] is not an object`);
|
|
2398
|
+
}
|
|
2399
|
+
const art = a;
|
|
2400
|
+
const type = String(art["type"] ?? "");
|
|
2401
|
+
if (!type) throw new Error(`Artifact[${i}] missing "type"`);
|
|
2402
|
+
let fields;
|
|
2403
|
+
if (art["fields"] && typeof art["fields"] === "object" && !Array.isArray(art["fields"])) {
|
|
2404
|
+
fields = art["fields"];
|
|
2405
|
+
} else {
|
|
2406
|
+
const { type: _t, ...rest } = art;
|
|
2407
|
+
fields = rest;
|
|
2408
|
+
}
|
|
2409
|
+
return { type, fields };
|
|
2410
|
+
});
|
|
2411
|
+
let scope;
|
|
2412
|
+
const rawScope = parsed["scope"];
|
|
2413
|
+
if (rawScope && typeof rawScope === "object" && !Array.isArray(rawScope)) {
|
|
2414
|
+
const s = rawScope;
|
|
2415
|
+
const prefix = String(s["prefix"] ?? "");
|
|
2416
|
+
if (prefix) {
|
|
2417
|
+
scope = {
|
|
2418
|
+
prefix,
|
|
2419
|
+
name: String(s["name"] ?? prefix),
|
|
2420
|
+
version: String(s["version"] ?? "1.0.0"),
|
|
2421
|
+
...s["vendor"] ? { vendor: String(s["vendor"]) } : {}
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return { name, description, scope, artifacts };
|
|
2426
|
+
}
|
|
2427
|
+
function parseBuildResponse(raw) {
|
|
2428
|
+
const json = extractJSON(raw);
|
|
2429
|
+
dbg("extracted JSON", json);
|
|
2430
|
+
const parsed = JSON.parse(json);
|
|
2431
|
+
return normaliseBuildResponse(parsed);
|
|
2432
|
+
}
|
|
2433
|
+
function slugify(str) {
|
|
2434
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2435
|
+
}
|
|
2436
|
+
function diffBuilds(previous, next) {
|
|
2437
|
+
if (!previous) {
|
|
2438
|
+
return { added: next.artifacts, updated: [], removed: [] };
|
|
2439
|
+
}
|
|
2440
|
+
const prevByKey = new Map(
|
|
2441
|
+
previous.artifacts.map((a) => [`${a.type}:${String(a.fields["name"] ?? "")}`, a])
|
|
2442
|
+
);
|
|
2443
|
+
const nextByKey = new Map(
|
|
2444
|
+
next.artifacts.map((a) => [`${a.type}:${String(a.fields["name"] ?? "")}`, a])
|
|
2445
|
+
);
|
|
2446
|
+
const added = [];
|
|
2447
|
+
const updated = [];
|
|
2448
|
+
const removed = [];
|
|
2449
|
+
for (const [key, artifact] of nextByKey) {
|
|
2450
|
+
if (!prevByKey.has(key)) {
|
|
2451
|
+
added.push(artifact);
|
|
2452
|
+
} else {
|
|
2453
|
+
const prev = prevByKey.get(key);
|
|
2454
|
+
if (JSON.stringify(prev.fields) !== JSON.stringify(artifact.fields)) {
|
|
2455
|
+
updated.push(artifact);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
for (const [key, artifact] of prevByKey) {
|
|
2460
|
+
if (!nextByKey.has(key)) removed.push(artifact);
|
|
2461
|
+
}
|
|
2462
|
+
return { added, updated, removed };
|
|
2463
|
+
}
|
|
2464
|
+
function printBuildSummary(build, previous = null) {
|
|
2465
|
+
console.log();
|
|
2466
|
+
if (previous && previous.name !== build.name) {
|
|
2467
|
+
console.log(`${chalk6.bold(build.name)} ${chalk6.dim(`(was: ${previous.name})`)}`);
|
|
2468
|
+
} else {
|
|
2469
|
+
console.log(chalk6.bold(build.name));
|
|
2470
|
+
}
|
|
2471
|
+
if (build.description) console.log(chalk6.dim(build.description));
|
|
2472
|
+
if (build.scope) {
|
|
2473
|
+
console.log(chalk6.dim(` scope: ${chalk6.white(build.scope.prefix)} v${build.scope.version}`));
|
|
2474
|
+
}
|
|
2475
|
+
console.log();
|
|
2476
|
+
if (previous) {
|
|
2477
|
+
const { added, updated, removed } = diffBuilds(previous, build);
|
|
2478
|
+
if (added.length === 0 && updated.length === 0 && removed.length === 0) {
|
|
2479
|
+
console.log(chalk6.dim(" No changes to artifacts."));
|
|
2480
|
+
} else {
|
|
2481
|
+
for (const a of added) {
|
|
2482
|
+
console.log(` ${chalk6.green("+")} ${chalk6.cyan(a.type.padEnd(16))} ${String(a.fields["name"] ?? "")}`);
|
|
2483
|
+
}
|
|
2484
|
+
for (const a of updated) {
|
|
2485
|
+
console.log(` ${chalk6.yellow("~")} ${chalk6.cyan(a.type.padEnd(16))} ${String(a.fields["name"] ?? "")}`);
|
|
2486
|
+
}
|
|
2487
|
+
for (const a of removed) {
|
|
2488
|
+
console.log(` ${chalk6.red("-")} ${chalk6.cyan(a.type.padEnd(16))} ${String(a.fields["name"] ?? "")}`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
} else {
|
|
2492
|
+
for (const a of build.artifacts) {
|
|
2493
|
+
const name = String(a.fields["name"] ?? "(unnamed)");
|
|
2494
|
+
console.log(` ${chalk6.green("+")} ${chalk6.cyan(a.type.padEnd(16))} ${name}`);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
console.log();
|
|
2498
|
+
}
|
|
2499
|
+
function saveBuild(build, customDir) {
|
|
2500
|
+
const dir = customDir ?? slugify(build.name);
|
|
2501
|
+
mkdirSync3(dir, { recursive: true });
|
|
2502
|
+
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");
|
|
2507
|
+
return dir;
|
|
2508
|
+
}
|
|
2509
|
+
function printSaveResult(dir, build) {
|
|
2510
|
+
const base = slugify(build.name);
|
|
2511
|
+
console.log(chalk6.green(`\u2714 ${dir}/`));
|
|
2512
|
+
console.log(chalk6.dim(` ${base}.xml`));
|
|
2513
|
+
console.log(chalk6.dim(` ${base}.manifest.json`));
|
|
2514
|
+
console.log(chalk6.dim(" Import: System Update Sets \u2192 Retrieved Update Sets \u2192 Import XML"));
|
|
2515
|
+
}
|
|
2516
|
+
async function doPush(build) {
|
|
2517
|
+
const instance = getActiveInstance();
|
|
2518
|
+
if (!instance) {
|
|
2519
|
+
console.error(chalk6.red("No active instance. Run `snow instance add` first."));
|
|
2520
|
+
process.exit(1);
|
|
2521
|
+
}
|
|
2522
|
+
const { ServiceNowClient: ServiceNowClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2523
|
+
const client = new ServiceNowClient2(instance);
|
|
2524
|
+
console.log(chalk6.bold(`Pushing to ${instance.alias} (${instance.url})\u2026`));
|
|
2525
|
+
const summary = await pushArtifacts(
|
|
2526
|
+
client,
|
|
2527
|
+
build,
|
|
2528
|
+
(msg) => console.log(chalk6.dim(msg))
|
|
2529
|
+
);
|
|
2530
|
+
console.log();
|
|
2531
|
+
for (const r of summary.results) {
|
|
2532
|
+
const action = r.action === "created" ? chalk6.green("created") : chalk6.yellow("updated");
|
|
2533
|
+
console.log(` ${action} ${r.type.padEnd(16)} ${r.name} ${chalk6.dim(r.sysId)}`);
|
|
2534
|
+
}
|
|
2535
|
+
for (const e of summary.errors) {
|
|
2536
|
+
console.log(` ${chalk6.red("error")} ${e.type.padEnd(16)} ${e.name} ${chalk6.red(e.error)}`);
|
|
2537
|
+
}
|
|
2538
|
+
console.log(chalk6.dim(`
|
|
2539
|
+
${summary.results.length} pushed, ${summary.errors.length} failed`));
|
|
2540
|
+
}
|
|
2541
|
+
async function confirmPush(build, dir, autoPush) {
|
|
2542
|
+
const instance = getActiveInstance();
|
|
2543
|
+
if (!instance) {
|
|
2544
|
+
console.log(chalk6.dim(` Run \`snow ai push ${dir}/\` to deploy later.`));
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
let shouldPush = autoPush ?? false;
|
|
2548
|
+
if (!shouldPush) {
|
|
2549
|
+
console.log();
|
|
2550
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2551
|
+
shouldPush = await confirm2({
|
|
2552
|
+
message: `Push ${build.artifacts.length} artifact(s) to ${chalk6.cyan(instance.alias)} (${instance.url})?`,
|
|
2553
|
+
default: false
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
if (shouldPush) {
|
|
2557
|
+
console.log();
|
|
2558
|
+
await doPush(build);
|
|
2559
|
+
} else {
|
|
2560
|
+
console.log(chalk6.dim(` Run \`snow ai push ${dir}/\` to deploy later.`));
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
var CODE_FIELDS = {
|
|
2564
|
+
script_include: [{ field: "script", ext: ".js", label: "Script" }],
|
|
2565
|
+
business_rule: [{ field: "script", ext: ".js", label: "Script" }],
|
|
2566
|
+
client_script: [{ field: "script", ext: ".js", label: "Script" }],
|
|
2567
|
+
ui_action: [
|
|
2568
|
+
{ field: "script", ext: ".js", label: "Server Script" },
|
|
2569
|
+
{ field: "onclick", ext: ".js", label: "Client onclick" }
|
|
2570
|
+
],
|
|
2571
|
+
ui_page: [
|
|
2572
|
+
{ field: "html", ext: ".html", label: "HTML" },
|
|
2573
|
+
{ field: "client_script", ext: ".js", label: "Client Script" },
|
|
2574
|
+
{ field: "processing_script", ext: ".js", label: "Processing Script" }
|
|
2575
|
+
],
|
|
2576
|
+
scheduled_job: [{ field: "script", ext: ".js", label: "Script" }],
|
|
2577
|
+
flow_action: [{ field: "script", ext: ".js", label: "Action Script" }]
|
|
2578
|
+
// table and decision_table have no inline code fields — schema is reviewed via the manifest
|
|
2579
|
+
};
|
|
2580
|
+
function resolveEditor2() {
|
|
2581
|
+
if (process.env["VISUAL"]) return process.env["VISUAL"];
|
|
2582
|
+
if (process.env["EDITOR"]) return process.env["EDITOR"];
|
|
2583
|
+
const isWin = process.platform === "win32";
|
|
2584
|
+
const lookup = isWin ? "where" : "which";
|
|
2585
|
+
const candidates = isWin ? ["code", "notepad++", "notepad"] : ["code", "nvim", "vim", "nano", "vi"];
|
|
2586
|
+
for (const e of candidates) {
|
|
2587
|
+
if (spawnSync2(lookup, [e], { encoding: "utf-8", shell: isWin }).status === 0) return e;
|
|
2588
|
+
}
|
|
2589
|
+
return isWin ? "notepad" : "vi";
|
|
2590
|
+
}
|
|
2591
|
+
function resolveManifestPath(path2) {
|
|
2592
|
+
if (path2.endsWith(".manifest.json") && existsSync3(path2)) return path2;
|
|
2593
|
+
if (path2.endsWith(".xml")) {
|
|
2594
|
+
const m = path2.replace(/\.xml$/, ".manifest.json");
|
|
2595
|
+
if (existsSync3(m)) return m;
|
|
2596
|
+
}
|
|
2597
|
+
if (existsSync3(path2) && !path2.includes(".")) {
|
|
2598
|
+
const files = readdirSync2(path2).filter((f) => f.endsWith(".manifest.json"));
|
|
2599
|
+
if (files.length > 0) return join3(path2, files[0]);
|
|
2600
|
+
}
|
|
2601
|
+
console.error(chalk6.red(`Cannot resolve build manifest from: ${path2}`));
|
|
2602
|
+
console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
|
|
2603
|
+
process.exit(1);
|
|
2604
|
+
}
|
|
2605
|
+
function printCodeBlock(code, label) {
|
|
2606
|
+
const lines = code.split("\n");
|
|
2607
|
+
const width = Math.min(process.stdout.columns ?? 100, 100);
|
|
2608
|
+
console.log(chalk6.dim("\u2500".repeat(width) + ` ${label}`));
|
|
2609
|
+
lines.forEach((line, i) => {
|
|
2610
|
+
const num = chalk6.dim(String(i + 1).padStart(4) + " ");
|
|
2611
|
+
console.log(num + line);
|
|
2612
|
+
});
|
|
2613
|
+
console.log(chalk6.dim("\u2500".repeat(width)));
|
|
2614
|
+
}
|
|
2615
|
+
async function runReview(build, buildDir) {
|
|
2616
|
+
const { select: select2, confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2617
|
+
const editor = resolveEditor2();
|
|
2618
|
+
let modified = false;
|
|
2619
|
+
console.log();
|
|
2620
|
+
console.log(chalk6.bold(`Reviewing: ${build.name}`));
|
|
2621
|
+
console.log(chalk6.dim(`${build.artifacts.length} artifact(s) \u2022 editor: ${editor}`));
|
|
2622
|
+
console.log();
|
|
2623
|
+
while (true) {
|
|
2624
|
+
const artifactChoices = build.artifacts.map((a, i) => {
|
|
2625
|
+
const name = String(a.fields["name"] ?? `(${a.type})`);
|
|
2626
|
+
const hasCode = (CODE_FIELDS[a.type] ?? []).some((f) => {
|
|
2627
|
+
const v = a.fields[f.field];
|
|
2628
|
+
return v !== void 0 && v !== null && String(v).trim() !== "";
|
|
2629
|
+
});
|
|
2630
|
+
return {
|
|
2631
|
+
name: `${chalk6.cyan(a.type.padEnd(16))} ${name}${hasCode ? "" : chalk6.dim(" (no code)")}`,
|
|
2632
|
+
value: i
|
|
2633
|
+
};
|
|
2634
|
+
});
|
|
2635
|
+
artifactChoices.push({ name: chalk6.dim("\u2500\u2500 Done reviewing \u2500\u2500"), value: -1 });
|
|
2636
|
+
const selectedIndex = await select2({
|
|
2637
|
+
message: "Select an artifact to review:",
|
|
2638
|
+
choices: artifactChoices
|
|
2639
|
+
});
|
|
2640
|
+
if (selectedIndex === -1) break;
|
|
2641
|
+
const artifact = build.artifacts[selectedIndex];
|
|
2642
|
+
const artifactName = String(artifact.fields["name"] ?? artifact.type);
|
|
2643
|
+
const codeFields = (CODE_FIELDS[artifact.type] ?? []).filter(
|
|
2644
|
+
(f) => artifact.fields[f.field] !== void 0
|
|
2645
|
+
);
|
|
2646
|
+
if (codeFields.length === 0) {
|
|
2647
|
+
console.log(chalk6.yellow(" No editable code fields for this artifact type."));
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
let fieldDef = codeFields[0];
|
|
2651
|
+
if (codeFields.length > 1) {
|
|
2652
|
+
const fieldIndex = await select2({
|
|
2653
|
+
message: `Which field of "${artifactName}" to review?`,
|
|
2654
|
+
choices: codeFields.map((f, i) => ({ name: f.label, value: i }))
|
|
2655
|
+
});
|
|
2656
|
+
fieldDef = codeFields[fieldIndex];
|
|
2657
|
+
}
|
|
2658
|
+
const currentCode = String(artifact.fields[fieldDef.field] ?? "");
|
|
2659
|
+
console.log();
|
|
2660
|
+
printCodeBlock(currentCode, `${artifactName} \u2014 ${fieldDef.label}`);
|
|
2661
|
+
console.log();
|
|
2662
|
+
const shouldEdit = await confirm2({ message: "Open in editor to edit?", default: false });
|
|
2663
|
+
if (shouldEdit) {
|
|
2664
|
+
const tmpFile = join3(tmpdir2(), `snow-review-${Date.now()}${fieldDef.ext}`);
|
|
2665
|
+
writeFileSync3(tmpFile, currentCode, "utf-8");
|
|
2666
|
+
const isWin = process.platform === "win32";
|
|
2667
|
+
spawnSync2(editor, [tmpFile], { stdio: "inherit", shell: isWin });
|
|
2668
|
+
const updatedCode = readFileSync3(tmpFile, "utf-8");
|
|
2669
|
+
if (updatedCode !== currentCode) {
|
|
2670
|
+
build.artifacts[selectedIndex].fields[fieldDef.field] = updatedCode;
|
|
2671
|
+
modified = true;
|
|
2672
|
+
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");
|
|
2677
|
+
console.log(chalk6.green(` \u2714 Saved changes to ${basename(manifestFile)}`));
|
|
2678
|
+
} else {
|
|
2679
|
+
console.log(chalk6.dim(" No changes made."));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
console.log();
|
|
2683
|
+
}
|
|
2684
|
+
if (modified) {
|
|
2685
|
+
console.log(chalk6.green("\u2714 Build updated with your edits."));
|
|
2686
|
+
}
|
|
2687
|
+
return build;
|
|
2688
|
+
}
|
|
2689
|
+
function aiCommand() {
|
|
2690
|
+
const cmd = new Command6("ai").description(
|
|
2691
|
+
"Generate ServiceNow applications using an LLM and export as an update set"
|
|
2692
|
+
);
|
|
2693
|
+
cmd.command("build <prompt>").description("Generate a ServiceNow application from a text description").option("-o, --output <dir>", "Output directory (default: slugified update set name)").option("--push", "Push artifacts directly to the active instance after generating").option("--provider <name>", "Override the active provider for this request").option("--debug", "Print raw LLM request/response and full error stack traces").action(
|
|
2694
|
+
async (prompt, opts) => {
|
|
2695
|
+
if (opts.debug) debugMode = true;
|
|
2696
|
+
let provider = resolveProvider();
|
|
2697
|
+
if (opts.provider) {
|
|
2698
|
+
const { getAIConfig: getAIConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2699
|
+
const ai = getAIConfig2();
|
|
2700
|
+
const pc = ai.providers[opts.provider];
|
|
2701
|
+
if (!pc) {
|
|
2702
|
+
console.error(chalk6.red(`Provider "${opts.provider}" is not configured.`));
|
|
2703
|
+
process.exit(1);
|
|
2704
|
+
}
|
|
2705
|
+
provider = buildProvider(opts.provider, pc.model, pc.apiKey, pc.baseUrl);
|
|
2706
|
+
}
|
|
2707
|
+
dbg("prompt", prompt);
|
|
2708
|
+
const history = [
|
|
2709
|
+
{ role: "system", content: SN_SYSTEM_PROMPT },
|
|
2710
|
+
{ role: "user", content: prompt }
|
|
2711
|
+
];
|
|
2712
|
+
const { input: promptInput } = await import("@inquirer/prompts");
|
|
2713
|
+
let build = null;
|
|
2714
|
+
while (!build) {
|
|
2715
|
+
const spinner = ora5(`Generating with ${provider.providerName}\u2026`).start();
|
|
2716
|
+
let raw;
|
|
2717
|
+
try {
|
|
2718
|
+
raw = await provider.complete(history);
|
|
2719
|
+
spinner.stop();
|
|
2720
|
+
dbg("raw LLM response", raw);
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
spinner.fail(chalk6.red("LLM request failed"));
|
|
2723
|
+
console.error(chalk6.red(opts.debug && err instanceof Error ? err.stack ?? err.message : err instanceof Error ? err.message : String(err)));
|
|
2724
|
+
process.exit(1);
|
|
2725
|
+
}
|
|
2726
|
+
history.push({ role: "assistant", content: raw });
|
|
2727
|
+
let parsed = null;
|
|
2728
|
+
try {
|
|
2729
|
+
parsed = parseBuildResponse(raw);
|
|
2730
|
+
dbg("parsed build", parsed);
|
|
2731
|
+
} catch {
|
|
2732
|
+
}
|
|
2733
|
+
if (parsed) {
|
|
2734
|
+
build = parsed;
|
|
2735
|
+
} else {
|
|
2736
|
+
console.log();
|
|
2737
|
+
const lines = raw.trim().split("\n");
|
|
2738
|
+
console.log(chalk6.magenta("AI > ") + lines[0]);
|
|
2739
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2740
|
+
console.log(" " + lines[i]);
|
|
2741
|
+
}
|
|
2742
|
+
console.log();
|
|
2743
|
+
let answer = "";
|
|
2744
|
+
while (!answer.trim()) {
|
|
2745
|
+
answer = await promptInput({ message: chalk6.cyan("You") });
|
|
2746
|
+
}
|
|
2747
|
+
history.push({ role: "user", content: answer.trim() });
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
const validationErrors = validateBuild(build);
|
|
2751
|
+
if (validationErrors.length > 0) {
|
|
2752
|
+
console.warn(chalk6.yellow("Warning: some artifacts are missing required fields:"));
|
|
2753
|
+
for (const e of validationErrors) {
|
|
2754
|
+
console.warn(chalk6.yellow(` [${e.artifactIndex}] ${e.type} "${e.name}" \u2014 missing: ${e.missing.join(", ")}`));
|
|
2755
|
+
}
|
|
2756
|
+
console.log();
|
|
2757
|
+
}
|
|
2758
|
+
printBuildSummary(build);
|
|
2759
|
+
const dir = saveBuild(build, opts.output);
|
|
2760
|
+
printSaveResult(dir, build);
|
|
2761
|
+
const { confirm: confirmReview } = await import("@inquirer/prompts");
|
|
2762
|
+
const wantReview = await confirmReview({
|
|
2763
|
+
message: "Review generated artifacts before pushing?",
|
|
2764
|
+
default: false
|
|
2765
|
+
});
|
|
2766
|
+
if (wantReview) {
|
|
2767
|
+
build = await runReview(build, dir);
|
|
2768
|
+
}
|
|
2769
|
+
await confirmPush(build, dir, opts.push);
|
|
2770
|
+
}
|
|
2771
|
+
);
|
|
2772
|
+
cmd.command("push <path>").description("Push a previously generated build to the active instance via Table API").action(async (path2) => {
|
|
2773
|
+
let manifestPath;
|
|
2774
|
+
if (path2.endsWith(".manifest.json") && existsSync3(path2)) {
|
|
2775
|
+
manifestPath = path2;
|
|
2776
|
+
} else if (path2.endsWith(".xml") && existsSync3(path2.replace(/\.xml$/, ".manifest.json"))) {
|
|
2777
|
+
manifestPath = path2.replace(/\.xml$/, ".manifest.json");
|
|
2778
|
+
} else if (!path2.includes(".") && existsSync3(path2)) {
|
|
2779
|
+
const files = (await import("fs")).readdirSync(path2).filter((f) => f.endsWith(".manifest.json"));
|
|
2780
|
+
if (files.length === 0) {
|
|
2781
|
+
console.error(chalk6.red(`No .manifest.json found in directory: ${path2}`));
|
|
2782
|
+
process.exit(1);
|
|
2783
|
+
}
|
|
2784
|
+
manifestPath = join3(path2, files[0]);
|
|
2785
|
+
} else {
|
|
2786
|
+
console.error(chalk6.red(`Cannot resolve build from: ${path2}`));
|
|
2787
|
+
console.error(chalk6.dim("Pass a build directory, a .xml file, or a .manifest.json file."));
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
const build = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
2791
|
+
await doPush(build);
|
|
2792
|
+
});
|
|
2793
|
+
cmd.command("chat").description("Interactively build a ServiceNow application through conversation with an LLM").option("-o, --output <dir>", "Directory to auto-save builds (default: slugified name)").option("--push", "Auto-push to the active instance when a build is generated").option("--debug", "Print raw LLM responses and full error stack traces").action(async (opts) => {
|
|
2794
|
+
if (opts.debug) debugMode = true;
|
|
2795
|
+
const provider = resolveProvider();
|
|
2796
|
+
const history = [
|
|
2797
|
+
{ role: "system", content: SN_SYSTEM_PROMPT }
|
|
2798
|
+
];
|
|
2799
|
+
const BANNER = `
|
|
2800
|
+
${chalk6.bold.cyan("Snow AI \u2014 ServiceNow App Builder")}
|
|
2801
|
+
${chalk6.dim(`Provider: ${chalk6.white(provider.providerName)}`)}
|
|
2802
|
+
${chalk6.dim("Describe what you want to build. The AI may ask clarifying questions before generating.")}
|
|
2803
|
+
${chalk6.dim("Slash commands:")}
|
|
2804
|
+
${chalk6.dim("/status show current build")}
|
|
2805
|
+
${chalk6.dim("/save write XML + manifest to disk")}
|
|
2806
|
+
${chalk6.dim("/push push current build to active instance")}
|
|
2807
|
+
${chalk6.dim("/clear reset the session")}
|
|
2808
|
+
${chalk6.dim("/exit quit")}
|
|
2809
|
+
`;
|
|
2810
|
+
console.log(BANNER);
|
|
2811
|
+
const rl = readline.createInterface({
|
|
2812
|
+
input: process.stdin,
|
|
2813
|
+
output: process.stdout,
|
|
2814
|
+
prompt: chalk6.cyan("You > "),
|
|
2815
|
+
terminal: true
|
|
2816
|
+
});
|
|
2817
|
+
let lastBuild = null;
|
|
2818
|
+
let buildDir = null;
|
|
2819
|
+
rl.prompt();
|
|
2820
|
+
rl.on("line", async (line) => {
|
|
2821
|
+
const input2 = line.trim();
|
|
2822
|
+
if (!input2) {
|
|
2823
|
+
rl.prompt();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
if (input2 === "/exit" || input2 === "/quit") {
|
|
2827
|
+
console.log(chalk6.dim("Bye."));
|
|
2828
|
+
rl.close();
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (input2 === "/clear") {
|
|
2832
|
+
history.splice(1);
|
|
2833
|
+
lastBuild = null;
|
|
2834
|
+
buildDir = null;
|
|
2835
|
+
console.log(chalk6.dim("Session cleared."));
|
|
2836
|
+
rl.prompt();
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (input2 === "/status") {
|
|
2840
|
+
if (!lastBuild) {
|
|
2841
|
+
console.log(chalk6.dim("No build yet. Describe what you want to build."));
|
|
2842
|
+
} else {
|
|
2843
|
+
printBuildSummary(lastBuild);
|
|
2844
|
+
if (buildDir) console.log(chalk6.dim(`Saved to: ${buildDir}/`));
|
|
2845
|
+
}
|
|
2846
|
+
rl.prompt();
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (input2 === "/save") {
|
|
2850
|
+
if (!lastBuild) {
|
|
2851
|
+
console.log(chalk6.yellow("Nothing generated yet."));
|
|
2852
|
+
rl.prompt();
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const dir = saveBuild(lastBuild, opts.output);
|
|
2856
|
+
buildDir = dir;
|
|
2857
|
+
printSaveResult(dir, lastBuild);
|
|
2858
|
+
rl.prompt();
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
if (input2 === "/push") {
|
|
2862
|
+
if (!lastBuild) {
|
|
2863
|
+
console.log(chalk6.yellow("Nothing generated yet."));
|
|
2864
|
+
rl.prompt();
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
rl.pause();
|
|
2868
|
+
await doPush(lastBuild);
|
|
2869
|
+
rl.resume();
|
|
2870
|
+
rl.prompt();
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
rl.pause();
|
|
2874
|
+
history.push({ role: "user", content: input2 });
|
|
2875
|
+
const spinner = ora5({ text: chalk6.dim("Thinking\u2026"), spinner: "dots", color: "cyan" }).start();
|
|
2876
|
+
let raw;
|
|
2877
|
+
try {
|
|
2878
|
+
raw = await provider.complete(history);
|
|
2879
|
+
spinner.stop();
|
|
2880
|
+
dbg("raw LLM response", raw);
|
|
2881
|
+
} catch (err) {
|
|
2882
|
+
spinner.stop();
|
|
2883
|
+
console.error(chalk6.red(opts.debug && err instanceof Error ? err.stack ?? err.message : `Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
2884
|
+
history.pop();
|
|
2885
|
+
rl.resume();
|
|
2886
|
+
rl.prompt();
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
history.push({ role: "assistant", content: raw });
|
|
2890
|
+
let parsedBuild = null;
|
|
2891
|
+
try {
|
|
2892
|
+
parsedBuild = parseBuildResponse(raw);
|
|
2893
|
+
dbg("parsed build", parsedBuild);
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
if (opts.debug) {
|
|
2896
|
+
console.error(chalk6.dim(`[debug] not a build response: ${err instanceof Error ? err.message : String(err)}`));
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (parsedBuild) {
|
|
2900
|
+
const previous = lastBuild;
|
|
2901
|
+
lastBuild = parsedBuild;
|
|
2902
|
+
printBuildSummary(parsedBuild, previous);
|
|
2903
|
+
const validationErrors = validateBuild(parsedBuild);
|
|
2904
|
+
if (validationErrors.length > 0) {
|
|
2905
|
+
console.warn(chalk6.yellow("Warning: some artifacts are missing required fields:"));
|
|
2906
|
+
for (const e of validationErrors) {
|
|
2907
|
+
console.warn(chalk6.yellow(` [${e.artifactIndex}] ${e.type} "${e.name}" \u2014 missing: ${e.missing.join(", ")}`));
|
|
2908
|
+
}
|
|
2909
|
+
console.log();
|
|
2910
|
+
}
|
|
2911
|
+
const dir = saveBuild(parsedBuild, opts.output ?? buildDir ?? void 0);
|
|
2912
|
+
buildDir = dir;
|
|
2913
|
+
printSaveResult(dir, parsedBuild);
|
|
2914
|
+
await confirmPush(parsedBuild, dir, opts.push);
|
|
2915
|
+
console.log(chalk6.dim(` /push to redeploy | /save to re-save | /status for summary`));
|
|
2916
|
+
console.log();
|
|
2917
|
+
} else {
|
|
2918
|
+
console.log();
|
|
2919
|
+
const lines = raw.trim().split("\n");
|
|
2920
|
+
const prefix = chalk6.magenta("AI > ");
|
|
2921
|
+
console.log(prefix + lines[0]);
|
|
2922
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2923
|
+
console.log(" " + lines[i]);
|
|
2924
|
+
}
|
|
2925
|
+
console.log();
|
|
2926
|
+
}
|
|
2927
|
+
rl.resume();
|
|
2928
|
+
rl.prompt();
|
|
2929
|
+
});
|
|
2930
|
+
rl.on("close", () => process.exit(0));
|
|
2931
|
+
process.on("SIGINT", () => {
|
|
2932
|
+
console.log(chalk6.dim("\nBye."));
|
|
2933
|
+
process.exit(0);
|
|
2934
|
+
});
|
|
2935
|
+
});
|
|
2936
|
+
cmd.command("review <path>").description("Review and edit generated artifacts, then optionally push to the active instance").action(async (path2) => {
|
|
2937
|
+
const manifestPath = resolveManifestPath(path2);
|
|
2938
|
+
const buildDir = dirname(manifestPath);
|
|
2939
|
+
let build = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
2940
|
+
build = await runReview(build, buildDir);
|
|
2941
|
+
console.log();
|
|
2942
|
+
await confirmPush(build, buildDir);
|
|
2943
|
+
});
|
|
2944
|
+
cmd.command("save-manifest <jsonFile>").description("Convert a raw LLM JSON response into a build directory").option("-o, --output <dir>", "Output directory").action(async (jsonFile, opts) => {
|
|
2945
|
+
if (!existsSync3(jsonFile)) {
|
|
2946
|
+
console.error(chalk6.red(`File not found: ${jsonFile}`));
|
|
2947
|
+
process.exit(1);
|
|
2948
|
+
}
|
|
2949
|
+
let build;
|
|
2950
|
+
try {
|
|
2951
|
+
build = parseBuildResponse(readFileSync3(jsonFile, "utf-8"));
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
console.error(chalk6.red(`Failed to parse: ${err instanceof Error ? err.message : String(err)}`));
|
|
2954
|
+
process.exit(1);
|
|
2955
|
+
}
|
|
2956
|
+
printBuildSummary(build);
|
|
2957
|
+
const dir = saveBuild(build, opts.output);
|
|
2958
|
+
printSaveResult(dir, build);
|
|
2959
|
+
});
|
|
2960
|
+
return cmd;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/index.ts
|
|
2964
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2965
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2966
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
2967
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2968
|
+
var __dirname2 = dirname2(__filename2);
|
|
2969
|
+
function getVersion() {
|
|
2970
|
+
try {
|
|
2971
|
+
const pkg = JSON.parse(
|
|
2972
|
+
readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8")
|
|
2973
|
+
);
|
|
2974
|
+
return pkg.version;
|
|
2975
|
+
} catch {
|
|
2976
|
+
return "0.0.0";
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
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(
|
|
2981
|
+
"after",
|
|
2982
|
+
`
|
|
2983
|
+
${chalk7.bold("Examples:")}
|
|
2984
|
+
${chalk7.dim("# Add a ServiceNow instance")}
|
|
2985
|
+
snow instance add
|
|
2986
|
+
|
|
2987
|
+
${chalk7.dim("# Query records from a table")}
|
|
2988
|
+
snow table get incident -q "active=true" -l 10
|
|
2989
|
+
|
|
2990
|
+
${chalk7.dim("# View the schema for a table")}
|
|
2991
|
+
snow schema incident
|
|
2992
|
+
|
|
2993
|
+
${chalk7.dim("# Pull a script field, edit it, and push back")}
|
|
2994
|
+
snow script pull sys_script_include <sys_id> script
|
|
2995
|
+
|
|
2996
|
+
${chalk7.dim("# Configure an LLM provider (OpenAI, Anthropic, xAI/Grok, or Ollama)")}
|
|
2997
|
+
snow provider set openai
|
|
2998
|
+
snow provider set anthropic
|
|
2999
|
+
snow provider set xai
|
|
3000
|
+
snow provider set ollama --model llama3
|
|
3001
|
+
|
|
3002
|
+
${chalk7.dim("# Generate a ServiceNow app and export as an update set XML")}
|
|
3003
|
+
snow ai build "Create a script include that auto-routes incidents by category"
|
|
3004
|
+
|
|
3005
|
+
${chalk7.dim("# Generate and immediately push artifacts to the active instance")}
|
|
3006
|
+
snow ai build "Create a business rule that sets priority on incident insert" --push
|
|
3007
|
+
|
|
3008
|
+
${chalk7.dim("# Interactive multi-turn app builder")}
|
|
3009
|
+
snow ai chat
|
|
3010
|
+
`
|
|
3011
|
+
);
|
|
3012
|
+
program.addCommand(instanceCommand());
|
|
3013
|
+
program.addCommand(tableCommand());
|
|
3014
|
+
program.addCommand(schemaCommand());
|
|
3015
|
+
program.addCommand(scriptCommand());
|
|
3016
|
+
program.addCommand(providerCommand());
|
|
3017
|
+
program.addCommand(aiCommand());
|
|
3018
|
+
program.command("instances", { hidden: true }).description("Alias for `snow instance list`").action(() => {
|
|
3019
|
+
const sub = instanceCommand();
|
|
3020
|
+
sub.parse(["node", "snow", "list"]);
|
|
3021
|
+
});
|
|
3022
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
3023
|
+
console.error(chalk7.red(err instanceof Error ? err.message : String(err)));
|
|
3024
|
+
process.exit(1);
|
|
3025
|
+
});
|