@jacebenson/jsn 0.1.2 → 0.2.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/package.json +1 -1
- package/src/app.js +15 -0
- package/src/auth.js +99 -2
- package/src/cli.js +2 -0
- package/src/commands/auth.js +20 -5
- package/src/commands/catalog.js +157 -0
- package/src/commands/dev/updatesets.js +50 -6
- package/src/helpers.js +38 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -118,6 +118,21 @@ export class App {
|
|
|
118
118
|
const updateSetStr = `]8;;${updateSetLink}\x07${updateSet}]8;;\x07`;
|
|
119
119
|
|
|
120
120
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
121
|
+
|
|
122
|
+
// ⚠️ Warning if in the Default update set
|
|
123
|
+
if (updateSet && updateSet.toLowerCase().includes('default')) {
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
'\x1b[33m' + // yellow
|
|
126
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
127
|
+
' ⚠ You are in the Default update set!\n' +
|
|
128
|
+
' Create a named update set to capture your changes:\n' +
|
|
129
|
+
' jsn dev updatesets create --name "My Feature"\n' +
|
|
130
|
+
' jsn dev updatesets set "My Feature"\n' +
|
|
131
|
+
' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
|
|
132
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
133
|
+
'\x1b[0m' // reset
|
|
134
|
+
);
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
ok(data, opts = {}) {
|
package/src/auth.js
CHANGED
|
@@ -44,6 +44,40 @@ function removePKCEState(instance) {
|
|
|
44
44
|
const DEFAULT_OAUTH_CLIENT_ID = '543e5655f77746a28228c6009a599dfb';
|
|
45
45
|
const REDIRECT_URI = '/sdk-oauth.do';
|
|
46
46
|
|
|
47
|
+
// ─── Basic auth from environment variables ───
|
|
48
|
+
// SN_USERNAME / SN_PASSWORD — global credentials
|
|
49
|
+
// SN_<INSTANCE>_USERNAME / SN_<INSTANCE>_PASSWORD — instance-specific (e.g. SN_DEV328604_USERNAME)
|
|
50
|
+
|
|
51
|
+
function envVarName(instance) {
|
|
52
|
+
// Normalize instance URL to an env-var-safe name
|
|
53
|
+
// e.g. https://dev328604.service-now.com → DEV328604
|
|
54
|
+
const host = instance.replace(/https?:\/\//, '').replace(/\/.*$/, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
55
|
+
return host;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getBasicAuthFromEnv(instance) {
|
|
59
|
+
if (!instance) {
|
|
60
|
+
// Try global env vars
|
|
61
|
+
const username = process.env.SN_USERNAME;
|
|
62
|
+
const password = process.env.SN_PASSWORD;
|
|
63
|
+
if (username && password) return { auth_method: 'basic', username, password };
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try instance-specific env vars first (e.g. SN_DEV328604_USERNAME)
|
|
68
|
+
const host = envVarName(instance);
|
|
69
|
+
const instanceUser = process.env[`SN_${host}_USERNAME`];
|
|
70
|
+
const instancePass = process.env[`SN_${host}_PASSWORD`];
|
|
71
|
+
if (instanceUser && instancePass) return { auth_method: 'basic', username: instanceUser, password: instancePass };
|
|
72
|
+
|
|
73
|
+
// Fall back to global env vars
|
|
74
|
+
const globalUser = process.env.SN_USERNAME;
|
|
75
|
+
const globalPass = process.env.SN_PASSWORD;
|
|
76
|
+
if (globalUser && globalPass) return { auth_method: 'basic', username: globalUser, password: globalPass };
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
// Keychain constants — same as Go version (internal/auth/store.go)
|
|
48
82
|
const KEYRING_SERVICE = 'servicenow-cli';
|
|
49
83
|
const KEYRING_ATTR_SERVICE = 'service';
|
|
@@ -218,6 +252,7 @@ export class AuthManager {
|
|
|
218
252
|
|
|
219
253
|
isAuthenticated() {
|
|
220
254
|
if (process.env.SERVICENOW_OAUTH_TOKEN) return true;
|
|
255
|
+
if (getBasicAuthFromEnv()) return true;
|
|
221
256
|
const instance = this.configProvider.getEffectiveInstance();
|
|
222
257
|
if (!instance) return false;
|
|
223
258
|
try {
|
|
@@ -230,6 +265,7 @@ export class AuthManager {
|
|
|
230
265
|
|
|
231
266
|
isAuthenticatedFor(instance) {
|
|
232
267
|
if (!instance) return false;
|
|
268
|
+
if (getBasicAuthFromEnv(instance)) return true;
|
|
233
269
|
const creds = loadCredentials(instance);
|
|
234
270
|
if (!creds) return false;
|
|
235
271
|
if (creds.expires_at && Date.now() >= creds.expires_at * 1000) return false;
|
|
@@ -244,10 +280,16 @@ export class AuthManager {
|
|
|
244
280
|
if (!instance) {
|
|
245
281
|
throw errAuth('No instance configured');
|
|
246
282
|
}
|
|
283
|
+
// Check basic auth from env vars first
|
|
284
|
+
const basicCreds = getBasicAuthFromEnv(instance);
|
|
285
|
+
if (basicCreds) return basicCreds;
|
|
247
286
|
return this.getCredentialsFor(instance);
|
|
248
287
|
}
|
|
249
288
|
|
|
250
289
|
getCredentialsFor(instance) {
|
|
290
|
+
// Check basic auth from env vars first
|
|
291
|
+
const basicCreds = getBasicAuthFromEnv(instance);
|
|
292
|
+
if (basicCreds) return basicCreds;
|
|
251
293
|
const creds = loadCredentials(instance);
|
|
252
294
|
if (!creds) {
|
|
253
295
|
throw errAuth(`Not authenticated for ${instance}`);
|
|
@@ -314,13 +356,68 @@ export class AuthManager {
|
|
|
314
356
|
* Build an OAuth authorization URL and persist PKCE state for later use.
|
|
315
357
|
* After calling this, the user can visit the URL in a browser and then
|
|
316
358
|
* call loginWithCode() with the resulting authorization code.
|
|
359
|
+
* If waitFile is provided, the method will read the code from that file
|
|
360
|
+
* (waits for file to appear, polling every 2 seconds, up to 5 minutes).
|
|
317
361
|
*/
|
|
318
|
-
buildAuthURL(instanceURL) {
|
|
362
|
+
buildAuthURL(instanceURL, waitFile) {
|
|
319
363
|
instanceURL = normalizeInstanceURL(instanceURL);
|
|
320
364
|
const clientID = getOAuthClientID();
|
|
321
365
|
const pkce = generatePKCE();
|
|
322
366
|
savePKCEState(instanceURL, pkce);
|
|
323
|
-
|
|
367
|
+
const url = buildAuthURL(instanceURL, clientID, pkce);
|
|
368
|
+
if (waitFile) {
|
|
369
|
+
console.log(url);
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(`Waiting for authorization code in file: ${waitFile}`);
|
|
372
|
+
console.log('(polling every 2 seconds — write the code to the file to complete login)');
|
|
373
|
+
return this._waitForCodeFile(instanceURL, clientID, pkce, waitFile);
|
|
374
|
+
}
|
|
375
|
+
return url;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async _waitForCodeFile(instanceURL, clientID, pkce, filePath, timeout = 300000) {
|
|
379
|
+
const start = Date.now();
|
|
380
|
+
const pollInterval = 2000;
|
|
381
|
+
while (Date.now() - start < timeout) {
|
|
382
|
+
try {
|
|
383
|
+
const code = fs.readFileSync(filePath, 'utf-8').trim();
|
|
384
|
+
if (code) {
|
|
385
|
+
console.log(`\nAuthorization code found in ${filePath}`);
|
|
386
|
+
removePKCEState(instanceURL);
|
|
387
|
+
const newCreds = await this.exchangeCode(instanceURL, clientID, code, pkce);
|
|
388
|
+
saveCredentials(instanceURL, newCreds);
|
|
389
|
+
console.log('Token exchange successful!\n');
|
|
390
|
+
return newCreds;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
// File not ready yet
|
|
394
|
+
}
|
|
395
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
396
|
+
}
|
|
397
|
+
throw errAuth(`Timed out waiting for authorization code in ${filePath} (${timeout / 1000}s)`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Authenticate with basic auth via environment variables.
|
|
402
|
+
* Reads SN_USERNAME/SN_PASSWORD (or SN_<INSTANCE>_USERNAME/SN_<INSTANCE>_PASSWORD).
|
|
403
|
+
* Stores the basic auth credentials so they persist across sessions.
|
|
404
|
+
*/
|
|
405
|
+
async loginWithPassword(instanceURL) {
|
|
406
|
+
instanceURL = normalizeInstanceURL(instanceURL);
|
|
407
|
+
const creds = getBasicAuthFromEnv(instanceURL);
|
|
408
|
+
if (!creds) {
|
|
409
|
+
throw errAuth(
|
|
410
|
+
`No basic auth credentials found in environment.\n\n` +
|
|
411
|
+
`Set the following environment variables:\n` +
|
|
412
|
+
` SN_USERNAME=admin (or SN_${envVarName(instanceURL)}_USERNAME)\n` +
|
|
413
|
+
` SN_PASSWORD=<password> (or SN_${envVarName(instanceURL)}_PASSWORD)\n\n` +
|
|
414
|
+
`Then run:\n` +
|
|
415
|
+
` jsn auth login --password ${instanceURL}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
saveCredentials(instanceURL, creds);
|
|
419
|
+
console.log(`✓ Basic auth credentials saved for ${instanceURL}`);
|
|
420
|
+
return creds;
|
|
324
421
|
}
|
|
325
422
|
|
|
326
423
|
/**
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { setupCmd } from './commands/setup.js';
|
|
|
12
12
|
import { authCmd } from './commands/auth.js';
|
|
13
13
|
import { profilesCmd } from './commands/profiles.js';
|
|
14
14
|
import { recordsCmd } from './commands/records.js';
|
|
15
|
+
import { catalogCmd } from './commands/catalog.js';
|
|
15
16
|
import { incidentsCmd } from './commands/incidents.js';
|
|
16
17
|
import { changesCmd } from './commands/changes.js';
|
|
17
18
|
import { requestsCmd } from './commands/requests.js';
|
|
@@ -125,6 +126,7 @@ export const cli = yargs(hideBin(process.argv))
|
|
|
125
126
|
.command(authCmd(wrap))
|
|
126
127
|
.command(profilesCmd(wrap))
|
|
127
128
|
.command(recordsCmd(wrap))
|
|
129
|
+
.command(catalogCmd(wrap))
|
|
128
130
|
.command(incidentsCmd(wrap))
|
|
129
131
|
.command(changesCmd(wrap))
|
|
130
132
|
.command(requestsCmd(wrap))
|
package/src/commands/auth.js
CHANGED
|
@@ -5,7 +5,7 @@ import process from 'node:process';
|
|
|
5
5
|
export function authCmd(wrap) {
|
|
6
6
|
return {
|
|
7
7
|
command: 'auth <subcommand>',
|
|
8
|
-
describe: 'Manage OAuth
|
|
8
|
+
describe: 'Manage authentication (OAuth or basic auth via env vars)',
|
|
9
9
|
builder: (yargs) => {
|
|
10
10
|
return yargs
|
|
11
11
|
.command({
|
|
@@ -16,9 +16,17 @@ export function authCmd(wrap) {
|
|
|
16
16
|
describe: 'Authorization code from browser (bypasses interactive prompt)',
|
|
17
17
|
type: 'string',
|
|
18
18
|
})
|
|
19
|
+
.option('password', {
|
|
20
|
+
describe: 'Authenticate with basic auth via env vars (SN_USERNAME/SN_PASSWORD)',
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
})
|
|
19
23
|
.option('print-url', {
|
|
20
24
|
describe: 'Print the OAuth URL and exit (saves PKCE state for --code)',
|
|
21
25
|
type: 'boolean',
|
|
26
|
+
})
|
|
27
|
+
.option('wait-file', {
|
|
28
|
+
describe: 'File path to watch for authorization code (used with --print-url)',
|
|
29
|
+
type: 'string',
|
|
22
30
|
}),
|
|
23
31
|
handler: wrap(async (argv, app) => {
|
|
24
32
|
let instanceURL;
|
|
@@ -55,22 +63,29 @@ export function authCmd(wrap) {
|
|
|
55
63
|
Examples:
|
|
56
64
|
jsn auth login https://dev12345.service-now.com
|
|
57
65
|
jsn auth login dev373698
|
|
58
|
-
jsn auth login https://
|
|
66
|
+
jsn auth login --password https://dev328604.service-now.com
|
|
59
67
|
|
|
60
68
|
Find your instance URL in your browser's address bar when logged into ServiceNow.`);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
// --password: authenticate with basic auth from env vars
|
|
74
|
+
if (argv.password) {
|
|
75
|
+
await app.auth.loginWithPassword(instanceURL);
|
|
76
|
+
}
|
|
77
|
+
// --print-url with --wait-file: print URL and wait for code file
|
|
78
|
+
else if (argv.printUrl && argv.waitFile) {
|
|
79
|
+
await app.auth.buildAuthURL(instanceURL, argv.waitFile);
|
|
80
|
+
}
|
|
65
81
|
// --print-url: just print the URL and exit
|
|
66
|
-
if (argv.printUrl) {
|
|
82
|
+
else if (argv.printUrl) {
|
|
67
83
|
const authURL = app.auth.buildAuthURL(instanceURL);
|
|
68
84
|
console.log(authURL);
|
|
69
85
|
return;
|
|
70
86
|
}
|
|
71
|
-
|
|
72
87
|
// --code: exchange the provided authorization code directly
|
|
73
|
-
if (argv.code) {
|
|
88
|
+
else if (argv.code) {
|
|
74
89
|
await app.auth.loginWithCode(instanceURL, argv.code);
|
|
75
90
|
} else {
|
|
76
91
|
// Interactive: full OAuth flow with browser + prompt
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Catalog item management commands
|
|
2
|
+
// NOTE: This is an AI-friendly high-level command that wraps sc_cat_item + item_option_new
|
|
3
|
+
|
|
4
|
+
import { resolveItemOptionType } from '../helpers.js';
|
|
5
|
+
|
|
6
|
+
export function catalogCmd(wrap) {
|
|
7
|
+
return {
|
|
8
|
+
command: 'catalog <subcommand>',
|
|
9
|
+
aliases: ['cat'],
|
|
10
|
+
describe: 'Manage Service Catalog items and variables',
|
|
11
|
+
builder: (yargs) => {
|
|
12
|
+
return yargs
|
|
13
|
+
.command({
|
|
14
|
+
command: 'create-item',
|
|
15
|
+
describe: 'Create a catalog item (sc_cat_item) with variables',
|
|
16
|
+
builder: (y) => y
|
|
17
|
+
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Catalog item name' })
|
|
18
|
+
.option('short-description', { alias: 'd', type: 'string', describe: 'Short description' })
|
|
19
|
+
.option('description', { type: 'string', describe: 'Full description' })
|
|
20
|
+
.option('category', { alias: 'c', type: 'string', describe: 'Category name (created if missing)' })
|
|
21
|
+
.option('variable', { alias: 'v', type: 'array', describe: 'Variable definition in format "name:type:label" or "name:type" (e.g. "reason:multilinetext:Reason" or "start_date:date")' })
|
|
22
|
+
.option('update-set', { type: 'string', describe: 'Update set sys_id or name to capture changes' }),
|
|
23
|
+
handler: wrap(async (argv, app) => {
|
|
24
|
+
const name = argv.name;
|
|
25
|
+
const shortDesc = argv['short-description'] || '';
|
|
26
|
+
const description = argv.description || shortDesc;
|
|
27
|
+
let categoryID = '';
|
|
28
|
+
|
|
29
|
+
// Resolve or create category
|
|
30
|
+
if (argv.category) {
|
|
31
|
+
const catParams = new URLSearchParams();
|
|
32
|
+
catParams.set('sysparm_query', `title=${argv.category}`);
|
|
33
|
+
catParams.set('sysparm_limit', '1');
|
|
34
|
+
catParams.set('sysparm_fields', 'sys_id,title');
|
|
35
|
+
const cats = await app.sdk.list('sc_category', catParams);
|
|
36
|
+
if (cats.length > 0) {
|
|
37
|
+
categoryID = cats[0].sys_id?.value || cats[0].sys_id;
|
|
38
|
+
} else {
|
|
39
|
+
const newCat = await app.sdk.create('sc_category', {
|
|
40
|
+
title: argv.category,
|
|
41
|
+
description: `Category for ${argv.category}`,
|
|
42
|
+
});
|
|
43
|
+
categoryID = newCat.sys_id?.value || newCat.sys_id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create the catalog item
|
|
48
|
+
const item = await app.sdk.create('sc_cat_item', {
|
|
49
|
+
name,
|
|
50
|
+
short_description: shortDesc,
|
|
51
|
+
description,
|
|
52
|
+
category: categoryID || undefined,
|
|
53
|
+
active: true,
|
|
54
|
+
type: 'item',
|
|
55
|
+
stage: 'requested',
|
|
56
|
+
});
|
|
57
|
+
const itemID = item.sys_id?.value || item.sys_id;
|
|
58
|
+
|
|
59
|
+
// Parse and create variables
|
|
60
|
+
const varDefs = [];
|
|
61
|
+
if (argv.variable) {
|
|
62
|
+
for (const v of argv.variable) {
|
|
63
|
+
// Parse "label:type" or "name:type" or "name:type:label" etc.
|
|
64
|
+
const parts = String(v).split(':');
|
|
65
|
+
if (parts.length >= 2) {
|
|
66
|
+
const typeName = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
67
|
+
const label = parts.length >= 3 ? parts[parts.length - 1] : parts[0];
|
|
68
|
+
const typeID = resolveItemOptionType(typeName);
|
|
69
|
+
const sysName = parts[0].replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
|
|
70
|
+
varDefs.push({
|
|
71
|
+
name: sysName,
|
|
72
|
+
question_text: label,
|
|
73
|
+
type: typeID,
|
|
74
|
+
order: varDefs.length * 100 + 100,
|
|
75
|
+
cat_item: itemID,
|
|
76
|
+
mandatory: true,
|
|
77
|
+
active: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const vd of varDefs) {
|
|
83
|
+
try {
|
|
84
|
+
await app.sdk.create('item_option_new', vd);
|
|
85
|
+
} catch {
|
|
86
|
+
// variable creation failure is non-fatal
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
app.ok({
|
|
92
|
+
sys_id: itemID,
|
|
93
|
+
name,
|
|
94
|
+
category: argv.category || null,
|
|
95
|
+
variables_created: varDefs.length,
|
|
96
|
+
update_set: argv['update-set'] || null,
|
|
97
|
+
instance_url: app.getEffectiveInstance(),
|
|
98
|
+
}, {
|
|
99
|
+
summary: `Created catalog item: ${name} (${varDefs.length} variable(s))`,
|
|
100
|
+
breadcrumbs: [
|
|
101
|
+
{ action: 'view', cmd: `jsn records get --table sc_cat_item --sys-id ${itemID}`, description: 'View the catalog item' },
|
|
102
|
+
{ action: 'edit', cmd: `jsn catalog update-item ${itemID} --name "${name}"`, description: 'Edit the catalog item' },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
}),
|
|
106
|
+
})
|
|
107
|
+
.command({
|
|
108
|
+
command: 'list-items',
|
|
109
|
+
aliases: ['ls'],
|
|
110
|
+
describe: 'List catalog items',
|
|
111
|
+
builder: (y) => y
|
|
112
|
+
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
|
|
113
|
+
.option('category', { type: 'string', describe: 'Filter by category' }),
|
|
114
|
+
handler: wrap(async (argv, app) => {
|
|
115
|
+
let query = 'ORDERBYname';
|
|
116
|
+
if (argv.category) query = `category.titleLIKE${argv.category}^${query}`;
|
|
117
|
+
const params = new URLSearchParams();
|
|
118
|
+
params.set('sysparm_query', query);
|
|
119
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
120
|
+
params.set('sysparm_display_value', 'all');
|
|
121
|
+
params.set('sysparm_fields', 'sys_id,name,short_description,category,active');
|
|
122
|
+
const items = await app.sdk.list('sc_cat_item', params);
|
|
123
|
+
app.ok({
|
|
124
|
+
table: 'sc_cat_item',
|
|
125
|
+
count: items.length,
|
|
126
|
+
columns: ['name', 'short_description', 'category', 'active'],
|
|
127
|
+
records: items.map(r => ({
|
|
128
|
+
sys_id: r.sys_id?.value || r.sys_id,
|
|
129
|
+
name: r.name?.display_value || r.name,
|
|
130
|
+
short_description: r.short_description?.display_value || r.short_description || '',
|
|
131
|
+
category: r.category?.display_value || '',
|
|
132
|
+
active: r.active?.display_value || r.active,
|
|
133
|
+
})),
|
|
134
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
135
|
+
}, { summary: `${items.length} catalog item(s)` });
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
handler: (argv) => {
|
|
140
|
+
if (!argv._[1]) {
|
|
141
|
+
console.log('Manage Service Catalog items.\n');
|
|
142
|
+
console.log('Commands:');
|
|
143
|
+
console.log(' create-item Create a catalog item (fast scaffold)');
|
|
144
|
+
console.log(' list-items List catalog items');
|
|
145
|
+
console.log('\nExamples:');
|
|
146
|
+
console.log(' jsn catalog create-item "Request PTO" \\');
|
|
147
|
+
console.log(' --category "Time Off" \\');
|
|
148
|
+
console.log(' --short-description "Submit a PTO request" \\');
|
|
149
|
+
console.log(' --variable "start_date:date:Start Date" \\');
|
|
150
|
+
console.log(' --variable "type:select:Type" \\');
|
|
151
|
+
console.log(' --variable "reason:multilinetext:Reason/Notes"');
|
|
152
|
+
console.log('\nVariable types: string, multilinetext, select, date, datetime, reference, checkbox, email');
|
|
153
|
+
console.log('\nRun "jsn catalog <command> --help" for details.');
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -113,13 +113,52 @@ export function updateSetsCmd(wrap) {
|
|
|
113
113
|
description: argv.description || argv.name,
|
|
114
114
|
state: 'in progress',
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
// Auto-set as current update set
|
|
118
|
+
const sysID = record?.sys_id?.value || record?.sys_id;
|
|
119
|
+
try {
|
|
120
|
+
const user = await app.sdk.list('sys_user', new URLSearchParams({
|
|
121
|
+
sysparm_query: 'user_name=javascript:gs.getUserName()',
|
|
122
|
+
sysparm_limit: '1',
|
|
123
|
+
sysparm_fields: 'sys_id',
|
|
124
|
+
}));
|
|
125
|
+
if (user.length > 0) {
|
|
126
|
+
const userSysID = user[0].sys_id?.value || user[0].sys_id;
|
|
127
|
+
// Check for existing preference
|
|
128
|
+
const prefParams = new URLSearchParams();
|
|
129
|
+
prefParams.set('sysparm_query', `user=${userSysID}^name=sys_update_set`);
|
|
130
|
+
prefParams.set('sysparm_limit', '1');
|
|
131
|
+
const prefs = await app.sdk.list('sys_user_preference', prefParams);
|
|
132
|
+
if (prefs.length > 0) {
|
|
133
|
+
const prefID = prefs[0].sys_id?.value || prefs[0].sys_id;
|
|
134
|
+
await app.sdk.update('sys_user_preference', prefID, { value: sysID });
|
|
135
|
+
} else {
|
|
136
|
+
await app.sdk.create('sys_user_preference', {
|
|
137
|
+
user: userSysID,
|
|
138
|
+
name: 'sys_update_set',
|
|
139
|
+
value: sysID,
|
|
140
|
+
type: 'string',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Non-fatal — auto-set is a convenience, not mandatory
|
|
146
|
+
}
|
|
147
|
+
|
|
116
148
|
app.ok(record, {
|
|
117
149
|
summary: `Created update set: ${argv.name}`,
|
|
118
|
-
breadcrumbs: [
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
breadcrumbs: [
|
|
151
|
+
{
|
|
152
|
+
action: 'set',
|
|
153
|
+
cmd: `jsn dev updatesets set "${argv.name}"`,
|
|
154
|
+
description: 'Switch to this update set',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
action: 'complete',
|
|
158
|
+
cmd: `jsn dev updatesets complete "${argv.name}"`,
|
|
159
|
+
description: 'Mark as complete when done',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
123
162
|
});
|
|
124
163
|
}),
|
|
125
164
|
})
|
|
@@ -132,8 +171,13 @@ export function updateSetsCmd(wrap) {
|
|
|
132
171
|
console.log(' list List update sets');
|
|
133
172
|
console.log(' show <name> Show an update set');
|
|
134
173
|
console.log(' set <name> Set the current update set');
|
|
135
|
-
console.log(' create Create a new update set');
|
|
174
|
+
console.log(' create Create a new update set (auto-sets as current)');
|
|
175
|
+
console.log(' complete <name> Mark an update set as complete (coming soon)');
|
|
176
|
+
console.log(' yolo Silence the "Default update set" warning');
|
|
136
177
|
console.log('\nRun "jsn dev updatesets <command> --help" for details.');
|
|
178
|
+
console.log('\nTip: Create an update set first:');
|
|
179
|
+
console.log(' jsn dev updatesets create --name "My Feature"');
|
|
180
|
+
console.log(' # → Auto-set as current, then record changes are captured.');
|
|
137
181
|
}
|
|
138
182
|
},
|
|
139
183
|
};
|
package/src/helpers.js
CHANGED
|
@@ -142,3 +142,41 @@ export function parseDataArg(argv) {
|
|
|
142
142
|
throw new Error(`Invalid JSON: ${e.message}\n\nHint: On Windows PowerShell, use --data-file instead of --data to avoid quote mangling.\nRaw value: ${raw.substring(0, 200)}`, { cause: e });
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Translate human-readable type names to ServiceNow item_option_new type IDs.
|
|
148
|
+
* Maps common names like "date", "select", "multilinetext" to their integer IDs.
|
|
149
|
+
* Passes through numeric values unchanged.
|
|
150
|
+
*/
|
|
151
|
+
const ITEM_OPTION_TYPE_NAMES = {
|
|
152
|
+
'1': 1, 'yesno': 1, 'yes/no': 1, 'boolean': 1,
|
|
153
|
+
'2': 2, 'multilinetext': 2, 'textarea': 2, 'multiline': 2,
|
|
154
|
+
'3': 3, 'multiplechoice': 3,
|
|
155
|
+
'4': 4, 'numericscale': 4, 'rating': 4,
|
|
156
|
+
'5': 5, 'select': 5, 'dropdown': 5, 'choice': 5, 'selectbox': 5,
|
|
157
|
+
'6': 6, 'string': 6, 'text': 6, 'singlelinetext': 6,
|
|
158
|
+
'7': 7, 'checkbox': 7, 'check': 7,
|
|
159
|
+
'8': 8, 'reference': 8, 'lookup': 8,
|
|
160
|
+
'9': 9, 'date': 9,
|
|
161
|
+
'10': 10, 'datetime': 10, 'date/time': 10,
|
|
162
|
+
'11': 11, 'label': 11,
|
|
163
|
+
'14': 14, 'custom': 14,
|
|
164
|
+
'18': 18, 'lookupselect': 18, 'lookupselectbox': 18,
|
|
165
|
+
'20': 20, 'containerstart': 20,
|
|
166
|
+
'21': 21, 'listcollector': 21,
|
|
167
|
+
'23': 23, 'html': 23,
|
|
168
|
+
'26': 26, 'email': 26,
|
|
169
|
+
'29': 29, 'duration': 29,
|
|
170
|
+
'31': 31, 'requestedfor': 31,
|
|
171
|
+
'32': 32, 'richtextlabel': 32, 'richtext': 32,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export function resolveItemOptionType(type) {
|
|
175
|
+
if (type == null) return 6; // default: Single Line Text
|
|
176
|
+
if (typeof type === 'number') return type;
|
|
177
|
+
const lower = String(type).toLowerCase().replace(/[\s_-]/g, '');
|
|
178
|
+
if (ITEM_OPTION_TYPE_NAMES[lower] != null) return ITEM_OPTION_TYPE_NAMES[lower];
|
|
179
|
+
const asNum = parseInt(String(type), 10);
|
|
180
|
+
if (!isNaN(asNum) && asNum > 0 && asNum < 100) return asNum;
|
|
181
|
+
return 6; // fallback to Single Line Text
|
|
182
|
+
}
|