@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,260 @@
|
|
|
1
|
+
// Generic command builder for CRUD operations on a ServiceNow table
|
|
2
|
+
// Used by incidents, changes, requests, tasks, and most dev subcommands
|
|
3
|
+
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix } from '../helpers.js';
|
|
5
|
+
import search from '@inquirer/search';
|
|
6
|
+
import { isTTY, FormatAuto } from '../output.js';
|
|
7
|
+
|
|
8
|
+
export function buildTicketCommands(table, displayName, alias, defaultColumns, stateMap, iconFn, wrap) {
|
|
9
|
+
return {
|
|
10
|
+
command: `${displayName} [subcommand]`,
|
|
11
|
+
aliases: [table, alias],
|
|
12
|
+
describe: `Manage ${displayName} (e.g. "${displayName} list --query priority=1")`,
|
|
13
|
+
builder: (yargs) => {
|
|
14
|
+
return yargs
|
|
15
|
+
.command({
|
|
16
|
+
command: 'list',
|
|
17
|
+
aliases: ['ls'],
|
|
18
|
+
describe: `List ${table}`,
|
|
19
|
+
builder: (y) => y
|
|
20
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
21
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
22
|
+
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
|
|
23
|
+
.option('offset', { alias: 'o', type: 'number', default: 0, describe: 'Offset for pagination' }),
|
|
24
|
+
handler: wrap(async (argv, app) => {
|
|
25
|
+
const query = argv.query || '';
|
|
26
|
+
const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
|
|
27
|
+
const limit = argv.limit;
|
|
28
|
+
const offset = argv.offset;
|
|
29
|
+
|
|
30
|
+
// Interactive picker in TTY with auto format and no explicit query/offset
|
|
31
|
+
const isInteractive = isTTY(process.stdout) && isTTY(process.stdin);
|
|
32
|
+
if (isInteractive && app.output.getFormat() === FormatAuto && !query && offset === 0) {
|
|
33
|
+
const pickerColumns = ['sys_id', 'number', 'short_description', 'state', 'assigned_to'];
|
|
34
|
+
const pickerParams = new URLSearchParams();
|
|
35
|
+
pickerParams.set('sysparm_limit', String(limit));
|
|
36
|
+
pickerParams.set('sysparm_offset', '0');
|
|
37
|
+
pickerParams.set('sysparm_display_value', 'all');
|
|
38
|
+
pickerParams.set('sysparm_fields', pickerColumns.join(','));
|
|
39
|
+
pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
|
|
40
|
+
|
|
41
|
+
const pickerRecords = await app.sdk.list(table, pickerParams);
|
|
42
|
+
if (pickerRecords.length === 0) {
|
|
43
|
+
app.ok({
|
|
44
|
+
table,
|
|
45
|
+
count: 0,
|
|
46
|
+
columns: pickerColumns,
|
|
47
|
+
records: [],
|
|
48
|
+
pagination: { limit, offset: 0 },
|
|
49
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
50
|
+
}, {
|
|
51
|
+
summary: `0 ${table}(s)`,
|
|
52
|
+
breadcrumbs: [
|
|
53
|
+
{ action: 'create', cmd: `jsn ${alias} create --description "..."`, description: `Create a new ${displayName}` },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const choices = pickerRecords.map((record) => {
|
|
60
|
+
const number = getStringField(record, 'number');
|
|
61
|
+
const desc = getStringField(record, 'short_description');
|
|
62
|
+
const state = getStringField(record, 'state');
|
|
63
|
+
const assigned = getStringField(record, 'assigned_to');
|
|
64
|
+
let label = `${number} ${desc} | ${state}`;
|
|
65
|
+
if (assigned) {
|
|
66
|
+
label += ` → ${assigned}`;
|
|
67
|
+
}
|
|
68
|
+
return { name: label, value: number };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let selectedNumber;
|
|
72
|
+
try {
|
|
73
|
+
selectedNumber = await search({
|
|
74
|
+
message: `Select a ${displayName.slice(0, -1)}:`,
|
|
75
|
+
source: async (input) => {
|
|
76
|
+
if (!input) return choices;
|
|
77
|
+
const term = input.toLowerCase();
|
|
78
|
+
return choices.filter(c => c.name.toLowerCase().includes(term));
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (selectedNumber) {
|
|
89
|
+
const showParams = new URLSearchParams();
|
|
90
|
+
showParams.set('sysparm_query', `number=${selectedNumber}`);
|
|
91
|
+
showParams.set('sysparm_display_value', 'all');
|
|
92
|
+
showParams.set('sysparm_limit', '1');
|
|
93
|
+
const showRecords = await app.sdk.list(table, showParams);
|
|
94
|
+
if (showRecords.length === 0) {
|
|
95
|
+
throw new Error(`${displayName} not found: ${selectedNumber}`);
|
|
96
|
+
}
|
|
97
|
+
const record = showRecords[0];
|
|
98
|
+
record._context = {
|
|
99
|
+
instance_url: app.getEffectiveInstance(),
|
|
100
|
+
table,
|
|
101
|
+
};
|
|
102
|
+
app.ok(record, {
|
|
103
|
+
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${selectedNumber}`,
|
|
104
|
+
breadcrumbs: [
|
|
105
|
+
{ action: 'update', cmd: `jsn ${alias} update ${selectedNumber} --data '{...}'`, description: `Update this ${displayName}` },
|
|
106
|
+
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const params = new URLSearchParams();
|
|
114
|
+
params.set('sysparm_limit', String(limit));
|
|
115
|
+
params.set('sysparm_offset', String(offset));
|
|
116
|
+
params.set('sysparm_display_value', 'all');
|
|
117
|
+
const fetchColumns = ['sys_id', ...columns];
|
|
118
|
+
params.set('sysparm_fields', fetchColumns.join(','));
|
|
119
|
+
const q = query ? query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
120
|
+
params.set('sysparm_query', q);
|
|
121
|
+
|
|
122
|
+
const records = await app.sdk.list(table, params);
|
|
123
|
+
const displayRecords = records.map(r => formatRecordForDisplay(r, columns));
|
|
124
|
+
|
|
125
|
+
const breadcrumbs = [
|
|
126
|
+
{ action: 'create', cmd: `jsn ${alias} create --description "..."`, description: `Create a new ${displayName}` },
|
|
127
|
+
{ action: 'filter', cmd: `jsn ${alias} list --query "priority=1"`, description: 'Filter: critical only' },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (records.length === limit) {
|
|
131
|
+
breadcrumbs.push({
|
|
132
|
+
action: 'next',
|
|
133
|
+
cmd: `jsn ${alias} list --limit ${limit} --offset ${offset + limit}${buildQuerySuffix(query)}`,
|
|
134
|
+
description: `Next page (offset ${offset + limit})`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (offset > 0) {
|
|
138
|
+
breadcrumbs.push({
|
|
139
|
+
action: 'prev',
|
|
140
|
+
cmd: `jsn ${alias} list --limit ${limit} --offset ${Math.max(0, offset - limit)}${buildQuerySuffix(query)}`,
|
|
141
|
+
description: 'Previous page',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
app.ok({
|
|
146
|
+
table,
|
|
147
|
+
count: records.length,
|
|
148
|
+
columns,
|
|
149
|
+
records: displayRecords,
|
|
150
|
+
pagination: { limit, offset },
|
|
151
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
152
|
+
}, {
|
|
153
|
+
summary: `${records.length} ${table}(s)`,
|
|
154
|
+
breadcrumbs,
|
|
155
|
+
});
|
|
156
|
+
}),
|
|
157
|
+
})
|
|
158
|
+
.command({
|
|
159
|
+
command: 'show <number>',
|
|
160
|
+
aliases: ['get'],
|
|
161
|
+
describe: `Show a specific ${displayName}`,
|
|
162
|
+
handler: wrap(async (argv, app) => {
|
|
163
|
+
const number = argv.number;
|
|
164
|
+
const params = new URLSearchParams();
|
|
165
|
+
params.set('sysparm_query', `number=${number}`);
|
|
166
|
+
params.set('sysparm_display_value', 'all');
|
|
167
|
+
params.set('sysparm_limit', '1');
|
|
168
|
+
const records = await app.sdk.list(table, params);
|
|
169
|
+
if (records.length === 0) {
|
|
170
|
+
throw new Error(`${displayName} not found: ${number}`);
|
|
171
|
+
}
|
|
172
|
+
const record = records[0];
|
|
173
|
+
record._context = {
|
|
174
|
+
instance_url: app.getEffectiveInstance(),
|
|
175
|
+
table,
|
|
176
|
+
};
|
|
177
|
+
app.ok(record, {
|
|
178
|
+
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${number}`,
|
|
179
|
+
breadcrumbs: [
|
|
180
|
+
{ action: 'update', cmd: `jsn ${alias} update ${number} --data '{...}'`, description: `Update this ${displayName}` },
|
|
181
|
+
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
}),
|
|
185
|
+
})
|
|
186
|
+
.command({
|
|
187
|
+
command: 'create',
|
|
188
|
+
describe: `Create a new ${displayName}`,
|
|
189
|
+
builder: (y) => y
|
|
190
|
+
.option('description', { alias: 'd', type: 'string', describe: 'Short description' })
|
|
191
|
+
.option('priority', { type: 'string', describe: 'Priority (1-5)' })
|
|
192
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' }),
|
|
193
|
+
handler: wrap(async (argv, app) => {
|
|
194
|
+
const recordData = {};
|
|
195
|
+
if (argv.data) {
|
|
196
|
+
Object.assign(recordData, JSON.parse(argv.data));
|
|
197
|
+
}
|
|
198
|
+
if (argv.description) recordData.short_description = argv.description;
|
|
199
|
+
if (argv.priority) recordData.priority = argv.priority;
|
|
200
|
+
if (!recordData.short_description) {
|
|
201
|
+
throw new Error('short_description is required (use --description or --data)');
|
|
202
|
+
}
|
|
203
|
+
const record = await app.sdk.create(table, recordData);
|
|
204
|
+
app.ok(record, {
|
|
205
|
+
summary: `Created ${displayName} ${getStringField(record, 'number')}`,
|
|
206
|
+
breadcrumbs: [
|
|
207
|
+
{ action: 'view', cmd: `jsn ${alias} show ${getStringField(record, 'number')}`, description: `View the new ${displayName}` },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}),
|
|
211
|
+
})
|
|
212
|
+
.command({
|
|
213
|
+
command: 'update <number>',
|
|
214
|
+
describe: `Update a ${displayName}`,
|
|
215
|
+
builder: (y) => y
|
|
216
|
+
.option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
|
|
217
|
+
handler: wrap(async (argv, app) => {
|
|
218
|
+
const number = argv.number;
|
|
219
|
+
const recordData = JSON.parse(argv.data);
|
|
220
|
+
const findParams = new URLSearchParams();
|
|
221
|
+
findParams.set('sysparm_query', `number=${number}`);
|
|
222
|
+
findParams.set('sysparm_limit', '1');
|
|
223
|
+
const records = await app.sdk.list(table, findParams);
|
|
224
|
+
if (records.length === 0) {
|
|
225
|
+
throw new Error(`${displayName} not found: ${number}`);
|
|
226
|
+
}
|
|
227
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
228
|
+
const updated = await app.sdk.update(table, sysID, recordData);
|
|
229
|
+
app.ok(updated, {
|
|
230
|
+
summary: `Updated ${displayName} ${number}`,
|
|
231
|
+
breadcrumbs: [
|
|
232
|
+
{ action: 'view', cmd: `jsn ${alias} show ${number}`, description: `View the updated ${displayName}` },
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
}),
|
|
236
|
+
})
|
|
237
|
+
.command({
|
|
238
|
+
command: 'delete <number>',
|
|
239
|
+
describe: `Delete a ${displayName}`,
|
|
240
|
+
handler: wrap(async (argv, app) => {
|
|
241
|
+
const number = argv.number;
|
|
242
|
+
const findParams = new URLSearchParams();
|
|
243
|
+
findParams.set('sysparm_query', `number=${number}`);
|
|
244
|
+
findParams.set('sysparm_limit', '1');
|
|
245
|
+
const records = await app.sdk.list(table, findParams);
|
|
246
|
+
if (records.length === 0) {
|
|
247
|
+
throw new Error(`${displayName} not found: ${number}`);
|
|
248
|
+
}
|
|
249
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
250
|
+
await app.sdk.delete(table, sysID);
|
|
251
|
+
app.ok({ number, message: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} deleted` }, {
|
|
252
|
+
summary: `Deleted ${displayName} ${number}`,
|
|
253
|
+
});
|
|
254
|
+
}),
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
},
|
|
258
|
+
handler: () => {}, // Handled by subcommands
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { getEffectiveInstance, normalizeInstanceURL } from '../config.js';
|
|
2
|
+
import { errUsage } from '../errors.js';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
export function authCmd(wrap) {
|
|
6
|
+
return {
|
|
7
|
+
command: 'auth <subcommand>',
|
|
8
|
+
describe: 'Manage authentication (OAuth or basic auth via env vars)',
|
|
9
|
+
builder: (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.command({
|
|
12
|
+
command: 'login [instance]',
|
|
13
|
+
describe: 'Authenticate with a ServiceNow instance via OAuth',
|
|
14
|
+
builder: (sub) => sub
|
|
15
|
+
.option('code', {
|
|
16
|
+
describe: 'Authorization code from browser (bypasses interactive prompt)',
|
|
17
|
+
type: 'string',
|
|
18
|
+
})
|
|
19
|
+
.option('password', {
|
|
20
|
+
describe: 'Authenticate with basic auth via env vars (SN_USERNAME/SN_PASSWORD)',
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
})
|
|
23
|
+
.option('print-url', {
|
|
24
|
+
describe: 'Print the OAuth URL and exit (saves PKCE state for --code)',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
})
|
|
27
|
+
.option('wait-file', {
|
|
28
|
+
describe: 'File path to watch for authorization code (used with --print-url)',
|
|
29
|
+
type: 'string',
|
|
30
|
+
}),
|
|
31
|
+
handler: wrap(async (argv, app) => {
|
|
32
|
+
let instanceURL;
|
|
33
|
+
if (argv.instance) {
|
|
34
|
+
// Check if it's a profile name first
|
|
35
|
+
const profiles = app.config.profiles || {};
|
|
36
|
+
if (profiles[argv.instance] && profiles[argv.instance].instance_url) {
|
|
37
|
+
instanceURL = normalizeInstanceURL(profiles[argv.instance].instance_url);
|
|
38
|
+
} else {
|
|
39
|
+
instanceURL = normalizeInstanceURL(argv.instance);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
43
|
+
if (!instanceURL) {
|
|
44
|
+
// Try interactive profile picker
|
|
45
|
+
const profileNames = Object.keys(app.config.profiles || {});
|
|
46
|
+
if (profileNames.length > 0 && app.isInteractive()) {
|
|
47
|
+
const { search } = await import('@inquirer/prompts');
|
|
48
|
+
const choices = profileNames.map(name => ({
|
|
49
|
+
name: `${name} — ${app.config.profiles[name].instance_url}`,
|
|
50
|
+
value: name,
|
|
51
|
+
}));
|
|
52
|
+
const selected = await search({
|
|
53
|
+
message: 'Select a profile:',
|
|
54
|
+
source: (input) => {
|
|
55
|
+
const filter = (input || '').toLowerCase();
|
|
56
|
+
return choices.filter(c => c.name.toLowerCase().includes(filter));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
instanceURL = normalizeInstanceURL(app.config.profiles[selected].instance_url);
|
|
60
|
+
} else {
|
|
61
|
+
throw errUsage(`Instance URL required.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
jsn auth login https://dev12345.service-now.com
|
|
65
|
+
jsn auth login dev373698
|
|
66
|
+
jsn auth login --password https://dev328604.service-now.com
|
|
67
|
+
|
|
68
|
+
Find your instance URL in your browser's address bar when logged into ServiceNow.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --password: authenticate with basic auth from env vars
|
|
74
|
+
if (argv.password) {
|
|
75
|
+
await app.auth.loginWithPassword(instanceURL);
|
|
76
|
+
}
|
|
77
|
+
// --print-url with --wait-file: print URL and wait for code file
|
|
78
|
+
else if (argv.printUrl && argv.waitFile) {
|
|
79
|
+
await app.auth.buildAuthURL(instanceURL, argv.waitFile);
|
|
80
|
+
}
|
|
81
|
+
// --print-url: just print the URL and exit
|
|
82
|
+
else if (argv.printUrl) {
|
|
83
|
+
const authURL = app.auth.buildAuthURL(instanceURL);
|
|
84
|
+
console.log(authURL);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// --code: exchange the provided authorization code directly
|
|
88
|
+
else if (argv.code) {
|
|
89
|
+
await app.auth.loginWithCode(instanceURL, argv.code);
|
|
90
|
+
} else {
|
|
91
|
+
// Interactive: full OAuth flow with browser + prompt
|
|
92
|
+
await app.auth.login(instanceURL);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Verify auth by fetching current user
|
|
96
|
+
let username = '';
|
|
97
|
+
try {
|
|
98
|
+
if (!app.sdk) {
|
|
99
|
+
const { SDKClient } = await import('../sdk.js');
|
|
100
|
+
app.sdk = new SDKClient(instanceURL, app.auth);
|
|
101
|
+
}
|
|
102
|
+
const user = await app.sdk.getCurrentUser();
|
|
103
|
+
username = user?.user_name || user?.name || '';
|
|
104
|
+
} catch {
|
|
105
|
+
// Non-fatal — token is saved, just couldn't verify
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Save to profiles
|
|
109
|
+
const profileName = instanceURL.replace(/https?:\/\//, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
110
|
+
if (!app.config.profiles) {
|
|
111
|
+
app.config.profiles = {};
|
|
112
|
+
}
|
|
113
|
+
app.config.profiles[profileName] = {
|
|
114
|
+
instance_url: instanceURL,
|
|
115
|
+
auth_method: 'oauth',
|
|
116
|
+
username: username || undefined,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Set as default if this is the first one
|
|
120
|
+
const setDefault = !app.config.instanceURL && !app.config.defaultProfile;
|
|
121
|
+
if (setDefault) {
|
|
122
|
+
app.config.instanceURL = instanceURL;
|
|
123
|
+
app.config.defaultProfile = profileName;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { saveConfig } = await import('../config.js');
|
|
127
|
+
saveConfig(app.config);
|
|
128
|
+
|
|
129
|
+
const result = {
|
|
130
|
+
authenticated: true,
|
|
131
|
+
instance: instanceURL,
|
|
132
|
+
username: username || undefined,
|
|
133
|
+
default: setDefault || undefined,
|
|
134
|
+
};
|
|
135
|
+
const summary = username
|
|
136
|
+
? `✓ Authenticated to ${instanceURL} as ${username}`
|
|
137
|
+
: `✓ Authenticated to ${instanceURL}`;
|
|
138
|
+
app.ok(result, { summary });
|
|
139
|
+
}),
|
|
140
|
+
})
|
|
141
|
+
.command({
|
|
142
|
+
command: 'logout [instance]',
|
|
143
|
+
describe: 'Remove stored OAuth credentials',
|
|
144
|
+
handler: wrap(async (argv, app) => {
|
|
145
|
+
let instanceURL;
|
|
146
|
+
if (argv.instance) {
|
|
147
|
+
instanceURL = normalizeInstanceURL(argv.instance);
|
|
148
|
+
} else {
|
|
149
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
150
|
+
if (!instanceURL) {
|
|
151
|
+
throw errUsage(`No instance specified.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
jsn auth logout
|
|
155
|
+
jsn auth logout https://dev12345.service-now.com`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
app.auth.logout(instanceURL);
|
|
159
|
+
app.ok({ logged_out: true, instance: instanceURL }, { summary: `✓ Logged out from ${instanceURL}` });
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
.command({
|
|
163
|
+
command: 'status',
|
|
164
|
+
describe: 'Show detailed authentication status',
|
|
165
|
+
handler: wrap(async (_argv, app) => {
|
|
166
|
+
const defaultInstance = getEffectiveInstance(app.config);
|
|
167
|
+
|
|
168
|
+
// Check environment auth
|
|
169
|
+
const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
|
|
170
|
+
|
|
171
|
+
const profiles = [];
|
|
172
|
+
for (const [name, profile] of Object.entries(app.config.profiles || {})) {
|
|
173
|
+
const isAuth = app.auth.isAuthenticatedFor(profile.instance_url);
|
|
174
|
+
profiles.push({
|
|
175
|
+
name,
|
|
176
|
+
instance: profile.instance_url,
|
|
177
|
+
authenticated: isAuth,
|
|
178
|
+
default: profile.instance_url === defaultInstance,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
app.ok({
|
|
183
|
+
default_instance: defaultInstance,
|
|
184
|
+
authenticated: app.auth.isAuthenticated(),
|
|
185
|
+
environment_auth: envToken ? true : undefined,
|
|
186
|
+
profiles,
|
|
187
|
+
}, { summary: `${profiles.length} profile(s)` });
|
|
188
|
+
}),
|
|
189
|
+
})
|
|
190
|
+
.command({
|
|
191
|
+
command: 'refresh [instance]',
|
|
192
|
+
describe: 'Refresh OAuth token for an instance',
|
|
193
|
+
handler: wrap(async (argv, app) => {
|
|
194
|
+
let instanceURL;
|
|
195
|
+
if (argv.instance) {
|
|
196
|
+
// Check if it's a profile name or URL
|
|
197
|
+
const profiles = app.config.profiles || {};
|
|
198
|
+
if (profiles[argv.instance] && profiles[argv.instance].instance_url) {
|
|
199
|
+
instanceURL = normalizeInstanceURL(profiles[argv.instance].instance_url);
|
|
200
|
+
} else {
|
|
201
|
+
instanceURL = normalizeInstanceURL(argv.instance);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
205
|
+
if (!instanceURL) {
|
|
206
|
+
throw errUsage(`No instance specified and no default configured.
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
jsn auth refresh
|
|
210
|
+
jsn auth refresh https://dev12345.service-now.com
|
|
211
|
+
jsn auth refresh dev12345`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const creds = await app.auth.getCredentialsFor(instanceURL);
|
|
216
|
+
const refreshed = await app.auth.refreshToken(instanceURL, creds);
|
|
217
|
+
app.ok({
|
|
218
|
+
refreshed: true,
|
|
219
|
+
instance: instanceURL,
|
|
220
|
+
expires_at: refreshed.expires_at,
|
|
221
|
+
}, { summary: `✓ Token refreshed for ${instanceURL}` });
|
|
222
|
+
}),
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
handler: () => {},
|
|
226
|
+
};
|
|
227
|
+
}
|