@jacebenson/jsn 4.0.4 → 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.
Files changed (47) hide show
  1. package/README.md +18 -2
  2. package/package.json +4 -3
  3. package/src/app.js +48 -64
  4. package/src/auth.js +46 -11
  5. package/src/capabilities.js +128 -0
  6. package/src/cli.js +129 -65
  7. package/src/commands/_ticket.js +37 -93
  8. package/src/commands/approvals.js +350 -0
  9. package/src/commands/atf.js +635 -0
  10. package/src/commands/auth.js +93 -13
  11. package/src/commands/catalog/enrich.js +139 -0
  12. package/src/commands/catalog.js +25 -81
  13. package/src/commands/cmdb.js +613 -0
  14. package/src/commands/codesearch.js +103 -0
  15. package/src/commands/dev/_generic.js +63 -111
  16. package/src/commands/dev/_simple.js +16 -12
  17. package/src/commands/dev/b4rules.js +1 -1
  18. package/src/commands/dev/domains.js +4 -0
  19. package/src/commands/dev/eval.js +65 -7
  20. package/src/commands/dev/flows.js +3 -0
  21. package/src/commands/dev/logs.js +97 -0
  22. package/src/commands/dev/rest.js +6 -0
  23. package/src/commands/dev/restmessage.js +1 -0
  24. package/src/commands/dev/scopes.js +49 -25
  25. package/src/commands/dev/scrapi.js +5 -0
  26. package/src/commands/dev/soapmessages.js +1 -0
  27. package/src/commands/dev/updatesets.js +145 -15
  28. package/src/commands/dev.js +3 -2
  29. package/src/commands/docs/db.js +14 -0
  30. package/src/commands/docs/docs.js +9 -21
  31. package/src/commands/docs/search.js +33 -10
  32. package/src/commands/groups.js +3 -0
  33. package/src/commands/records.js +274 -12
  34. package/src/commands/setup.js +5 -0
  35. package/src/commands/skill.js +3 -0
  36. package/src/commands/snippets.js +155 -0
  37. package/src/commands/tickets.js +9 -29
  38. package/src/commands/users.js +9 -35
  39. package/src/commands/version.js +3 -0
  40. package/src/errors.js +107 -1
  41. package/src/help.js +1 -0
  42. package/src/helpers.js +131 -15
  43. package/src/mutations.js +36 -91
  44. package/src/output.js +195 -64
  45. package/src/resolve-record.js +71 -0
  46. package/src/sdk.js +108 -8
  47. package/src/session.js +214 -0
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
+ }