@jacebenson/jsn 0.0.10 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -280
- package/package.json +3 -1
- package/skills/servicenow/SKILL.md +137 -0
- package/src/app.js +1 -1
- package/src/auth.js +139 -2
- package/src/cli.js +18 -8
- package/src/commands/_ticket.js +7 -3
- package/src/commands/auth.js +182 -32
- package/src/commands/dev/_generic.js +128 -11
- package/src/commands/dev/eval.js +27 -6
- package/src/commands/dev/flows.js +46 -1
- package/src/commands/dev/logs.js +15 -2
- package/src/commands/dev/scopes.js +62 -2
- package/src/commands/dev/updatesets.js +45 -2
- package/src/commands/groupmembers.js +114 -1
- package/src/commands/grouproles.js +114 -1
- package/src/commands/groups.js +87 -3
- package/src/commands/profiles.js +14 -2
- package/src/commands/records.js +23 -5
- package/src/commands/requests.js +53 -1
- package/src/commands/skill.js +109 -0
- package/src/commands/users.js +90 -3
- package/src/commands/version.js +58 -2
- package/src/help.js +85 -0
- package/src/helpers.js +84 -0
- package/src/output.js +30 -0
- package/src/sdk.js +258 -0
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
// Generic dev subcommand builder for table-based CRUD
|
|
2
2
|
|
|
3
|
-
import { formatRecordForDisplay, getStringField, isHexString } from '../../helpers.js';
|
|
3
|
+
import { formatRecordForDisplay, getStringField, isHexString, parseDataArg } from '../../helpers.js';
|
|
4
4
|
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
import search from '@inquirer/search';
|
|
7
|
+
import { isTTY, FormatAuto } from '../../output.js';
|
|
6
8
|
|
|
7
9
|
function vowelArticle(word) {
|
|
8
10
|
const first = word.charAt(0).toLowerCase();
|
|
9
11
|
return first === 'a' || first === 'e' || first === 'i' || first === 'o' || first === 'u' ? 'an' : 'a';
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
function buildHints(name, singular, readOnly) {
|
|
15
|
+
const crumbs = [];
|
|
16
|
+
if (!readOnly) {
|
|
17
|
+
crumbs.push({
|
|
18
|
+
action: 'create',
|
|
19
|
+
cmd: `jsn dev ${name} create --name ... --label "..."`,
|
|
20
|
+
description: `Create ${vowelArticle(singular)} ${singular}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
crumbs.push({
|
|
24
|
+
action: 'show',
|
|
25
|
+
cmd: `jsn dev ${name} show <name_or_sys_id>`,
|
|
26
|
+
description: `Show ${singular} details`,
|
|
27
|
+
});
|
|
28
|
+
return crumbs;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
function toSingular(name, explicitSingular) {
|
|
13
32
|
if (explicitSingular) return explicitSingular;
|
|
14
33
|
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
|
|
@@ -65,12 +84,92 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
65
84
|
.option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
|
|
66
85
|
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
67
86
|
handler: wrap(async (argv, app) => {
|
|
87
|
+
const query = argv.query || '';
|
|
68
88
|
const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
|
|
89
|
+
const limit = argv.limit;
|
|
90
|
+
|
|
91
|
+
// Interactive picker in TTY with auto format and no explicit query
|
|
92
|
+
const interactive = isTTY(process.stdout) && isTTY(process.stdin);
|
|
93
|
+
if (interactive && app.output.getFormat() === FormatAuto && !query) {
|
|
94
|
+
const pickerColumns = ['sys_id', 'name', ...columns.filter(c => c !== 'name' && c !== 'sys_id')];
|
|
95
|
+
const pickerParams = new URLSearchParams();
|
|
96
|
+
pickerParams.set('sysparm_limit', String(limit));
|
|
97
|
+
pickerParams.set('sysparm_display_value', 'all');
|
|
98
|
+
pickerParams.set('sysparm_fields', pickerColumns.join(','));
|
|
99
|
+
pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
|
|
100
|
+
|
|
101
|
+
const pickerRecords = await app.sdk.list(table, pickerParams);
|
|
102
|
+
if (pickerRecords.length === 0) {
|
|
103
|
+
app.ok({
|
|
104
|
+
table,
|
|
105
|
+
count: 0,
|
|
106
|
+
columns: pickerColumns,
|
|
107
|
+
records: [],
|
|
108
|
+
pagination: { limit, offset: 0 },
|
|
109
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
110
|
+
}, {
|
|
111
|
+
summary: `0 ${name}(s)`,
|
|
112
|
+
breadcrumbs: buildHints(name, singular, readOnly),
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const choices = pickerRecords.map(r => {
|
|
118
|
+
const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
|
|
119
|
+
const scope = getStringField(r, 'sys_scope') || '';
|
|
120
|
+
let label = recordName;
|
|
121
|
+
if (scope && scope !== 'global') label += ` [${scope}]`;
|
|
122
|
+
return { name: label, value: recordName };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
let selectedName;
|
|
126
|
+
try {
|
|
127
|
+
selectedName = await search({
|
|
128
|
+
message: `Select ${vowelArticle(singular)} ${singular}:`,
|
|
129
|
+
source: async (input) => {
|
|
130
|
+
if (!input) return choices;
|
|
131
|
+
const term = input.toLowerCase();
|
|
132
|
+
return choices.filter(c => c.name.toLowerCase().includes(term));
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (selectedName) {
|
|
143
|
+
const showParams = new URLSearchParams();
|
|
144
|
+
showParams.set('sysparm_query', `name=${selectedName}`);
|
|
145
|
+
showParams.set('sysparm_display_value', 'all');
|
|
146
|
+
showParams.set('sysparm_limit', '1');
|
|
147
|
+
const showRecords = await app.sdk.list(table, showParams);
|
|
148
|
+
if (showRecords.length === 0) {
|
|
149
|
+
throw new Error(`${singular} not found: ${selectedName}`);
|
|
150
|
+
}
|
|
151
|
+
const record = showRecords[0];
|
|
152
|
+
record._context = { instance_url: app.getEffectiveInstance(), table };
|
|
153
|
+
if (opts.onShow) {
|
|
154
|
+
await opts.onShow(record, app);
|
|
155
|
+
}
|
|
156
|
+
app.ok(record, {
|
|
157
|
+
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${selectedName}`,
|
|
158
|
+
breadcrumbs: [
|
|
159
|
+
...(readOnly ? [] : [{ action: 'update', cmd: `jsn dev ${name} update ${selectedName} --data '{...}'`, description: `Update this ${singular}` }]),
|
|
160
|
+
{ action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Non-interactive: text/table output
|
|
69
168
|
const params = new URLSearchParams();
|
|
70
|
-
params.set('sysparm_limit', String(
|
|
169
|
+
params.set('sysparm_limit', String(limit));
|
|
71
170
|
params.set('sysparm_display_value', 'all');
|
|
72
171
|
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
73
|
-
const q =
|
|
172
|
+
const q = query ? query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
74
173
|
params.set('sysparm_query', q);
|
|
75
174
|
const records = await app.sdk.list(table, params);
|
|
76
175
|
app.ok({
|
|
@@ -79,7 +178,10 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
79
178
|
columns,
|
|
80
179
|
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
81
180
|
context: { instance_url: app.getEffectiveInstance() },
|
|
82
|
-
}, {
|
|
181
|
+
}, {
|
|
182
|
+
summary: `${records.length} ${name}(s)`,
|
|
183
|
+
breadcrumbs: buildHints(name, singular, readOnly),
|
|
184
|
+
});
|
|
83
185
|
}),
|
|
84
186
|
})
|
|
85
187
|
.command({
|
|
@@ -123,9 +225,11 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
123
225
|
.command({
|
|
124
226
|
command: 'create',
|
|
125
227
|
describe: `Create a new ${singular}`,
|
|
126
|
-
builder: (y) => y
|
|
228
|
+
builder: (y) => y
|
|
229
|
+
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
230
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' }),
|
|
127
231
|
handler: wrap(async (argv, app) => {
|
|
128
|
-
const recordData =
|
|
232
|
+
const recordData = parseDataArg(argv);
|
|
129
233
|
const record = await app.sdk.create(table, recordData);
|
|
130
234
|
app.ok(record, {
|
|
131
235
|
summary: `Created ${singular}`,
|
|
@@ -138,7 +242,9 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
138
242
|
.command({
|
|
139
243
|
command: 'update <identifier>',
|
|
140
244
|
describe: `Update ${vowelArticle(singular)} ${singular}`,
|
|
141
|
-
builder: (y) => y
|
|
245
|
+
builder: (y) => y
|
|
246
|
+
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
247
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' }),
|
|
142
248
|
handler: wrap(async (argv, app) => {
|
|
143
249
|
const id = argv.identifier;
|
|
144
250
|
const queryField = isHexString(id) && id.length === 32 ? 'sys_id' : 'name';
|
|
@@ -159,7 +265,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
159
265
|
}
|
|
160
266
|
}
|
|
161
267
|
|
|
162
|
-
const recordData =
|
|
268
|
+
const recordData = parseDataArg(argv);
|
|
163
269
|
const updated = await app.sdk.update(table, sysID, recordData);
|
|
164
270
|
app.ok(updated, { summary: `Updated ${singular} ${id}` });
|
|
165
271
|
}),
|
|
@@ -215,9 +321,20 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
215
321
|
aliases: aliases || [],
|
|
216
322
|
describe: `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
|
|
217
323
|
builder,
|
|
218
|
-
handler: (
|
|
219
|
-
|
|
220
|
-
|
|
324
|
+
handler: (_argv) => {
|
|
325
|
+
// This handler only runs when no subcommand is matched (the default case).
|
|
326
|
+
// Show help for the subcommands available.
|
|
327
|
+
console.log(`Manage ${name} from the ${table} table.`);
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log('Available subcommands:');
|
|
330
|
+
console.log(' list List all records');
|
|
331
|
+
console.log(' show Show record details');
|
|
332
|
+
if (!readOnly) {
|
|
333
|
+
console.log(' create Create a new record');
|
|
334
|
+
console.log(' update Update a record');
|
|
335
|
+
console.log(' delete Delete a record');
|
|
336
|
+
}
|
|
337
|
+
console.log(`\nRun "jsn dev ${name} <command> --help" for details.`);
|
|
221
338
|
},
|
|
222
339
|
};
|
|
223
340
|
}
|
package/src/commands/dev/eval.js
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
1
3
|
export function evalCmd(wrap) {
|
|
2
4
|
return {
|
|
3
5
|
command: 'eval',
|
|
4
|
-
describe: 'Execute background scripts
|
|
6
|
+
describe: 'Execute background scripts on the instance',
|
|
5
7
|
builder: (yargs) => {
|
|
6
8
|
return yargs
|
|
7
|
-
.option('script', { alias: 's', type: 'string', describe: 'JavaScript code to execute
|
|
9
|
+
.option('script', { alias: 's', type: 'string', describe: 'JavaScript code to execute' })
|
|
10
|
+
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file' });
|
|
8
11
|
},
|
|
9
12
|
handler: wrap(async (argv, app) => {
|
|
13
|
+
let script;
|
|
14
|
+
|
|
15
|
+
if (argv.file) {
|
|
16
|
+
script = fs.readFileSync(argv.file, 'utf-8');
|
|
17
|
+
} else if (argv.script) {
|
|
18
|
+
script = argv.script;
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error('--script or --file is required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const output = await app.sdk.executeScript(script);
|
|
10
24
|
app.ok({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}, {
|
|
25
|
+
script,
|
|
26
|
+
output,
|
|
27
|
+
instance: app.getEffectiveInstance(),
|
|
28
|
+
}, {
|
|
29
|
+
summary: 'Script executed',
|
|
30
|
+
breadcrumbs: [{
|
|
31
|
+
action: 'eval',
|
|
32
|
+
cmd: 'jsn dev eval --script \'...\'',
|
|
33
|
+
description: 'Execute a background script on the ServiceNow instance',
|
|
34
|
+
}],
|
|
35
|
+
});
|
|
15
36
|
}),
|
|
16
37
|
};
|
|
17
38
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField } from '../../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function flowsCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -17,6 +17,23 @@ export function flowsCmd(wrap) {
|
|
|
17
17
|
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
18
|
handler: wrap(async (argv, app) => {
|
|
19
19
|
const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
// Interactive picker
|
|
23
|
+
const picked = await interactiveList({
|
|
24
|
+
app, table: 'sys_hub_flow', singular: 'flow', columns, limit: argv.limit, query, labelField: 'name',
|
|
25
|
+
formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'active') === 'true' ? '' : '[inactive]'}`,
|
|
26
|
+
});
|
|
27
|
+
if (picked) {
|
|
28
|
+
const inspection = await app.sdk.inspectFlow(picked.sys_id);
|
|
29
|
+
const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
|
|
30
|
+
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
31
|
+
summary: `Flow: ${inspection.flow.name}`,
|
|
32
|
+
breadcrumbs: [{ action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' }],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Text/table fallback
|
|
20
37
|
const params = new URLSearchParams();
|
|
21
38
|
params.set('sysparm_limit', String(argv.limit));
|
|
22
39
|
params.set('sysparm_display_value', 'all');
|
|
@@ -48,6 +65,34 @@ export function flowsCmd(wrap) {
|
|
|
48
65
|
],
|
|
49
66
|
});
|
|
50
67
|
}),
|
|
68
|
+
})
|
|
69
|
+
.command({
|
|
70
|
+
command: 'create',
|
|
71
|
+
describe: 'Create a new flow (not yet implemented)',
|
|
72
|
+
builder: (y) => y
|
|
73
|
+
.option('data', { type: 'string', describe: 'JSON data for the flow' }),
|
|
74
|
+
handler: wrap(async (_argv, _app) => {
|
|
75
|
+
throw new Error('Flow creation requires the Flow Designer GraphQL API - not yet implemented.\n'
|
|
76
|
+
+ 'Use the ServiceNow web UI to create flows, then use "jsn dev flows list" to view them.');
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
.command({
|
|
80
|
+
command: 'update <identifier>',
|
|
81
|
+
describe: 'Update a flow (not yet implemented)',
|
|
82
|
+
builder: (y) => y
|
|
83
|
+
.option('data', { type: 'string', describe: 'JSON data to update' }),
|
|
84
|
+
handler: wrap(async (_argv, _app) => {
|
|
85
|
+
throw new Error('Flow updates require the Flow Designer GraphQL API - not yet implemented.\n'
|
|
86
|
+
+ 'Use the ServiceNow web UI to update flows, then use "jsn dev flows list" to view them.');
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
.command({
|
|
90
|
+
command: 'delete <identifier>',
|
|
91
|
+
describe: 'Delete a flow (not yet implemented)',
|
|
92
|
+
handler: wrap(async (_argv, _app) => {
|
|
93
|
+
throw new Error('Flow deletion requires the Flow Designer GraphQL API - not yet implemented.\n'
|
|
94
|
+
+ 'Use the ServiceNow web UI to delete flows.');
|
|
95
|
+
}),
|
|
51
96
|
});
|
|
52
97
|
},
|
|
53
98
|
handler: () => {},
|
package/src/commands/dev/logs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, interactiveList, getStringField } from '../../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function logsCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -17,11 +17,24 @@ export function logsCmd(wrap) {
|
|
|
17
17
|
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
18
18
|
handler: wrap(async (argv, app) => {
|
|
19
19
|
const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'created'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
// Try interactive picker first
|
|
23
|
+
const picked = await interactiveList({
|
|
24
|
+
app, table: 'syslog', singular: 'log entry', columns, limit: argv.limit, query, labelField: 'message',
|
|
25
|
+
formatLabel: r => `[${getStringField(r, 'level') || '?'}] ${(getStringField(r, 'message') || '').substring(0, 80)}`,
|
|
26
|
+
});
|
|
27
|
+
if (picked) {
|
|
28
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table: 'syslog' };
|
|
29
|
+
return app.ok(picked, { summary: `Log entry` });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fall back to text/table
|
|
20
33
|
const params = new URLSearchParams();
|
|
21
34
|
params.set('sysparm_limit', String(argv.limit));
|
|
22
35
|
params.set('sysparm_display_value', 'all');
|
|
23
36
|
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
24
|
-
const q =
|
|
37
|
+
const q = query ? query + '^ORDERBYDESCsys_created_on' : 'ORDERBYDESCsys_created_on';
|
|
25
38
|
params.set('sysparm_query', q);
|
|
26
39
|
const records = await app.sdk.list('syslog', params);
|
|
27
40
|
app.ok({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField } from '../../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function scopesCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -17,6 +17,17 @@ export function scopesCmd(wrap) {
|
|
|
17
17
|
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
18
|
handler: wrap(async (argv, app) => {
|
|
19
19
|
const columns = argv.columns ? argv.columns.split(',') : ['name', 'scope', 'short_description', 'active'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
const picked = await interactiveList({
|
|
23
|
+
app, table: 'sys_scope', singular: 'scope', columns, limit: argv.limit, query, labelField: 'name',
|
|
24
|
+
formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'scope') || '?'}]`,
|
|
25
|
+
});
|
|
26
|
+
if (picked) {
|
|
27
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_scope' };
|
|
28
|
+
return app.ok(picked, { summary: `Scope: ${getStringField(picked, 'name')}` });
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
const params = new URLSearchParams();
|
|
21
32
|
params.set('sysparm_limit', String(argv.limit));
|
|
22
33
|
params.set('sysparm_display_value', 'all');
|
|
@@ -89,8 +100,57 @@ export function scopesCmd(wrap) {
|
|
|
89
100
|
app.ok({ scope: argv.scope, sys_id: scopeSysID }, { summary: `Current scope: ${argv.scope}` });
|
|
90
101
|
}),
|
|
91
102
|
})
|
|
103
|
+
.command({
|
|
104
|
+
command: 'create',
|
|
105
|
+
describe: 'Create a new application scope',
|
|
106
|
+
builder: (y) => y
|
|
107
|
+
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Application name' })
|
|
108
|
+
.option('scope', { type: 'string', describe: 'Scope value (auto-generated from name if omitted)' }),
|
|
109
|
+
handler: wrap(async (argv, app) => {
|
|
110
|
+
let scope = argv.scope;
|
|
111
|
+
if (!scope) {
|
|
112
|
+
// Auto-generate scope from name: lowercase, replace spaces/special chars
|
|
113
|
+
scope = 'x_' + argv.name.toLowerCase()
|
|
114
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
115
|
+
.replace(/_+/g, '_')
|
|
116
|
+
.replace(/^_|_$/g, '')
|
|
117
|
+
.substring(0, 38); // x_ + max 35 chars = 37, leave room
|
|
118
|
+
}
|
|
119
|
+
// Check for existing scope
|
|
120
|
+
const existing = await app.sdk.list('sys_scope', new URLSearchParams({
|
|
121
|
+
sysparm_query: `scope=${scope}`,
|
|
122
|
+
sysparm_limit: '1',
|
|
123
|
+
}));
|
|
124
|
+
if (existing.length > 0) {
|
|
125
|
+
throw new Error(`Scope '${scope}' already exists. Use a different name or --scope flag.`);
|
|
126
|
+
}
|
|
127
|
+
const record = await app.sdk.create('sys_scope', {
|
|
128
|
+
name: argv.name,
|
|
129
|
+
scope,
|
|
130
|
+
short_description: argv.name,
|
|
131
|
+
});
|
|
132
|
+
app.ok(record, {
|
|
133
|
+
summary: `Created scope: ${scope}`,
|
|
134
|
+
breadcrumbs: [{
|
|
135
|
+
action: 'show',
|
|
136
|
+
cmd: `jsn dev scopes show ${scope}`,
|
|
137
|
+
description: 'View the new scope',
|
|
138
|
+
}],
|
|
139
|
+
});
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
92
142
|
|
|
93
143
|
},
|
|
94
|
-
handler: () => {
|
|
144
|
+
handler: (argv) => {
|
|
145
|
+
if (!argv._[1]) {
|
|
146
|
+
console.log('Manage ServiceNow application scopes.\n');
|
|
147
|
+
console.log('Commands:');
|
|
148
|
+
console.log(' list List application scopes');
|
|
149
|
+
console.log(' show <scope> Show a scope');
|
|
150
|
+
console.log(' set <scope> Set the current application scope');
|
|
151
|
+
console.log(' create Create a new application scope');
|
|
152
|
+
console.log('\nRun "jsn dev scopes <command> --help" for details.');
|
|
153
|
+
}
|
|
154
|
+
},
|
|
95
155
|
};
|
|
96
156
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField } from '../../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function updateSetsCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -17,6 +17,17 @@ export function updateSetsCmd(wrap) {
|
|
|
17
17
|
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
18
|
handler: wrap(async (argv, app) => {
|
|
19
19
|
const columns = argv.columns ? argv.columns.split(',') : ['name', 'state', 'application'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
const picked = await interactiveList({
|
|
23
|
+
app, table: 'sys_update_set', singular: 'update set', columns, limit: argv.limit, query, labelField: 'name',
|
|
24
|
+
formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'state') || '?'}]`,
|
|
25
|
+
});
|
|
26
|
+
if (picked) {
|
|
27
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_update_set' };
|
|
28
|
+
return app.ok(picked, { summary: `Update set: ${getStringField(picked, 'name')}` });
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
const params = new URLSearchParams();
|
|
21
32
|
params.set('sysparm_limit', String(argv.limit));
|
|
22
33
|
params.set('sysparm_display_value', 'all');
|
|
@@ -90,8 +101,40 @@ export function updateSetsCmd(wrap) {
|
|
|
90
101
|
app.ok({ update_set: argv.name, sys_id: sysID }, { summary: `Current update set: ${argv.name}` });
|
|
91
102
|
}),
|
|
92
103
|
})
|
|
104
|
+
.command({
|
|
105
|
+
command: 'create',
|
|
106
|
+
describe: 'Create a new update set',
|
|
107
|
+
builder: (y) => y
|
|
108
|
+
.option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Update set name' })
|
|
109
|
+
.option('description', { type: 'string', describe: 'Description' }),
|
|
110
|
+
handler: wrap(async (argv, app) => {
|
|
111
|
+
const record = await app.sdk.create('sys_update_set', {
|
|
112
|
+
name: argv.name,
|
|
113
|
+
description: argv.description || argv.name,
|
|
114
|
+
state: 'in progress',
|
|
115
|
+
});
|
|
116
|
+
app.ok(record, {
|
|
117
|
+
summary: `Created update set: ${argv.name}`,
|
|
118
|
+
breadcrumbs: [{
|
|
119
|
+
action: 'set',
|
|
120
|
+
cmd: `jsn dev updatesets set "${argv.name}"`,
|
|
121
|
+
description: 'Set as current update set',
|
|
122
|
+
}],
|
|
123
|
+
});
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
93
126
|
|
|
94
127
|
},
|
|
95
|
-
handler: () => {
|
|
128
|
+
handler: (argv) => {
|
|
129
|
+
if (!argv._[1]) {
|
|
130
|
+
console.log('Manage ServiceNow update sets.\n');
|
|
131
|
+
console.log('Commands:');
|
|
132
|
+
console.log(' list List update sets');
|
|
133
|
+
console.log(' show <name> Show an update set');
|
|
134
|
+
console.log(' set <name> Set the current update set');
|
|
135
|
+
console.log(' create Create a new update set');
|
|
136
|
+
console.log('\nRun "jsn dev updatesets <command> --help" for details.');
|
|
137
|
+
}
|
|
138
|
+
},
|
|
96
139
|
};
|
|
97
140
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function groupMembersCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -32,7 +32,120 @@ export function groupMembersCmd(wrap) {
|
|
|
32
32
|
}, { summary: `${records.length} group member(s)` });
|
|
33
33
|
}),
|
|
34
34
|
})
|
|
35
|
+
.command({
|
|
36
|
+
command: 'add',
|
|
37
|
+
describe: 'Add a user to a group',
|
|
38
|
+
builder: (y) => y
|
|
39
|
+
.option('group', { alias: 'g', type: 'string', demandOption: true, describe: 'Group name or sys_id (required)' })
|
|
40
|
+
.option('user', { alias: 'u', type: 'string', demandOption: true, describe: 'Username or sys_id (required)' }),
|
|
41
|
+
handler: wrap(async (argv, app) => {
|
|
42
|
+
const groupName = argv.group;
|
|
43
|
+
const userName = argv.user;
|
|
44
|
+
|
|
45
|
+
// Resolve group sys_id
|
|
46
|
+
const isGroupSysID = groupName.length === 32 && /^[0-9a-fA-F]+$/.test(groupName);
|
|
47
|
+
const groupParams = new URLSearchParams();
|
|
48
|
+
groupParams.set('sysparm_query', isGroupSysID ? `sys_id=${groupName}` : `name=${groupName}`);
|
|
49
|
+
groupParams.set('sysparm_limit', '1');
|
|
50
|
+
groupParams.set('sysparm_fields', 'sys_id');
|
|
51
|
+
const groupRecords = await app.sdk.list('sys_user_group', groupParams);
|
|
52
|
+
if (groupRecords.length === 0) {
|
|
53
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
54
|
+
}
|
|
55
|
+
const groupSysID = getStringField(groupRecords[0], 'sys_id');
|
|
56
|
+
|
|
57
|
+
// Resolve user sys_id
|
|
58
|
+
const isUserSysID = userName.length === 32 && /^[0-9a-fA-F]+$/.test(userName);
|
|
59
|
+
const userParams = new URLSearchParams();
|
|
60
|
+
userParams.set('sysparm_query', isUserSysID ? `sys_id=${userName}` : `user_name=${userName}`);
|
|
61
|
+
userParams.set('sysparm_limit', '1');
|
|
62
|
+
userParams.set('sysparm_fields', 'sys_id');
|
|
63
|
+
const userRecords = await app.sdk.list('sys_user', userParams);
|
|
64
|
+
if (userRecords.length === 0) {
|
|
65
|
+
throw new Error(`User not found: ${userName}`);
|
|
66
|
+
}
|
|
67
|
+
const userSysID = getStringField(userRecords[0], 'sys_id');
|
|
68
|
+
|
|
69
|
+
// Check if membership already exists
|
|
70
|
+
const checkParams = new URLSearchParams();
|
|
71
|
+
checkParams.set('sysparm_query', `group=${groupSysID}^user=${userSysID}`);
|
|
72
|
+
checkParams.set('sysparm_limit', '1');
|
|
73
|
+
checkParams.set('sysparm_fields', 'sys_id');
|
|
74
|
+
const existing = await app.sdk.list('sys_user_grmember', checkParams);
|
|
75
|
+
if (existing.length > 0) {
|
|
76
|
+
throw new Error(`User ${userName} is already a member of group ${groupName}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create the membership
|
|
80
|
+
const record = await app.sdk.create('sys_user_grmember', {
|
|
81
|
+
group: groupSysID,
|
|
82
|
+
user: userSysID,
|
|
83
|
+
});
|
|
84
|
+
app.ok(record, {
|
|
85
|
+
summary: `Added ${userName} to ${groupName}`,
|
|
86
|
+
breadcrumbs: [
|
|
87
|
+
{ action: 'list', cmd: `jsn groupmembers list --query "group.name=${groupName}"`, description: 'List group members' },
|
|
88
|
+
{ action: 'remove', cmd: `jsn groupmembers remove --group "${groupName}" --user "${userName}"`, description: 'Remove this member' },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
.command({
|
|
94
|
+
command: 'remove',
|
|
95
|
+
describe: 'Remove a user from a group',
|
|
96
|
+
builder: (y) => y
|
|
97
|
+
.option('group', { alias: 'g', type: 'string', demandOption: true, describe: 'Group name or sys_id (required)' })
|
|
98
|
+
.option('user', { alias: 'u', type: 'string', demandOption: true, describe: 'Username or sys_id (required)' }),
|
|
99
|
+
handler: wrap(async (argv, app) => {
|
|
100
|
+
const groupName = argv.group;
|
|
101
|
+
const userName = argv.user;
|
|
35
102
|
|
|
103
|
+
// Resolve group sys_id
|
|
104
|
+
const isGroupSysID = groupName.length === 32 && /^[0-9a-fA-F]+$/.test(groupName);
|
|
105
|
+
const groupParams = new URLSearchParams();
|
|
106
|
+
groupParams.set('sysparm_query', isGroupSysID ? `sys_id=${groupName}` : `name=${groupName}`);
|
|
107
|
+
groupParams.set('sysparm_limit', '1');
|
|
108
|
+
groupParams.set('sysparm_fields', 'sys_id');
|
|
109
|
+
const groupRecords = await app.sdk.list('sys_user_group', groupParams);
|
|
110
|
+
if (groupRecords.length === 0) {
|
|
111
|
+
throw new Error(`Group not found: ${groupName}`);
|
|
112
|
+
}
|
|
113
|
+
const groupSysID = getStringField(groupRecords[0], 'sys_id');
|
|
114
|
+
|
|
115
|
+
// Resolve user sys_id
|
|
116
|
+
const isUserSysID = userName.length === 32 && /^[0-9a-fA-F]+$/.test(userName);
|
|
117
|
+
const userParams = new URLSearchParams();
|
|
118
|
+
userParams.set('sysparm_query', isUserSysID ? `sys_id=${userName}` : `user_name=${userName}`);
|
|
119
|
+
userParams.set('sysparm_limit', '1');
|
|
120
|
+
userParams.set('sysparm_fields', 'sys_id');
|
|
121
|
+
const userRecords = await app.sdk.list('sys_user', userParams);
|
|
122
|
+
if (userRecords.length === 0) {
|
|
123
|
+
throw new Error(`User not found: ${userName}`);
|
|
124
|
+
}
|
|
125
|
+
const userSysID = getStringField(userRecords[0], 'sys_id');
|
|
126
|
+
|
|
127
|
+
// Find the membership record
|
|
128
|
+
const findParams = new URLSearchParams();
|
|
129
|
+
findParams.set('sysparm_query', `group=${groupSysID}^user=${userSysID}`);
|
|
130
|
+
findParams.set('sysparm_limit', '1');
|
|
131
|
+
findParams.set('sysparm_fields', 'sys_id');
|
|
132
|
+
const membershipRecords = await app.sdk.list('sys_user_grmember', findParams);
|
|
133
|
+
if (membershipRecords.length === 0) {
|
|
134
|
+
throw new Error(`User ${userName} is not a member of group ${groupName}`);
|
|
135
|
+
}
|
|
136
|
+
const membershipSysID = getStringField(membershipRecords[0], 'sys_id');
|
|
137
|
+
|
|
138
|
+
// Delete the membership
|
|
139
|
+
await app.sdk.delete('sys_user_grmember', membershipSysID);
|
|
140
|
+
app.ok({ user: userName, group: groupName }, {
|
|
141
|
+
summary: `Removed ${userName} from ${groupName}`,
|
|
142
|
+
breadcrumbs: [
|
|
143
|
+
{ action: 'list', cmd: `jsn groupmembers list --query "group.name=${groupName}"`, description: 'List group members' },
|
|
144
|
+
{ action: 'add', cmd: `jsn groupmembers add --group "${groupName}" --user "${userName}"`, description: 'Add user back to group' },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
36
149
|
},
|
|
37
150
|
handler: () => {},
|
|
38
151
|
};
|