@jacebenson/jsn 4.1.0 → 4.2.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 +18 -2
- package/package.json +4 -1
- package/src/app.js +48 -64
- package/src/auth.js +46 -11
- package/src/capabilities.js +128 -0
- package/src/cli.js +129 -79
- package/src/commands/_ticket.js +36 -92
- package/src/commands/approvals.js +350 -0
- package/src/commands/atf.js +635 -0
- package/src/commands/auth.js +93 -13
- package/src/commands/catalog/enrich.js +139 -0
- package/src/commands/catalog.js +15 -133
- package/src/commands/cmdb.js +613 -0
- package/src/commands/codesearch.js +103 -0
- package/src/commands/dev/_generic.js +62 -110
- package/src/commands/dev/_simple.js +16 -12
- package/src/commands/dev/b4rules.js +1 -1
- package/src/commands/dev/domains.js +4 -0
- package/src/commands/dev/eval.js +40 -4
- package/src/commands/dev/flows.js +3 -0
- package/src/commands/dev/logs.js +97 -0
- package/src/commands/dev/rest.js +6 -0
- package/src/commands/dev/restmessage.js +1 -0
- package/src/commands/dev/scopes.js +49 -25
- package/src/commands/dev/scrapi.js +5 -0
- package/src/commands/dev/soapmessages.js +1 -0
- package/src/commands/dev/updatesets.js +145 -15
- package/src/commands/dev.js +3 -2
- package/src/commands/docs/db.js +14 -0
- package/src/commands/docs/docs.js +9 -21
- package/src/commands/docs/search.js +33 -10
- package/src/commands/groups.js +3 -0
- package/src/commands/records.js +254 -9
- package/src/commands/setup.js +5 -0
- package/src/commands/skill.js +3 -0
- package/src/commands/snippets.js +155 -0
- package/src/commands/tickets.js +9 -29
- package/src/commands/users.js +9 -35
- package/src/commands/version.js +3 -0
- package/src/errors.js +92 -1
- package/src/help.js +1 -0
- package/src/helpers.js +22 -11
- package/src/mutations.js +36 -91
- package/src/output.js +187 -61
- package/src/resolve-record.js +71 -0
- package/src/sdk.js +108 -8
- package/src/session.js +214 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Shared resolver for "find one record by a human identifier".
|
|
2
|
+
//
|
|
3
|
+
// Every command that accepts a number / name / user_name / sys_id used to
|
|
4
|
+
// hand-roll the same five steps (classify the identifier, guard the query,
|
|
5
|
+
// build params, fetch, unwrap sys_id) — and they drifted: hex detection
|
|
6
|
+
// differed per file and some lookups forgot sysparm_display_value=all, so a
|
|
7
|
+
// wrapped {value, display_value} sys_id leaked into update/delete calls.
|
|
8
|
+
// This module is the single deep seam; commands shrink to
|
|
9
|
+
// resolve → mutate → report.
|
|
10
|
+
|
|
11
|
+
import { assertSafeExactMatch, isHexString } from './helpers.js';
|
|
12
|
+
import { errNotFound } from './errors.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* True when the identifier looks like a ServiceNow sys_id (32 hex chars).
|
|
16
|
+
* Exported so call sites that only need classification share one definition.
|
|
17
|
+
*/
|
|
18
|
+
export function isSysId(identifier) {
|
|
19
|
+
return typeof identifier === 'string' && identifier.length === 32 && isHexString(identifier);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unwrap a record's sys_id, which may be a plain string or a
|
|
24
|
+
* {value, display_value} object when display values were requested.
|
|
25
|
+
*/
|
|
26
|
+
export function unwrapSysId(record) {
|
|
27
|
+
const sysId = record?.sys_id;
|
|
28
|
+
if (sysId && typeof sysId === 'object') return sysId.value || sysId.display_value || '';
|
|
29
|
+
return sysId || '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a single record by human identifier.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} sdk — SDK handle (uses sdk.list)
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @param {string} opts.table — ServiceNow table
|
|
38
|
+
* @param {string} opts.identifier — sys_id (32 hex) or value of matchField
|
|
39
|
+
* @param {string} opts.matchField — field to match non-sys_id identifiers on
|
|
40
|
+
* (e.g. 'number', 'name', 'user_name')
|
|
41
|
+
* @param {string} [opts.resource] — label for the not-found error
|
|
42
|
+
* (e.g. 'User' → "User not found: <id>"); defaults to matchField
|
|
43
|
+
* @param {string[]} [opts.fields] — restrict sysparm_fields when set
|
|
44
|
+
* @returns {Promise<object>} the record (display values requested)
|
|
45
|
+
* @throws {AppError} code not_found when no record matches
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveRecord(sdk, { table, identifier, matchField, resource, fields }) {
|
|
48
|
+
assertSafeExactMatch(identifier);
|
|
49
|
+
const queryField = isSysId(identifier) ? 'sys_id' : matchField;
|
|
50
|
+
const params = new URLSearchParams();
|
|
51
|
+
params.set('sysparm_query', `${queryField}=${identifier}`);
|
|
52
|
+
params.set('sysparm_limit', '1');
|
|
53
|
+
params.set('sysparm_display_value', 'all');
|
|
54
|
+
if (fields && fields.length > 0) {
|
|
55
|
+
params.set('sysparm_fields', fields.join(','));
|
|
56
|
+
}
|
|
57
|
+
const records = await sdk.list(table, params);
|
|
58
|
+
if (records.length === 0) {
|
|
59
|
+
throw errNotFound(resource || matchField, identifier);
|
|
60
|
+
}
|
|
61
|
+
return records[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a record and return just its raw sys_id.
|
|
66
|
+
* Same options as resolveRecord; defaults to fetching only sys_id.
|
|
67
|
+
*/
|
|
68
|
+
export async function resolveSysId(sdk, opts) {
|
|
69
|
+
const record = await resolveRecord(sdk, { fields: ['sys_id'], ...opts });
|
|
70
|
+
return unwrapSysId(record);
|
|
71
|
+
}
|
package/src/sdk.js
CHANGED
|
@@ -6,6 +6,23 @@ import { getStringField } from './helpers.js';
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TIMEOUT = 30000;
|
|
8
8
|
|
|
9
|
+
// Build the sys.scripts.do POST body for background script execution.
|
|
10
|
+
// Extracted for testability (issue #177): maps script-mode options to the
|
|
11
|
+
// form fields on ServiceNow's Script Background form. Defaults preserve the
|
|
12
|
+
// historical behavior (rollback recording on, quota-managed transaction on).
|
|
13
|
+
export function buildScriptFormBody({ script, csrf, scope, rollback = true, quotaManagedTransaction = true, sandbox = false, scriptlet = false }) {
|
|
14
|
+
const formBody = new URLSearchParams();
|
|
15
|
+
formBody.set('script', script);
|
|
16
|
+
formBody.set('sysparm_ck', csrf);
|
|
17
|
+
formBody.set('runscript', 'Run script');
|
|
18
|
+
formBody.set('sys_scope', scope || 'global');
|
|
19
|
+
if (rollback) formBody.set('record_for_rollback', 'on');
|
|
20
|
+
if (quotaManagedTransaction) formBody.set('quota_managed_transaction', 'on');
|
|
21
|
+
if (sandbox) formBody.set('sandbox', 'on');
|
|
22
|
+
if (scriptlet) formBody.set('scriptlet', 'on');
|
|
23
|
+
return formBody;
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export class SDKClient {
|
|
10
27
|
constructor(baseURL, authProvider, opts = {}) {
|
|
11
28
|
this.baseURL = baseURL.replace(/\/$/, '');
|
|
@@ -177,6 +194,55 @@ export class SDKClient {
|
|
|
177
194
|
await this.request(endpoint, { method: 'DELETE' });
|
|
178
195
|
}
|
|
179
196
|
|
|
197
|
+
// ─── Attachments ───
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* List attachments on a record.
|
|
201
|
+
* @param {string} sysID - record sys_id
|
|
202
|
+
* @returns {Promise<Array>} attachment metadata rows
|
|
203
|
+
*/
|
|
204
|
+
async listAttachments(sysID) {
|
|
205
|
+
const params = new URLSearchParams();
|
|
206
|
+
params.set('sysparm_query', `table_sys_id=${sysID}`);
|
|
207
|
+
params.set('sysparm_display_value', 'all');
|
|
208
|
+
params.set('sysparm_fields', 'sys_id,file_name,size_bytes,content_type,sys_created_by,sys_created_on');
|
|
209
|
+
const endpoint = `${this.baseURL}/api/now/attachment?${params.toString()}`;
|
|
210
|
+
const result = await this.request(endpoint, { method: 'GET' });
|
|
211
|
+
return result?.result || [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Download an attachment's file contents as a Buffer.
|
|
216
|
+
* @param {string} sysID - attachment sys_id
|
|
217
|
+
* @returns {Promise<Buffer>}
|
|
218
|
+
*/
|
|
219
|
+
async getAttachment(sysID) {
|
|
220
|
+
const endpoint = `${this.baseURL}/api/now/attachment/${sysID}/file`;
|
|
221
|
+
const resp = await this._fetchWithAuth(endpoint, { method: 'GET', headers: { Accept: 'application/octet-stream' } });
|
|
222
|
+
if (!resp.ok) {
|
|
223
|
+
throw errAPI(resp.status, String(resp.statusText));
|
|
224
|
+
}
|
|
225
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
226
|
+
return buf;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Upload a file as a new attachment on a record.
|
|
231
|
+
* @param {string} table - parent table name (e.g. 'incident')
|
|
232
|
+
* @param {string} sysID - parent record sys_id
|
|
233
|
+
* @param {Buffer|string} content - file bytes or path
|
|
234
|
+
* @param {string} fileName - display file name
|
|
235
|
+
* @returns {Promise<object>} created attachment row
|
|
236
|
+
*/
|
|
237
|
+
async addAttachment(table, sysID, content, fileName) {
|
|
238
|
+
const form = new FormData();
|
|
239
|
+
const blob = Buffer.isBuffer(content) ? new Blob([content]) : new Blob([String(content)]);
|
|
240
|
+
form.append('file', blob, fileName);
|
|
241
|
+
const endpoint = `${this.baseURL}/api/now/attachment?table_name=${encodeURIComponent(table)}&table_sys_id=${encodeURIComponent(sysID)}`;
|
|
242
|
+
const result = await this.request(endpoint, { method: 'POST', body: form });
|
|
243
|
+
return result?.result || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
180
246
|
async getCurrentUser() {
|
|
181
247
|
const params = new URLSearchParams();
|
|
182
248
|
params.set('sysparm_query', 'user_name=javascript:gs.getUserName()');
|
|
@@ -433,6 +499,31 @@ export class SDKClient {
|
|
|
433
499
|
|
|
434
500
|
// ─── Background Script Execution (3-step OAuth session flow) ───
|
|
435
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Export an update set to XML via the fluent export endpoint.
|
|
504
|
+
* Same session dance as executeScript: warm the session for cookies,
|
|
505
|
+
* grab the CSRF token from sys.scripts.do, then GET the export endpoint.
|
|
506
|
+
*
|
|
507
|
+
* @param {string} updateSetSysId - sys_id of the sys_update_set record
|
|
508
|
+
* @param {string} appSysId - sys_scope sys_id (scope of the update set)
|
|
509
|
+
* @returns {Promise<string>} The update set XML document
|
|
510
|
+
*/
|
|
511
|
+
async exportUpdateSet(updateSetSysId, appSysId = '') {
|
|
512
|
+
const cookies = await this._warmSession();
|
|
513
|
+
const csrfToken = await this._getScriptsPageCSRF(cookies);
|
|
514
|
+
|
|
515
|
+
const params = new URLSearchParams();
|
|
516
|
+
params.set('sysparm_ck', csrfToken);
|
|
517
|
+
params.set('sysparm_sys_id', updateSetSysId);
|
|
518
|
+
params.set('sysparm_app_sys_id', appSysId);
|
|
519
|
+
|
|
520
|
+
const endpoint = `${this.baseURL}/fluent_update_set_export.do?${params.toString()}`;
|
|
521
|
+
return this.rawRequest(endpoint, {
|
|
522
|
+
method: 'GET',
|
|
523
|
+
headers: cookies ? { Cookie: cookies } : {},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
436
527
|
/**
|
|
437
528
|
* Execute a background script on the ServiceNow instance via sys.scripts.do.
|
|
438
529
|
* Uses a 3-step session-establishment flow compatible with OAuth tokens:
|
|
@@ -441,9 +532,16 @@ export class SDKClient {
|
|
|
441
532
|
* 3. POST /sys.scripts.do with the script, CSRF token, and cookies
|
|
442
533
|
*
|
|
443
534
|
* @param {string} script - JavaScript code to execute
|
|
535
|
+
* @param {string} scope - Scope sys_id to run under ('' = global)
|
|
536
|
+
* @param {object} [opts] - Script-mode flags (issue #177), mapping to the
|
|
537
|
+
* Script Background form checkboxes:
|
|
538
|
+
* - rollback (default true) → record_for_rollback
|
|
539
|
+
* - quotaManagedTransaction (default true) → quota_managed_transaction
|
|
540
|
+
* - sandbox (default false) → sandbox (KittyScript, single expression)
|
|
541
|
+
* - scriptlet (default false) → scriptlet
|
|
444
542
|
* @returns {Promise<string>} The script's output text
|
|
445
543
|
*/
|
|
446
|
-
async executeScript(script, scope) {
|
|
544
|
+
async executeScript(script, scope, opts = {}) {
|
|
447
545
|
// Step 1: Warm up the session by hitting any REST API — this makes
|
|
448
546
|
// ServiceNow issue session cookies for subsequent UI page requests.
|
|
449
547
|
// We capture cookies to forward them (Node.js fetch() has no built-in cookie jar).
|
|
@@ -454,13 +552,15 @@ export class SDKClient {
|
|
|
454
552
|
|
|
455
553
|
// Step 3: POST the script with form data including the CSRF token.
|
|
456
554
|
const endpoint = `${this.baseURL}/sys.scripts.do`;
|
|
457
|
-
const formBody =
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
555
|
+
const formBody = buildScriptFormBody({
|
|
556
|
+
script,
|
|
557
|
+
csrf: csrfToken,
|
|
558
|
+
scope,
|
|
559
|
+
rollback: opts.rollback,
|
|
560
|
+
quotaManagedTransaction: opts.quotaManagedTransaction,
|
|
561
|
+
sandbox: opts.sandbox,
|
|
562
|
+
scriptlet: opts.scriptlet,
|
|
563
|
+
});
|
|
464
564
|
|
|
465
565
|
const html = await this.rawRequest(endpoint, {
|
|
466
566
|
method: 'POST',
|
package/src/session.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Session resolver — one module owns the answer to "which instance am I
|
|
2
|
+
// talking to, as whom, with what profile flags?"
|
|
3
|
+
//
|
|
4
|
+
// Before this module existed, that composite answer was assembled across
|
|
5
|
+
// four files with shared mutable state:
|
|
6
|
+
// - cli.js middleware resolved --profile by mutating cfg.activeProfile as
|
|
7
|
+
// a side effect, and poked BOTH app._overrideInstance and
|
|
8
|
+
// argv._overrideInstance (the --instance branch duplicated across two
|
|
9
|
+
// adjacent middlewares);
|
|
10
|
+
// - app.js carried _overrideInstance + getEffectiveInstance +
|
|
11
|
+
// setEffectiveInstance, re-deriving the active profile three ways;
|
|
12
|
+
// - auth.js reached into configProvider.config.profiles to re-derive the
|
|
13
|
+
// username for credential keying;
|
|
14
|
+
// - config.js's getEffectiveInstance duplicated the
|
|
15
|
+
// activeProfile || defaultProfile incantation.
|
|
16
|
+
//
|
|
17
|
+
// Now: resolveSession({argv-ish}, config) → a session object (pure — no
|
|
18
|
+
// mutation of argv OR config), applySession(app, session) is the SINGLE
|
|
19
|
+
// point where a flag override changes app/config state, and App keeps the
|
|
20
|
+
// resolved session on app.session for anything that needs identity
|
|
21
|
+
// (AuthManager is handed a provider over it, so it no longer spelunks
|
|
22
|
+
// config internals).
|
|
23
|
+
//
|
|
24
|
+
// Precedence (pinned by test/session.test.js — preserve exactly):
|
|
25
|
+
// --instance flag > --profile reference > active profile >
|
|
26
|
+
// default profile > legacy bare instanceURL
|
|
27
|
+
//
|
|
28
|
+
// Deliberately NOT here: env var loading (loadConfig already folds
|
|
29
|
+
// SERVICENOW_INSTANCE_URL into cfg.instanceURL before the resolver runs)
|
|
30
|
+
// and credential storage I/O (auth.js owns the keyring/file stores).
|
|
31
|
+
|
|
32
|
+
import { normalizeInstanceURL } from './config.js';
|
|
33
|
+
import { extractProfileName } from './helpers.js';
|
|
34
|
+
import { errUsage } from './errors.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the composite session from parsed argv flags + loaded config.
|
|
38
|
+
* Pure: never mutates argv or config.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} [argv] — anything with optional { instance, profile }
|
|
41
|
+
* (a yargs argv, or a hand-built { instance, profile } shape)
|
|
42
|
+
* @param {object} config — the loaded config (from loadConfig())
|
|
43
|
+
* @returns {{
|
|
44
|
+
* instance: string, // effective instance URL ('' when unresolved)
|
|
45
|
+
* profileName: string|null,// active profile name after flags
|
|
46
|
+
* profile: object|null, // the profile entry (instance_url, flags, ...)
|
|
47
|
+
* username: string|null, // profile username for credential keying
|
|
48
|
+
* domain: string, // domain-separation sys_id ('' = no scoping)
|
|
49
|
+
* readOnly: boolean, // profile.read_only === true (the armed guard)
|
|
50
|
+
* override: string|null, // normalized flag override URL, null when none
|
|
51
|
+
* profileExplicit: boolean,// true when --profile named the active profile
|
|
52
|
+
* unknownProfile: string|null, // set when --profile named no known profile
|
|
53
|
+
* }}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSession(argv = {}, config) {
|
|
56
|
+
const cfg = config || {};
|
|
57
|
+
const profiles = cfg.profiles || {};
|
|
58
|
+
|
|
59
|
+
// --profile selects the active profile for this run. Unknown names are
|
|
60
|
+
// surfaced as data (unknownProfile) — the caller (cli.js middleware)
|
|
61
|
+
// decides how to render the guard exit; resolving must not throw because
|
|
62
|
+
// App is constructed before argv exists.
|
|
63
|
+
let unknownProfile = null;
|
|
64
|
+
let profileName = null;
|
|
65
|
+
if (argv.profile) {
|
|
66
|
+
if (profiles[argv.profile]) {
|
|
67
|
+
profileName = argv.profile;
|
|
68
|
+
} else {
|
|
69
|
+
unknownProfile = argv.profile;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!profileName) {
|
|
73
|
+
const fallback = cfg.activeProfile || cfg.defaultProfile;
|
|
74
|
+
if (fallback && profiles[fallback]) profileName = fallback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const profile = profileName ? profiles[profileName] : null;
|
|
78
|
+
|
|
79
|
+
// Instance: --instance flag > profile reference > active/default
|
|
80
|
+
// profile > legacy bare instanceURL. The override is normalized here so
|
|
81
|
+
// every downstream consumer (SDK client, context header links) sees the
|
|
82
|
+
// canonical form. --profile counts as an override too (the old code
|
|
83
|
+
// poked the profile's URL into app._overrideInstance and rebuilt the SDK
|
|
84
|
+
// via setEffectiveInstance's normalization) — session.instance is always
|
|
85
|
+
// the normalized effective URL either way.
|
|
86
|
+
let override = null;
|
|
87
|
+
if (argv.instance) {
|
|
88
|
+
override = normalizeInstanceURL(argv.instance) || null;
|
|
89
|
+
} else if (argv.profile && profile?.instance_url) {
|
|
90
|
+
override = normalizeInstanceURL(profile.instance_url) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const instance = normalizeInstanceURL(override || (profile && profile.instance_url) || cfg.instanceURL || '');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
instance,
|
|
97
|
+
profileName,
|
|
98
|
+
profile,
|
|
99
|
+
username: (profile && profile.username) || null,
|
|
100
|
+
domain: (profile && profile.domain) || '',
|
|
101
|
+
readOnly: profile?.read_only === true,
|
|
102
|
+
override,
|
|
103
|
+
profileExplicit: Boolean(argv.profile) && !unknownProfile,
|
|
104
|
+
unknownProfile,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Derive the legacy context block (profileName + username for the context
|
|
110
|
+
* header) from a session. Kept separate from the session itself because
|
|
111
|
+
* printContextHeader() enriches it with live instance data (scope, update
|
|
112
|
+
* set) — the session stays a snapshot of resolution-time truth.
|
|
113
|
+
*
|
|
114
|
+
* Legacy fallback preserved: with no active profile NAME, find the profile
|
|
115
|
+
* whose instance_url matches the effective instance (legacy configs that
|
|
116
|
+
* predate named profiles), else derive a display name from the URL host.
|
|
117
|
+
*/
|
|
118
|
+
export function contextFromSession(session, config) {
|
|
119
|
+
const context = { profileName: '', username: '', scope: '', updateSet: '' };
|
|
120
|
+
if (!session) return context;
|
|
121
|
+
|
|
122
|
+
if (session.profileName) {
|
|
123
|
+
context.profileName = session.profileName;
|
|
124
|
+
context.username = session.username || '';
|
|
125
|
+
return context;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!session.instance) return context;
|
|
129
|
+
context.profileName = extractProfileName(session.instance);
|
|
130
|
+
for (const [name, profile] of Object.entries(config?.profiles || {})) {
|
|
131
|
+
if (profile.instance_url === session.instance) {
|
|
132
|
+
context.profileName = name;
|
|
133
|
+
context.username = profile.username || '';
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return context;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Apply a resolved session to the App — the ONLY place flag overrides
|
|
142
|
+
* change app/config state. Explicit, greppable, and called from exactly
|
|
143
|
+
* one middleware (cli.js). Replaces the old scattered trio:
|
|
144
|
+
* cfg.activeProfile = argv.profile (buried side effect)
|
|
145
|
+
* app._overrideInstance = url; argv._overrideInstance = url (mirrored)
|
|
146
|
+
*
|
|
147
|
+
* @param {import('./app.js').App} app
|
|
148
|
+
* @param {object} sessionOrArgv — a resolveSession() result, or a bare
|
|
149
|
+
* { instance, profile } argv shape (resolved here for convenience)
|
|
150
|
+
* @returns {object} the applied session
|
|
151
|
+
*/
|
|
152
|
+
export function applySession(app, sessionOrArgv) {
|
|
153
|
+
// A resolved session carries `override` (null when no flag); bare argv
|
|
154
|
+
// ({ instance, profile }) does not. (Object.hasOwn, not `in` — plain
|
|
155
|
+
// objects inherit Object.prototype, which has neither, but hasOwn is the
|
|
156
|
+
// intent-precise check.)
|
|
157
|
+
const isResolved = sessionOrArgv != null && Object.hasOwn(sessionOrArgv, 'override');
|
|
158
|
+
const session = isResolved
|
|
159
|
+
? sessionOrArgv
|
|
160
|
+
: resolveSession(sessionOrArgv, app.config);
|
|
161
|
+
|
|
162
|
+
// --profile: switch the active profile on the config so anything still
|
|
163
|
+
// reading config directly (legacy callers, help rendering) sees the same
|
|
164
|
+
// answer the session computed. Flag args are pinned on _sessionArgv so
|
|
165
|
+
// refreshSession() (after config edits) keeps the same flags in force.
|
|
166
|
+
if (session.profileExplicit) {
|
|
167
|
+
app.config.activeProfile = session.profileName;
|
|
168
|
+
app._sessionArgv = { ...(app._sessionArgv || {}), profile: session.profileName };
|
|
169
|
+
}
|
|
170
|
+
if (!isResolved && sessionOrArgv?.instance !== undefined) {
|
|
171
|
+
app._sessionArgv = { ...(app._sessionArgv || {}), instance: sessionOrArgv.instance };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Flag overrides (--instance, --profile) pin the effective instance for
|
|
175
|
+
// this run and rebuild the SDK client against it. No flags → clear any
|
|
176
|
+
// stale override so the config-sourced instance is authoritative. (The
|
|
177
|
+
// SDK must be rebuilt even at the same URL: the domain scope comes from
|
|
178
|
+
// the newly-active profile.)
|
|
179
|
+
app._overrideInstance = session.override;
|
|
180
|
+
if (session.instance) {
|
|
181
|
+
app._buildSdk(session.instance, session);
|
|
182
|
+
} else {
|
|
183
|
+
app.sdk = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
app.session = session;
|
|
187
|
+
app.loadContext();
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Re-resolve the session after config edits (profile switched, created,
|
|
193
|
+
* or removed by auth/setup commands). Explicit flag overrides are pinned
|
|
194
|
+
* by re-resolving against the pinned argv; without them the config is
|
|
195
|
+
* authoritative.
|
|
196
|
+
*
|
|
197
|
+
* @param {import('./app.js').App} app
|
|
198
|
+
* @returns {object} the refreshed session
|
|
199
|
+
*/
|
|
200
|
+
export function refreshSession(app) {
|
|
201
|
+
return applySession(app, app._sessionArgv || {});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The missing-instance guard, backed by the session. The message and code
|
|
206
|
+
* (usage → exit 2 via the unified renderer) are observable behavior pinned
|
|
207
|
+
* by test/core.test.js — do not reword.
|
|
208
|
+
*/
|
|
209
|
+
export function requireSessionInstance(session) {
|
|
210
|
+
if (!session || !session.instance) {
|
|
211
|
+
throw errUsage('Instance URL required. Set via --instance flag, SERVICENOW_INSTANCE_URL env, or config file.');
|
|
212
|
+
}
|
|
213
|
+
return session.instance;
|
|
214
|
+
}
|