@jacebenson/jsn 1.0.6 → 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.
Files changed (45) hide show
  1. package/README.md +196 -243
  2. package/bin/jsn.js +2 -57
  3. package/package.json +35 -32
  4. package/skills/servicenow/SKILL.md +137 -0
  5. package/src/app.js +172 -0
  6. package/src/auth.js +517 -0
  7. package/src/cli.js +174 -0
  8. package/src/commands/_ticket.js +260 -0
  9. package/src/commands/auth.js +227 -0
  10. package/src/commands/catalog.js +325 -0
  11. package/src/commands/changes.js +7 -0
  12. package/src/commands/dev/_generic.js +342 -0
  13. package/src/commands/dev/_simple.js +89 -0
  14. package/src/commands/dev/eval.js +38 -0
  15. package/src/commands/dev/flows.js +573 -0
  16. package/src/commands/dev/forms.js +313 -0
  17. package/src/commands/dev/lists.js +233 -0
  18. package/src/commands/dev/logs.js +64 -0
  19. package/src/commands/dev/rest.js +64 -0
  20. package/src/commands/dev/scopes.js +156 -0
  21. package/src/commands/dev/updatesets.js +184 -0
  22. package/src/commands/dev.js +53 -0
  23. package/src/commands/groupmembers.js +152 -0
  24. package/src/commands/grouproles.js +152 -0
  25. package/src/commands/groups.js +141 -0
  26. package/src/commands/incidents.js +7 -0
  27. package/src/commands/profiles.js +91 -0
  28. package/src/commands/records.js +171 -0
  29. package/src/commands/requests.js +59 -0
  30. package/src/commands/setup.js +39 -0
  31. package/src/commands/skill.js +144 -0
  32. package/src/commands/tasks.js +7 -0
  33. package/src/commands/tickets.js +121 -0
  34. package/src/commands/users.js +144 -0
  35. package/src/commands/version.js +81 -0
  36. package/src/config.js +154 -0
  37. package/src/context.js +62 -0
  38. package/src/errors.js +101 -0
  39. package/src/help.js +85 -0
  40. package/src/helpers.js +182 -0
  41. package/src/output.js +440 -0
  42. package/src/sdk.js +615 -0
  43. package/scripts/install.sh +0 -227
  44. package/scripts/npm-install.js +0 -235
  45. 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
+ }