@jacebenson/jsn 4.3.4 → 4.3.5-dev.7
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/package.json +2 -1
- package/src/commands/setup.js +40 -12
- package/src/sdk.js +25 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.5-dev.7",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"automation"
|
|
24
24
|
],
|
|
25
25
|
"author": "Jace Benson",
|
|
26
|
+
"repository": "https://github.com/jacebenson/jsn",
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=22.5.0"
|
package/src/commands/setup.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } from '@inquirer/core';
|
|
1
2
|
import { loginWizard, modifyProfile, removeProfile, pickProfile } from './auth.js';
|
|
2
3
|
import { setActiveProfile } from '../config.js';
|
|
3
4
|
import { errUsage } from '../errors.js';
|
|
@@ -7,18 +8,44 @@ import { declareCapabilities } from '../capabilities.js';
|
|
|
7
8
|
// (Not in the daily-check skip-list: legacy behavior checks for updates here.)
|
|
8
9
|
declareCapabilities('setup', { noInstance: true });
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
const HUB_CHOICES = [
|
|
12
|
+
{ name: 'Add a new instance', value: 'add' },
|
|
13
|
+
{ name: 'Switch to a different instance', value: 'switch' },
|
|
14
|
+
{ name: 'Remove an instance', value: 'remove' },
|
|
15
|
+
{ name: 'Modify an instance', value: 'modify' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** The setup hub prompt; q and Escape resolve with no selected action. */
|
|
19
|
+
export const authHubPrompt = createPrompt((config, done) => {
|
|
20
|
+
const [active, setActive] = useState(0);
|
|
21
|
+
const choices = config.choices;
|
|
22
|
+
|
|
23
|
+
useKeypress((key) => {
|
|
24
|
+
if (key.name === 'q' || key.name === 'escape') {
|
|
25
|
+
done(undefined);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (isEnterKey(key)) {
|
|
29
|
+
done(choices[active].value);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isUpKey(key) && active > 0) setActive(active - 1);
|
|
33
|
+
if (isDownKey(key) && active < choices.length - 1) setActive(active + 1);
|
|
21
34
|
});
|
|
35
|
+
|
|
36
|
+
const page = usePagination({
|
|
37
|
+
items: choices,
|
|
38
|
+
active,
|
|
39
|
+
pageSize: choices.length,
|
|
40
|
+
loop: false,
|
|
41
|
+
renderItem: ({ item, isActive }) => `${isActive ? '❯' : ' '} ${item.name}`,
|
|
42
|
+
});
|
|
43
|
+
return `${config.message}\n${page}\n\n↑↓ navigate • ⏎ select • q/Esc exit`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Interactive hub: what do you want to do? Returns an action or undefined on exit. */
|
|
47
|
+
export async function authHubMenu({ promptFn = authHubPrompt, input, output } = {}) {
|
|
48
|
+
return promptFn({ message: 'What would you like to do?', choices: HUB_CHOICES }, { input, output });
|
|
22
49
|
}
|
|
23
50
|
|
|
24
51
|
/**
|
|
@@ -36,7 +63,7 @@ export async function dispatchSetupAction(app, argv, action) {
|
|
|
36
63
|
} else if (action === 'remove') {
|
|
37
64
|
const name = await pickProfile(app, 'Remove which instance?');
|
|
38
65
|
await removeProfile(app, name);
|
|
39
|
-
} else
|
|
66
|
+
} else if (action === 'modify') {
|
|
40
67
|
await modifyProfile(app, argv);
|
|
41
68
|
}
|
|
42
69
|
}
|
|
@@ -60,6 +87,7 @@ export function setupCmd(wrap) {
|
|
|
60
87
|
return;
|
|
61
88
|
}
|
|
62
89
|
const action = await authHubMenu();
|
|
90
|
+
if (action === undefined) return;
|
|
63
91
|
await dispatchSetupAction(app, argv, action);
|
|
64
92
|
}),
|
|
65
93
|
};
|
package/src/sdk.js
CHANGED
|
@@ -266,6 +266,8 @@ export class SDKClient {
|
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
268
|
* Upload a file as a new attachment on a record.
|
|
269
|
+
* Uses ServiceNow's binary attachment endpoint so file bytes are not expanded
|
|
270
|
+
* into base64 or embedded in a sys.scripts.do form body.
|
|
269
271
|
* @param {string} table - parent table name (e.g. 'incident')
|
|
270
272
|
* @param {string} sysID - parent record sys_id
|
|
271
273
|
* @param {Buffer|string} content - file bytes or path
|
|
@@ -274,29 +276,30 @@ export class SDKClient {
|
|
|
274
276
|
*/
|
|
275
277
|
async addAttachment(table, sysID, content, fileName) {
|
|
276
278
|
const contentBuffer = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
279
|
+
const params = new URLSearchParams({
|
|
280
|
+
table_name: String(table),
|
|
281
|
+
table_sys_id: String(sysID),
|
|
282
|
+
file_name: String(fileName),
|
|
283
|
+
});
|
|
284
|
+
const endpoint = `${this.baseURL}/api/now/attachment/file?${params.toString()}`;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const response = await this.request(endpoint, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
290
|
+
body: contentBuffer,
|
|
291
|
+
});
|
|
292
|
+
const result = response?.result;
|
|
293
|
+
if (!result?.sys_id) {
|
|
294
|
+
throw new Error('Attachment upload returned no attachment sys_id');
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (err?.status === 413) {
|
|
299
|
+
throw errAPI(413, 'Attachment is too large for the ServiceNow attachment endpoint. Reduce the file size or increase the instance attachment limit.');
|
|
300
|
+
}
|
|
301
|
+
throw err;
|
|
298
302
|
}
|
|
299
|
-
return result;
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
async getCurrentUser({ touchLastSeen = true } = {}) {
|
|
@@ -921,27 +924,6 @@ export class SDKClient {
|
|
|
921
924
|
}
|
|
922
925
|
}
|
|
923
926
|
|
|
924
|
-
function scriptString(value) {
|
|
925
|
-
return JSON.stringify(String(value)).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function parseAttachmentResult(output) {
|
|
929
|
-
if (!output) return null;
|
|
930
|
-
const lines = String(output).split(/\r?\n/).reverse();
|
|
931
|
-
for (const line of lines) {
|
|
932
|
-
const candidate = line.replace(/^\*\*\* Script:\s*/, '').trim();
|
|
933
|
-
const markerCandidate = candidate.replace(/^JSN_ATTACHMENT_RESULT:\s*/, '');
|
|
934
|
-
if (!markerCandidate.startsWith('{')) continue;
|
|
935
|
-
try {
|
|
936
|
-
const result = JSON.parse(markerCandidate);
|
|
937
|
-
if (result && typeof result === 'object') return result;
|
|
938
|
-
} catch {
|
|
939
|
-
// Ignore unrelated script output and keep looking for the result marker.
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
927
|
function detectScriptError(output) {
|
|
946
928
|
const lines = String(output || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
947
929
|
const errorLine = lines.find(line => /(?:ReferenceError|TypeError|RhinoEcmaError|Script execution error|undefined is not a function|is not defined)/i.test(line));
|