@jacebenson/jsn 0.0.9 → 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 -6
- package/src/commands/_ticket.js +8 -4
- package/src/commands/auth.js +182 -32
- package/src/commands/dev/_generic.js +129 -12
- 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 +24 -6
- package/src/commands/requests.js +53 -1
- package/src/commands/skill.js +109 -0
- package/src/commands/tickets.js +1 -1
- 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,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function groupRolesCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -32,7 +32,120 @@ export function groupRolesCmd(wrap) {
|
|
|
32
32
|
}, { summary: `${records.length} group role(s)` });
|
|
33
33
|
}),
|
|
34
34
|
})
|
|
35
|
+
.command({
|
|
36
|
+
command: 'add',
|
|
37
|
+
describe: 'Add a role 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('role', { alias: 'r', type: 'string', demandOption: true, describe: 'Role name or sys_id (required)' }),
|
|
41
|
+
handler: wrap(async (argv, app) => {
|
|
42
|
+
const groupName = argv.group;
|
|
43
|
+
const roleName = argv.role;
|
|
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 role sys_id
|
|
58
|
+
const isRoleSysID = roleName.length === 32 && /^[0-9a-fA-F]+$/.test(roleName);
|
|
59
|
+
const roleParams = new URLSearchParams();
|
|
60
|
+
roleParams.set('sysparm_query', isRoleSysID ? `sys_id=${roleName}` : `name=${roleName}`);
|
|
61
|
+
roleParams.set('sysparm_limit', '1');
|
|
62
|
+
roleParams.set('sysparm_fields', 'sys_id');
|
|
63
|
+
const roleRecords = await app.sdk.list('sys_user_role', roleParams);
|
|
64
|
+
if (roleRecords.length === 0) {
|
|
65
|
+
throw new Error(`Role not found: ${roleName}`);
|
|
66
|
+
}
|
|
67
|
+
const roleSysID = getStringField(roleRecords[0], 'sys_id');
|
|
68
|
+
|
|
69
|
+
// Check if role assignment already exists
|
|
70
|
+
const checkParams = new URLSearchParams();
|
|
71
|
+
checkParams.set('sysparm_query', `group=${groupSysID}^role=${roleSysID}`);
|
|
72
|
+
checkParams.set('sysparm_limit', '1');
|
|
73
|
+
checkParams.set('sysparm_fields', 'sys_id');
|
|
74
|
+
const existing = await app.sdk.list('sys_group_has_role', checkParams);
|
|
75
|
+
if (existing.length > 0) {
|
|
76
|
+
throw new Error(`Role ${roleName} is already assigned to group ${groupName}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create the role assignment
|
|
80
|
+
const record = await app.sdk.create('sys_group_has_role', {
|
|
81
|
+
group: groupSysID,
|
|
82
|
+
role: roleSysID,
|
|
83
|
+
});
|
|
84
|
+
app.ok(record, {
|
|
85
|
+
summary: `Added role ${roleName} to ${groupName}`,
|
|
86
|
+
breadcrumbs: [
|
|
87
|
+
{ action: 'list', cmd: `jsn grouproles list --query "group.name=${groupName}"`, description: 'List group roles' },
|
|
88
|
+
{ action: 'remove', cmd: `jsn grouproles remove --group "${groupName}" --role "${roleName}"`, description: 'Remove this role' },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
.command({
|
|
94
|
+
command: 'remove',
|
|
95
|
+
describe: 'Remove a role 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('role', { alias: 'r', type: 'string', demandOption: true, describe: 'Role name or sys_id (required)' }),
|
|
99
|
+
handler: wrap(async (argv, app) => {
|
|
100
|
+
const groupName = argv.group;
|
|
101
|
+
const roleName = argv.role;
|
|
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 role sys_id
|
|
116
|
+
const isRoleSysID = roleName.length === 32 && /^[0-9a-fA-F]+$/.test(roleName);
|
|
117
|
+
const roleParams = new URLSearchParams();
|
|
118
|
+
roleParams.set('sysparm_query', isRoleSysID ? `sys_id=${roleName}` : `name=${roleName}`);
|
|
119
|
+
roleParams.set('sysparm_limit', '1');
|
|
120
|
+
roleParams.set('sysparm_fields', 'sys_id');
|
|
121
|
+
const roleRecords = await app.sdk.list('sys_user_role', roleParams);
|
|
122
|
+
if (roleRecords.length === 0) {
|
|
123
|
+
throw new Error(`Role not found: ${roleName}`);
|
|
124
|
+
}
|
|
125
|
+
const roleSysID = getStringField(roleRecords[0], 'sys_id');
|
|
126
|
+
|
|
127
|
+
// Find the role assignment record
|
|
128
|
+
const findParams = new URLSearchParams();
|
|
129
|
+
findParams.set('sysparm_query', `group=${groupSysID}^role=${roleSysID}`);
|
|
130
|
+
findParams.set('sysparm_limit', '1');
|
|
131
|
+
findParams.set('sysparm_fields', 'sys_id');
|
|
132
|
+
const assignmentRecords = await app.sdk.list('sys_group_has_role', findParams);
|
|
133
|
+
if (assignmentRecords.length === 0) {
|
|
134
|
+
throw new Error(`Role ${roleName} is not assigned to group ${groupName}`);
|
|
135
|
+
}
|
|
136
|
+
const assignmentSysID = getStringField(assignmentRecords[0], 'sys_id');
|
|
137
|
+
|
|
138
|
+
// Delete the role assignment
|
|
139
|
+
await app.sdk.delete('sys_group_has_role', assignmentSysID);
|
|
140
|
+
app.ok({ group: groupName, role: roleName }, {
|
|
141
|
+
summary: `Removed role ${roleName} from ${groupName}`,
|
|
142
|
+
breadcrumbs: [
|
|
143
|
+
{ action: 'list', cmd: `jsn grouproles list --query "group.name=${groupName}"`, description: 'List group roles' },
|
|
144
|
+
{ action: 'add', cmd: `jsn grouproles add --group "${groupName}" --role "${roleName}"`, description: 'Add role back to group' },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
36
149
|
},
|
|
37
150
|
handler: () => {},
|
|
38
151
|
};
|
package/src/commands/groups.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function groupsCmd(wrap) {
|
|
4
4
|
return {
|
|
5
5
|
command: 'groups [subcommand]',
|
|
6
6
|
aliases: ['group'],
|
|
7
|
-
describe: '
|
|
7
|
+
describe: 'Manage ServiceNow groups',
|
|
8
8
|
builder: (yargs) => {
|
|
9
9
|
return yargs
|
|
10
10
|
.command({
|
|
@@ -17,6 +17,17 @@ export function groupsCmd(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', 'manager', 'email'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
const picked = await interactiveList({
|
|
23
|
+
app, table: 'sys_user_group', singular: 'group', columns, limit: argv.limit, query, labelField: 'name',
|
|
24
|
+
formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'manager') ? '→ ' + getStringField(r, 'manager') : ''}`,
|
|
25
|
+
});
|
|
26
|
+
if (picked) {
|
|
27
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_user_group' };
|
|
28
|
+
return app.ok(picked, { summary: `Group: ${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');
|
|
@@ -50,7 +61,80 @@ export function groupsCmd(wrap) {
|
|
|
50
61
|
app.ok(records[0], { summary: `Group ${id}` });
|
|
51
62
|
}),
|
|
52
63
|
})
|
|
53
|
-
|
|
64
|
+
.command({
|
|
65
|
+
command: 'create',
|
|
66
|
+
describe: 'Create a new group',
|
|
67
|
+
builder: (y) => y
|
|
68
|
+
.option('name', { alias: 'n', type: 'string', describe: 'Group name' })
|
|
69
|
+
.option('manager', { type: 'string', describe: 'Manager name or sys_id' })
|
|
70
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' }),
|
|
71
|
+
handler: wrap(async (argv, app) => {
|
|
72
|
+
const recordData = {};
|
|
73
|
+
if (argv.data) {
|
|
74
|
+
Object.assign(recordData, JSON.parse(argv.data));
|
|
75
|
+
}
|
|
76
|
+
if (argv.name) recordData.name = argv.name;
|
|
77
|
+
if (argv.manager) recordData.manager = argv.manager;
|
|
78
|
+
if (!recordData.name) {
|
|
79
|
+
throw new Error('name is required (use --name or --data)');
|
|
80
|
+
}
|
|
81
|
+
const record = await app.sdk.create('sys_user_group', recordData);
|
|
82
|
+
app.ok(record, {
|
|
83
|
+
summary: `Created group ${getStringField(record, 'name')}`,
|
|
84
|
+
breadcrumbs: [
|
|
85
|
+
{ action: 'view', cmd: `jsn groups show ${getStringField(record, 'name')}`, description: `View the new group` },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
89
|
+
})
|
|
90
|
+
.command({
|
|
91
|
+
command: 'update <name>',
|
|
92
|
+
describe: 'Update a group by name or sys_id',
|
|
93
|
+
builder: (y) => y
|
|
94
|
+
.option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
|
|
95
|
+
handler: wrap(async (argv, app) => {
|
|
96
|
+
const id = argv.name;
|
|
97
|
+
const recordData = JSON.parse(argv.data);
|
|
98
|
+
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
99
|
+
const params = new URLSearchParams();
|
|
100
|
+
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `name=${id}`);
|
|
101
|
+
params.set('sysparm_limit', '1');
|
|
102
|
+
params.set('sysparm_fields', 'sys_id');
|
|
103
|
+
const records = await app.sdk.list('sys_user_group', params);
|
|
104
|
+
if (records.length === 0) {
|
|
105
|
+
throw new Error(`Group not found: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
108
|
+
const updated = await app.sdk.update('sys_user_group', sysID, recordData);
|
|
109
|
+
app.ok(updated, {
|
|
110
|
+
summary: `Updated group ${id}`,
|
|
111
|
+
breadcrumbs: [
|
|
112
|
+
{ action: 'view', cmd: `jsn groups show ${id}`, description: `View the updated group` },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}),
|
|
116
|
+
})
|
|
117
|
+
.command({
|
|
118
|
+
command: 'delete <name>',
|
|
119
|
+
describe: 'Delete a group by name or sys_id',
|
|
120
|
+
handler: wrap(async (argv, app) => {
|
|
121
|
+
const id = argv.name;
|
|
122
|
+
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `name=${id}`);
|
|
125
|
+
params.set('sysparm_limit', '1');
|
|
126
|
+
params.set('sysparm_fields', 'sys_id');
|
|
127
|
+
const records = await app.sdk.list('sys_user_group', params);
|
|
128
|
+
if (records.length === 0) {
|
|
129
|
+
throw new Error(`Group not found: ${id}`);
|
|
130
|
+
}
|
|
131
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
132
|
+
await app.sdk.delete('sys_user_group', sysID);
|
|
133
|
+
app.ok({ identifier: id, message: 'Group deleted' }, {
|
|
134
|
+
summary: `Deleted group ${id}`,
|
|
135
|
+
});
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
54
138
|
},
|
|
55
139
|
handler: () => {},
|
|
56
140
|
};
|
package/src/commands/profiles.js
CHANGED
|
@@ -2,7 +2,7 @@ import { saveConfig } from '../config.js';
|
|
|
2
2
|
|
|
3
3
|
export function profilesCmd(wrap) {
|
|
4
4
|
return {
|
|
5
|
-
command: 'profiles
|
|
5
|
+
command: 'profiles [subcommand]',
|
|
6
6
|
aliases: ['profile'],
|
|
7
7
|
describe: 'Manage configuration profiles',
|
|
8
8
|
builder: (yargs) => {
|
|
@@ -74,6 +74,18 @@ export function profilesCmd(wrap) {
|
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
},
|
|
77
|
-
handler: () => {
|
|
77
|
+
handler: (argv) => {
|
|
78
|
+
// When no subcommand is given, show help
|
|
79
|
+
if (!argv._[1]) {
|
|
80
|
+
console.log('Manage your ServiceNow instance profiles.\n');
|
|
81
|
+
console.log('Usage: jsn profiles <list|show|use|remove> [options]\n');
|
|
82
|
+
console.log('Commands:');
|
|
83
|
+
console.log(' list List all profiles');
|
|
84
|
+
console.log(' show [name] Show profile details');
|
|
85
|
+
console.log(' use <name> Switch to a different profile');
|
|
86
|
+
console.log(' remove <name> Remove a profile');
|
|
87
|
+
console.log('\nRun "jsn profiles <command> --help" for more details.');
|
|
88
|
+
}
|
|
89
|
+
},
|
|
78
90
|
};
|
|
79
91
|
}
|
package/src/commands/records.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, buildQuerySuffix } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
const tableDefaultColumns = {
|
|
4
4
|
incident: ['number', 'short_description', 'priority', 'state', 'assigned_to'],
|
|
@@ -22,7 +22,7 @@ function getDefaultColumns(table) {
|
|
|
22
22
|
export function recordsCmd(wrap) {
|
|
23
23
|
return {
|
|
24
24
|
command: 'records <subcommand>',
|
|
25
|
-
describe: 'Query and manage records in any table',
|
|
25
|
+
describe: 'Query and manage records in any table (e.g. "records list --table incident --query priority=1")',
|
|
26
26
|
builder: (yargs) => {
|
|
27
27
|
return yargs
|
|
28
28
|
.command({
|
|
@@ -37,6 +37,22 @@ export function recordsCmd(wrap) {
|
|
|
37
37
|
handler: wrap(async (argv, app) => {
|
|
38
38
|
const table = argv.table;
|
|
39
39
|
const columns = argv.columns ? argv.columns.split(',') : getDefaultColumns(table);
|
|
40
|
+
const query = argv.query || '';
|
|
41
|
+
|
|
42
|
+
// Interactive picker
|
|
43
|
+
const picked = await interactiveList({
|
|
44
|
+
app, table, singular: 'record', columns, limit: argv.limit, query, labelField: 'sys_id',
|
|
45
|
+
formatLabel: r => {
|
|
46
|
+
const cols = getDefaultColumns(table);
|
|
47
|
+
return cols.map(c => `${c}: ${getStringField(r, c) || '-'}`).join(' | ');
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (picked) {
|
|
51
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table };
|
|
52
|
+
return app.ok(picked, { summary: `Record from ${table}` });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Text/table fallback
|
|
40
56
|
const params = new URLSearchParams();
|
|
41
57
|
params.set('sysparm_limit', String(argv.limit));
|
|
42
58
|
params.set('sysparm_offset', String(argv.offset));
|
|
@@ -99,9 +115,10 @@ export function recordsCmd(wrap) {
|
|
|
99
115
|
describe: 'Create a new record',
|
|
100
116
|
builder: (y) => y
|
|
101
117
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
102
|
-
.option('data', { type: 'string',
|
|
118
|
+
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
119
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' }),
|
|
103
120
|
handler: wrap(async (argv, app) => {
|
|
104
|
-
const recordData =
|
|
121
|
+
const recordData = parseDataArg(argv);
|
|
105
122
|
const record = await app.sdk.create(argv.table, recordData);
|
|
106
123
|
app.ok(record, { summary: `Created record in ${argv.table}` });
|
|
107
124
|
}),
|
|
@@ -112,9 +129,10 @@ export function recordsCmd(wrap) {
|
|
|
112
129
|
builder: (y) => y
|
|
113
130
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
114
131
|
.option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' })
|
|
115
|
-
.option('data', { type: 'string',
|
|
132
|
+
.option('data', { type: 'string', describe: 'JSON fields (e.g. \'{"state":"2"}\')' })
|
|
133
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' }),
|
|
116
134
|
handler: wrap(async (argv, app) => {
|
|
117
|
-
const recordData =
|
|
135
|
+
const recordData = parseDataArg(argv);
|
|
118
136
|
const record = await app.sdk.update(argv.table, argv['sys-id'], recordData);
|
|
119
137
|
app.ok(record, { summary: `Updated record in ${argv.table}` });
|
|
120
138
|
}),
|
package/src/commands/requests.js
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
1
|
import { buildTicketCommands } from './_ticket.js';
|
|
2
|
+
import { getStringField } from '../helpers.js';
|
|
2
3
|
|
|
3
4
|
const requestDefaultColumns = ['number', 'short_description', 'stage', 'assigned_to'];
|
|
4
5
|
|
|
5
6
|
export function requestsCmd(wrap) {
|
|
6
|
-
|
|
7
|
+
// Get the base command definition from the shared ticket builder
|
|
8
|
+
const baseCmd = buildTicketCommands('sc_req_item', 'requests', 'req', requestDefaultColumns, {}, null, wrap);
|
|
9
|
+
|
|
10
|
+
// Override the builder to inject our enriched show handler.
|
|
11
|
+
// yargs allows re-registration — the last .command() call wins the slot.
|
|
12
|
+
const originalBuilder = baseCmd.builder;
|
|
13
|
+
baseCmd.builder = (yargs) => {
|
|
14
|
+
const y = originalBuilder(yargs);
|
|
15
|
+
return y.command({
|
|
16
|
+
command: 'show <number>',
|
|
17
|
+
aliases: ['get'],
|
|
18
|
+
describe: 'Show a specific request with attachments and catalog variables',
|
|
19
|
+
handler: wrap(enrichedShowHandler),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return baseCmd;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function enrichedShowHandler(argv, app) {
|
|
27
|
+
const number = argv.number;
|
|
28
|
+
const params = new URLSearchParams();
|
|
29
|
+
params.set('sysparm_query', `number=${number}`);
|
|
30
|
+
params.set('sysparm_display_value', 'all');
|
|
31
|
+
params.set('sysparm_limit', '1');
|
|
32
|
+
const records = await app.sdk.list('sc_req_item', params);
|
|
33
|
+
if (records.length === 0) {
|
|
34
|
+
throw new Error(`Request not found: ${number}`);
|
|
35
|
+
}
|
|
36
|
+
const record = records[0];
|
|
37
|
+
const sysID = getStringField(record, 'sys_id');
|
|
38
|
+
|
|
39
|
+
// Fetch attachments and catalog variables in parallel
|
|
40
|
+
const [attachments, variables] = await Promise.all([
|
|
41
|
+
app.sdk.fetchAttachments('sc_req_item', sysID).catch(() => []),
|
|
42
|
+
app.sdk.fetchCatalogVariables(sysID).catch(() => []),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
record._attachments = attachments;
|
|
46
|
+
record._variables = variables;
|
|
47
|
+
record._context = {
|
|
48
|
+
instance_url: app.getEffectiveInstance(),
|
|
49
|
+
table: 'sc_req_item',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
app.ok(record, {
|
|
53
|
+
summary: `Request ${number}`,
|
|
54
|
+
breadcrumbs: [
|
|
55
|
+
{ action: 'update', cmd: `jsn req update ${number} --data '{...}'`, description: 'Update this request' },
|
|
56
|
+
{ action: 'list', cmd: `jsn req list`, description: 'Back to all requests' },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
7
59
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const SKILL_REPO_PATH = path.join(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
'..', '..', 'skills', 'servicenow', 'SKILL.md'
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const SKILL_RAW_URL = 'https://raw.githubusercontent.com/jacebenson/jsn/nodejs/skills/servicenow/SKILL.md';
|
|
12
|
+
|
|
13
|
+
function readBundledSkill() {
|
|
14
|
+
try {
|
|
15
|
+
return fs.readFileSync(SKILL_REPO_PATH, 'utf-8');
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function skillCmd(wrap) {
|
|
22
|
+
return {
|
|
23
|
+
command: 'skill',
|
|
24
|
+
describe: 'Manage the jsn AI agent skill file (for Hermes, Claude Code, Cursor, etc.)',
|
|
25
|
+
builder: (y) => {
|
|
26
|
+
return y
|
|
27
|
+
.command({
|
|
28
|
+
command: 'show',
|
|
29
|
+
describe: 'Show the bundled skill file content',
|
|
30
|
+
handler: wrap(async (_argv, app) => {
|
|
31
|
+
const content = readBundledSkill();
|
|
32
|
+
if (!content) {
|
|
33
|
+
throw new Error('Skill file not found in package (expected at skills/servicenow/SKILL.md)');
|
|
34
|
+
}
|
|
35
|
+
app.ok({
|
|
36
|
+
content,
|
|
37
|
+
bundled: SKILL_REPO_PATH,
|
|
38
|
+
}, {
|
|
39
|
+
summary: 'jsn AI agent skill file (bundled)',
|
|
40
|
+
});
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
.command({
|
|
44
|
+
command: 'fetch',
|
|
45
|
+
describe: 'Download the latest skill file from GitHub to stdout',
|
|
46
|
+
handler: wrap(async (_argv, _app) => {
|
|
47
|
+
const res = await fetch(SKILL_RAW_URL);
|
|
48
|
+
if (!res.ok) throw new Error(`Failed to fetch skill: ${res.status} ${res.statusText}`);
|
|
49
|
+
const content = await res.text();
|
|
50
|
+
// Print raw content to stdout for pipe/save usage
|
|
51
|
+
process.stdout.write(content);
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
.command({
|
|
55
|
+
command: 'path',
|
|
56
|
+
describe: 'Show skill file locations and install targets',
|
|
57
|
+
handler: wrap(async (_argv, app) => {
|
|
58
|
+
const hermesDir = path.join(os.homedir(), '.hermes', 'skills', 'servicenow');
|
|
59
|
+
const hermesPath = path.join(hermesDir, 'SKILL.md');
|
|
60
|
+
app.ok({
|
|
61
|
+
bundled: SKILL_REPO_PATH,
|
|
62
|
+
hermes: hermesPath,
|
|
63
|
+
raw_url: SKILL_RAW_URL,
|
|
64
|
+
}, {
|
|
65
|
+
summary: 'Skill file locations',
|
|
66
|
+
breadcrumbs: [
|
|
67
|
+
{ action: 'view', cmd: 'jsn skill show', description: 'Show bundled skill content' },
|
|
68
|
+
{ action: 'fetch', cmd: 'jsn skill fetch', description: 'Download latest from GitHub' },
|
|
69
|
+
{ action: 'install', cmd: 'jsn skill install', description: 'Download + save to Hermes' },
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
.command({
|
|
75
|
+
command: 'install [dir]',
|
|
76
|
+
describe: 'Download and save the latest skill file',
|
|
77
|
+
builder: (y) => y
|
|
78
|
+
.positional('dir', {
|
|
79
|
+
type: 'string',
|
|
80
|
+
describe: 'Target directory (default: ~/.hermes/skills/servicenow/)',
|
|
81
|
+
}),
|
|
82
|
+
handler: wrap(async (argv, app) => {
|
|
83
|
+
const targetDir = argv.dir || path.join(os.homedir(), '.hermes', 'skills', 'servicenow');
|
|
84
|
+
const targetPath = path.join(targetDir, 'SKILL.md');
|
|
85
|
+
|
|
86
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
const res = await fetch(SKILL_RAW_URL);
|
|
89
|
+
if (!res.ok) throw new Error(`Failed to fetch skill: ${res.status} ${res.statusText}`);
|
|
90
|
+
const content = await res.text();
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
93
|
+
|
|
94
|
+
app.ok({
|
|
95
|
+
installed: targetPath,
|
|
96
|
+
from: SKILL_RAW_URL,
|
|
97
|
+
}, {
|
|
98
|
+
summary: `Skill installed to ${targetPath}`,
|
|
99
|
+
breadcrumbs: [
|
|
100
|
+
{ action: 'verify', cmd: `head -30 ${targetPath}`, description: 'Verify the installed skill' },
|
|
101
|
+
{ action: 'reinstall', cmd: 'jsn skill install', description: 'Re-download and reinstall' },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
handler: () => {}, // Handled by subcommands
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/commands/tickets.js
CHANGED
|
@@ -4,7 +4,7 @@ export function ticketsCmd(wrap) {
|
|
|
4
4
|
return {
|
|
5
5
|
command: 'tickets [subcommand]',
|
|
6
6
|
aliases: ['ticket'],
|
|
7
|
-
describe: 'Query generic tickets',
|
|
7
|
+
describe: 'Query generic tickets (e.g. "tickets list --query active=true")',
|
|
8
8
|
builder: (yargs) => {
|
|
9
9
|
return yargs
|
|
10
10
|
.command({
|
package/src/commands/users.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
export function usersCmd(wrap) {
|
|
4
4
|
return {
|
|
5
5
|
command: 'users [subcommand]',
|
|
6
6
|
aliases: ['user'],
|
|
7
|
-
describe: '
|
|
7
|
+
describe: 'Manage ServiceNow users',
|
|
8
8
|
builder: (yargs) => {
|
|
9
9
|
return yargs
|
|
10
10
|
.command({
|
|
@@ -17,6 +17,17 @@ export function usersCmd(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(',') : ['user_name', 'name', 'email', 'active'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
const picked = await interactiveList({
|
|
23
|
+
app, table: 'sys_user', singular: 'user', columns, limit: argv.limit, query, labelField: 'user_name',
|
|
24
|
+
formatLabel: r => `${getStringField(r, 'user_name')} (${getStringField(r, 'name') || '-'})`,
|
|
25
|
+
});
|
|
26
|
+
if (picked) {
|
|
27
|
+
picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_user' };
|
|
28
|
+
return app.ok(picked, { summary: `User: ${getStringField(picked, 'user_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');
|
|
@@ -50,7 +61,83 @@ export function usersCmd(wrap) {
|
|
|
50
61
|
app.ok(records[0], { summary: `User ${id}` });
|
|
51
62
|
}),
|
|
52
63
|
})
|
|
53
|
-
|
|
64
|
+
.command({
|
|
65
|
+
command: 'create',
|
|
66
|
+
describe: 'Create a new user',
|
|
67
|
+
builder: (y) => y
|
|
68
|
+
.option('username', { alias: 'u', type: 'string', describe: 'Username (e.g. john.doe)' })
|
|
69
|
+
.option('name', { alias: 'n', type: 'string', describe: 'Full name (e.g. John Doe)' })
|
|
70
|
+
.option('email', { alias: 'e', type: 'string', describe: 'Email address' })
|
|
71
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' }),
|
|
72
|
+
handler: wrap(async (argv, app) => {
|
|
73
|
+
const recordData = {};
|
|
74
|
+
if (argv.data) {
|
|
75
|
+
Object.assign(recordData, JSON.parse(argv.data));
|
|
76
|
+
}
|
|
77
|
+
if (argv.username) recordData.user_name = argv.username;
|
|
78
|
+
if (argv.name) recordData.name = argv.name;
|
|
79
|
+
if (argv.email) recordData.email = argv.email;
|
|
80
|
+
if (!recordData.user_name) {
|
|
81
|
+
throw new Error('user_name is required (use --username or --data)');
|
|
82
|
+
}
|
|
83
|
+
const record = await app.sdk.create('sys_user', recordData);
|
|
84
|
+
app.ok(record, {
|
|
85
|
+
summary: `Created user ${getStringField(record, 'user_name')}`,
|
|
86
|
+
breadcrumbs: [
|
|
87
|
+
{ action: 'view', cmd: `jsn users show ${getStringField(record, 'user_name')}`, description: `View the new user` },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
.command({
|
|
93
|
+
command: 'update <identifier>',
|
|
94
|
+
describe: 'Update a user by user_name or sys_id',
|
|
95
|
+
builder: (y) => y
|
|
96
|
+
.option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
|
|
97
|
+
handler: wrap(async (argv, app) => {
|
|
98
|
+
const id = argv.identifier;
|
|
99
|
+
const recordData = JSON.parse(argv.data);
|
|
100
|
+
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `user_name=${id}`);
|
|
103
|
+
params.set('sysparm_limit', '1');
|
|
104
|
+
params.set('sysparm_display_value', 'all');
|
|
105
|
+
params.set('sysparm_fields', 'sys_id');
|
|
106
|
+
const records = await app.sdk.list('sys_user', params);
|
|
107
|
+
if (records.length === 0) {
|
|
108
|
+
throw new Error(`User not found: ${id}`);
|
|
109
|
+
}
|
|
110
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
111
|
+
const updated = await app.sdk.update('sys_user', sysID, recordData);
|
|
112
|
+
app.ok(updated, {
|
|
113
|
+
summary: `Updated user ${id}`,
|
|
114
|
+
breadcrumbs: [
|
|
115
|
+
{ action: 'view', cmd: `jsn users show ${id}`, description: `View the updated user` },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
.command({
|
|
121
|
+
command: 'delete <identifier>',
|
|
122
|
+
describe: 'Delete a user by user_name or sys_id',
|
|
123
|
+
handler: wrap(async (argv, app) => {
|
|
124
|
+
const id = argv.identifier;
|
|
125
|
+
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
126
|
+
const params = new URLSearchParams();
|
|
127
|
+
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `user_name=${id}`);
|
|
128
|
+
params.set('sysparm_limit', '1');
|
|
129
|
+
params.set('sysparm_fields', 'sys_id');
|
|
130
|
+
const records = await app.sdk.list('sys_user', params);
|
|
131
|
+
if (records.length === 0) {
|
|
132
|
+
throw new Error(`User not found: ${id}`);
|
|
133
|
+
}
|
|
134
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
135
|
+
await app.sdk.delete('sys_user', sysID);
|
|
136
|
+
app.ok({ identifier: id, message: 'User deleted' }, {
|
|
137
|
+
summary: `Deleted user ${id}`,
|
|
138
|
+
});
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
54
141
|
},
|
|
55
142
|
handler: () => {},
|
|
56
143
|
};
|