@jacebenson/jsn 1.0.7 → 2.0.1
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 +196 -243
- package/bin/jsn.js +2 -57
- package/package.json +35 -32
- package/skills/servicenow/SKILL.md +137 -0
- package/src/app.js +172 -0
- package/src/auth.js +517 -0
- package/src/cli.js +174 -0
- package/src/commands/_ticket.js +260 -0
- package/src/commands/auth.js +227 -0
- package/src/commands/catalog.js +325 -0
- package/src/commands/changes.js +7 -0
- package/src/commands/dev/_generic.js +342 -0
- package/src/commands/dev/_simple.js +89 -0
- package/src/commands/dev/eval.js +38 -0
- package/src/commands/dev/flows.js +573 -0
- package/src/commands/dev/forms.js +313 -0
- package/src/commands/dev/lists.js +233 -0
- package/src/commands/dev/logs.js +64 -0
- package/src/commands/dev/rest.js +64 -0
- package/src/commands/dev/scopes.js +156 -0
- package/src/commands/dev/updatesets.js +184 -0
- package/src/commands/dev.js +53 -0
- package/src/commands/groupmembers.js +152 -0
- package/src/commands/grouproles.js +152 -0
- package/src/commands/groups.js +141 -0
- package/src/commands/incidents.js +7 -0
- package/src/commands/profiles.js +91 -0
- package/src/commands/records.js +171 -0
- package/src/commands/requests.js +59 -0
- package/src/commands/setup.js +39 -0
- package/src/commands/skill.js +144 -0
- package/src/commands/tasks.js +7 -0
- package/src/commands/tickets.js +121 -0
- package/src/commands/users.js +144 -0
- package/src/commands/version.js +81 -0
- package/src/config.js +154 -0
- package/src/context.js +62 -0
- package/src/errors.js +101 -0
- package/src/help.js +85 -0
- package/src/helpers.js +182 -0
- package/src/output.js +440 -0
- package/src/sdk.js +615 -0
- package/scripts/install.sh +0 -227
- package/scripts/npm-install.js +0 -235
- package/scripts/pre-commit-check.sh +0 -61
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
|
+
|
|
3
|
+
export function scopesCmd(wrap) {
|
|
4
|
+
return {
|
|
5
|
+
command: 'scopes [subcommand]',
|
|
6
|
+
aliases: ['scope', 'sc'],
|
|
7
|
+
describe: 'Manage application scopes',
|
|
8
|
+
builder: (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: 'list',
|
|
12
|
+
aliases: ['ls'],
|
|
13
|
+
describe: 'List application scopes',
|
|
14
|
+
builder: (y) => y
|
|
15
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
17
|
+
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
|
+
handler: wrap(async (argv, app) => {
|
|
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
|
+
|
|
31
|
+
const params = new URLSearchParams();
|
|
32
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
33
|
+
params.set('sysparm_display_value', 'all');
|
|
34
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
35
|
+
const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
36
|
+
params.set('sysparm_query', q);
|
|
37
|
+
const records = await app.sdk.list('sys_scope', params);
|
|
38
|
+
app.ok({
|
|
39
|
+
table: 'sys_scope',
|
|
40
|
+
count: records.length,
|
|
41
|
+
columns,
|
|
42
|
+
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
43
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
44
|
+
}, { summary: `${records.length} scope(s)` });
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
.command({
|
|
48
|
+
command: 'show <scope>',
|
|
49
|
+
aliases: ['get'],
|
|
50
|
+
describe: 'Show a scope',
|
|
51
|
+
handler: wrap(async (argv, app) => {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
params.set('sysparm_query', `scope=${argv.scope}`);
|
|
54
|
+
params.set('sysparm_limit', '1');
|
|
55
|
+
params.set('sysparm_display_value', 'all');
|
|
56
|
+
const records = await app.sdk.list('sys_scope', params);
|
|
57
|
+
if (records.length === 0) {
|
|
58
|
+
throw new Error(`Scope not found: ${argv.scope}`);
|
|
59
|
+
}
|
|
60
|
+
app.ok(records[0], { summary: `Scope ${argv.scope}` });
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
.command({
|
|
64
|
+
command: 'set <scope>',
|
|
65
|
+
describe: 'Set the current application scope',
|
|
66
|
+
handler: wrap(async (argv, app) => {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
params.set('sysparm_query', `scope=${argv.scope}`);
|
|
69
|
+
params.set('sysparm_limit', '1');
|
|
70
|
+
params.set('sysparm_fields', 'sys_id,scope');
|
|
71
|
+
const records = await app.sdk.list('sys_scope', params);
|
|
72
|
+
if (records.length === 0) {
|
|
73
|
+
throw new Error(`Scope not found: ${argv.scope}`);
|
|
74
|
+
}
|
|
75
|
+
const scopeSysID = getStringField(records[0], 'sys_id');
|
|
76
|
+
// Update user preference
|
|
77
|
+
const user = await app.sdk.list('sys_user', new URLSearchParams({
|
|
78
|
+
sysparm_query: 'user_name=javascript:gs.getUserName()',
|
|
79
|
+
sysparm_limit: '1',
|
|
80
|
+
sysparm_fields: 'sys_id',
|
|
81
|
+
}));
|
|
82
|
+
if (user.length === 0) {
|
|
83
|
+
throw new Error('Could not determine current user');
|
|
84
|
+
}
|
|
85
|
+
const userSysID = getStringField(user[0], 'sys_id');
|
|
86
|
+
const prefParams = new URLSearchParams();
|
|
87
|
+
prefParams.set('sysparm_query', `user=${userSysID}^name=apps.current_app`);
|
|
88
|
+
prefParams.set('sysparm_limit', '1');
|
|
89
|
+
const prefs = await app.sdk.list('sys_user_preference', prefParams);
|
|
90
|
+
if (prefs.length > 0) {
|
|
91
|
+
await app.sdk.update('sys_user_preference', getStringField(prefs[0], 'sys_id'), { value: scopeSysID });
|
|
92
|
+
} else {
|
|
93
|
+
await app.sdk.create('sys_user_preference', {
|
|
94
|
+
user: userSysID,
|
|
95
|
+
name: 'apps.current_app',
|
|
96
|
+
value: scopeSysID,
|
|
97
|
+
type: 'string',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
app.ok({ scope: argv.scope, sys_id: scopeSysID }, { summary: `Current scope: ${argv.scope}` });
|
|
101
|
+
}),
|
|
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
|
+
})
|
|
142
|
+
|
|
143
|
+
},
|
|
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
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
|
+
|
|
3
|
+
export function updateSetsCmd(wrap) {
|
|
4
|
+
return {
|
|
5
|
+
command: 'updatesets [subcommand]',
|
|
6
|
+
aliases: ['updateset', 'us'],
|
|
7
|
+
describe: 'Manage update sets',
|
|
8
|
+
builder: (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: 'list',
|
|
12
|
+
aliases: ['ls'],
|
|
13
|
+
describe: 'List update sets',
|
|
14
|
+
builder: (y) => y
|
|
15
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
17
|
+
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
|
+
handler: wrap(async (argv, app) => {
|
|
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
|
+
|
|
31
|
+
const params = new URLSearchParams();
|
|
32
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
33
|
+
params.set('sysparm_display_value', 'all');
|
|
34
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
35
|
+
const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
36
|
+
params.set('sysparm_query', q);
|
|
37
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
38
|
+
app.ok({
|
|
39
|
+
table: 'sys_update_set',
|
|
40
|
+
count: records.length,
|
|
41
|
+
columns,
|
|
42
|
+
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
43
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
44
|
+
}, { summary: `${records.length} update set(s)` });
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
.command({
|
|
48
|
+
command: 'show <name>',
|
|
49
|
+
aliases: ['get'],
|
|
50
|
+
describe: 'Show an update set',
|
|
51
|
+
handler: wrap(async (argv, app) => {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
params.set('sysparm_query', `name=${argv.name}`);
|
|
54
|
+
params.set('sysparm_limit', '1');
|
|
55
|
+
params.set('sysparm_display_value', 'all');
|
|
56
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
57
|
+
if (records.length === 0) {
|
|
58
|
+
throw new Error(`Update set not found: ${argv.name}`);
|
|
59
|
+
}
|
|
60
|
+
app.ok(records[0], { summary: `Update set ${argv.name}` });
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
.command({
|
|
64
|
+
command: 'set <name>',
|
|
65
|
+
describe: 'Set the current update set',
|
|
66
|
+
handler: wrap(async (argv, app) => {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
params.set('sysparm_query', `name=${argv.name}`);
|
|
69
|
+
params.set('sysparm_limit', '1');
|
|
70
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
71
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
72
|
+
if (records.length === 0) {
|
|
73
|
+
throw new Error(`Update set not found: ${argv.name}`);
|
|
74
|
+
}
|
|
75
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
76
|
+
// Update user preference
|
|
77
|
+
const user = await app.sdk.list('sys_user', new URLSearchParams({
|
|
78
|
+
sysparm_query: 'user_name=javascript:gs.getUserName()',
|
|
79
|
+
sysparm_limit: '1',
|
|
80
|
+
sysparm_fields: 'sys_id',
|
|
81
|
+
}));
|
|
82
|
+
if (user.length === 0) {
|
|
83
|
+
throw new Error('Could not determine current user');
|
|
84
|
+
}
|
|
85
|
+
const userSysID = getStringField(user[0], 'sys_id');
|
|
86
|
+
// Find or create preference
|
|
87
|
+
const prefParams = new URLSearchParams();
|
|
88
|
+
prefParams.set('sysparm_query', `user=${userSysID}^name=sys_update_set`);
|
|
89
|
+
prefParams.set('sysparm_limit', '1');
|
|
90
|
+
const prefs = await app.sdk.list('sys_user_preference', prefParams);
|
|
91
|
+
if (prefs.length > 0) {
|
|
92
|
+
await app.sdk.update('sys_user_preference', getStringField(prefs[0], 'sys_id'), { value: sysID });
|
|
93
|
+
} else {
|
|
94
|
+
await app.sdk.create('sys_user_preference', {
|
|
95
|
+
user: userSysID,
|
|
96
|
+
name: 'sys_update_set',
|
|
97
|
+
value: sysID,
|
|
98
|
+
type: 'string',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
app.ok({ update_set: argv.name, sys_id: sysID }, { summary: `Current update set: ${argv.name}` });
|
|
102
|
+
}),
|
|
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
|
+
|
|
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
|
+
|
|
148
|
+
app.ok(record, {
|
|
149
|
+
summary: `Created update set: ${argv.name}`,
|
|
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
|
+
],
|
|
162
|
+
});
|
|
163
|
+
}),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
},
|
|
167
|
+
handler: (argv) => {
|
|
168
|
+
if (!argv._[1]) {
|
|
169
|
+
console.log('Manage ServiceNow update sets.\n');
|
|
170
|
+
console.log('Commands:');
|
|
171
|
+
console.log(' list List update sets');
|
|
172
|
+
console.log(' show <name> Show an update set');
|
|
173
|
+
console.log(' set <name> Set the current 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');
|
|
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.');
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Parent dev command
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
actionsCmd, includesCmd, rulesCmd,
|
|
5
|
+
clientScriptsCmd, uiActionsCmd, uiPoliciesCmd,
|
|
6
|
+
tablesCmd, columnsCmd, importCmd,
|
|
7
|
+
spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd, scRAPICmd,
|
|
8
|
+
aclsCmd, rolesCmd, propertiesCmd,
|
|
9
|
+
} from './dev/_simple.js';
|
|
10
|
+
import { flowsCmd } from './dev/flows.js';
|
|
11
|
+
import { formsCmd } from './dev/forms.js';
|
|
12
|
+
import { listsCmd } from './dev/lists.js';
|
|
13
|
+
import { updateSetsCmd } from './dev/updatesets.js';
|
|
14
|
+
import { scopesCmd } from './dev/scopes.js';
|
|
15
|
+
import { evalCmd } from './dev/eval.js';
|
|
16
|
+
import { restCmd } from './dev/rest.js';
|
|
17
|
+
import { logsCmd } from './dev/logs.js';
|
|
18
|
+
|
|
19
|
+
export function devCmd(wrap) {
|
|
20
|
+
return {
|
|
21
|
+
command: 'dev [subcommand]',
|
|
22
|
+
describe: 'Manage ServiceNow development artifacts',
|
|
23
|
+
builder: (yargs) => {
|
|
24
|
+
return yargs
|
|
25
|
+
.command(flowsCmd(wrap))
|
|
26
|
+
.command(actionsCmd(wrap))
|
|
27
|
+
.command(includesCmd(wrap))
|
|
28
|
+
.command(rulesCmd(wrap))
|
|
29
|
+
.command(clientScriptsCmd(wrap))
|
|
30
|
+
.command(uiActionsCmd(wrap))
|
|
31
|
+
.command(uiPoliciesCmd(wrap))
|
|
32
|
+
.command(tablesCmd(wrap))
|
|
33
|
+
.command(columnsCmd(wrap))
|
|
34
|
+
.command(formsCmd(wrap))
|
|
35
|
+
.command(listsCmd(wrap))
|
|
36
|
+
.command(importCmd(wrap))
|
|
37
|
+
.command(spPagesCmd(wrap))
|
|
38
|
+
.command(spWidgetsCmd(wrap))
|
|
39
|
+
.command(uiPagesCmd(wrap))
|
|
40
|
+
.command(appMenuCmd(wrap))
|
|
41
|
+
.command(scRAPICmd(wrap))
|
|
42
|
+
.command(aclsCmd(wrap))
|
|
43
|
+
.command(rolesCmd(wrap))
|
|
44
|
+
.command(updateSetsCmd(wrap))
|
|
45
|
+
.command(scopesCmd(wrap))
|
|
46
|
+
.command(propertiesCmd(wrap))
|
|
47
|
+
.command(logsCmd(wrap))
|
|
48
|
+
.command(evalCmd(wrap))
|
|
49
|
+
.command(restCmd(wrap));
|
|
50
|
+
},
|
|
51
|
+
handler: () => {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { formatRecordForDisplay, getStringField } from '../helpers.js';
|
|
2
|
+
|
|
3
|
+
export function groupMembersCmd(wrap) {
|
|
4
|
+
return {
|
|
5
|
+
command: 'groupmembers [subcommand]',
|
|
6
|
+
aliases: ['groupmember', 'gmember', 'gm'],
|
|
7
|
+
describe: 'Manage group memberships',
|
|
8
|
+
builder: (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: 'list',
|
|
12
|
+
aliases: ['ls'],
|
|
13
|
+
describe: 'List group members',
|
|
14
|
+
builder: (y) => y
|
|
15
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
17
|
+
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
|
|
18
|
+
handler: wrap(async (argv, app) => {
|
|
19
|
+
const columns = argv.columns ? argv.columns.split(',') : ['user.name', 'group.name'];
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
22
|
+
params.set('sysparm_display_value', 'all');
|
|
23
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
24
|
+
if (argv.query) params.set('sysparm_query', argv.query);
|
|
25
|
+
const records = await app.sdk.list('sys_user_grmember', params);
|
|
26
|
+
app.ok({
|
|
27
|
+
table: 'sys_user_grmember',
|
|
28
|
+
count: records.length,
|
|
29
|
+
columns,
|
|
30
|
+
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
31
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
32
|
+
}, { summary: `${records.length} group member(s)` });
|
|
33
|
+
}),
|
|
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;
|
|
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
|
+
});
|
|
149
|
+
},
|
|
150
|
+
handler: () => {},
|
|
151
|
+
};
|
|
152
|
+
}
|